Skip to content
Open
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
17 changes: 16 additions & 1 deletion src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
96 changes: 96 additions & 0 deletions tests/opencode-zen-deepseek-reasoning.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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<string, unknown>[];
} {
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<string, unknown>[];
};
}

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");
});
Comment thread
justjxke marked this conversation as resolved.

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");
});
});
Loading