mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 03:42:21 +03:00
fix(providers): route Muse Spark 1.3 to the Responses API on OpenCode Zen, OpenCode and OpenCode Go (#13471)
Registers Muse Spark 1.3 (with its effort aliases) on OpenCode Zen, OpenCode and OpenCode Go with `targetFormat: openai-responses` and a 1M context window, so the model no longer falls back to `/chat/completions` (#12674, #12698). Superset of #12675, #12973 and #13111, whose authors are credited in the PR.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
This commit is contained in:
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, readonly string[]> = {
|
||||
"deepseek-v4-pro": EFFORT_LEVELS,
|
||||
@@ -107,6 +109,7 @@ const EFFORT_TIERS: Record<string, readonly string[]> = {
|
||||
"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"],
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
75
tests/unit/opencode-go-muse-spark-1-3-12674.test.ts
Normal file
75
tests/unit/opencode-go-muse-spark-1-3-12674.test.ts
Normal file
@@ -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}`);
|
||||
}
|
||||
});
|
||||
90
tests/unit/opencode-muse-spark-1-3-responses-12674.test.ts
Normal file
90
tests/unit/opencode-muse-spark-1-3-responses-12674.test.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user