diff --git a/src/providers/command-code-efforts.ts b/src/providers/command-code-efforts.ts index 122a224605..b790c8779d 100644 --- a/src/providers/command-code-efforts.ts +++ b/src/providers/command-code-efforts.ts @@ -9,14 +9,31 @@ const COMMAND_CODE_MODEL_EFFORTS = { efforts: ["high", "max"], profileUrl: "https://commandcode.ai/models/deepseek-v4-flash", }, - "zai-org/glm-5.3": { - efforts: ["low", "high", "max"], - profileUrl: "https://commandcode.ai/models/glm-5-3", + // Keys must match the EXACT upstream /provider/v1/models ids (GLM ships as + // `zai-org/GLM-5.3`, not `zai-org/glm-5.3`). The table doubles as the router's + // known-ids decode source (via `knownModelIdsForProvider`), so a case mismatch + // makes the Codex-facing slug `commandcode/zai-org-GLM-5.3` pass through + // undecoded and upstream rejects it with `unsupported_model`. + "zai-org/GLM-5": { + efforts: ["high", "max"], + profileUrl: "https://commandcode.ai/models/glm-5", }, - "zai-org/glm-5.2": { + "zai-org/GLM-5.1": { + efforts: ["high", "max"], + profileUrl: "https://commandcode.ai/models/glm-5-1", + }, + "zai-org/GLM-5.2": { efforts: ["high", "max"], profileUrl: "https://commandcode.ai/models/glm-5-2", }, + "zai-org/GLM-5.2-Fast": { + efforts: ["high", "max"], + profileUrl: "https://commandcode.ai/models/glm-5-2-fast", + }, + "zai-org/GLM-5.3": { + efforts: ["low", "high", "max"], + profileUrl: "https://commandcode.ai/models/glm-5-3", + }, // Muse Spark: CLI currently prints "has no adjustable reasoning effort" and // blocks --effort locally, but the upstream /alpha/generate endpoint accepts // reasoning_effort low..max for meta/muse-spark-1.2-contributor (verified @@ -53,8 +70,14 @@ function keyFor(modelId: string): string { export function commandCodeReasoningEfforts(modelId: string): readonly string[] | undefined { const key = keyFor(modelId); - return refreshedEfforts.get(key) - ?? (Object.hasOwn(COMMAND_CODE_MODEL_REASONING_EFFORTS, key) ? COMMAND_CODE_MODEL_REASONING_EFFORTS[key] : undefined); + const refreshed = refreshedEfforts.get(key); + if (refreshed !== undefined) return refreshed; + // Case-insensitive: the table keys match the EXACT upstream ids (e.g. `zai-org/GLM-5.3`), + // but callers may pass either case. + for (const [id, efforts] of Object.entries(COMMAND_CODE_MODEL_REASONING_EFFORTS)) { + if (keyFor(id) === key) return efforts; + } + return undefined; } function parsedProfileEfforts(page: string): string[] | undefined { @@ -82,9 +105,13 @@ export async function refreshCommandCodeReasoningEfforts( fetchFn: typeof globalThis.fetch = globalThis.fetch, ): Promise { const key = keyFor(modelId); - const profile = Object.hasOwn(COMMAND_CODE_MODEL_EFFORTS, key) - ? COMMAND_CODE_MODEL_EFFORTS[key as keyof typeof COMMAND_CODE_MODEL_EFFORTS] - : undefined; + let profile: { efforts: readonly string[]; profileUrl: string } | undefined; + for (const [id, row] of Object.entries(COMMAND_CODE_MODEL_EFFORTS)) { + if (keyFor(id) === key) { + profile = row; + break; + } + } if (!profile) return undefined; try { const response = await fetchFn(profile.profileUrl, { diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 72bf1ba217..18d920436a 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1730,6 +1730,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ apiKeyValidation: "unknown", // The public catalog reports ids/context windows only; no trustworthy reasoning contract. reasoningEfforts: [], + // Official Command Code model-profile reasoning facts (shared with the OAuth + // `command-code` entry). Without them the API-key preset never advertises a + // reasoning picker, and the router's known-ids decode source misses the native + // slash ids — so a Codex-facing slug like `commandcode/deepseek-deepseek-v4-pro` + // is sent upstream verbatim and rejected with `unsupported_model`. + modelReasoningEfforts: COMMAND_CODE_MODEL_REASONING_EFFORTS, modelDiscovery: { path: "models", maxResponseBytes: 256 * 1024, diff --git a/tests/command-code-provider.test.ts b/tests/command-code-provider.test.ts index ce50b8972d..867d5dc21e 100644 --- a/tests/command-code-provider.test.ts +++ b/tests/command-code-provider.test.ts @@ -51,7 +51,7 @@ describe("Command Code provider", () => { expect(registry?.models).toBeUndefined(); expect(registry?.modelReasoningEfforts).toMatchObject({ "deepseek/deepseek-v4-flash": ["high", "max"], - "zai-org/glm-5.2": ["high", "max"], + "zai-org/GLM-5.2": ["high", "max"], }); expect(OAUTH_PROVIDERS["command-code"]?.providerConfig).toMatchObject({ adapter: "command-code", @@ -60,6 +60,29 @@ describe("Command Code provider", () => { }); }); + test("API-key preset shares the official reasoning-facts table with the OAuth entry", () => { + const oauth = PROVIDER_REGISTRY.find(row => row.id === "command-code"); + const apiKey = PROVIDER_REGISTRY.find(row => row.id === "commandcode"); + expect(apiKey).toMatchObject({ + adapter: "openai-chat", + authKind: "key", + baseUrl: "https://api.commandcode.ai/provider/v1", + liveModels: true, + }); + // Without this the API-key preset never advertises a reasoning picker, and the + // router's known-ids decode source misses the native slash ids — the Codex-facing + // slug `commandcode/deepseek-deepseek-v4-pro` is then sent upstream verbatim and + // rejected with `unsupported_model`. + expect(apiKey?.modelReasoningEfforts).toEqual(oauth?.modelReasoningEfforts); + expect(apiKey?.modelReasoningEfforts).toMatchObject({ + "deepseek/deepseek-v4-pro": ["high", "max"], + "zai-org/GLM-5": ["high", "max"], + "zai-org/GLM-5.1": ["high", "max"], + "zai-org/GLM-5.2-Fast": ["high", "max"], + "zai-org/GLM-5.3": ["low", "high", "max"], + }); + }); + test("validates callback shape and state without exposing the key", () => { const secret = "super-secret-callback-key"; const parsedCallback = parseCommandCodeCallback({ apiKey: secret, state: "state", userId: "u", userName: "name", keyName: "cli" }, "state"); diff --git a/tests/slug-codec.test.ts b/tests/slug-codec.test.ts index 70f8a2fd02..cb2ad121d7 100644 --- a/tests/slug-codec.test.ts +++ b/tests/slug-codec.test.ts @@ -166,6 +166,26 @@ describe("routeModel decode (proxy layer)", () => { expect(ids).toContain("moonshotai/kimi-k3-free"); expect(ids).toContain("moonshotai/kimi-k3"); }); + + test("commandcode API-key preset decodes its native slash ids from the registry effort table", () => { + // Regression: the `commandcode` (API-key) registry entry must share the official + // reasoning-facts table with the OAuth `command-code` entry. Without it the router's + // known-ids source misses `deepseek/deepseek-v4-pro` / `zai-org/GLM-5.3`, so the + // Codex-facing slugs (`commandcode/deepseek-deepseek-v4-pro`) pass through unchanged + // and upstream rejects them with `unsupported_model`. + const prov = { + adapter: "openai-chat", + baseUrl: "https://api.commandcode.ai/provider/v1", + authMode: "key" as const, + models: ["deepseek/deepseek-v4-flash"], + liveModels: true, + }; + const ids = knownModelIdsForProvider("commandcode", prov); + expect(ids).toContain("deepseek/deepseek-v4-pro"); + expect(ids).toContain("zai-org/GLM-5.3"); + expect(decodeRoutedModelId("deepseek-deepseek-v4-pro", ids)).toBe("deepseek/deepseek-v4-pro"); + expect(decodeRoutedModelId("zai-org-GLM-5.3", ids)).toBe("zai-org/GLM-5.3"); + }); }); describe("catalog emission (Codex-facing)", () => {