From 6a2362514285402d8e66dde70158a86e28342c16 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 13 Sep 2026 11:01:40 -0700 Subject: [PATCH 1/3] Give Muse Spark its own reasoning effort ladder muse-spark-1.3-contributor fell through to UNKNOWN_MODEL_EFFORTS, so `minimal` was unreachable from the picker, the flag, and agent profiles. Measured against the Go Responses endpoint, reasoning tokens are ~95% of every completion and scale 6x from minimal to medium with no change in pass rate on two objectively graded tasks. The gateway accepts minimal and rejects `none` with HTTP 400, so the rung set is minimal/low/medium/ high and the family default is `low`. Also pins SOURCE_MAX_TOKENS above the measured truncation floor: reasoning consumes max_output_tokens before any answer text, so a 512 cap at medium effort returns 3 tokens of answer, not a shorter answer. CL-7867 --- src/config.test.ts | 9 +++++++ src/provider/reasoning-effort.test.ts | 34 +++++++++++++++++++++++++++ src/provider/reasoning-effort.ts | 20 +++++++++++++++- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/config.test.ts b/src/config.test.ts index 6c68801e1..ca139373c 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -1460,6 +1460,15 @@ describe("buildOpenAISource", () => { expect(source.baseURL).toBe("https://fp/v1"); }); + test("stays above the reasoning truncation floor", () => { + // Reasoning tokens consume max_output_tokens before any answer text is + // emitted. Measured on muse-spark-1.3-contributor, a 512-token cap at + // medium effort spent 397 tokens reasoning and returned 3 tokens of + // answer; 1024 was the lowest cap that answered on every rung. 4096 is + // the floor we will not drop below. See CL-7867. + expect(SOURCE_MAX_TOKENS).toBeGreaterThanOrEqual(4096); + }); + test("omits reasoning_effort when effort is absent", () => { const source = buildOpenAISource({ id: "fp", diff --git a/src/provider/reasoning-effort.test.ts b/src/provider/reasoning-effort.test.ts index 996d791cc..e7f69d376 100644 --- a/src/provider/reasoning-effort.test.ts +++ b/src/provider/reasoning-effort.test.ts @@ -163,6 +163,28 @@ describe("supportedEfforts", () => { expect(supportedEfforts("glm-5.3")).toEqual(["low", "high", "max"]); expect(supportedEfforts("glm-5.3-flash")).toEqual(["low", "high", "max"]); }); + + test("Muse Spark supports minimal through high", () => { + expect(supportedEfforts("muse-spark-1.3-contributor")).toEqual([ + "minimal", + "low", + "medium", + "high", + ]); + expect(supportedEfforts("muse-spark-1.2-contributor")).toEqual([ + "minimal", + "low", + "medium", + "high", + ]); + }); + + test("Muse Spark never offers none", () => { + // The Go gateway answers HTTP 400 on reasoning.effort: "none". + expect(supportedEfforts("muse-spark-1.3-contributor")).not.toContain( + "none", + ); + }); }); describe("validateEffort", () => { @@ -189,6 +211,13 @@ describe("validateEffort", () => { expect(validateEffort("grok-4.5", "xhigh").ok).toBe(false); }); + test("accepts minimal on Muse Spark and rejects none", () => { + expect(validateEffort("muse-spark-1.3-contributor", "minimal")).toEqual({ + ok: true, + }); + expect(validateEffort("muse-spark-1.3-contributor", "none").ok).toBe(false); + }); + test("rejects medium on glm-5.3 family", () => { expect(validateEffort("glm-5.3", "medium").ok).toBe(false); expect(validateEffort("glm-5.3-flash", "medium").ok).toBe(false); @@ -472,6 +501,11 @@ describe("defaultEffortForModel", () => { expect(defaultEffortForModel("glm-5.3-flash")).toBe("max"); }); + test("Muse Spark defaults to low", () => { + expect(defaultEffortForModel("muse-spark-1.3-contributor")).toBe("low"); + expect(defaultEffortForModel("muse-spark-1.2-contributor")).toBe("low"); + }); + test("gpt-5 and o-series default to medium", () => { expect(defaultEffortForModel("gpt-5")).toBe("medium"); expect(defaultEffortForModel("o1")).toBe("medium"); diff --git a/src/provider/reasoning-effort.ts b/src/provider/reasoning-effort.ts index 4fe0c0841..0551dc846 100644 --- a/src/provider/reasoning-effort.ts +++ b/src/provider/reasoning-effort.ts @@ -61,6 +61,20 @@ const UNKNOWN_MODEL_EFFORTS: readonly ReasoningEffort[] = [ "high", ]; +// Muse Spark (OpenCode Go, Responses protocol) accepts minimal through high. +// Not `none` — the Go gateway rejects it with HTTP 400 on `reasoning.effort`. +// Measured against https://opencode.ai/zen/go/v1/responses; see CL-7867. +const MUSE_SPARK_EFFORTS: readonly ReasoningEffort[] = [ + "minimal", + "low", + "medium", + "high", +]; +const MUSE_SPARK_MODELS: readonly string[] = [ + "muse-spark-1.3-contributor", + "muse-spark-1.2-contributor", +]; + // grok-4.6 accepts xhigh; grok-4.5 and composer stay on the unknown-model subset. const GROK_46_EFFORTS: readonly ReasoningEffort[] = [ "low", @@ -143,6 +157,9 @@ export function supportedEfforts( if (GLM_53_MODELS.includes(model)) { return [...GLM_53_EFFORTS]; } + if (MUSE_SPARK_MODELS.includes(model)) { + return [...MUSE_SPARK_EFFORTS]; + } return [...UNKNOWN_MODEL_EFFORTS]; } @@ -196,7 +213,7 @@ export function cycleReasoningEffort( * (`defaultEffortForDirector`): this is what the prompt shows and what Shift+Tab * advances from when the operator has not picked a level. * - * Family table: grok* → high; glm-5.3* → max; Codex → medium; gpt-5.1 chat (`none` on the + * Family table: grok* → high; glm-5.3* → max; muse-spark* → low; Codex → medium; gpt-5.1 chat (`none` on the * ladder, not Codex) → none; gpt-5/gpt-6/o1/o3/o4 → medium. Unknown models with a * conservative rung set stay undefined so we do not invent a family default. */ @@ -210,6 +227,7 @@ export function defaultEffortForModel( supported.includes(desired) ? desired : undefined; if (model.startsWith("grok")) return pick("high"); if (GLM_53_MODELS.includes(model)) return pick("max"); + if (MUSE_SPARK_MODELS.includes(model)) return pick("low"); if (!isCodex && supported.includes("none")) return "none"; if (isCodex || isKnownOpenAIReasoningModel(model)) return pick("medium"); return undefined; From 5ff7f61630e44b100727f5bd49546e017e0bc3cf Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 13 Sep 2026 11:07:43 -0700 Subject: [PATCH 2/3] Match Muse Spark models by prefix, not an exact id list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The id list covered the two ids in packages/opencode-go and missed the three in packages/zen: muse-spark-1.3, muse-spark-1.2, and muse-spark-1.3-contributor-free. Nothing normalizes the model string on the way to supportedEfforts, so those three still fell through to the unknown-model ladder and still could not select minimal — the exact symptom this change exists to fix, surviving on the ids nobody tested. Confirmed against the live endpoints that minimal returns 200 and none returns 400 for the two contributor ids and for contributor-free. muse-spark-1.3 and -1.2 answered with a billing gate rather than a model rejection, so their ladder is inferred from the family, not measured. A prefix also matches isMuseSparkLeafProvider in provider-family.ts, which already keyed off /^muse-spark/i — the two would otherwise have disagreed about which models are Muse Spark. CL-7867 --- src/provider/reasoning-effort.test.ts | 25 +++++++++++++++--------- src/provider/reasoning-effort.ts | 28 ++++++++++++++++++--------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/provider/reasoning-effort.test.ts b/src/provider/reasoning-effort.test.ts index e7f69d376..d658b227d 100644 --- a/src/provider/reasoning-effort.test.ts +++ b/src/provider/reasoning-effort.test.ts @@ -164,15 +164,27 @@ describe("supportedEfforts", () => { expect(supportedEfforts("glm-5.3-flash")).toEqual(["low", "high", "max"]); }); - test("Muse Spark supports minimal through high", () => { - expect(supportedEfforts("muse-spark-1.3-contributor")).toEqual([ + // Five ids ship across two catalogs (packages/opencode-go and packages/zen) + // and nothing normalizes the model string before it reaches supportedEfforts, + // so every one of them has to land on the same ladder. + test.each([ + "muse-spark-1.3-contributor", + "muse-spark-1.2-contributor", + "muse-spark-1.3", + "muse-spark-1.2", + "muse-spark-1.3-contributor-free", + ])("Muse Spark id %s supports minimal through high", (model) => { + expect(supportedEfforts(model)).toEqual([ "minimal", "low", "medium", "high", ]); - expect(supportedEfforts("muse-spark-1.2-contributor")).toEqual([ - "minimal", + expect(defaultEffortForModel(model)).toBe("low"); + }); + + test("a model merely containing muse-spark is not matched", () => { + expect(supportedEfforts("not-muse-spark-1.3")).toEqual([ "low", "medium", "high", @@ -501,11 +513,6 @@ describe("defaultEffortForModel", () => { expect(defaultEffortForModel("glm-5.3-flash")).toBe("max"); }); - test("Muse Spark defaults to low", () => { - expect(defaultEffortForModel("muse-spark-1.3-contributor")).toBe("low"); - expect(defaultEffortForModel("muse-spark-1.2-contributor")).toBe("low"); - }); - test("gpt-5 and o-series default to medium", () => { expect(defaultEffortForModel("gpt-5")).toBe("medium"); expect(defaultEffortForModel("o1")).toBe("medium"); diff --git a/src/provider/reasoning-effort.ts b/src/provider/reasoning-effort.ts index 0551dc846..18329f9ae 100644 --- a/src/provider/reasoning-effort.ts +++ b/src/provider/reasoning-effort.ts @@ -61,19 +61,29 @@ const UNKNOWN_MODEL_EFFORTS: readonly ReasoningEffort[] = [ "high", ]; -// Muse Spark (OpenCode Go, Responses protocol) accepts minimal through high. -// Not `none` — the Go gateway rejects it with HTTP 400 on `reasoning.effort`. -// Measured against https://opencode.ai/zen/go/v1/responses; see CL-7867. +// Muse Spark (Responses protocol) accepts minimal through high. Not `none` — +// the gateway rejects it with HTTP 400 on `reasoning.effort`. Measured on +// muse-spark-1.3-contributor and muse-spark-1.2-contributor via the Go +// endpoint and muse-spark-1.3-contributor-free via Zen: `minimal` returns 200 +// and `none` returns 400 on all three. See CL-7867. const MUSE_SPARK_EFFORTS: readonly ReasoningEffort[] = [ "minimal", "low", "medium", "high", ]; -const MUSE_SPARK_MODELS: readonly string[] = [ - "muse-spark-1.3-contributor", - "muse-spark-1.2-contributor", -]; + +// Matched by prefix, not by an id list. The family ships under five ids across +// two catalogs — `muse-spark-1.3-contributor` / `-1.2-contributor` in +// packages/opencode-go, and `muse-spark-1.3` / `-1.2` / +// `-1.3-contributor-free` in packages/zen — and nothing normalizes the model +// string before it reaches here. An exact list silently missed three of them +// and left the ladder at the unknown-model default. This also matches +// isMuseSparkLeafProvider in src/subagent/provider-family.ts, which already +// keyed off the same prefix. +function isMuseSparkModel(model: string): boolean { + return /^muse-spark/i.test(model.trim()); +} // grok-4.6 accepts xhigh; grok-4.5 and composer stay on the unknown-model subset. const GROK_46_EFFORTS: readonly ReasoningEffort[] = [ @@ -157,7 +167,7 @@ export function supportedEfforts( if (GLM_53_MODELS.includes(model)) { return [...GLM_53_EFFORTS]; } - if (MUSE_SPARK_MODELS.includes(model)) { + if (isMuseSparkModel(model)) { return [...MUSE_SPARK_EFFORTS]; } return [...UNKNOWN_MODEL_EFFORTS]; @@ -227,7 +237,7 @@ export function defaultEffortForModel( supported.includes(desired) ? desired : undefined; if (model.startsWith("grok")) return pick("high"); if (GLM_53_MODELS.includes(model)) return pick("max"); - if (MUSE_SPARK_MODELS.includes(model)) return pick("low"); + if (isMuseSparkModel(model)) return pick("low"); if (!isCodex && supported.includes("none")) return "none"; if (isCodex || isKnownOpenAIReasoningModel(model)) return pick("medium"); return undefined; From eed26f0b552b9d63ebf93068854275dae3da0382 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 13 Sep 2026 21:51:38 -0700 Subject: [PATCH 3/3] Correct Muse Spark prefix comment to cite provider-family convention The prior comment named isMuseSparkLeafProvider, which exists nowhere. --- src/provider/reasoning-effort.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/provider/reasoning-effort.ts b/src/provider/reasoning-effort.ts index 18329f9ae..78a3a6359 100644 --- a/src/provider/reasoning-effort.ts +++ b/src/provider/reasoning-effort.ts @@ -78,9 +78,9 @@ const MUSE_SPARK_EFFORTS: readonly ReasoningEffort[] = [ // packages/opencode-go, and `muse-spark-1.3` / `-1.2` / // `-1.3-contributor-free` in packages/zen — and nothing normalizes the model // string before it reaches here. An exact list silently missed three of them -// and left the ladder at the unknown-model default. This also matches -// isMuseSparkLeafProvider in src/subagent/provider-family.ts, which already -// keyed off the same prefix. +// and left the ladder at the unknown-model default. The `/^.../i` + `trim()` +// shape mirrors the grok/kimi prefix checks in +// src/subagent/provider-family.ts. function isMuseSparkModel(model: string): boolean { return /^muse-spark/i.test(model.trim()); }