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:
@@ -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) })),
|
||||
|
||||
Reference in New Issue
Block a user