feat(opencode-go): muse-spark-1.2 and Responses tool fixes (#3820)

- Add muse-spark-1.2-contributor as responses-only model on OpenCode Go
- Normalize object tool schemas without properties in OpenCode Go executor
- Make fallback Responses call_ids unique across same-millisecond calls
- Make Responses output coercion fail-soft for circular and non-stringifiable values
This commit is contained in:
Sina Sadeghi
2026-09-05 22:39:22 +07:00
parent e214fb1c30
commit 11222eff0f
6 changed files with 70 additions and 4 deletions

View File

@@ -27,7 +27,7 @@ describe("OpenCode Go model catalog", () => {
"mimo-v2.5", "mimo-v2.5-pro",
"minimax-m3", "minimax-m2.7", "minimax-m2.5",
"qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus",
"muse-spark-1.3-contributor",
"muse-spark-1.2-contributor", "muse-spark-1.3-contributor",
]);
});
});
@@ -90,6 +90,15 @@ describe("OpenCode Go per-model transport guard (chatCore logic)", () => {
}
});
it("routes Muse Spark (responses-only) to /responses, never to /messages", () => {
for (const m of ["muse-spark-1.2-contributor", "muse-spark-1.3-contributor"]) {
expect(getModelSupportedFormats("opencode-go", m)).toEqual(["openai-responses"]);
expect(pickTransport("opencode-go", "openai-responses", "opencode-go", m)?.baseUrl).toBe("https://opencode.ai/zen/go/v1/responses");
expect(pickTransport("opencode-go", "claude", "opencode-go", m)).toBeNull();
expect(pickTransport("opencode-go", "openai", "opencode-go", m)).toBeNull();
}
});
it("does NOT route MiniMax (no responses support) to /responses", () => {
for (const m of CLAUDE_CAPABLE) {
expect(pickTransport("opencode-go", "openai-responses", "opencode-go", m)).toBeNull();

View File

@@ -105,6 +105,21 @@ describe("OpenCodeGoExecutor routing + sanitization", () => {
expect(JSON.parse(outputs[0].output)).toEqual({ ok: true, text: "héllo \"w\"" });
expect(outputs[1].output).toBe("");
});
it("fills in properties for object tool schemas missing them", () => {
const ex = new OpenCodeGoExecutor();
const body = {
model: MODEL,
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }],
tools: [
{ type: "function", function: { name: "bare", parameters: { type: "object" } } },
{ type: "function", function: { name: "full", parameters: { type: "object", properties: { a: { type: "string" } } } } },
],
};
const out = ex.transformRequest(MODEL, body, true, {});
expect(out.tools.find((t) => t.name === "bare").parameters).toEqual({ type: "object", properties: {} });
expect(out.tools.find((t) => t.name === "full").parameters).toEqual({ type: "object", properties: { a: { type: "string" } } });
});
});
describe("chat/claude clients translate to Responses without breaking tools", () => {

View File

@@ -6,6 +6,7 @@
import { describe, expect, it } from "vitest";
import "../translator/registerAll.js";
import { openaiResponsesToOpenAIResponse } from "../../open-sse/translator/response/openai-responses.js";
import { clampResponsesCallId, coerceResponsesOutput, MAX_RESPONSES_CALL_ID_LEN } from "../../open-sse/translator/formats/responsesApi.js";
import { initState, translateResponse } from "../../open-sse/translator/index.js";
import { FORMATS } from "../../open-sse/translator/formats.js";
@@ -138,3 +139,27 @@ describe("responses → claude end-to-end keeps parallel tool_use blocks separat
]);
});
});
describe("fallback call_ids stay unique within a batch", () => {
it("same-millisecond fallbacks never collide", () => {
const ids = new Set(Array.from({ length: 50 }, () => clampResponsesCallId(undefined)));
expect(ids.size).toBe(50);
for (const id of ids) {
expect(id.startsWith("call_")).toBe(true);
expect(id.length).toBeLessThanOrEqual(MAX_RESPONSES_CALL_ID_LEN);
}
expect(new Set([clampResponsesCallId(""), clampResponsesCallId(null)]).size).toBe(2);
});
});
describe("output coercion stays fail-soft on unstringifiable values", () => {
it("never throws on BigInt/circular array elements", () => {
const circular = {};
circular.self = circular;
const input = [1n, circular, { text: "ok" }];
expect(() => coerceResponsesOutput(input)).not.toThrow();
const out = coerceResponsesOutput(input);
expect(typeof out).toBe("string");
expect(out).toContain("ok");
});
});