Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions backend/src/lib/llm/modelDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const MODEL_ENDPOINTS: Partial<Record<Provider, string>> = {
moonshot: "https://api.moonshot.ai/v1/models",
};

const MODEL_DISCOVERY_ALIASES: Readonly<Record<string, readonly string[]>> = {
"gpt-5.6": ["gpt-5.6-sol"],
};

export type DiscoveredModel = ModelCapability & {
available: boolean;
availability: "live" | "configured" | "unavailable" | "fallback";
Expand Down Expand Up @@ -44,6 +48,11 @@ async function discoverModelIds(
);
}

function isModelListed(modelId: string, ids: ReadonlySet<string>): 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.
Expand Down Expand Up @@ -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.`,
Expand Down
8 changes: 3 additions & 5 deletions frontend/src/app/(pages)/account/api-keys/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
</button>
Expand Down
51 changes: 51 additions & 0 deletions tests/baseline/api-key-settings.test.mjs
Original file line number Diff line number Diff line change
@@ -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",
);
});