Skip to content
Merged
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
23 changes: 14 additions & 9 deletions src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1901,15 +1901,20 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
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",
// #1043: without this the proxy forwards image parts to text-only Zen models and
// the upstream rejects the whole request with a 400.
noVisionModels: OPENCODE_ZEN_TEXT_ONLY_MODELS,
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, deepseekThinkingEffortsFor(id)]),
),
modelReasoningEffortMap: Object.fromEntries(
[...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS].map(id => [id, deepseekReasoningMapFor(id)]),
Comment on lines +1911 to +1914

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add opencode-zen to the DeepSeek parity matrix

These new opencode-zen calls to deepseekThinkingEffortsFor / deepseekReasoningMapFor put three more model ids under the shared Flash-vs-Pro substring classifier, but tests/provider-registry-parity.test.ts’s “every DeepSeek V4 entry…” matrix still omits this provider. That matrix is the existing guard for every registry id that uses the classifier; without the Zen cases, a future Zen-specific id typo/name change could be mis-advertised without failing the parity test, so please add the three opencode-zen models there.

AGENTS.md reference: src/AGENTS.md:L22-L26

Useful? React with 👍 / 👎.

),
preserveReasoningContentModels: [...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS],
noVisionModels: [...OPENCODE_ZEN_TEXT_ONLY_MODELS, ...DEEPSEEK_THINKING_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" },
{
Expand Down
104 changes: 104 additions & 0 deletions tests/opencode-zen-deepseek-reasoning.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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");

const expectedEffort = modelId.includes("flash") ? "high" : "max";
expect(body.reasoning_effort).toBe(expectedEffort);
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("Zen text-only free models (measured #1043) stay in noVisionModels", () => {
const route = routeModel(configFor("big-pickle"), "opencode-zen/big-pickle");

expect(route.provider.noVisionModels).toContain("big-pickle");
});


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");
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
Loading