به‌روزرسانی: دریافت زندهٔ کاتالوگ مدل‌ها و افورت‌های استدلال پشتیبانی‌شده بر اساس اکانت انتخابی

This commit is contained in:
mjalmoudy94
2026-10-13 07:41:00 +00:00
parent da5a2a5d1a
commit 0b72c4ec6d
10 changed files with 229 additions and 39 deletions
+51 -18
View File
@@ -6,6 +6,7 @@ import {
effortLabel,
type AdminModel,
type AgyAccount,
type AgyAccountModel,
type Capability,
type ReasoningEffort,
} from "@agy/contracts";
@@ -29,8 +30,8 @@ import { ApiError, api } from "@/lib/api";
*
* agy is the only provider, and nothing about a model is typed by hand: the
* console asks the selected *account* for its live catalogue, the owner picks a
* model and a reasoning effort, and the rest is metadata. The model id itself
* never leaves the console — a customer only ever sees routing choices.
* model and a reasoning effort supported by that model, and the rest is metadata.
* The model id itself never leaves the console — a customer only ever sees routing choices.
*/
const CAPABILITIES: Capability[] = ["text", "vision", "tools", "reasoning", "long_context", "code"];
@@ -47,7 +48,7 @@ export default function AdminModelsPage() {
// The add-model cascade: provider -> account -> model -> effort.
const [providerId, setProviderId] = React.useState("agy");
const [accountId, setAccountId] = React.useState("");
const [catalog, setCatalog] = React.useState<string[] | null>(null);
const [catalog, setCatalog] = React.useState<AgyAccountModel[] | null>(null);
const [catalogLoading, setCatalogLoading] = React.useState(false);
const [catalogError, setCatalogError] = React.useState<string | null>(null);
const [modelId, setModelId] = React.useState("");
@@ -135,14 +136,27 @@ export default function AdminModelsPage() {
const onModelChange = (value: string) => {
setModelId(value);
setEffort("");
// Sensible defaults the owner can still adjust: everything answers text,
// and a thinking-capable model is a reasoning model.
setCaps(
effortsForModel(value).length > 0
? ["text", "reasoning"]
: ["text"],
);
const selected = (catalog ?? []).find((m) => m.id === value);
const supported = selected ? selected.supported_efforts : effortsForModel(value);
// Pick default effort if supported by this model
if (selected?.default_effort && supported.includes(selected.default_effort)) {
setEffort(selected.default_effort);
} else if (supported.length > 0) {
setEffort(supported.includes("high") ? "high" : supported[0]);
} else {
setEffort("");
}
// Auto-detect capabilities based on agy model metadata
const nextCaps: Capability[] = ["text"];
if (selected?.supports_thinking || supported.length > 0) {
nextCaps.push("reasoning");
}
if (selected?.supports_images) {
nextCaps.push("vision");
}
setCaps(nextCaps);
};
const toggleCap = (cap: Capability) => {
@@ -162,10 +176,12 @@ export default function AdminModelsPage() {
setFormError("حداقل یک توانایی انتخاب کنید.");
return;
}
const selectedModel = (catalog ?? []).find((m) => m.id === modelId);
setCreating(true);
try {
const created = await api.adminCreateModel({
internal_name: modelId,
display_name_for_admin: selectedModel?.display_name || modelId,
capabilities: caps,
input_price_per_1m: Number(inputPrice) || 0,
output_price_per_1m: Number(outputPrice) || 0,
@@ -184,13 +200,24 @@ export default function AdminModelsPage() {
}
};
const efforts: ReasoningEffort[] = modelId ? effortsForModel(modelId) : [];
const selectedModel = (catalog ?? []).find((m) => m.id === modelId);
const efforts: ReasoningEffort[] = selectedModel
? selectedModel.supported_efforts
: modelId
? effortsForModel(modelId)
: [];
const accountOptions = accounts.map((a) => ({
value: a.id,
label: a.email || a.name || a.id,
}));
const modelOptions = (catalog ?? []).map((id) => ({ value: id, label: id }));
const effortOptions = efforts.map((e) => ({ value: e, label: effortLabel(e) }));
const modelOptions = (catalog ?? []).map((m) => ({
value: m.id,
label: m.display_name && m.display_name !== m.id ? `${m.display_name} (${m.id})` : m.id,
}));
const effortOptions = [
{ value: "", label: "پیش‌فرض سامانه" },
...efforts.map((e) => ({ value: e, label: effortLabel(e) })),
];
return (
<div className="anim-stagger flex flex-col gap-5">
@@ -251,12 +278,18 @@ export default function AdminModelsPage() {
label="افورت استدلال"
value={effort}
onChange={(e) => setEffort(e.target.value)}
options={effortOptions}
disabled={efforts.length === 0}
options={
!modelId
? [{ value: "", label: "ابتدا یک مدل انتخاب کنید" }]
: efforts.length === 0
? [{ value: "", label: "این مدل از افورت استدلال پشتیبانی نمی‌کند" }]
: effortOptions
}
disabled={!modelId || efforts.length === 0}
hint={
efforts.length === 0
? "این مدل افورت استدلال ندارد."
: "بودجهٔ تفکر مدل؛ روی مدل‌های غیراستدلالی نادیده گرفته می‌شود."
? "این مدل افورت استدلال ندارد (مدل غیراستدلالی یا با استدلال ثابت)."
: "افورت‌های استدلال پشتیبانی‌شدهٔ همین مدل در agy."
}
/>
</div>
+18 -3
View File
@@ -447,9 +447,24 @@ export const api = {
output_tokens: number;
}>(`/api/admin/agy/accounts/${accountId}/test`, { method: "POST" }),
// Live model catalog of one account — fetched from Google on every call, so
// the model dropdown always matches that subscription's entitlement.
agyAccountModels: (accountId: string) =>
apiRequest<string[]>(`/api/admin/agy/accounts/${accountId}/models`),
// the model dropdown always matches that subscription's entitlement along with
// its supported reasoning efforts.
agyAccountModels: async (accountId: string) => {
const raw = await apiRequest<(import("@agy/contracts").AgyAccountModel | string)[]>(
`/api/admin/agy/accounts/${accountId}/models`,
);
return raw.map((item): import("@agy/contracts").AgyAccountModel => {
if (typeof item === "string") {
return {
id: item,
display_name: item,
supports_thinking: false,
supported_efforts: [],
};
}
return item;
});
},
adminModels: () => apiRequest<import("@agy/contracts").AdminModel[]>("/api/admin/models"),
adminCreateModel: (body: {
internal_name: string;
+1 -1
View File
@@ -12,7 +12,7 @@
* "the app is on the wrong version" bug into a failing test, which is the
* whole point of keeping them separate.
*/
export const APP_VERSION = "0.1.0";
export const APP_VERSION = "0.1.1";
/** What the footer pill renders — ``v`` prefix keeps it unambiguous in the UI. */
export const APP_VERSION_LABEL = `v${APP_VERSION}`;
+16 -2
View File
@@ -169,11 +169,25 @@ describe("agy account client", () => {
});
it("fetches the live model catalog for one account", async () => {
const calls = stubJson(["gemini-3-flash", "gemini-3-pro-high"]);
const calls = stubJson([
{
id: "gemini-3-flash",
display_name: "Gemini 3 Flash",
supports_thinking: true,
supported_efforts: ["low", "medium", "high"],
},
{
id: "gemini-3-pro-high",
display_name: "Gemini 3 Pro High",
supports_thinking: true,
supported_efforts: ["high"],
},
]);
const models = await api.agyAccountModels("a1");
expect(calls[0].method).toBe("GET");
expect(calls[0].url).toContain("/api/admin/agy/accounts/a1/models");
expect(models).toContain("gemini-3-pro-high");
expect(models.map((m) => m.id)).toContain("gemini-3-pro-high");
expect(models[0].supported_efforts).toEqual(["low", "medium", "high"]);
});
it("creates a model from the cascade — no manual names", async () => {