Skip to content

Commit 604c12e

Browse files
Merge pull request #979 from corbitsdev/cl-7867-muse-spark-cannot-select-minimal-reasoning-effort-paying-6x
Give Muse Spark its own reasoning effort ladder
2 parents 1a94470 + eed26f0 commit 604c12e

3 files changed

Lines changed: 79 additions & 1 deletion

File tree

src/config.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,6 +1460,15 @@ describe("buildOpenAISource", () => {
14601460
expect(source.baseURL).toBe("https://fp/v1");
14611461
});
14621462

1463+
test("stays above the reasoning truncation floor", () => {
1464+
// Reasoning tokens consume max_output_tokens before any answer text is
1465+
// emitted. Measured on muse-spark-1.3-contributor, a 512-token cap at
1466+
// medium effort spent 397 tokens reasoning and returned 3 tokens of
1467+
// answer; 1024 was the lowest cap that answered on every rung. 4096 is
1468+
// the floor we will not drop below. See CL-7867.
1469+
expect(SOURCE_MAX_TOKENS).toBeGreaterThanOrEqual(4096);
1470+
});
1471+
14631472
test("omits reasoning_effort when effort is absent", () => {
14641473
const source = buildOpenAISource({
14651474
id: "fp",

src/provider/reasoning-effort.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,40 @@ describe("supportedEfforts", () => {
163163
expect(supportedEfforts("glm-5.3")).toEqual(["low", "high", "max"]);
164164
expect(supportedEfforts("glm-5.3-flash")).toEqual(["low", "high", "max"]);
165165
});
166+
167+
// Five ids ship across two catalogs (packages/opencode-go and packages/zen)
168+
// and nothing normalizes the model string before it reaches supportedEfforts,
169+
// so every one of them has to land on the same ladder.
170+
test.each([
171+
"muse-spark-1.3-contributor",
172+
"muse-spark-1.2-contributor",
173+
"muse-spark-1.3",
174+
"muse-spark-1.2",
175+
"muse-spark-1.3-contributor-free",
176+
])("Muse Spark id %s supports minimal through high", (model) => {
177+
expect(supportedEfforts(model)).toEqual([
178+
"minimal",
179+
"low",
180+
"medium",
181+
"high",
182+
]);
183+
expect(defaultEffortForModel(model)).toBe("low");
184+
});
185+
186+
test("a model merely containing muse-spark is not matched", () => {
187+
expect(supportedEfforts("not-muse-spark-1.3")).toEqual([
188+
"low",
189+
"medium",
190+
"high",
191+
]);
192+
});
193+
194+
test("Muse Spark never offers none", () => {
195+
// The Go gateway answers HTTP 400 on reasoning.effort: "none".
196+
expect(supportedEfforts("muse-spark-1.3-contributor")).not.toContain(
197+
"none",
198+
);
199+
});
166200
});
167201

168202
describe("validateEffort", () => {
@@ -189,6 +223,13 @@ describe("validateEffort", () => {
189223
expect(validateEffort("grok-4.5", "xhigh").ok).toBe(false);
190224
});
191225

226+
test("accepts minimal on Muse Spark and rejects none", () => {
227+
expect(validateEffort("muse-spark-1.3-contributor", "minimal")).toEqual({
228+
ok: true,
229+
});
230+
expect(validateEffort("muse-spark-1.3-contributor", "none").ok).toBe(false);
231+
});
232+
192233
test("rejects medium on glm-5.3 family", () => {
193234
expect(validateEffort("glm-5.3", "medium").ok).toBe(false);
194235
expect(validateEffort("glm-5.3-flash", "medium").ok).toBe(false);

src/provider/reasoning-effort.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,30 @@ const UNKNOWN_MODEL_EFFORTS: readonly ReasoningEffort[] = [
6161
"high",
6262
];
6363

64+
// Muse Spark (Responses protocol) accepts minimal through high. Not `none` —
65+
// the gateway rejects it with HTTP 400 on `reasoning.effort`. Measured on
66+
// muse-spark-1.3-contributor and muse-spark-1.2-contributor via the Go
67+
// endpoint and muse-spark-1.3-contributor-free via Zen: `minimal` returns 200
68+
// and `none` returns 400 on all three. See CL-7867.
69+
const MUSE_SPARK_EFFORTS: readonly ReasoningEffort[] = [
70+
"minimal",
71+
"low",
72+
"medium",
73+
"high",
74+
];
75+
76+
// Matched by prefix, not by an id list. The family ships under five ids across
77+
// two catalogs — `muse-spark-1.3-contributor` / `-1.2-contributor` in
78+
// packages/opencode-go, and `muse-spark-1.3` / `-1.2` /
79+
// `-1.3-contributor-free` in packages/zen — and nothing normalizes the model
80+
// string before it reaches here. An exact list silently missed three of them
81+
// and left the ladder at the unknown-model default. The `/^.../i` + `trim()`
82+
// shape mirrors the grok/kimi prefix checks in
83+
// src/subagent/provider-family.ts.
84+
function isMuseSparkModel(model: string): boolean {
85+
return /^muse-spark/i.test(model.trim());
86+
}
87+
6488
// grok-4.6 accepts xhigh; grok-4.5 and composer stay on the unknown-model subset.
6589
const GROK_46_EFFORTS: readonly ReasoningEffort[] = [
6690
"low",
@@ -143,6 +167,9 @@ export function supportedEfforts(
143167
if (GLM_53_MODELS.includes(model)) {
144168
return [...GLM_53_EFFORTS];
145169
}
170+
if (isMuseSparkModel(model)) {
171+
return [...MUSE_SPARK_EFFORTS];
172+
}
146173
return [...UNKNOWN_MODEL_EFFORTS];
147174
}
148175

@@ -196,7 +223,7 @@ export function cycleReasoningEffort(
196223
* (`defaultEffortForDirector`): this is what the prompt shows and what Shift+Tab
197224
* advances from when the operator has not picked a level.
198225
*
199-
* Family table: grok* → high; glm-5.3* → max; Codex → medium; gpt-5.1 chat (`none` on the
226+
* Family table: grok* → high; glm-5.3* → max; muse-spark* → low; Codex → medium; gpt-5.1 chat (`none` on the
200227
* ladder, not Codex) → none; gpt-5/gpt-6/o1/o3/o4 → medium. Unknown models with a
201228
* conservative rung set stay undefined so we do not invent a family default.
202229
*/
@@ -210,6 +237,7 @@ export function defaultEffortForModel(
210237
supported.includes(desired) ? desired : undefined;
211238
if (model.startsWith("grok")) return pick("high");
212239
if (GLM_53_MODELS.includes(model)) return pick("max");
240+
if (isMuseSparkModel(model)) return pick("low");
213241
if (!isCodex && supported.includes("none")) return "none";
214242
if (isCodex || isKnownOpenAIReasoningModel(model)) return pick("medium");
215243
return undefined;

0 commit comments

Comments
 (0)