بهروزرسانی: دریافت زندهٔ کاتالوگ مدلها و افورتهای استدلال پشتیبانیشده بر اساس اکانت انتخابی
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@agy/web",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"description": "Smart Organization frontend — Next.js App Router, RTL, dark-first.",
|
||||
"scripts": {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user