diff --git a/open-sse/config/opencodeZenGoSharedModels.ts b/open-sse/config/opencodeZenGoSharedModels.ts index 9f9d6628e9..b30279a366 100644 --- a/open-sse/config/opencodeZenGoSharedModels.ts +++ b/open-sse/config/opencodeZenGoSharedModels.ts @@ -12,5 +12,15 @@ export const OPENCODE_ZEN_GO_SHARED_MODELS = Object.freeze([ { id: "kimi-k2.7-code", name: "Kimi K2.7 Code" }, { id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false }, - { id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", supportsVision: false }, + { + id: "qwen3.6-plus", + name: "Qwen3.6 Plus", + targetFormat: "claude", + supportsVision: false, + // #10788: effort-tier aliases exist as explicit registry rows; declare the + // vocabulary on the shared base row so variant synthesis and the sanitizer + // agree on it for both opencode-go and opencode-zen. + supportsReasoning: true, + supportedThinkingEfforts: ["high", "max"], + }, ]); diff --git a/open-sse/config/providers/registry/nvidia/index.ts b/open-sse/config/providers/registry/nvidia/index.ts index 3603700d30..0966fa5941 100644 --- a/open-sse/config/providers/registry/nvidia/index.ts +++ b/open-sse/config/providers/registry/nvidia/index.ts @@ -18,7 +18,17 @@ export const nvidiaProvider: RegistryEntry = { passthroughModels: true, models: [ // #6108: z-ai/glm-5.1 EOL'd 2026-07-02 (direct probe returns 410) — dropped. - { id: "z-ai/glm-5.2", name: "GLM 5.2" }, + // #10788: NVIDIA's hosted GLM-5.2 exposes a BINARY thinking switch + // (chat_template_kwargs.enable_thinking), not effort tiers — see + // mapNvidiaGlm52ReasoningParams. Declaring an empty tier list keeps the + // catalog from synthesizing unresolvable -low/-high/-max variant ids while + // still marking the model reasoning-capable. + { + id: "z-ai/glm-5.2", + name: "GLM 5.2", + supportsReasoning: true, + supportedThinkingEfforts: [], + }, // #3329/#6108: minimaxai/minimax-m3 stays excluded from the nvidia tier — it // still 404s here for most callers; the single 200 probe in #6108 was not // reproducible enough to override the #3329 guard. Re-add only once NVIDIA diff --git a/open-sse/config/providers/registry/opencode/go/index.ts b/open-sse/config/providers/registry/opencode/go/index.ts index 21170b5b16..645a828fed 100644 --- a/open-sse/config/providers/registry/opencode/go/index.ts +++ b/open-sse/config/providers/registry/opencode/go/index.ts @@ -19,9 +19,15 @@ export const opencode_goProvider: RegistryEntry = { // `kimi-k2.7-code` (the live API rejects the plain `kimi-k2.7` alias for // `/chat/completions`, even though the docs config example uses it). // GLM-5.2 — base model + effort-tier aliases (#6922). - // OpencodeExecutor rewrites the alias to the canonical id and injects - // reasoning_effort, mirroring the deepseek-v4-pro-* pattern. - { id: "glm-5.2", name: "GLM-5.2", supportsReasoning: true }, + // #10788: the tier vocabulary is declared on the base row so the catalog's + // variant synthesis (#9485) and the effort sanitizer share one source of + // truth with OpencodeExecutor's EFFORT_TIERS. + { + id: "glm-5.2", + name: "GLM-5.2", + supportsReasoning: true, + supportedThinkingEfforts: ["high", "max"], + }, { id: "glm-5.2-high", name: "GLM-5.2 (high effort)", supportsReasoning: true }, { id: "glm-5.2-max", name: "GLM-5.2 (max effort)", supportsReasoning: true }, @@ -34,11 +40,16 @@ export const opencode_goProvider: RegistryEntry = { { id: "kimi-k2.6", name: "Kimi K2.6" }, { id: "kimi-k2.5", name: "Kimi K2.5" }, // #8353: Kimi K3 base + max-effort alias from the OpenCode Go registry. - { id: "kimi-k3", name: "Kimi K3", supportsReasoning: true }, + { id: "kimi-k3", name: "Kimi K3", supportsReasoning: true, supportedThinkingEfforts: ["max"] }, { id: "kimi-k3-max", name: "Kimi K3 (max effort)", supportsReasoning: true }, // MiMo-V2.5 — base model + effort-tier aliases (#6922). { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", supportsReasoning: true }, - { id: "mimo-v2.5", name: "MiMo-V2.5", supportsReasoning: true }, + { + id: "mimo-v2.5", + name: "MiMo-V2.5", + supportsReasoning: true, + supportedThinkingEfforts: ["high", "max"], + }, { id: "mimo-v2.5-high", name: "MiMo-V2.5 (high effort)", supportsReasoning: true }, { id: "mimo-v2.5-max", name: "MiMo-V2.5 (max effort)", supportsReasoning: true }, // #3110: MiniMax M3 via OpenCode Go tier @@ -59,7 +70,14 @@ export const opencode_goProvider: RegistryEntry = { // so combo routing skips them when the request contains image blocks, // preventing image content from reaching a vision-incapable upstream. // #8353: effort-tier aliases from the OpenCode Go registry. - { id: "qwen3.7-max", name: "Qwen3.7 Max", targetFormat: "claude", supportsVision: false }, + { + id: "qwen3.7-max", + name: "Qwen3.7 Max", + targetFormat: "claude", + supportsVision: false, + supportsReasoning: true, + supportedThinkingEfforts: ["high", "max"], + }, { id: "qwen3.7-max-high", name: "Qwen3.7 Max (high effort)", @@ -79,6 +97,8 @@ export const opencode_goProvider: RegistryEntry = { name: "Qwen3.7 Plus", targetFormat: "claude", supportsVision: false, + supportsReasoning: true, + supportedThinkingEfforts: ["high", "max"], }, { id: "qwen3.7-plus-high", @@ -111,7 +131,13 @@ export const opencode_goProvider: RegistryEntry = { supportsReasoning: true, }, // #8353: hy3 is the Go-tier base id (distinct from hy3-preview / hy3-free). - { id: "hy3", name: "Hunyuan3", contextLength: 256000, supportsReasoning: true }, + { + id: "hy3", + name: "Hunyuan3", + contextLength: 256000, + supportsReasoning: true, + supportedThinkingEfforts: ["none", "low", "high"], + }, { id: "hy3-none", name: "Hunyuan3 (none effort)", @@ -201,7 +227,12 @@ export const opencode_goProvider: RegistryEntry = { targetFormat: "openai-responses", }, // #8353: Grok 4.5 + effort tiers from the OpenCode Go registry. - { id: "grok-4.5", name: "Grok 4.5", supportsReasoning: true }, + { + id: "grok-4.5", + name: "Grok 4.5", + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high"], + }, { id: "grok-4.5-low", name: "Grok 4.5 (low effort)", supportsReasoning: true }, { id: "grok-4.5-medium", name: "Grok 4.5 (medium effort)", supportsReasoning: true }, { id: "grok-4.5-high", name: "Grok 4.5 (high effort)", supportsReasoning: true }, diff --git a/open-sse/config/providers/registry/opencode/zen/index.ts b/open-sse/config/providers/registry/opencode/zen/index.ts index 9fdca2fc0a..64a9ccf0cb 100644 --- a/open-sse/config/providers/registry/opencode/zen/index.ts +++ b/open-sse/config/providers/registry/opencode/zen/index.ts @@ -79,18 +79,35 @@ export const opencode_zenProvider: RegistryEntry = { }, // ── DeepSeek ──────────────────────────────────────────────── - { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" }, - { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" }, + // #10788: same tier vocabulary as opencode-go's DeepSeek rows — the Zen + // upstream accepts the identical effort set on these models. + { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + supportsReasoning: true, + supportedThinkingEfforts: ["none", "low", "high", "max"], + }, + { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + supportsReasoning: true, + supportedThinkingEfforts: ["none", "low", "high", "max"], + }, // ── GLM / Z.AI ───────────────────────────────────────────── - { id: "glm-5.2", name: "GLM-5.2" }, + { + id: "glm-5.2", + name: "GLM-5.2", + supportsReasoning: true, + supportedThinkingEfforts: ["high", "max"], + }, // ── MiniMax ──────────────────────────────────────────────── // #3110: MiniMax M3 — frontier coding model with 1M context { id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true }, // ── Kimi / Moonshot ──────────────────────────────────────── - { id: "kimi-k3", name: "Kimi K3" }, + { id: "kimi-k3", name: "Kimi K3", supportsReasoning: true, supportedThinkingEfforts: ["max"] }, // kimi-k2.7-code declared identically on opencode-go — see OPENCODE_ZEN_GO_SHARED_MODELS. // ── Qwen ─────────────────────────────────────────────────── diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts index c83b08ae86..a79f8f9f88 100644 --- a/open-sse/executors/opencode.ts +++ b/open-sse/executors/opencode.ts @@ -895,10 +895,21 @@ export class OpencodeExecutor extends BaseExecutor { const mb = modifiedBody as Record; const parsed = parseEffortLevel(model); if (parsed) { - mb.model = parsed.baseModel; - if (mb.reasoning_effort === undefined) { - mb.reasoning_effort = parsed.effort; + const deepseekFamily = + parsed.baseModel === "deepseek-v4-pro" || parsed.baseModel === "deepseek-v4-flash"; + if (deepseekFamily) { + // DeepSeek via opencode-go proxies the native DeepSeek contract, which + // accepts a flat reasoning_effort field (#4647). + mb.model = parsed.baseModel; + if (mb.reasoning_effort === undefined) { + mb.reasoning_effort = parsed.effort; + } } + // #10788: every other family's ONLY native effort mechanism is the + // - suffix in the model id itself (the ids `opencode models + // opencode-go --verbose` lists). The opencode-go ChatCompletionRequest + // carries no flat reasoning_effort field, so rewriting to the base id + // silently dropped the tier — forward the aliased id verbatim instead. } } // #1543 / upstream PR #1099: thinking-mode upstreams routed through OpenCode diff --git a/tests/unit/opencode-go-effort-aliases-6922.test.ts b/tests/unit/opencode-go-effort-aliases-6922.test.ts index b599534a85..4222652bcb 100644 --- a/tests/unit/opencode-go-effort-aliases-6922.test.ts +++ b/tests/unit/opencode-go-effort-aliases-6922.test.ts @@ -96,35 +96,43 @@ test("#6922 parseEffortLevel: base model without tier → null", () => { assert.strictEqual(parseEffortLevel("glm-5.2"), null); }); -// ─── transformRequest: end-to-end model-id rewrite + reasoning_effort inject ── +// ─── transformRequest: #6922 wiring, updated by #10788 ───────────────────── // -// parseEffortLevel is a pure function, but the actual bug (#6922) surfaces -// through OpencodeExecutor.transformRequest — the caller that rewrites the -// outbound model id and injects reasoning_effort. These tests exercise that -// public entry point directly so a broken wiring (e.g. parseEffortLevel -// correct but never called, or its result dropped) would fail here even if -// the parseEffortLevel-only tests above stayed green. +// parseEffortLevel is a pure function, but the original bug (#6922) surfaces +// through OpencodeExecutor.transformRequest. Since #10788, glm-5.2 / mimo-v2.5 +// (non-DeepSeek families) forward the effort-suffixed alias VERBATIM — the +// suffix is their only native effort mechanism and opencode-go has no flat +// reasoning_effort field to receive a rewritten tier. Only DeepSeek V4 keeps +// the base-rewrite + field-injection contract. const CREDENTIALS = { apiKey: "k" } as Record; -test("#6922 transformRequest: glm-5.2-high → model rewritten to glm-5.2, reasoning_effort injected", () => { +test("#6922/#10788 transformRequest: glm-5.2-high forwards the alias verbatim", () => { const executor = new OpencodeExecutor("opencode-go"); const body = { model: "glm-5.2-high", messages: [{ role: "user", content: "hi" }] }; const out = executor.transformRequest("glm-5.2-high", body, true, CREDENTIALS); - assert.equal(out.model, "glm-5.2", "model id must be rewritten to the base id"); - assert.equal(out.reasoning_effort, "high", "reasoning_effort must be injected from the alias"); + assert.equal(out.model, "glm-5.2-high", "alias id must reach the wire untouched"); + assert.equal( + out.reasoning_effort, + undefined, + "no flat reasoning_effort may be injected for non-DeepSeek families" + ); }); -test("#6922 transformRequest: mimo-v2.5-max → model rewritten to mimo-v2.5, reasoning_effort injected", () => { +test("#6922/#10788 transformRequest: mimo-v2.5-max forwards the alias verbatim", () => { const executor = new OpencodeExecutor("opencode-go"); const body = { model: "mimo-v2.5-max", messages: [{ role: "user", content: "hi" }] }; const out = executor.transformRequest("mimo-v2.5-max", body, true, CREDENTIALS); - assert.equal(out.model, "mimo-v2.5", "model id must be rewritten to the base id"); - assert.equal(out.reasoning_effort, "max", "reasoning_effort must be injected from the alias"); + assert.equal(out.model, "mimo-v2.5-max", "alias id must reach the wire untouched"); + assert.equal( + out.reasoning_effort, + undefined, + "no flat reasoning_effort may be injected for non-DeepSeek families" + ); }); test("#6922 transformRequest: does not clobber an already-set reasoning_effort", () => { @@ -137,7 +145,7 @@ test("#6922 transformRequest: does not clobber an already-set reasoning_effort", const out = executor.transformRequest("glm-5.2-high", body, true, CREDENTIALS); - assert.equal(out.model, "glm-5.2", "model id is still rewritten to the base id"); + assert.equal(out.model, "glm-5.2-high", "non-DeepSeek alias id is left untouched"); assert.equal( out.reasoning_effort, "caller-supplied", diff --git a/tests/unit/opencode-go-effort-aliases-8353.test.ts b/tests/unit/opencode-go-effort-aliases-8353.test.ts index 4be4ab42df..762de5fcd2 100644 --- a/tests/unit/opencode-go-effort-aliases-8353.test.ts +++ b/tests/unit/opencode-go-effort-aliases-8353.test.ts @@ -27,7 +27,15 @@ const { parseEffortLevel, OpencodeExecutor } = const { REGISTRY } = (await import("../../open-sse/config/providerRegistry.ts")) as { REGISTRY: Record< string, - { models?: Array<{ id: string; name?: string; targetFormat?: string }> } + { + models?: Array<{ + id: string; + name?: string; + targetFormat?: string; + supportsReasoning?: boolean; + supportedThinkingEfforts?: string[]; + }>; + } >; }; @@ -189,29 +197,46 @@ test("#8353 parseEffortLevel: MiniMax M3 has no effort-tier aliases", () => { const CREDENTIALS = { apiKey: "k" } as Record; +// #10788: DeepSeek V4 keeps the base-rewrite + reasoning_effort injection (the +// opencode-go DeepSeek contract accepts the flat field); every other family +// must receive the effort-suffixed alias VERBATIM, because the suffix is their +// only native effort mechanism and opencode-go has no flat reasoning_effort. const TRANSFORM_SAMPLES = [ - { alias: "deepseek-v4-flash-low", base: "deepseek-v4-flash", effort: "low" }, - { alias: "grok-4.5-medium", base: "grok-4.5", effort: "medium" }, - { alias: "hy3-none", base: "hy3", effort: "none" }, - { alias: "kimi-k3-max", base: "kimi-k3", effort: "max" }, - { alias: "qwen3.7-plus-max", base: "qwen3.7-plus", effort: "max" }, - { alias: "qwen3.7-max-high", base: "qwen3.7-max", effort: "high" }, + { + alias: "deepseek-v4-flash-low", + wireModel: "deepseek-v4-flash", + effort: "low" as const, + note: "DeepSeek rewrites to base + reasoning_effort (#4647)", + }, + { alias: "grok-4.5-medium", wireModel: "grok-4.5-medium", effort: null }, + { alias: "hy3-none", wireModel: "hy3-none", effort: null }, + { alias: "kimi-k3-max", wireModel: "kimi-k3-max", effort: null }, + { alias: "qwen3.7-plus-max", wireModel: "qwen3.7-plus-max", effort: null }, + { alias: "qwen3.7-max-high", wireModel: "qwen3.7-max-high", effort: null }, { alias: "muse-spark-1.2-contributor-xhigh", - base: "muse-spark-1.2-contributor", - effort: "xhigh", + wireModel: "muse-spark-1.2-contributor-xhigh", + effort: null, }, ] as const; -for (const { alias, base, effort } of TRANSFORM_SAMPLES) { - test(`#8353 transformRequest: ${alias} → model=${base}, reasoning_effort=${effort}`, () => { +for (const { alias, wireModel, effort, note } of TRANSFORM_SAMPLES) { + test(`#8353/#10788 transformRequest: ${alias} → model=${wireModel}`, () => { const executor = new OpencodeExecutor("opencode-go"); const body = { model: alias, messages: [{ role: "user", content: "hi" }] }; const out = executor.transformRequest(alias, body, true, CREDENTIALS); - assert.equal(out.model, base, "model id must be rewritten to the base id"); - assert.equal(out.reasoning_effort, effort, "reasoning_effort must be injected from the alias"); + assert.equal(out.model, wireModel); + if (effort === null) { + assert.equal( + out.reasoning_effort, + undefined, + "non-DeepSeek families must not receive a flat reasoning_effort field" + ); + } else { + assert.equal(out.reasoning_effort, effort, note); + } }); } @@ -228,3 +253,42 @@ test("#8353 transformRequest: does not clobber an already-set reasoning_effort", assert.equal(out.model, "deepseek-v4-flash"); assert.equal(out.reasoning_effort, "caller-supplied"); }); + +// ─── #10788: base-model tier declarations match the executor vocabulary ──── + +test("#10788 registry base rows declare the same tiers EFFORT_TIERS parses", () => { + const expectedTiers: Record = { + "glm-5.2": ["high", "max"], + "mimo-v2.5": ["high", "max"], + "grok-4.5": ["low", "medium", "high"], + hy3: ["none", "low", "high"], + "kimi-k3": ["max"], + "qwen3.6-plus": ["high", "max"], + "qwen3.7-max": ["high", "max"], + "qwen3.7-plus": ["high", "max"], + }; + for (const providerId of ["opencode-go", "opencode-zen"]) { + const entry = REGISTRY[providerId]; + assert.ok(entry?.models, `${providerId} must expose models`); + for (const [base, tiers] of Object.entries(expectedTiers)) { + if (providerId === "opencode-zen" && !entry.models.some((m) => m.id === base)) continue; + const row = entry.models.find((m) => m.id === base); + assert.ok(row, `${providerId} must declare base model ${base}`); + assert.ok(row.supportsReasoning, `${providerId}/${base} must be reasoning-capable`); + assert.deepEqual( + [...(row.supportedThinkingEfforts ?? [])].sort(), + [...tiers].sort(), + `${providerId}/${base} tier vocabulary must match EFFORT_TIERS` + ); + } + } +}); + +test("#10788 nvidia z-ai/glm-5.2 declares reasoning with an empty tier list (binary switch)", () => { + const entry = REGISTRY["nvidia"]; + assert.ok(entry?.models, "nvidia must expose models"); + const row = entry.models.find((m) => m.id === "z-ai/glm-5.2"); + assert.ok(row, "nvidia z-ai/glm-5.2 must exist"); + assert.equal(row.supportsReasoning, true); + assert.deepEqual(row.supportedThinkingEfforts, []); +});