From f92075bb63ea2ee27c8dd92ec9f1eeb1cbce493e Mon Sep 17 00:00:00 2001 From: Ke Jin Date: Tue, 18 Aug 2026 21:51:52 +0800 Subject: [PATCH] fix(deepseek): align V4 reasoning efforts across DeepSeek and OpenCode Go (#10540) * fix(deepseek): align V4 reasoning efforts * docs(changelog): note DeepSeek effort fix * fix(deepseek): align OpenCode V4 effort aliases * test(deepseek): align effort alias expectation * fix(deepseek): scope low effort to v4 * fix(opencode-go): route DeepSeek V4 through Responses --- .../fixes/10540-deepseek-v4-efforts.md | 1 + .../providers/registry/deepseek/index.ts | 2 +- .../providers/registry/opencode/go/index.ts | 24 ++-- open-sse/executors/base/reasoningEffort.ts | 20 ++-- open-sse/executors/opencode.ts | 10 +- .../base-executor-sanitize-effort.test.ts | 73 ++++++------ ...combo-builder-effort-variants-8072.test.ts | 5 +- tests/unit/deepseek-thinking-efforts.test.ts | 112 +++++++++++------- .../models-catalog-combo-metadata.test.ts | 43 +++++++ tests/unit/opencode-executor.test.ts | 62 ++++++---- .../opencode-go-effort-aliases-6922.test.ts | 62 +++++----- .../opencode-go-effort-aliases-8353.test.ts | 42 +++---- 12 files changed, 267 insertions(+), 189 deletions(-) create mode 100644 changelog.d/fixes/10540-deepseek-v4-efforts.md diff --git a/changelog.d/fixes/10540-deepseek-v4-efforts.md b/changelog.d/fixes/10540-deepseek-v4-efforts.md new file mode 100644 index 0000000000..339758ebcf --- /dev/null +++ b/changelog.d/fixes/10540-deepseek-v4-efforts.md @@ -0,0 +1 @@ +- **fix(deepseek):** Advertise `none`, `low`, `high`, and `max` for V4 Pro and Flash, derive OpenCode Go effort aliases from base-model metadata, and route those models through native Responses ([#10540](https://github.com/diegosouzapw/OmniRoute/pull/10540)) — thanks @jackjinke diff --git a/open-sse/config/providers/registry/deepseek/index.ts b/open-sse/config/providers/registry/deepseek/index.ts index 933fb9bba1..e45f471bb8 100644 --- a/open-sse/config/providers/registry/deepseek/index.ts +++ b/open-sse/config/providers/registry/deepseek/index.ts @@ -24,7 +24,7 @@ export const deepseekProvider: RegistryEntry = { contextLength: 1_000_000, maxOutputTokens: 384_000, supportsReasoning: true, - supportedThinkingEfforts: ["none", "high", "max"], + supportedThinkingEfforts: ["none", "low", "high", "max"], toolCalling: true, }, { diff --git a/open-sse/config/providers/registry/opencode/go/index.ts b/open-sse/config/providers/registry/opencode/go/index.ts index 9ff9deda62..54e30e5738 100644 --- a/open-sse/config/providers/registry/opencode/go/index.ts +++ b/open-sse/config/providers/registry/opencode/go/index.ts @@ -131,27 +131,19 @@ export const opencode_goProvider: RegistryEntry = { { 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 }, - { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true }, - // OpencodeExecutor rewrites these aliases to the canonical upstream id and injects reasoning_effort. - { id: "deepseek-v4-pro-low", name: "DeepSeek V4 Pro (low effort)", supportsReasoning: true }, { - id: "deepseek-v4-pro-medium", - name: "DeepSeek V4 Pro (medium effort)", - supportsReasoning: true, - }, - { id: "deepseek-v4-pro-high", name: "DeepSeek V4 Pro (high effort)", supportsReasoning: true }, - { id: "deepseek-v4-pro-max", name: "DeepSeek V4 Pro (max effort)", supportsReasoning: true }, - { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true }, - // #8353: DeepSeek V4 Flash effort tiers from the OpenCode Go registry. - { - id: "deepseek-v4-flash-high", - name: "DeepSeek V4 Flash (high effort)", + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", supportsReasoning: true, + supportedThinkingEfforts: ["none", "low", "high", "max"], + targetFormat: "openai-responses", }, { - id: "deepseek-v4-flash-max", - name: "DeepSeek V4 Flash (max effort)", + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", supportsReasoning: true, + supportedThinkingEfforts: ["none", "low", "high", "max"], + targetFormat: "openai-responses", }, ], }; diff --git a/open-sse/executors/base/reasoningEffort.ts b/open-sse/executors/base/reasoningEffort.ts index 8356e6e131..f3d138bb79 100644 --- a/open-sse/executors/base/reasoningEffort.ts +++ b/open-sse/executors/base/reasoningEffort.ts @@ -297,23 +297,17 @@ export function sanitizeReasoningEffortForProvider( return writeEffortValue(b, "max", c); } - // Native DeepSeek (api.deepseek.com) — V4 thinking mode uses the native - // {low, high, max} vocabulary on Flash and {high, max} on Pro. OmniRoute's - // internal top tier xhigh maps to DeepSeek's literal max. Pro's unsupported - // low/medium values still clamp to high; Flash's documented low tier passes - // through. This is the INVERSE of the OpenRouter-DeepSeek path, whose - // normalized API expects xhigh, not max (pi#4055). `none` is already the - // OpenAI no-thinking carrier and passes through unchanged. + // Native DeepSeek (api.deepseek.com) — V4 Pro and Flash use the native + // {low, high, max} vocabulary, while other model ids retain the {high, max} + // floor. OmniRoute's internal top tier xhigh maps to DeepSeek's literal max, + // while compatibility-only medium maps to high. `none` is already the OpenAI + // no-thinking carrier and passes through unchanged. if (provider === "deepseek") { - // Match the Flash family even when the sanitizer sees a suffixed or prefixed - // id — exact-match would silently clamp Flash `low → high` if a future route - // forwards the raw catalog id (`deepseek-v4-flash-low`) before resolution - // (#9485 review). - const isFlash = modelStr.toLowerCase().startsWith("deepseek-v4-flash"); + const isV4 = modelStr.toLowerCase().startsWith("deepseek-v4-"); const mapped = effortStr === "xhigh" ? "max" - : effortStr === "medium" || (effortStr === "low" && !isFlash) + : effortStr === "medium" || (effortStr === "low" && !isV4) ? "high" : null; if (mapped && mapped !== effortStr) { diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts index 26be70bf2c..b16a4a5688 100644 --- a/open-sse/executors/opencode.ts +++ b/open-sse/executors/opencode.ts @@ -31,7 +31,7 @@ interface OpencodeAccountState extends RotatableAccount { fingerprint: string; } -const EFFORT_LEVELS = ["low", "medium", "high", "max"] as const; +const EFFORT_LEVELS = ["none", "low", "high", "max"] as const; /** * Models that work WITHOUT any API key on the free/noauth opencode tier. @@ -62,7 +62,7 @@ const OPENCODE_FREE_MODELS = new Set([ * Models on opencode-go that support effort-tier aliases. Each entry maps the * canonical base id to the set of effort suffixes the upstream supports. * - * - deepseek-v4-pro: all four tiers (low/medium/high/max) + * - DeepSeek V4 Pro and Flash: none/low/high/max * - glm-5.2: high/max only (Z.AI maps these through the reasoning plane; * low/medium are not supported on the OpenAI transport) * - mimo-v2.5: high/max only (same reasoning; Xiaomi MiMo does not document @@ -70,12 +70,12 @@ const OPENCODE_FREE_MODELS = new Set([ * - #8353 OpenCode Go registry effort variants (exact suffix sets from * `opencode models opencode-go --verbose`; MiniMax M3 excluded — different * thinking-mode mapping): - * deepseek-v4-flash high/max; grok-4.5 low/medium/high; hy3 none/low/high; - * kimi-k3 max; qwen3.6-plus / qwen3.7-max / qwen3.7-plus high/max + * grok-4.5 low/medium/high; hy3 none/low/high; kimi-k3 max; + * qwen3.6-plus / qwen3.7-max / qwen3.7-plus high/max */ const EFFORT_TIERS: Record = { "deepseek-v4-pro": EFFORT_LEVELS, - "deepseek-v4-flash": ["high", "max"], + "deepseek-v4-flash": EFFORT_LEVELS, "glm-5.2": ["high", "max"], "mimo-v2.5": ["high", "max"], "grok-4.5": ["low", "medium", "high"], diff --git a/tests/unit/base-executor-sanitize-effort.test.ts b/tests/unit/base-executor-sanitize-effort.test.ts index ba7ce28955..282b908246 100644 --- a/tests/unit/base-executor-sanitize-effort.test.ts +++ b/tests/unit/base-executor-sanitize-effort.test.ts @@ -676,11 +676,9 @@ test("sanitizeReasoningEffortForProvider: NVIDIA GLM-5.2 mapping is narrowly sco }); // ── Native DeepSeek (api.deepseek.com) ─────────────────────────────────────── -// DeepSeek V4 thinking mode accepts reasoning_effort ONLY as {high, max}. The -// internal OmniRoute scale (low|medium|high|xhigh, xhigh = top) must be mapped -// onto DeepSeek's native vocabulary so the client's requested effort is honored -// instead of silently dropped to the default. This is the INVERSE of the -// OpenRouter-DeepSeek path, whose normalized API expects xhigh, not max. +// DeepSeek V4 thinking mode accepts reasoning_effort as {low, high, max}. +// The internal OmniRoute scale maps medium → high and xhigh → max so the client's +// requested effort is honored instead of silently dropped to the default. test("sanitizeReasoningEffortForProvider: native deepseek maps xhigh → max", () => { const log = makeLog(); @@ -716,19 +714,25 @@ test("sanitizeReasoningEffortForProvider: native deepseek preserves max", () => assert.equal(log.messages.length, 0); }); -test("sanitizeReasoningEffortForProvider: native deepseek clamps low → high", () => { +test("sanitizeReasoningEffortForProvider: native deepseek preserves low", () => { const body = { model: "deepseek-v4-pro", reasoning_effort: "low", messages: [{ role: "user", content: "hi" }], }; const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null); + assert.equal(result, body, "low is already valid — passes through unchanged"); +}); + +test("sanitizeReasoningEffortForProvider: native non-V4 deepseek clamps low → high", () => { + const body = { + model: "deepseek-chat", + reasoning_effort: "low", + messages: [{ role: "user", content: "hi" }], + }; + const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-chat", null); assert.notEqual(result, body, "must return a new object when mutating"); - assert.equal( - (result as Record).reasoning_effort, - "high", - "below the {high, max} floor → high" - ); + assert.equal((result as Record).reasoning_effort, "high"); }); test("sanitizeReasoningEffortForProvider: native deepseek clamps medium → high", () => { @@ -786,11 +790,10 @@ test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek still preserves xh assert.equal((result as Record).reasoning_effort, "xhigh"); }); -// ── opencode-go DeepSeek V4 Pro effort variants (#4647) ────────────────────── -// opencode-go proxies DeepSeek with the native DeepSeek API contract, which -// accepts {high, max} literally. The OpencodeExecutor's transformRequest sets -// reasoning_effort to the variant suffix (low|medium|high|max), and the -// sanitizer must NOT rewrite `max` → `xhigh` for this provider+model combo. +// ── opencode-go DeepSeek V4 effort variants (#4647) ────────────────────────── +// opencode-go proxies DeepSeek with the native DeepSeek API contract. Both V4 +// models advertise none/low/high/max, and the sanitizer must preserve those +// literal values rather than rewriting `max` to `xhigh`. test("sanitizeReasoningEffortForProvider: opencode-go DeepSeek V4 Pro preserves max", () => { const body = { @@ -803,24 +806,26 @@ test("sanitizeReasoningEffortForProvider: opencode-go DeepSeek V4 Pro preserves assert.equal((result as Record).reasoning_effort, "max"); }); -test("sanitizeReasoningEffortForProvider: opencode-go DeepSeek V4 Pro preserves variant suffix levels", () => { - for (const level of ["low", "medium", "high", "max"]) { - const body = { - model: `deepseek-v4-pro-${level}`, - reasoning_effort: level, - messages: [], - }; - const result = sanitizeReasoningEffortForProvider( - body, - "opencode-go", - `deepseek-v4-pro-${level}`, - null - ); - assert.equal( - (result as Record).reasoning_effort, - level, - `opencode-go deepseek-v4-pro-${level} preserves reasoning_effort=${level}` - ); +test("sanitizeReasoningEffortForProvider: opencode-go preserves both V4 models' tiers", () => { + for (const model of ["deepseek-v4-pro", "deepseek-v4-flash"]) { + for (const level of ["none", "low", "high", "max"]) { + const body = { + model: `${model}-${level}`, + reasoning_effort: level, + messages: [], + }; + const result = sanitizeReasoningEffortForProvider( + body, + "opencode-go", + `${model}-${level}`, + null + ); + assert.equal( + (result as Record).reasoning_effort, + level, + `opencode-go ${model}-${level} preserves reasoning_effort=${level}` + ); + } } }); diff --git a/tests/unit/combo-builder-effort-variants-8072.test.ts b/tests/unit/combo-builder-effort-variants-8072.test.ts index 7333071c3c..86db44fdc0 100644 --- a/tests/unit/combo-builder-effort-variants-8072.test.ts +++ b/tests/unit/combo-builder-effort-variants-8072.test.ts @@ -141,6 +141,7 @@ test("#9485 static DeepSeek effort aliases appear when synced rows omit supporte `${flashId}-high`, `${flashId}-max`, `${proId}-none`, + `${proId}-low`, `${proId}-high`, `${proId}-max`, ]); @@ -150,10 +151,6 @@ test("#9485 static DeepSeek effort aliases appear when synced rows omit supporte .filter((id) => id.startsWith(`${flashId}-`) || id.startsWith(`${proId}-`)) ); assert.deepEqual(deepSeekAliases, expectedAliases); - assert.equal( - provider!.models.some((model) => model.id === `${proId}-low`), - false - ); assert.equal( provider!.models.some((model) => model.id === `${proId}-medium`), false diff --git a/tests/unit/deepseek-thinking-efforts.test.ts b/tests/unit/deepseek-thinking-efforts.test.ts index e1ae8eb146..a47dc88c76 100644 --- a/tests/unit/deepseek-thinking-efforts.test.ts +++ b/tests/unit/deepseek-thinking-efforts.test.ts @@ -29,20 +29,21 @@ test.after(() => { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); -test("DeepSeek registry declares the documented per-model thinking efforts", () => { - const models = new Map((REGISTRY.deepseek?.models || []).map((model) => [model.id, model])); - - assert.deepEqual(models.get("deepseek-v4-flash")?.supportedThinkingEfforts, [ - "none", - "low", - "high", - "max", - ]); - assert.deepEqual(models.get("deepseek-v4-pro")?.supportedThinkingEfforts, [ - "none", - "high", - "max", - ]); +test("DeepSeek registries declare none/low/high/max on both V4 models", () => { + const expectedEfforts = ["none", "low", "high", "max"]; + for (const providerId of ["deepseek", "opencode-go"]) { + const models = new Map((REGISTRY[providerId]?.models || []).map((model) => [model.id, model])); + for (const modelId of ["deepseek-v4-flash", "deepseek-v4-pro"]) { + assert.deepEqual(models.get(modelId)?.supportedThinkingEfforts, expectedEfforts); + for (const effort of expectedEfforts) { + assert.equal( + models.has(`${modelId}-${effort}`), + false, + `${providerId} should derive ${modelId}-${effort} from the base model metadata` + ); + } + } + } }); test("DeepSeek catalog exposes only the declared effort aliases", async () => { @@ -66,13 +67,38 @@ test("DeepSeek catalog exposes only the declared effort aliases", async () => { assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-flash-high"))); assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-flash-max"))); assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-none"))); + assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-low"))); assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-high"))); assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-max"))); - assert.equal( - [...ids].some((id) => id.endsWith("deepseek-v4-pro-low")), - false, - "Pro does not advertise low" +}); + +test("OpenCode Go catalog derives the declared V4 effort aliases from base models", async () => { + await providersDb.createProviderConnection({ + provider: "opencode-go", + authType: "apikey", + name: "opencode-go-deepseek-efforts", + apiKey: "opencode-go-test-key", + isActive: true, + testStatus: "active", + }); + + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models") ); + const body = (await response.json()) as { + data: Array<{ id: string; capabilities?: { effort_tiers?: string[] } }>; + }; + const models = new Map(body.data.map((model) => [model.id, model])); + const expectedEfforts = ["none", "low", "high", "max"]; + + for (const modelId of ["deepseek-v4-flash", "deepseek-v4-pro"]) { + const baseId = `opencode-go/${modelId}`; + assert.deepEqual(models.get(baseId)?.capabilities?.effort_tiers, expectedEfforts); + for (const effort of expectedEfforts) { + assert.ok(models.has(`${baseId}-${effort}`), `${baseId}-${effort} must be advertised`); + } + assert.equal(models.has(`${baseId}-medium`), false); + } }); test("Crof synced reasoning metadata exposes exactly none/low/medium/high/max aliases", async () => { const connection = await providersDb.createProviderConnection({ @@ -181,25 +207,31 @@ test("hardcoded DeepSeek effort suffixes resolve through the static registry", a assert.equal(flashNone.model, "deepseek-v4-flash"); assert.equal(flashNone.resolvedThinkingEffort, "none"); - const unsupportedProLow = await getModelInfo("ds/deepseek-v4-pro-low"); - assert.equal(unsupportedProLow.model, "deepseek-v4-pro-low"); - assert.equal(unsupportedProLow.resolvedThinkingEffort, undefined); + const proLow = await getModelInfo("ds/deepseek-v4-pro-low"); + assert.equal(proLow.model, "deepseek-v4-pro"); + assert.equal(proLow.resolvedThinkingEffort, "low"); }); -test("native DeepSeek preserves Flash low while clamping unsupported Pro low", () => { - const flash = sanitizeReasoningEffortForProvider( - { model: "deepseek-v4-flash", reasoning_effort: "low" }, - "deepseek", - "deepseek-v4-flash" - ) as Record; - assert.equal(flash.reasoning_effort, "low"); +test("OpenCode Go V4 suffixes resolve from base-model effort metadata", async () => { + for (const modelId of ["deepseek-v4-flash", "deepseek-v4-pro"]) { + for (const effort of ["none", "low", "high", "max"]) { + const info = await getModelInfo(`opencode-go/${modelId}-${effort}`); + assert.equal(info.provider, "opencode-go"); + assert.equal(info.model, modelId); + assert.equal(info.resolvedThinkingEffort, effort); + } + } +}); - const pro = sanitizeReasoningEffortForProvider( - { model: "deepseek-v4-pro", reasoning_effort: "low" }, - "deepseek", - "deepseek-v4-pro" - ) as Record; - assert.equal(pro.reasoning_effort, "high"); +test("native DeepSeek preserves the documented low effort for Flash and Pro", () => { + for (const model of ["deepseek-v4-flash", "deepseek-v4-pro"]) { + const body = { model, reasoning_effort: "low" }; + assert.equal( + sanitizeReasoningEffortForProvider(body, "deepseek", model), + body, + `${model} must pass low through unchanged` + ); + } }); test("non-DeepSeek static reasoning models do not advertise unresolvable effort aliases", async () => { @@ -266,9 +298,9 @@ test("custom model named deepseek-v4-flash-low is not rewritten by registry suff assert.equal(info.resolvedThinkingEffort, undefined); }); -test("none effort resolves and passes through the native DeepSeek sanitizer unchanged", async () => { - // The -none suffix resolves to base + effort "none", which reaches the native - // DeepSeek endpoint as reasoning_effort: "none" unchanged (#9485 review #8). +test("none effort resolves and stays explicit through provider sanitation", async () => { + // The format translator subsequently carries this as reasoning.effort:"none" + // on DeepSeek's default Responses route, which disables thinking. const flashNone = await getModelInfo("ds/deepseek-v4-flash-none"); assert.equal(flashNone.model, "deepseek-v4-flash"); assert.equal(flashNone.resolvedThinkingEffort, "none"); @@ -281,9 +313,9 @@ test("none effort resolves and passes through the native DeepSeek sanitizer unch assert.equal(sanitized.reasoning_effort, "none"); }); -test("isFlash check is robust to suffixed model ids", () => { - // A suffixed id like deepseek-v4-flash-low must still be recognized as Flash - // so its low effort is preserved, not clamped to high (#9485 review #5). +test("suffixed Flash low remains valid before alias resolution", () => { + // Preserve low even if a future route sanitizes the raw suffixed id before + // resolving it to the registered base model. const sanitizedSuffixed = sanitizeReasoningEffortForProvider( { model: "deepseek-v4-flash-low", reasoning_effort: "low" }, "deepseek", diff --git a/tests/unit/models-catalog-combo-metadata.test.ts b/tests/unit/models-catalog-combo-metadata.test.ts index 017faca297..6b2b23ee24 100644 --- a/tests/unit/models-catalog-combo-metadata.test.ts +++ b/tests/unit/models-catalog-combo-metadata.test.ts @@ -155,3 +155,46 @@ test("single-target combo reflects unblocked Antigravity Gemini reasoning", asyn assert.equal(capabilities.supportsThinking, true); assert.equal(Object.hasOwn(capabilities, "effort_tiers"), true); }); + +test("mixed DeepSeek combos advertise the efforts accepted by every V4 target", async () => { + await providersDb.createProviderConnection({ + provider: "deepseek", + authType: "apikey", + name: "deepseek-v4-combos", + apiKey: "deepseek-test-key", + isActive: true, + testStatus: "active", + }); + await providersDb.createProviderConnection({ + provider: "opencode-go", + authType: "apikey", + name: "opencode-go-deepseek-v4-combos", + apiKey: "opencode-go-test-key", + isActive: true, + testStatus: "active", + }); + for (const modelId of ["deepseek-v4-flash", "deepseek-v4-pro"]) { + await combosDb.createCombo({ + name: `${modelId}-combo`, + strategy: "auto", + models: [`deepseek/${modelId}`, `opencode-go/${modelId}`], + }); + } + + const response = await catalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models") + ); + const body = (await response.json()) as { data: Array> }; + + assert.equal(response.status, 200); + for (const modelId of ["deepseek-v4-flash", "deepseek-v4-pro"]) { + const combo = body.data.find((item) => item.id === `${modelId}-combo`); + assert.ok(combo); + assert.deepEqual((combo.capabilities as Record).effort_tiers, [ + "none", + "low", + "high", + "max", + ]); + } +}); diff --git a/tests/unit/opencode-executor.test.ts b/tests/unit/opencode-executor.test.ts index 716c0f3d91..e8713a9300 100644 --- a/tests/unit/opencode-executor.test.ts +++ b/tests/unit/opencode-executor.test.ts @@ -78,19 +78,25 @@ describe("OpencodeExecutor", () => { assert.equal(model.supportsReasoning, true); }); - it("exposes DeepSeek V4 Pro effort variants on opencode-go only", () => { + it("declares V4 effort tiers on OpenCode Go base models only", () => { const goModels = PROVIDER_MODELS["opencode-go"] || []; const zenModels = PROVIDER_MODELS["opencode-zen"] || []; - const variants = ["low", "medium", "high", "max"].map((level) => `deepseek-v4-pro-${level}`); - for (const variant of variants) { - const model = goModels.find((m) => m.id === variant); - assert.ok(model, `${variant} should be in opencode-go model list`); - assert.equal(model?.supportsReasoning, true); - assert.equal( - zenModels.some((m) => m.id === variant), - false, - `${variant} should not be exposed on opencode-zen` - ); + const efforts = ["none", "low", "high", "max"]; + + for (const modelId of ["deepseek-v4-pro", "deepseek-v4-flash"]) { + const base = goModels.find((model) => model.id === modelId); + assert.deepEqual(base?.supportedThinkingEfforts, efforts); + for (const effort of efforts) { + const variant = `${modelId}-${effort}`; + assert.equal( + goModels.some((model) => model.id === variant), + false + ); + assert.equal( + zenModels.some((model) => model.id === variant), + false + ); + } } }); @@ -137,6 +143,16 @@ describe("OpencodeExecutor", () => { assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/responses"); }); + it("routes OpenCode Go DeepSeek V4 models to the responses endpoint", async () => { + const models = ["deepseek-v4-pro", "deepseek-v4-flash"]; + + for (const [index, model] of models.entries()) { + const result = await goExecutor.execute(createInput(model)); + assert.equal(result.url, "https://opencode.ai/zen/go/v1/responses"); + assert.equal(fetchCalls[index].url, "https://opencode.ai/zen/go/v1/responses"); + } + }); + it("routes gemini streaming requests to streamGenerateContent", async () => { registerModel("opencode-zen", { id: "gemini-2.5-pro", @@ -544,7 +560,7 @@ describe("OpencodeExecutor", () => { }); }); - describe("DeepSeek V4 Pro reasoning-effort variants", () => { + describe("DeepSeek V4 reasoning-effort variants", () => { function baseBody(model) { return { model, @@ -554,17 +570,19 @@ describe("OpencodeExecutor", () => { }; } - const levels = ["low", "medium", "high", "max"]; - for (const level of levels) { - it(`maps deepseek-v4-pro-${level} to base id + reasoning_effort=${level}`, () => { - const variant = `deepseek-v4-pro-${level}`; - const out = goExecutor.transformRequest(variant, baseBody(variant), false, { - apiKey: "test-key", + const levels = ["none", "low", "high", "max"]; + for (const model of ["deepseek-v4-pro", "deepseek-v4-flash"]) { + for (const level of levels) { + it(`maps ${model}-${level} to base id + reasoning_effort=${level}`, () => { + const variant = `${model}-${level}`; + const out = goExecutor.transformRequest(variant, baseBody(variant), false, { + apiKey: "test-key", + }); + assert.equal(out.model, model); + assert.equal(out.reasoning_effort, level); + assert.ok(!String(out.model).endsWith(`-${level}`)); }); - assert.equal(out.model, "deepseek-v4-pro"); - assert.equal(out.reasoning_effort, level); - assert.ok(!String(out.model).endsWith(`-${level}`)); - }); + } } it("preserves explicit reasoning_effort over the variant suffix", () => { diff --git a/tests/unit/opencode-go-effort-aliases-6922.test.ts b/tests/unit/opencode-go-effort-aliases-6922.test.ts index 67831a7ab4..b599534a85 100644 --- a/tests/unit/opencode-go-effort-aliases-6922.test.ts +++ b/tests/unit/opencode-go-effort-aliases-6922.test.ts @@ -19,41 +19,29 @@ import assert from "node:assert/strict"; // node:test process without triggering side effects (DB init, fetch, // etc.). We only need parseEffortLevel, which is a pure function. ─── -const { parseEffortLevel, OpencodeExecutor } = (await import( - "../../open-sse/executors/opencode.ts" -)) as { - parseEffortLevel: (model: string) => { baseModel: string; effort: string } | null; - OpencodeExecutor: new (provider: string) => { - transformRequest: ( - model: string, - body: Record, - stream: boolean, - credentials: unknown - ) => Record; +const { parseEffortLevel, OpencodeExecutor } = + (await import("../../open-sse/executors/opencode.ts")) as { + parseEffortLevel: (model: string) => { baseModel: string; effort: string } | null; + OpencodeExecutor: new (provider: string) => { + transformRequest: ( + model: string, + body: Record, + stream: boolean, + credentials: unknown + ) => Record; + }; }; -}; -// ─── DeepSeek v4-pro: all 4 tiers ───────────────────────────────────────── +// ─── DeepSeek V4 Pro: none/low/high/max ─────────────────────────────────── -test("#6922 parseEffortLevel: deepseek-v4-pro-low → low", () => { - const result = parseEffortLevel("deepseek-v4-pro-low"); - assert.deepEqual(result, { baseModel: "deepseek-v4-pro", effort: "low" }); -}); - -test("#6922 parseEffortLevel: deepseek-v4-pro-medium → medium", () => { - const result = parseEffortLevel("deepseek-v4-pro-medium"); - assert.deepEqual(result, { baseModel: "deepseek-v4-pro", effort: "medium" }); -}); - -test("#6922 parseEffortLevel: deepseek-v4-pro-high → high", () => { - const result = parseEffortLevel("deepseek-v4-pro-high"); - assert.deepEqual(result, { baseModel: "deepseek-v4-pro", effort: "high" }); -}); - -test("#6922 parseEffortLevel: deepseek-v4-pro-max → max", () => { - const result = parseEffortLevel("deepseek-v4-pro-max"); - assert.deepEqual(result, { baseModel: "deepseek-v4-pro", effort: "max" }); -}); +for (const effort of ["none", "low", "high", "max"]) { + test(`#6922 parseEffortLevel: deepseek-v4-pro-${effort} → ${effort}`, () => { + assert.deepEqual(parseEffortLevel(`deepseek-v4-pro-${effort}`), { + baseModel: "deepseek-v4-pro", + effort, + }); + }); +} // ─── GLM-5.2: high + max only ──────────────────────────────────────────── @@ -81,6 +69,10 @@ test("#6922 parseEffortLevel: mimo-v2.5-max → max", () => { // ─── Negative cases ──────────────────────────────────────────────────────── +test("#6922 parseEffortLevel: deepseek-v4-pro-medium → null (unsupported tier)", () => { + assert.strictEqual(parseEffortLevel("deepseek-v4-pro-medium"), null); +}); + test("#6922 parseEffortLevel: unknown model → null", () => { const result = parseEffortLevel("nonexistent-model-high"); assert.strictEqual(result, null); @@ -160,5 +152,9 @@ test("#6922 transformRequest: unaliased model passes through unchanged", () => { const out = executor.transformRequest("gpt-4o-mini", body, true, CREDENTIALS); assert.equal(out.model, "gpt-4o-mini", "unaliased model id is left untouched"); - assert.equal(out.reasoning_effort, undefined, "no reasoning_effort is injected for a non-tier model"); + assert.equal( + out.reasoning_effort, + undefined, + "no reasoning_effort is injected for a non-tier model" + ); }); diff --git a/tests/unit/opencode-go-effort-aliases-8353.test.ts b/tests/unit/opencode-go-effort-aliases-8353.test.ts index ee248b504a..2d6bc632ae 100644 --- a/tests/unit/opencode-go-effort-aliases-8353.test.ts +++ b/tests/unit/opencode-go-effort-aliases-8353.test.ts @@ -1,9 +1,8 @@ /** * Issue #8353 — Missing OpenCode Go reasoning variants. * - * OpenCode's local Go registry exposes effort-tier aliases that OmniRoute did - * not register or resolve. These tests cover: - * 1. Catalog exposure on opencode-go (and absence on opencode-zen) + * These tests cover: + * 1. Static registry metadata for aliases that have not migrated to derived variants * 2. parseEffortLevel → base + effort for every listed alias * 3. transformRequest rewrite + reasoning_effort injection * 4. MiniMax M3 stays out of the effort-alias path @@ -12,19 +11,18 @@ import test from "node:test"; import assert from "node:assert/strict"; -const { parseEffortLevel, OpencodeExecutor } = (await import( - "../../open-sse/executors/opencode.ts" -)) as { - parseEffortLevel: (model: string) => { baseModel: string; effort: string } | null; - OpencodeExecutor: new (provider: string) => { - transformRequest: ( - model: string, - body: Record, - stream: boolean, - credentials: unknown - ) => Record; +const { parseEffortLevel, OpencodeExecutor } = + (await import("../../open-sse/executors/opencode.ts")) as { + parseEffortLevel: (model: string) => { baseModel: string; effort: string } | null; + OpencodeExecutor: new (provider: string) => { + transformRequest: ( + model: string, + body: Record, + stream: boolean, + credentials: unknown + ) => Record; + }; }; -}; const { REGISTRY } = (await import("../../open-sse/config/providerRegistry.ts")) as { REGISTRY: Record< @@ -33,8 +31,10 @@ const { REGISTRY } = (await import("../../open-sse/config/providerRegistry.ts")) >; }; -/** Exact alias set from #8353 (MiniMax M3 intentionally excluded). */ +/** Exact alias set from #8353, plus the newly declared DeepSeek tiers. */ const ISSUE_ALIASES: ReadonlyArray<{ alias: string; base: string; effort: string }> = [ + { alias: "deepseek-v4-flash-none", base: "deepseek-v4-flash", effort: "none" }, + { alias: "deepseek-v4-flash-low", base: "deepseek-v4-flash", effort: "low" }, { alias: "deepseek-v4-flash-high", base: "deepseek-v4-flash", effort: "high" }, { alias: "deepseek-v4-flash-max", base: "deepseek-v4-flash", effort: "max" }, { alias: "grok-4.5-low", base: "grok-4.5", effort: "low" }, @@ -68,10 +68,10 @@ function zenModelIds(): string[] { // ─── Catalog exposure ────────────────────────────────────────────────────── -test("#8353 catalog: every listed alias is registered on opencode-go", () => { +test("#8353 catalog: DeepSeek aliases are derived instead of registered as duplicate rows", () => { const ids = new Set(goModelIds()); - for (const { alias } of ISSUE_ALIASES) { - assert.ok(ids.has(alias), `opencode-go must expose ${alias}`); + for (const { alias, base } of ISSUE_ALIASES) { + assert.equal(ids.has(alias), base !== "deepseek-v4-flash", alias); } }); @@ -128,7 +128,7 @@ for (const { alias, base, effort } of ISSUE_ALIASES) { } test("#8353 parseEffortLevel: unsupported tiers stay null", () => { - assert.equal(parseEffortLevel("deepseek-v4-flash-low"), null); + assert.equal(parseEffortLevel("deepseek-v4-flash-medium"), null); assert.equal(parseEffortLevel("grok-4.5-max"), null); assert.equal(parseEffortLevel("hy3-max"), null); assert.equal(parseEffortLevel("kimi-k3-high"), null); @@ -155,7 +155,7 @@ test("#8353 parseEffortLevel: MiniMax M3 has no effort-tier aliases", () => { const CREDENTIALS = { apiKey: "k" } as Record; const TRANSFORM_SAMPLES = [ - { alias: "deepseek-v4-flash-high", base: "deepseek-v4-flash", effort: "high" }, + { 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" },