diff --git a/backend/src/lib/llm/modelDiscovery.ts b/backend/src/lib/llm/modelDiscovery.ts index 6775e6bdad..963de57c52 100644 --- a/backend/src/lib/llm/modelDiscovery.ts +++ b/backend/src/lib/llm/modelDiscovery.ts @@ -9,6 +9,10 @@ const MODEL_ENDPOINTS: Partial> = { moonshot: "https://api.moonshot.ai/v1/models", }; +const MODEL_DISCOVERY_ALIASES: Readonly> = { + "gpt-5.6": ["gpt-5.6-sol"], +}; + export type DiscoveredModel = ModelCapability & { available: boolean; availability: "live" | "configured" | "unavailable" | "fallback"; @@ -44,6 +48,11 @@ async function discoverModelIds( ); } +function isModelListed(modelId: string, ids: ReadonlySet): boolean { + if (ids.has(modelId)) return true; + return (MODEL_DISCOVERY_ALIASES[modelId] ?? []).some((id) => ids.has(id)); +} + /** * Key-scoped provider availability is combined with ROSS's compatibility * registry. Credentials are used only by the backend and are never returned. @@ -90,11 +99,12 @@ export async function discoverCompatibleModels( }; } + const available = isModelListed(capability.id, ids); return { ...capability, - available: ids.has(capability.id), - availability: ids.has(capability.id) ? "live" : "unavailable", - ...(ids.has(capability.id) + available, + availability: available ? "live" : "unavailable", + ...(available ? {} : { availabilityReason: `This ${providerLabel(capability.provider)} project does not currently list this model as available.`, diff --git a/frontend/src/app/(pages)/account/api-keys/page.tsx b/frontend/src/app/(pages)/account/api-keys/page.tsx index 70125ab37f..fae04d18b4 100644 --- a/frontend/src/app/(pages)/account/api-keys/page.tsx +++ b/frontend/src/app/(pages)/account/api-keys/page.tsx @@ -60,10 +60,8 @@ const OTHER_API_KEY_FIELDS = [ export default function ApiKeysPage() { const { profile, updateApiKey } = useUserProfile(); const { approvedProviders, selfHosted } = useModelCatalog(); - const modelApiKeyFields = MODEL_API_KEY_FIELDS.filter((field) => - field.provider === "openrouter" - ? selfHosted - : approvedProviders.includes(field.provider), + const modelApiKeyFields = MODEL_API_KEY_FIELDS.filter( + (field) => field.provider !== "openrouter" || selfHosted, ); return ( @@ -283,7 +281,7 @@ function ApiKeyField({ type="button" onClick={handleRemove} disabled={isSaving} - className="text-xs font-medium text-red-600 transition-colors hover:text-red-700 disabled:cursor-not-allowed disabled:text-red-300" + className="text-xs font-medium text-red-600 transition-colors hover:text-red-700 disabled:text-red-300" > Remove diff --git a/tests/baseline/api-key-settings.test.mjs b/tests/baseline/api-key-settings.test.mjs new file mode 100644 index 0000000000..28d666e26c --- /dev/null +++ b/tests/baseline/api-key-settings.test.mjs @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const read = (path) => readFileSync(resolve(root, path), "utf8"); + +test("core model-provider API key fields remain visible after catalog loading", () => { + const page = read("frontend/src/app/(pages)/account/api-keys/page.tsx"); + + for (const provider of ["claude", "gemini", "openai"]) { + assert.match( + page, + new RegExp(`provider: ["']${provider}["']`), + `${provider} must remain a core API-key field`, + ); + } + + assert.match( + page, + /field\.provider !== ["']openrouter["'] \|\| selfHosted/, + "only the self-hosted OpenRouter field should be conditionally hidden", + ); + assert.doesNotMatch( + page, + /approvedProviders\.includes\(field\.provider\)/, + "catalog approval must not remove core credential inputs after hydration", + ); +}); + +test("OpenAI model discovery recognizes the GPT-5.6 API alias", () => { + const discovery = read("backend/src/lib/llm/modelDiscovery.ts"); + + assert.match( + discovery, + /["']gpt-5\.6["']:\s*\[["']gpt-5\.6-sol["']\]/, + "the public gpt-5.6 alias must map to the provider-listed gpt-5.6-sol identifier", + ); + assert.match( + discovery, + /isModelListed\(capability\.id, ids\)/, + "availability must use alias-aware model discovery", + ); + assert.doesNotMatch( + discovery, + /available:\s*ids\.has\(capability\.id\)/, + "literal-only discovery would incorrectly disable supported aliases", + ); +});