From 391bae123716504205b3ff968164647a1580ef39 Mon Sep 17 00:00:00 2001 From: justjxke Date: Wed, 5 Aug 2026 17:34:00 +0100 Subject: [PATCH 1/2] fix(providers): replay DeepSeek reasoning for opencode-zen (#994) --- src/providers/registry.ts | 17 +++- tests/opencode-zen-deepseek-reasoning.test.ts | 81 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/opencode-zen-deepseek-reasoning.test.ts diff --git a/src/providers/registry.ts b/src/providers/registry.ts index e8fc10dc9..2ff281e1b 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1649,7 +1649,22 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ autoToolChoiceOnlyModels: KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS, preserveReasoningContentModels: KIMI_THINKING_MODELS, }, - { id: "opencode-zen", label: "opencode zen", baseUrl: "https://opencode.ai/zen/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://opencode.ai/auth" }, + { + id: "opencode-zen", label: "opencode zen", baseUrl: "https://opencode.ai/zen/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://opencode.ai/auth", + // Same opencode.ai/zen/v1 gateway as `opencode-free` (keyed tier): DeepSeek thinking mode + // requires the assistant's original reasoning_content to be replayed on tool-call + // continuations, or the gateway answers HTTP 400 (issues #950/#994). Mirror the DeepSeek + // reasoning + thinking metadata so `opencode-zen/deepseek-v4-flash-free` — and the other + // Zen DeepSeek thinking models — never serialize a bare tool-call turn. + modelReasoningEfforts: Object.fromEntries( + [...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS].map(id => [id, DEEPSEEK_THINKING_EFFORTS]), + ), + modelReasoningEffortMap: Object.fromEntries( + [...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS].map(id => [id, DEEPSEEK_THINKING_REASONING_MAP]), + ), + preserveReasoningContentModels: [...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS], + noVisionModels: [...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS], + }, { id: "vercel-ai-gateway", label: "Vercel AI Gateway", baseUrl: "https://ai-gateway.vercel.sh/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://vercel.com/dashboard" }, { id: "opencode-free", diff --git a/tests/opencode-zen-deepseek-reasoning.test.ts b/tests/opencode-zen-deepseek-reasoning.test.ts new file mode 100644 index 000000000..60b3ca360 --- /dev/null +++ b/tests/opencode-zen-deepseek-reasoning.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "bun:test"; +import { createOpenAIChatAdapter } from "../src/adapters/openai-chat"; +import { routeModel } from "../src/router"; +import type { OcxConfig } from "../src/types"; + +function configFor(modelId: string): OcxConfig { + return { + port: 10110, + defaultProvider: "opencode-zen", + providers: { + "opencode-zen": { + adapter: "openai-chat", + baseUrl: "https://opencode.ai/zen/v1", + apiKey: "key", + models: [modelId], + }, + }, + }; +} + +function buildToolCallBody(modelId: string, reasoning?: string): { + reasoning_effort?: string; + messages: Record[]; +} { + const route = routeModel(configFor(modelId), `opencode-zen/${modelId}`); + const req = createOpenAIChatAdapter(route.provider).buildRequest({ + modelId: route.modelId, + context: { + messages: [ + { role: "user", content: "inspect the repo", timestamp: 0 }, + { role: "assistant", timestamp: 1, content: [ + { type: "thinking", thinking: "I need to inspect files before answering." }, + { type: "toolCall", id: "call_1", name: "read_file", arguments: { path: "README.md" } }, + ] }, + { + role: "toolResult", + toolCallId: "call_1", + toolName: "read_file", + content: "contents", + isError: false, + timestamp: 2, + }, + ], + }, + stream: true, + options: { reasoning: reasoning ?? "max" }, + }); + + return JSON.parse(req.body as string) as { + reasoning_effort?: string; + messages: Record[]; + }; +} + +describe("opencode-zen DeepSeek thinking mode", () => { + test.each(["deepseek-v4-flash-free", "deepseek-v4-flash", "deepseek-v4-pro"])( + "%s replays tool-call reasoning_content and maps Codex efforts (issue #950/#994)", + modelId => { + const body = buildToolCallBody(modelId, "xhigh"); + + expect(body.reasoning_effort).toBe("max"); + expect(body.messages[1].reasoning_content).toBe("I need to inspect files before answering."); + expect(body.messages[1]).toMatchObject({ + role: "assistant", + content: "", + tool_calls: [{ + id: "call_1", + type: "function", + function: { name: "read_file", arguments: JSON.stringify({ path: "README.md" }) }, + }], + }); + }, + ); + + test("non-DeepSeek opencode-zen models do not replay reasoning_content", () => { + const body = buildToolCallBody("minimax-m2.7"); + + expect(body.messages[1].reasoning_content).toBeUndefined(); + expect(body.messages[1]).toHaveProperty("tool_calls"); + }); +}); \ No newline at end of file From f3770886a7a278bc742a044762f71ba29e94f6b5 Mon Sep 17 00:00:00 2001 From: justjxke Date: Wed, 5 Aug 2026 17:52:45 +0100 Subject: [PATCH 2/2] test(providers): pin opencode-zen noVisionModels for DeepSeek models --- tests/opencode-zen-deepseek-reasoning.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/opencode-zen-deepseek-reasoning.test.ts b/tests/opencode-zen-deepseek-reasoning.test.ts index 60b3ca360..2096058be 100644 --- a/tests/opencode-zen-deepseek-reasoning.test.ts +++ b/tests/opencode-zen-deepseek-reasoning.test.ts @@ -78,4 +78,19 @@ describe("opencode-zen DeepSeek thinking mode", () => { expect(body.messages[1].reasoning_content).toBeUndefined(); expect(body.messages[1]).toHaveProperty("tool_calls"); }); + + test.each(["deepseek-v4-flash-free", "deepseek-v4-flash", "deepseek-v4-pro"])( + "%s is listed in opencode-zen noVisionModels for the vision sidecar", + modelId => { + const route = routeModel(configFor(modelId), `opencode-zen/${modelId}`); + + expect(route.provider.noVisionModels).toContain(modelId); + }, + ); + + test("non-DeepSeek opencode-zen models stay out of noVisionModels", () => { + const route = routeModel(configFor("minimax-m2.7"), "opencode-zen/minimax-m2.7"); + + expect(route.provider.noVisionModels).not.toContain("minimax-m2.7"); + }); }); \ No newline at end of file