From 660e73097aefd5029b652ed083242fb603663491 Mon Sep 17 00:00:00 2001 From: Jefersson Lemes Date: Wed, 27 May 2026 11:28:39 -0300 Subject: [PATCH 1/3] fix(opencode-go): route qwen3.x models through claude messages endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream opencode-go returns 401 "Model qwen3.x-* is not supported for format oa-compat" when qwen3.6-plus / qwen3.5-plus are sent to /chat/completions, matching the behavior already documented for the same model family on opencode-zen (Issue #2292). - Adds `targetFormat: "claude"` to the qwen entries in the opencode-go provider registry so requests route through /messages with the anthropic-version: 2023-06-01 header instead of /chat/completions. - Replaces the qwen rows in the PR #1510 executor test (they registered the models without targetFormat and asserted chat/completions, which is now incorrect). - Adds a new test asserting both qwen ids route to /zen/go/v1/messages and carry the anthropic-version header. Reproduces with `curl https://opencode.ai/zen/go/v1/chat/completions -d '{"model":"qwen3.6-plus", ...}'` (server side error precedes auth). No executor or translator code changed — the existing claude targetFormat handling for minimax-m2.5 / m2.7 on opencode-go already covers this path. --- open-sse/config/providerRegistry.ts | 8 +++++-- tests/unit/opencode-executor.test.ts | 31 +++++++++++++++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 02b40ebf9ad..3470fb5ebfc 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -1290,8 +1290,12 @@ export const REGISTRY: Record = { { id: "mimo-v2.5", name: "MiMo-V2.5" }, { id: "minimax-m2.7", name: "MiniMax M2.7", targetFormat: "claude" }, { id: "minimax-m2.5", name: "MiniMax M2.5", targetFormat: "claude" }, - { id: "qwen3.6-plus", name: "Qwen3.6 Plus" }, - { id: "qwen3.5-plus", name: "Qwen3.5 Plus" }, + // Issue #2292: Qwen models on opencode-go reject oa-compat format + // ("Model qwen3.x-* is not supported for format oa-compat") — same + // upstream behavior already declared for opencode-zen. Route them + // through /messages with the Claude translator. + { id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude" }, + { id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude" }, { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true }, { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true }, ], diff --git a/tests/unit/opencode-executor.test.ts b/tests/unit/opencode-executor.test.ts index 986fc94bbe6..d421b5a8d10 100644 --- a/tests/unit/opencode-executor.test.ts +++ b/tests/unit/opencode-executor.test.ts @@ -213,8 +213,6 @@ describe("OpencodeExecutor", () => { registerModel("opencode-go", { id: "kimi-k2.6", name: "Kimi K2.6" }); registerModel("opencode-go", { id: "mimo-v2-pro", name: "MiMo V2 Pro" }); registerModel("opencode-go", { id: "mimo-v2-omni", name: "MiMo V2 Omni" }); - registerModel("opencode-go", { id: "qwen3.6-plus", name: "Qwen 3.6 Plus" }); - registerModel("opencode-go", { id: "qwen3.5-plus", name: "Qwen 3.5 Plus" }); // glm-5.1 const glm51 = await goExecutor.execute(createInput("glm-5.1")); @@ -231,14 +229,31 @@ describe("OpencodeExecutor", () => { // mimo-v2-omni const mimoOmni = await goExecutor.execute(createInput("mimo-v2-omni")); assert.equal(mimoOmni.url, "https://opencode.ai/zen/go/v1/chat/completions"); + }); + + it("routes opencode-go qwen models to claude messages endpoint", async () => { + registerModel("opencode-go", { + id: "qwen3.6-plus", + name: "Qwen3.6 Plus", + targetFormat: "claude", + }); + registerModel("opencode-go", { + id: "qwen3.5-plus", + name: "Qwen3.5 Plus", + targetFormat: "claude", + }); - // qwen3.6-plus - const qwen36 = await goExecutor.execute(createInput("qwen3.6-plus")); - assert.equal(qwen36.url, "https://opencode.ai/zen/go/v1/chat/completions"); + const qwen36 = await goExecutor.execute( + createInput("qwen3.6-plus", true, { apiKey: "claude-key" }) + ); + assert.equal(qwen36.url, "https://opencode.ai/zen/go/v1/messages"); + assert.equal(qwen36.headers["anthropic-version"], "2023-06-01"); - // qwen3.5-plus - const qwen35 = await goExecutor.execute(createInput("qwen3.5-plus")); - assert.equal(qwen35.url, "https://opencode.ai/zen/go/v1/chat/completions"); + const qwen35 = await goExecutor.execute( + createInput("qwen3.5-plus", true, { apiKey: "claude-key" }) + ); + assert.equal(qwen35.url, "https://opencode.ai/zen/go/v1/messages"); + assert.equal(qwen35.headers["anthropic-version"], "2023-06-01"); }); it("builds bearer auth headers for opencode-go openai models", async () => { From d4d94f58fa36c710ce9d4807dbf2cbf5aea31de0 Mon Sep 17 00:00:00 2001 From: Jefersson Lemes Date: Wed, 27 May 2026 16:11:55 -0300 Subject: [PATCH 2/3] fix(translator): insert Claude tool_result block in fixMissingToolResponses for Claude-shape assistants When an assistant message uses Claude content blocks (tool_use) without a matching OpenAI tool_calls array, insert a role:"user" message with tool_result blocks instead of a role:"tool" message. Hybrid Claude/OpenAI bodies were rejected by strict third-party Claude-shape upstreams (Bailian behind opencode-go/opencode-zen) with 400 "Request body format invalid". --- open-sse/translator/helpers/toolCallHelper.ts | 26 +++++++--- tests/unit/translator-helper-branches.test.ts | 47 +++++++++++++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/open-sse/translator/helpers/toolCallHelper.ts b/open-sse/translator/helpers/toolCallHelper.ts index ea8d21607c8..e9177fa3740 100644 --- a/open-sse/translator/helpers/toolCallHelper.ts +++ b/open-sse/translator/helpers/toolCallHelper.ts @@ -118,7 +118,9 @@ export function hasToolResults(msg, toolCallIds) { return false; } -// Fix missing tool responses - insert empty tool_result if assistant has tool_use but next message has no tool_result +// Fix missing tool responses - insert empty tool_result if assistant has tool_use but next message has no tool_result. +// Inserts in the same shape as the opening assistant message: OpenAI tool_calls → role:"tool"; +// Claude tool_use blocks → role:"user" with tool_result content blocks. export function fixMissingToolResponses(body) { if (!body.messages || !Array.isArray(body.messages)) return body; @@ -136,13 +138,23 @@ export function fixMissingToolResponses(body) { // Check if next message has tool_result if (nextMsg && !hasToolResults(nextMsg, toolCallIds)) { - // Insert tool responses for each tool_call - for (const id of toolCallIds) { - // OpenAI format: role = "tool" + const hasOpenAIToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0; + if (hasOpenAIToolCalls) { + for (const id of toolCallIds) { + newMessages.push({ + role: "tool", + tool_call_id: id, + content: "", + }); + } + } else { newMessages.push({ - role: "tool", - tool_call_id: id, - content: "", + role: "user", + content: toolCallIds.map((id) => ({ + type: "tool_result", + tool_use_id: id, + content: "", + })), }); } } diff --git a/tests/unit/translator-helper-branches.test.ts b/tests/unit/translator-helper-branches.test.ts index 397ee3424b2..d41a913eafe 100644 --- a/tests/unit/translator-helper-branches.test.ts +++ b/tests/unit/translator-helper-branches.test.ts @@ -489,6 +489,53 @@ test("toolCallHelper normalizes ids, links tool responses and inserts missing to assert.deepEqual(toolCallHelper.fixMissingToolResponses({ messages: null }), { messages: null }); }); +test("fixMissingToolResponses inserts Claude tool_result block when assistant uses Claude shape", () => { + const fixed = toolCallHelper.fixMissingToolResponses({ + messages: [ + { role: "user", content: [{ type: "text", text: "do it" }] }, + { + role: "assistant", + content: [ + { type: "tool_use", id: "tool_a", name: "bash", input: { cmd: "ls" } }, + { type: "tool_use", id: "tool_b", name: "bash", input: { cmd: "pwd" } }, + ], + }, + { role: "user", content: [{ type: "text", text: "continue" }] }, + ], + }); + + assert.equal(fixed.messages.length, 4); + const inserted = fixed.messages[2]; + assert.equal(inserted.role, "user"); + assert.ok(Array.isArray(inserted.content)); + assert.equal(inserted.content.length, 2); + assert.equal(inserted.content[0].type, "tool_result"); + assert.equal(inserted.content[0].tool_use_id, "tool_a"); + assert.equal(inserted.content[0].content, ""); + assert.equal(inserted.content[1].tool_use_id, "tool_b"); +}); + +test("fixMissingToolResponses keeps OpenAI role:tool when assistant uses OpenAI tool_calls", () => { + const fixed = toolCallHelper.fixMissingToolResponses({ + messages: [ + { + role: "assistant", + tool_calls: [ + { id: "call_a", type: "function", function: { name: "lookup", arguments: "{}" } }, + { id: "call_b", type: "function", function: { name: "search", arguments: "{}" } }, + ], + }, + { role: "user", content: "no tool result here" }, + ], + }); + + assert.equal(fixed.messages.length, 4); + assert.equal(fixed.messages[1].role, "tool"); + assert.equal(fixed.messages[1].tool_call_id, "call_a"); + assert.equal(fixed.messages[2].role, "tool"); + assert.equal(fixed.messages[2].tool_call_id, "call_b"); +}); + test("translateRequest replays cached DeepSeek reasoning messages without tool calls", () => { clearReasoningCacheAll(); cacheReasoningByKey( From 9d89d23cb8f83663f04f65ed1fc9ccd1313c9c0a Mon Sep 17 00:00:00 2001 From: Jefersson Lemes Date: Wed, 27 May 2026 16:16:10 -0300 Subject: [PATCH 3/3] test(opencode-executor): drop redundant registerModel for qwen3.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qwen3.6-plus and qwen3.5-plus are already declared on opencode-go in providerRegistry.ts (loaded into PROVIDER_MODELS at module init), so the registerModel calls in this test created duplicate entries. Mirror how minimax-m2.7 / minimax-m2.5 are exercised — rely on the registry default and just assert routing behavior. --- tests/unit/opencode-executor.test.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/unit/opencode-executor.test.ts b/tests/unit/opencode-executor.test.ts index d421b5a8d10..081578eb7e8 100644 --- a/tests/unit/opencode-executor.test.ts +++ b/tests/unit/opencode-executor.test.ts @@ -232,17 +232,6 @@ describe("OpencodeExecutor", () => { }); it("routes opencode-go qwen models to claude messages endpoint", async () => { - registerModel("opencode-go", { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - targetFormat: "claude", - }); - registerModel("opencode-go", { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - targetFormat: "claude", - }); - const qwen36 = await goExecutor.execute( createInput("qwen3.6-plus", true, { apiKey: "claude-key" }) );