diff --git a/changelog.d/fixes/13471-opencode-muse-spark-1-3-responses.md b/changelog.d/fixes/13471-opencode-muse-spark-1-3-responses.md new file mode 100644 index 0000000000..14aeff3b5d --- /dev/null +++ b/changelog.d/fixes/13471-opencode-muse-spark-1-3-responses.md @@ -0,0 +1 @@ +- **fix(providers):** Muse Spark 1.3 works on OpenCode Zen, OpenCode and OpenCode Go instead of failing with a 500, and gets its real 1M context window ([#13471](https://github.com/diegosouzapw/OmniRoute/pull/13471)) — thanks @maxmad64bis (with thanks to @bacnh85, @shermzy and @atakhadiviom for #12675, #12973 and #13111) diff --git a/open-sse/config/providers/registry/opencode/go/index.ts b/open-sse/config/providers/registry/opencode/go/index.ts index 2d364bd5fb..dcc428037f 100644 --- a/open-sse/config/providers/registry/opencode/go/index.ts +++ b/open-sse/config/providers/registry/opencode/go/index.ts @@ -226,6 +226,78 @@ export const opencode_goProvider: RegistryEntry = { supportsVideo: true, targetFormat: "openai-responses", }, + // #12674: Muse Spark 1.3 Contributor — base + effort-tier aliases from the + // OpenCode Go registry (`opencode models opencode-go --refresh --verbose`; + // exact suffix set: minimal/low/medium/high/xhigh, no max — same as 1.2). + // Upstream serves Muse Spark only on the Responses API; without + // targetFormat:"openai-responses" these fall through to /chat/completions + // and the upstream returns 500 (same class as #12196). + { + id: "muse-spark-1.3-contributor", + name: "Muse Spark 1.3 Contributor", + contextLength: 1048576, + maxOutputTokens: 131072, + supportsReasoning: true, + supportsVision: true, + supportsAudio: true, + supportsVideo: true, + targetFormat: "openai-responses", + }, + { + id: "muse-spark-1.3-contributor-minimal", + name: "Muse Spark 1.3 Contributor (minimal effort)", + contextLength: 1048576, + maxOutputTokens: 131072, + supportsReasoning: true, + supportsVision: true, + supportsAudio: true, + supportsVideo: true, + targetFormat: "openai-responses", + }, + { + id: "muse-spark-1.3-contributor-low", + name: "Muse Spark 1.3 Contributor (low effort)", + contextLength: 1048576, + maxOutputTokens: 131072, + supportsReasoning: true, + supportsVision: true, + supportsAudio: true, + supportsVideo: true, + targetFormat: "openai-responses", + }, + { + id: "muse-spark-1.3-contributor-medium", + name: "Muse Spark 1.3 Contributor (medium effort)", + contextLength: 1048576, + maxOutputTokens: 131072, + supportsReasoning: true, + supportsVision: true, + supportsAudio: true, + supportsVideo: true, + targetFormat: "openai-responses", + }, + { + id: "muse-spark-1.3-contributor-high", + name: "Muse Spark 1.3 Contributor (high effort)", + contextLength: 1048576, + maxOutputTokens: 131072, + supportsReasoning: true, + supportsVision: true, + supportsAudio: true, + supportsVideo: true, + targetFormat: "openai-responses", + }, + { + id: "muse-spark-1.3-contributor-xhigh", + name: "Muse Spark 1.3 Contributor (xhigh effort)", + contextLength: 1048576, + maxOutputTokens: 131072, + supportsReasoning: true, + supportsVision: true, + supportsAudio: true, + supportsVideo: true, + targetFormat: "openai-responses", + }, // #8353: Grok 4.5 + effort tiers from the OpenCode Go registry. { id: "grok-4.5", diff --git a/open-sse/config/providers/registry/opencode/index.ts b/open-sse/config/providers/registry/opencode/index.ts index e402a6e9cf..b667331289 100644 --- a/open-sse/config/providers/registry/opencode/index.ts +++ b/open-sse/config/providers/registry/opencode/index.ts @@ -50,6 +50,25 @@ export const opencodeProvider: RegistryEntry = { contextLength: 1048576, maxOutputTokens: 131072, }, + // Muse Spark 1.3 is served only on the Responses API, same as 1.2 above. + // Its window matches the published OpenCode catalog instead of the + // 200000 provider default. + { + id: "muse-spark-1.3", + name: "Muse Spark 1.3", + supportsReasoning: true, + targetFormat: "openai-responses", + contextLength: 1048576, + maxOutputTokens: 131072, + }, + { + id: "muse-spark-1.3-contributor-free", + name: "Muse Spark 1.3 Contributor Free", + supportsReasoning: true, + targetFormat: "openai-responses", + contextLength: 1048576, + maxOutputTokens: 131072, + }, { id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash Free", supportsReasoning: true }, // #6998: 2026-07-14 refresh — the upstream free tier rotated its lineup; // minimax-m3-free, minimax-m2.5-free, ling-2.6-1t-free, diff --git a/open-sse/config/providers/registry/opencode/zen/index.ts b/open-sse/config/providers/registry/opencode/zen/index.ts index 75849d65a6..1bb2382aad 100644 --- a/open-sse/config/providers/registry/opencode/zen/index.ts +++ b/open-sse/config/providers/registry/opencode/zen/index.ts @@ -85,6 +85,25 @@ export const opencode_zenProvider: RegistryEntry = { contextLength: 1048576, maxOutputTokens: 131072, }, + // Muse Spark 1.3 is served only on the Responses API, same as 1.2 above. + // Its window matches the published OpenCode catalog instead of the + // 200000 provider default. + { + id: "muse-spark-1.3", + name: "Muse Spark 1.3", + supportsReasoning: true, + targetFormat: "openai-responses", + contextLength: 1048576, + maxOutputTokens: 131072, + }, + { + id: "muse-spark-1.3-contributor-free", + name: "Muse Spark 1.3 Contributor Free", + supportsReasoning: true, + targetFormat: "openai-responses", + contextLength: 1048576, + maxOutputTokens: 131072, + }, // ── DeepSeek ──────────────────────────────────────────────── // #10788: same tier vocabulary as opencode-go's DeepSeek rows — the Zen diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts index 463c8bec65..ce8ea29b60 100644 --- a/open-sse/executors/opencode.ts +++ b/open-sse/executors/opencode.ts @@ -94,6 +94,8 @@ const OPENCODE_FREE_MODELS = new Set([ * grok-4.5 low/medium/high; hy3 none/low/high; kimi-k3 max; * qwen3.6-plus / qwen3.7-max / qwen3.7-plus high/max; * muse-spark-1.2-contributor minimal/low/medium/high/xhigh (no max) + * - #12674 Muse Spark 1.3 Contributor: minimal/low/medium/high/xhigh (no max), + * verified via `opencode models opencode-go --refresh --verbose` */ const EFFORT_TIERS: Record = { "deepseek-v4-pro": EFFORT_LEVELS, @@ -107,6 +109,7 @@ const EFFORT_TIERS: Record = { "qwen3.7-max": ["high", "max"], "qwen3.7-plus": ["high", "max"], "muse-spark-1.2-contributor": ["minimal", "low", "medium", "high", "xhigh"], + "muse-spark-1.3-contributor": ["minimal", "low", "medium", "high", "xhigh"], }; /** diff --git a/tests/unit/opencode-go-muse-spark-1-3-12674.test.ts b/tests/unit/opencode-go-muse-spark-1-3-12674.test.ts new file mode 100644 index 0000000000..bc9f485fb0 --- /dev/null +++ b/tests/unit/opencode-go-muse-spark-1-3-12674.test.ts @@ -0,0 +1,75 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + parseEffortLevel, + resolveOpencodeTargetFormat, +} from "../../open-sse/executors/opencode.ts"; + +const { REGISTRY } = (await import("../../open-sse/config/providerRegistry.ts")) as { + REGISTRY: Record< + string, + { + models?: Array<{ + id: string; + targetFormat?: string; + supportsReasoning?: boolean; + contextLength?: number; + maxOutputTokens?: number; + }>; + } + >; +}; + +// #12674: opencode-go/muse-spark-1.3-contributor (upstream release 2026-09-02) +// 500s via /v1/chat/completions because the upstream serves Muse Spark only on +// the Responses API and the model had no registry targetFormat entry, so the +// executor fell back to "openai". Base + effort-tier alias set verified via +// `opencode models opencode-go --refresh --verbose` (minimal/low/medium/high/ +// xhigh, no max — same as 1.2). +const BASE = "muse-spark-1.3-contributor"; +const ALIASES = ["minimal", "low", "medium", "high", "xhigh"].map((effort) => ({ + alias: `${BASE}-${effort}`, + effort, +})); + +function goModels() { + const entry = REGISTRY["opencode-go"]; + assert.ok(entry, "opencode-go registry entry must exist"); + return entry.models ?? []; +} + +test("#12674 registry: 1.3 base + effort aliases target the Responses API", () => { + const models = goModels(); + for (const id of [BASE, ...ALIASES.map((a) => a.alias)]) { + const model = models.find((m) => m.id === id); + assert.ok(model, `${id} must be registered on opencode-go`); + assert.equal(model?.targetFormat, "openai-responses", `${id} must target Responses`); + assert.equal(model?.supportsReasoning, true, `${id} must support reasoning`); + assert.equal(model?.contextLength, 1048576, `${id} context must be 1M`); + assert.equal(model?.maxOutputTokens, 131072, `${id} max output must be 128K`); + } +}); + +test("#12674 executor: 1.3 resolves to openai-responses (URL selection)", () => { + assert.equal(resolveOpencodeTargetFormat("opencode-go", BASE), "openai-responses"); + assert.equal(resolveOpencodeTargetFormat("opencode-go", `${BASE}-high`), "openai-responses"); +}); + +for (const { alias, effort } of ALIASES) { + test(`#12674 parseEffortLevel: ${alias} → ${effort}`, () => { + assert.deepEqual(parseEffortLevel(alias), { baseModel: BASE, effort }); + }); +} + +test("#12674 parseEffortLevel: 1.3 has no max tier", () => { + assert.equal(parseEffortLevel(`${BASE}-max`), null); +}); + +test("#12674 catalog: 1.3 aliases stay Go-only (not on opencode-zen)", () => { + const zenIds = new Set((REGISTRY["opencode-zen"]?.models ?? []).map((m) => m.id)); + assert.equal(zenIds.has(BASE), false, "opencode-zen must not expose 1.3 base"); + for (const { alias } of ALIASES) { + assert.equal(zenIds.has(alias), false, `opencode-zen must not expose ${alias}`); + } +}); diff --git a/tests/unit/opencode-muse-spark-1-3-responses-12674.test.ts b/tests/unit/opencode-muse-spark-1-3-responses-12674.test.ts new file mode 100644 index 0000000000..fcb70fdadf --- /dev/null +++ b/tests/unit/opencode-muse-spark-1-3-responses-12674.test.ts @@ -0,0 +1,90 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { opencode_zenProvider } from "../../open-sse/config/providers/registry/opencode/zen/index.ts"; +import { opencodeProvider } from "../../open-sse/config/providers/registry/opencode/index.ts"; +import { getTokenLimit } from "../../open-sse/services/contextManager.ts"; +import { + OpencodeExecutor, + resolveOpencodeTargetFormat, +} from "../../open-sse/executors/opencode.ts"; + +// Issue 12674: Muse Spark 1.3 is served on the Responses API with a 1M +// context window. These entries only set the wire format; without the +// explicit window the limit fell back to the 200000 provider default. +const PROVIDERS = [ + { id: "opencode-zen", entry: opencode_zenProvider }, + { id: "opencode", entry: opencodeProvider }, +] as const; +const MODEL_IDS = ["muse-spark-1.3", "muse-spark-1.3-contributor-free"] as const; + +test("muse-spark-1.3 models target the Responses API with reasoning enabled", () => { + for (const { id, entry } of PROVIDERS) { + for (const modelId of MODEL_IDS) { + const model = entry.models.find((m) => m.id === modelId); + assert.ok(model, `${modelId} should be registered on ${id}`); + assert.equal( + model?.targetFormat, + "openai-responses", + `${modelId} on ${id} must target the Responses API, not the default chat/completions pass-through` + ); + assert.equal(model?.supportsReasoning, true); + } + } +}); + +test("muse-spark-1.3 models declare the real 1M context window and 128K output limit", () => { + for (const { id, entry } of PROVIDERS) { + for (const modelId of MODEL_IDS) { + const model = entry.models.find((m) => m.id === modelId); + assert.ok(model, `${modelId} should be registered on ${id}`); + assert.equal(model?.contextLength, 1048576, `${modelId} on ${id} must declare a 1M window`); + assert.equal( + model?.maxOutputTokens, + 131072, + `${modelId} on ${id} must declare a 128K output limit` + ); + } + } +}); + +test("context limit resolves muse-spark-1.3 models to the 1M window, not the provider default", () => { + for (const { id } of PROVIDERS) { + for (const modelId of MODEL_IDS) { + assert.equal( + getTokenLimit(id, modelId), + 1048576, + `getTokenLimit(${id}, ${modelId}) must return the 1M window` + ); + } + } +}); + +test("executor resolves muse-spark-1.3 models to the Responses API", () => { + for (const { id } of PROVIDERS) { + for (const modelId of MODEL_IDS) { + assert.equal( + resolveOpencodeTargetFormat(id, modelId), + "openai-responses", + `resolveOpencodeTargetFormat(${id}, ${modelId}) must return openai-responses` + ); + } + } +}); + +test("Responses-format executor hits /responses with x-api-key auth and no Bearer header", () => { + for (const { id } of PROVIDERS) { + for (const modelId of MODEL_IDS) { + const executor = new OpencodeExecutor(id); + executor._requestFormat = resolveOpencodeTargetFormat(id, modelId); + const url = executor.buildUrl(modelId, true); + assert.ok( + url.endsWith("/responses"), + `${id}/${modelId} must build a /responses URL, got ${url}` + ); + const headers = executor.buildHeaders({ apiKey: "sk-test" }, true, null, modelId); + assert.equal(headers["x-api-key"], "sk-test"); + assert.equal(headers["Authorization"], undefined); + } + } +});