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
10 changes: 7 additions & 3 deletions open-sse/config/providerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1292,9 +1292,13 @@ export const REGISTRY: Record<string, RegistryEntry> = {
{ id: "mimo-v2-omni", name: "MiMo-V2-Omni" },
{ id: "minimax-m2.7", name: "MiniMax M2.7", targetFormat: "claude" },
{ id: "minimax-m2.5", name: "MiniMax M2.5", targetFormat: "claude" },
{ id: "qwen3.7-max", name: "Qwen3.7 Max" },
{ 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.7-max", name: "Qwen3.7 Max", targetFormat: "claude" },
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude" },
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude" },
{ id: "hy3-preview", name: "Hunyuan3 Preview" },
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true },
Expand Down
26 changes: 19 additions & 7 deletions open-sse/translator/helpers/toolCallHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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: "",
})),
});
}
}
Expand Down
20 changes: 12 additions & 8 deletions tests/unit/opencode-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand All @@ -231,14 +229,20 @@ 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");
});

// qwen3.6-plus
const qwen36 = await goExecutor.execute(createInput("qwen3.6-plus"));
assert.equal(qwen36.url, "https://opencode.ai/zen/go/v1/chat/completions");
it("routes opencode-go qwen models to claude messages endpoint", async () => {
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 () => {
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/translator-helper-branches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down