mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 21:02:50 +03:00
fix(combos): stop Gemini thinking from failing dashboard combo tests (#13560)
Two related fixes: the combo health probe sends `reasoning_effort: "none"` for Gemini-family models so the probe budget is not spent on thinking, and `detectMalformedNonStream` stops classifying a response with `finish_reason` `length`/`tool_calls`/`content_filter` and empty content as `empty_choices`. The second half is the important one: it brings the post-translation check in line with `isEmptyContentResponse` (`open-sse/services/errorClassifier.ts`, `LEGIT_EMPTY_OPENAI_FINISH`), which already treated those finish reasons as legitimate. Until now a response could pass the pre-translation check and still be rewritten into a synthetic 502 afterwards — for every non-streaming completion, not just combo probes. Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thanks @HouMinXi!
This commit is contained in:
1
changelog.d/fixes/combo-probe-no-thinking.md
Normal file
1
changelog.d/fixes/combo-probe-no-thinking.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(combos):** Gemini combo probes send `reasoning_effort: none` so thinking does not eat the health-check budget; truncated `finish_reason: length` responses are no longer rewritten as empty-content 502s
|
||||
@@ -313,7 +313,23 @@ export function detectMalformedNonStream(resp: unknown): MalformedReason | null
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!anyHasOutput) return "empty_choices";
|
||||
if (!anyHasOutput) {
|
||||
// Same terminal stops isEmptyContentResponse already accepts as
|
||||
// successful truncation, not a silent fake-success. Gemini 3.8
|
||||
// health probes that spend max_tokens on thinking come back as
|
||||
// content:"" + finish_reason:"length". Treating that as empty_choices
|
||||
// rewrites a valid 200 into 502 and fails dashboard Test all.
|
||||
const truncatedAtLimit = choices.some((choice) => {
|
||||
const c = choice as Record<string, unknown>;
|
||||
return (
|
||||
c?.finish_reason === "length" ||
|
||||
c?.finish_reason === "tool_calls" ||
|
||||
c?.finish_reason === "content_filter"
|
||||
);
|
||||
});
|
||||
if (truncatedAtLimit) return null;
|
||||
return "empty_choices";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -323,7 +339,8 @@ export function describeMalformedNonStream(
|
||||
): { message: string; code: string; type: string } {
|
||||
const body = resp && typeof resp === "object" ? (resp as Record<string, unknown>) : null;
|
||||
if (body?.object === "response" && body.status === "failed") {
|
||||
const err = body.error && typeof body.error === "object" ? (body.error as Record<string, unknown>) : null;
|
||||
const err =
|
||||
body.error && typeof body.error === "object" ? (body.error as Record<string, unknown>) : null;
|
||||
const rawMessage =
|
||||
typeof err?.message === "string" && err.message.trim().length > 0 ? err.message.trim() : null;
|
||||
return {
|
||||
|
||||
@@ -111,6 +111,10 @@ export function buildComboTestPrompt() {
|
||||
return COMBO_TEST_PROMPT;
|
||||
}
|
||||
|
||||
function isGeminiComboProbe(modelStr: string) {
|
||||
return /(?:^|\/)gemini(?:-|$)/i.test(modelStr);
|
||||
}
|
||||
|
||||
export function buildComboTestRequestBody(
|
||||
modelStr: string,
|
||||
isEmbedding: boolean = false,
|
||||
@@ -123,7 +127,13 @@ export function buildComboTestRequestBody(
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
const body: {
|
||||
model: string;
|
||||
messages: { role: string; content: string }[];
|
||||
max_tokens: number;
|
||||
stream: boolean;
|
||||
reasoning_effort?: "none";
|
||||
} = {
|
||||
model: modelStr,
|
||||
messages: [{ role: "user", content: buildComboTestPrompt() }],
|
||||
// Keep the smoke probe short so reasoning-heavy models do not burn the
|
||||
@@ -133,6 +143,13 @@ export function buildComboTestRequestBody(
|
||||
(options.stream ? STREAMING_MODEL_TEST_MAX_TOKENS : COMBO_TEST_MAX_TOKENS),
|
||||
stream: options.stream ?? false,
|
||||
};
|
||||
// Gemini 3.8 flash-high injects thinkingLevel=high unless the documented
|
||||
// off-switch is set. Other providers must not see this field: some
|
||||
// OpenAI-compatible endpoints 400 unknown parameters.
|
||||
if (isGeminiComboProbe(modelStr)) {
|
||||
body.reasoning_effort = "none";
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
export type ComboTestStreamResult = {
|
||||
|
||||
@@ -8,7 +8,7 @@ const {
|
||||
extractComboTestStreamText,
|
||||
} = await import("../../src/lib/combos/testHealth.ts");
|
||||
|
||||
test("combo test helper builds a short smoke payload", () => {
|
||||
test("combo test helper builds short smoke payload", () => {
|
||||
const body = buildComboTestRequestBody("openrouter/openai/gpt-5.4");
|
||||
|
||||
assert.equal(body.model, "openrouter/openai/gpt-5.4");
|
||||
@@ -16,6 +16,15 @@ test("combo test helper builds a short smoke payload", () => {
|
||||
assert.equal(body.max_tokens, 64);
|
||||
assert.equal("temperature" in body, false);
|
||||
assert.equal(body.stream, false);
|
||||
assert.equal("reasoning_effort" in body, false);
|
||||
});
|
||||
|
||||
test("combo test helper turns off thinking for Gemini 3.8 flash-high probes", () => {
|
||||
const body = buildComboTestRequestBody("agy/gemini-3.8-flash-high");
|
||||
|
||||
assert.equal(body.messages[0].content, "Reply with exactly: pong");
|
||||
assert.equal(body.max_tokens, 64);
|
||||
assert.equal(body.reasoning_effort, "none");
|
||||
});
|
||||
|
||||
test("combo test helper builds a small streaming model probe", () => {
|
||||
|
||||
@@ -148,6 +148,7 @@ test("combo test route marks a model healthy only when it returns assistant text
|
||||
assert.equal(forwardedBody.model, "openrouter/openai/gpt-5.4");
|
||||
assert.equal(forwardedBody.messages[0].content, "Reply with exactly: pong");
|
||||
assert.equal(forwardedBody.max_tokens, 64);
|
||||
assert.equal("reasoning_effort" in forwardedBody, false);
|
||||
assert.equal("temperature" in forwardedBody, false);
|
||||
assert.equal(body.resolvedBy, "openrouter/openai/gpt-5.4");
|
||||
assert.equal(body.results[0].status, "ok");
|
||||
|
||||
@@ -128,6 +128,14 @@ test("detectMalformedNonStream returns 'empty_choices' when choice message has n
|
||||
assert.equal(detectMalformedNonStream(body), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null when empty content stopped at token limit", () => {
|
||||
const body = {
|
||||
choices: [{ index: 0, message: { role: "assistant", content: "" }, finish_reason: "length" }],
|
||||
usage: { reasoning_tokens: 28 },
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null for valid chat completion", () => {
|
||||
const body = {
|
||||
choices: [{ message: { content: "Hello!", tool_calls: null }, finish_reason: "stop" }],
|
||||
|
||||
Reference in New Issue
Block a user