mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 08:32:11 +03:00
Validated on the combined batch board over tip 0b41259f: static gates clean (changelog, file-size 160 frozen, complexity 2628<=2774, cognitive 1187<=1223, dead-code 408<=416, docs-counts green at 351 providers, provider-consistency 268/351/0), typecheck:core clean, 430+ focused tests green across 5 groups.
muse-spark's all-reasoning empty-answer payloads (41 failures in 2h captured live) now floor the output budget so the model can actually emit content — no more empty-content 502 churn. Thank you @linhdmn!
112 lines
4.8 KiB
TypeScript
112 lines
4.8 KiB
TypeScript
/**
|
||
* muse-spark (opencode-go) burns its entire output budget on invisible
|
||
* server-side reasoning before emitting any content. With small caller-set
|
||
* budgets the upstream answers 200 with an empty message
|
||
* (`{"message":{"role":"assistant"},"finish_reason":null}` and
|
||
* `completion_tokens == max_tokens`) — chatCore then flags the fake success as
|
||
* "Provider returned empty content" / 502.
|
||
*
|
||
* Verified live 2026-08-23: max_tokens=64 → empty; 100 → empty;
|
||
* 256/512/1024 → content present (reasoning consumed 196–253 of it).
|
||
*
|
||
* Fix: OpencodeExecutor clamps muse-spark* output budgets UP to
|
||
* MUSE_SPARK_MIN_OUTPUT_TOKENS so the reasoning phase can never consume the
|
||
* whole budget. Other models are untouched.
|
||
*/
|
||
import test from "node:test";
|
||
import assert from "node:assert/strict";
|
||
|
||
const { applyMuseSparkMinOutputTokens, MUSE_SPARK_MIN_OUTPUT_TOKENS } = await import(
|
||
"../../open-sse/executors/opencode.ts"
|
||
);
|
||
const {
|
||
normalizeMuseSparkFinishReason,
|
||
createMuseSparkStreamFinishNormalizer,
|
||
} = await import("../../open-sse/executors/opencode.ts");
|
||
|
||
test("RED: muse-spark tiny max_tokens is raised to the floor", () => {
|
||
const body: Record<string, unknown> = { model: "x", max_tokens: 64, messages: [] };
|
||
applyMuseSparkMinOutputTokens("muse-spark-1.2-contributor", body);
|
||
assert.equal(body.max_tokens, MUSE_SPARK_MIN_OUTPUT_TOKENS);
|
||
});
|
||
|
||
test("RED: all muse-spark id variants are covered by the prefix match", () => {
|
||
for (const model of ["muse-spark-1", "muse-spark-1.2", "muse-spark-1.2-contributor"]) {
|
||
const body: Record<string, unknown> = { max_tokens: 100 };
|
||
applyMuseSparkMinOutputTokens(model, body);
|
||
assert.equal(body.max_tokens, MUSE_SPARK_MIN_OUTPUT_TOKENS, model);
|
||
}
|
||
});
|
||
|
||
test("RED: budgets already at or above the floor are untouched", () => {
|
||
const body: Record<string, unknown> = { max_tokens: 4096 };
|
||
applyMuseSparkMinOutputTokens("muse-spark-1.2-contributor", body);
|
||
assert.equal(body.max_tokens, 4096);
|
||
});
|
||
|
||
test("RED: non-muse-spark models are never modified", () => {
|
||
const body: Record<string, unknown> = { max_tokens: 16 };
|
||
applyMuseSparkMinOutputTokens("ox-alpha-free", body);
|
||
assert.equal(body.max_tokens, 16);
|
||
});
|
||
|
||
test("RED: missing/non-numeric max_tokens stays absent (no synthetic budget)", () => {
|
||
const body: Record<string, unknown> = { messages: [] };
|
||
applyMuseSparkMinOutputTokens("muse-spark-1.2-contributor", body);
|
||
assert.equal("max_tokens" in body, false);
|
||
});
|
||
|
||
test("RED: finish_reason length is rewritten to stop when completion is far under budget", () => {
|
||
const payload: Record<string, unknown> = {
|
||
choices: [{ index: 0, message: { role: "assistant" }, finish_reason: "length" }],
|
||
usage: { completion_tokens: 270 },
|
||
};
|
||
normalizeMuseSparkFinishReason(payload, 128000);
|
||
assert.equal((payload.choices as Array<Record<string, unknown>>)[0].finish_reason, "stop");
|
||
});
|
||
|
||
test("RED: genuine truncation at the budget keeps finish_reason length", () => {
|
||
const payload: Record<string, unknown> = {
|
||
choices: [{ index: 0, message: { role: "assistant" }, finish_reason: "length" }],
|
||
usage: { completion_tokens: 127000 },
|
||
};
|
||
normalizeMuseSparkFinishReason(payload, 128000);
|
||
assert.equal((payload.choices as Array<Record<string, unknown>>)[0].finish_reason, "length");
|
||
});
|
||
|
||
test("RED: non-length finish reasons and missing usage are untouched", () => {
|
||
const payload: Record<string, unknown> = {
|
||
choices: [{ index: 0, message: { role: "assistant" }, finish_reason: "stop" }],
|
||
};
|
||
normalizeMuseSparkFinishReason(payload, 128000);
|
||
assert.equal((payload.choices as Array<Record<string, unknown>>)[0].finish_reason, "stop");
|
||
|
||
const noUsage: Record<string, unknown> = {
|
||
choices: [{ index: 0, message: { role: "assistant" }, finish_reason: "length" }],
|
||
};
|
||
normalizeMuseSparkFinishReason(noUsage, 128000);
|
||
assert.equal(
|
||
(noUsage.choices as Array<Record<string, unknown>>)[0].finish_reason,
|
||
"length",
|
||
"without a completion count the rewrite must stay conservative"
|
||
);
|
||
});
|
||
|
||
test("RED: stream normalizer rewrites the finish frame after the usage frame", () => {
|
||
const norm = createMuseSparkStreamFinishNormalizer(128000);
|
||
const usageLine =
|
||
'data: {"id":"r","object":"chat.completion.chunk","choices":[],"usage":{"completion_tokens":270}}';
|
||
assert.equal(norm(usageLine), usageLine, "usage frame itself must not change");
|
||
const finishLine =
|
||
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"length"}]}';
|
||
const out = JSON.parse(norm(finishLine).slice(5).trim());
|
||
assert.equal(out.choices[0].finish_reason, "stop");
|
||
});
|
||
|
||
test("RED: stream normalizer passes through [DONE], comments and non-JSON lines", () => {
|
||
const norm = createMuseSparkStreamFinishNormalizer(128000);
|
||
assert.equal(norm("data: [DONE]"), "data: [DONE]");
|
||
assert.equal(norm(": keepalive"), ": keepalive");
|
||
assert.equal(norm("data: not-json"), "data: not-json");
|
||
});
|