fix(admin/models): collapse agy effort variants into one clean option

The model dropdown labeled each option as "{display} ({id})", and since
agy's display name already bakes in the effort, entries rendered like
"Gemini 3.8 Flash (High) (gemini-3.8-flash-high)" — the name, its effort,
AND the raw id all in one, redundant with the separate effort field below.

Group the live catalog into families by base name (effort qualifier
stripped): "Gemini 3.8 Flash" is now one option, and the reasoning effort
is chosen in the existing effort dropdown. On create, the family + chosen
effort resolves back to the concrete agy catalog entry
(gemini-3.8-flash-medium etc.), so the saved internal_name and
reasoning_effort are byte-for-byte what a direct pick produced before.

Frontend-only; no backend/catalog change. tsc --noEmit clean.
This commit is contained in:
mjalmoudy94
2026-10-13 16:54:33 +03:30
parent e50c86c451
commit 91a8ae6c63
+87 -27
View File
@@ -2,7 +2,6 @@
import * as React from "react";
import {
effortsForModel,
effortLabel,
type AdminModel,
type AgyAccount,
@@ -23,6 +22,65 @@ import {
LoadingState,
Select,
} from "@agy/ui";
/** Effort tokens an agy model id/name can encode, weakest -> strongest. */
const EFFORT_ORDER = ["minimal", "low", "medium", "high", "xhigh"];
/** The single effort one agy catalog entry encodes, or ``null`` for a model
* that is not effort-specific (e.g. Claude "(Thinking)"). Detected from the
* id's dash-separated parts to avoid substring accidents ("xhigh" not "high"). */
function entryEffort(m: AgyAccountModel): string | null {
const parts = m.id.toLowerCase().split(/[-_]/);
for (const token of ["xhigh", "high", "medium", "low", "minimal"]) {
if (parts.includes(token)) return token;
}
const supported = m.supported_efforts ?? [];
return supported.length === 1 ? supported[0] : null;
}
/** Base label with any single-effort qualifier (" (High)") removed so effort
* variants of one family collapse into a single dropdown option. */
function baseModelName(m: AgyAccountModel): string {
const stripped = m.display_name
.replace(/\s*\((high|medium|low|minimal|xhigh)\)/i, "")
.trim();
return stripped || m.display_name;
}
/** A model family: N agy catalog entries that share a base name and differ only
* by reasoning effort (e.g. gemini-3.8-flash-high/-medium/-low). */
interface ModelFamily {
base: string;
entries: AgyAccountModel[];
efforts: string[];
supportsThinking: boolean;
supportsImages: boolean;
}
/** Group the live catalog into families for the model dropdown. */
function groupCatalog(catalog: AgyAccountModel[] | null): ModelFamily[] {
const groups = new Map<string, ModelFamily>();
for (const m of catalog ?? []) {
const base = baseModelName(m);
let g = groups.get(base);
if (!g) {
g = { base, entries: [], efforts: [], supportsThinking: false, supportsImages: false };
groups.set(base, g);
}
g.entries.push(m);
g.supportsThinking = g.supportsThinking || !!m.supports_thinking;
g.supportsImages = g.supportsImages || !!m.supports_images;
const own = entryEffort(m);
if (own && !g.efforts.includes(own)) g.efforts.push(own);
for (const e of m.supported_efforts ?? []) {
if (!g.efforts.includes(e)) g.efforts.push(e);
}
}
for (const g of groups.values()) {
g.efforts.sort((a, b) => EFFORT_ORDER.indexOf(a) - EFFORT_ORDER.indexOf(b));
}
return [...groups.values()];
}
import { ApiError, api } from "@/lib/api";
/**
@@ -115,6 +173,10 @@ export default function AdminModelsPage() {
}
}, []);
// The model dropdown shows one option per family (base name), not one per
// effort variant; the effort is chosen separately in the field below.
const groupedModels = React.useMemo(() => groupCatalog(catalog), [catalog]);
const onProviderChange = (value: string) => {
setProviderId(value);
// One provider exists, but the flow is wired for real: picking it reloads
@@ -136,24 +198,19 @@ export default function AdminModelsPage() {
const onModelChange = (value: string) => {
setModelId(value);
const selected = (catalog ?? []).find((m) => m.id === value);
const supported = selected ? selected.supported_efforts : effortsForModel(value);
const family = groupedModels.find((g) => g.base === value);
const supported = family ? family.efforts : [];
// 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("");
}
// Default to the strongest effort the family offers (matches agy's default);
// a family with no efforts (e.g. Claude) leaves the field at its placeholder.
setEffort(supported.includes("high") ? "high" : (supported[0] ?? ""));
// Auto-detect capabilities based on agy model metadata
// Auto-detect capabilities from the family metadata.
const nextCaps: Capability[] = ["text"];
if (selected?.supports_thinking || supported.length > 0) {
if (family?.supportsThinking || supported.length > 0) {
nextCaps.push("reasoning");
}
if (selected?.supports_images) {
if (family?.supportsImages) {
nextCaps.push("vision");
}
setCaps(nextCaps);
@@ -176,12 +233,20 @@ export default function AdminModelsPage() {
setFormError("حداقل یک توانایی انتخاب کنید.");
return;
}
const selectedModel = (catalog ?? []).find((m) => m.id === modelId);
const family = groupedModels.find((g) => g.base === modelId);
// Resolve the family + chosen effort back to the concrete agy catalog entry
// (e.g. "Gemini 3.8 Flash" + medium -> gemini-3.8-flash-medium), so the saved
// internal_name and reasoning_effort are exactly what a direct pick produced.
const resolved = family
? (family.entries.find((e) => entryEffort(e) === effort) ??
family.entries.find((e) => entryEffort(e) === "high") ??
family.entries[0])
: undefined;
setCreating(true);
try {
const created = await api.adminCreateModel({
internal_name: modelId,
display_name_for_admin: selectedModel?.display_name || modelId,
internal_name: resolved?.id ?? modelId,
display_name_for_admin: resolved?.display_name || modelId,
capabilities: caps,
input_price_per_1m: Number(inputPrice) || 0,
output_price_per_1m: Number(outputPrice) || 0,
@@ -200,20 +265,15 @@ export default function AdminModelsPage() {
}
};
const selectedModel = (catalog ?? []).find((m) => m.id === modelId);
const efforts: ReasoningEffort[] = selectedModel
? selectedModel.supported_efforts
: modelId
? effortsForModel(modelId)
: [];
const selectedFamily = groupedModels.find((g) => g.base === modelId);
const efforts: ReasoningEffort[] = selectedFamily
? (selectedFamily.efforts as ReasoningEffort[])
: [];
const accountOptions = accounts.map((a) => ({
value: a.id,
label: a.email || a.name || a.id,
}));
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 modelOptions = groupedModels.map((g) => ({ value: g.base, label: g.base }));
const effortOptions = [
{ value: "", label: "پیش‌فرض سامانه" },
...efforts.map((e) => ({ value: e, label: effortLabel(e) })),