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 * as React from "react";
|
||||||
import {
|
import {
|
||||||
effortsForModel,
|
|
||||||
effortLabel,
|
effortLabel,
|
||||||
type AdminModel,
|
type AdminModel,
|
||||||
type AgyAccount,
|
type AgyAccount,
|
||||||
@@ -23,6 +22,65 @@ import {
|
|||||||
LoadingState,
|
LoadingState,
|
||||||
Select,
|
Select,
|
||||||
} from "@agy/ui";
|
} 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";
|
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) => {
|
const onProviderChange = (value: string) => {
|
||||||
setProviderId(value);
|
setProviderId(value);
|
||||||
// One provider exists, but the flow is wired for real: picking it reloads
|
// 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) => {
|
const onModelChange = (value: string) => {
|
||||||
setModelId(value);
|
setModelId(value);
|
||||||
const selected = (catalog ?? []).find((m) => m.id === value);
|
const family = groupedModels.find((g) => g.base === value);
|
||||||
const supported = selected ? selected.supported_efforts : effortsForModel(value);
|
const supported = family ? family.efforts : [];
|
||||||
|
|
||||||
// Pick default effort if supported by this model
|
// Default to the strongest effort the family offers (matches agy's default);
|
||||||
if (selected?.default_effort && supported.includes(selected.default_effort)) {
|
// a family with no efforts (e.g. Claude) leaves the field at its placeholder.
|
||||||
setEffort(selected.default_effort);
|
setEffort(supported.includes("high") ? "high" : (supported[0] ?? ""));
|
||||||
} else if (supported.length > 0) {
|
|
||||||
setEffort(supported.includes("high") ? "high" : supported[0]);
|
|
||||||
} else {
|
|
||||||
setEffort("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-detect capabilities based on agy model metadata
|
// Auto-detect capabilities from the family metadata.
|
||||||
const nextCaps: Capability[] = ["text"];
|
const nextCaps: Capability[] = ["text"];
|
||||||
if (selected?.supports_thinking || supported.length > 0) {
|
if (family?.supportsThinking || supported.length > 0) {
|
||||||
nextCaps.push("reasoning");
|
nextCaps.push("reasoning");
|
||||||
}
|
}
|
||||||
if (selected?.supports_images) {
|
if (family?.supportsImages) {
|
||||||
nextCaps.push("vision");
|
nextCaps.push("vision");
|
||||||
}
|
}
|
||||||
setCaps(nextCaps);
|
setCaps(nextCaps);
|
||||||
@@ -176,12 +233,20 @@ export default function AdminModelsPage() {
|
|||||||
setFormError("حداقل یک توانایی انتخاب کنید.");
|
setFormError("حداقل یک توانایی انتخاب کنید.");
|
||||||
return;
|
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);
|
setCreating(true);
|
||||||
try {
|
try {
|
||||||
const created = await api.adminCreateModel({
|
const created = await api.adminCreateModel({
|
||||||
internal_name: modelId,
|
internal_name: resolved?.id ?? modelId,
|
||||||
display_name_for_admin: selectedModel?.display_name || modelId,
|
display_name_for_admin: resolved?.display_name || modelId,
|
||||||
capabilities: caps,
|
capabilities: caps,
|
||||||
input_price_per_1m: Number(inputPrice) || 0,
|
input_price_per_1m: Number(inputPrice) || 0,
|
||||||
output_price_per_1m: Number(outputPrice) || 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 selectedFamily = groupedModels.find((g) => g.base === modelId);
|
||||||
const efforts: ReasoningEffort[] = selectedModel
|
const efforts: ReasoningEffort[] = selectedFamily
|
||||||
? selectedModel.supported_efforts
|
? (selectedFamily.efforts as ReasoningEffort[])
|
||||||
: modelId
|
|
||||||
? effortsForModel(modelId)
|
|
||||||
: [];
|
: [];
|
||||||
const accountOptions = accounts.map((a) => ({
|
const accountOptions = accounts.map((a) => ({
|
||||||
value: a.id,
|
value: a.id,
|
||||||
label: a.email || a.name || a.id,
|
label: a.email || a.name || a.id,
|
||||||
}));
|
}));
|
||||||
const modelOptions = (catalog ?? []).map((m) => ({
|
const modelOptions = groupedModels.map((g) => ({ value: g.base, label: g.base }));
|
||||||
value: m.id,
|
|
||||||
label: m.display_name && m.display_name !== m.id ? `${m.display_name} (${m.id})` : m.id,
|
|
||||||
}));
|
|
||||||
const effortOptions = [
|
const effortOptions = [
|
||||||
{ value: "", label: "پیشفرض سامانه" },
|
{ value: "", label: "پیشفرض سامانه" },
|
||||||
...efforts.map((e) => ({ value: e, label: effortLabel(e) })),
|
...efforts.map((e) => ({ value: e, label: effortLabel(e) })),
|
||||||
|
|||||||
Reference in New Issue
Block a user