From ff320cbfd5ff89bb1de5105a29b84741bcd8bc73 Mon Sep 17 00:00:00 2001 From: "Bob.Hou" Date: Wed, 22 Jul 2026 01:34:47 -0400 Subject: [PATCH] fix(combo): exempt content_filter from empty-content detection (#7973) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem When Gemini Flash returns a safety-filtered response (finish_reason: content_filter, empty content), isEmptyContentResponse() misclassifies it as a fake-success empty response and returns HTTP 502. This triggers the combo fallback chain and account cooldown escalation (5s → 10s → 20s → 40s), even though the response is a legitimate terminal state. ## Root cause errorClassifier.ts line 14: LEGIT_EMPTY_OPENAI_FINISH only exempts "length" and "tool_calls". The "content_filter" finish reason (mapped from Gemini's SAFETY/PROHIBITED_CONTENT) is not exempted, so safety-filtered responses are treated as empty content failures. ## Fix Add "content_filter" to LEGIT_EMPTY_OPENAI_FINISH so safety-filtered responses pass through as valid (though filtered) completions. ## Testing - 10/10 unit tests pass (empty-content-stopreason-3572.test.ts) including 2 new content_filter test cases - E2E: hot-patched OmniRoute v3.8.48 on X500, verified the previously failing prompt (4.6KB review) now returns valid content instead of empty-content 502 Signed-off-by: Minxi Hou --- open-sse/services/errorClassifier.ts | 2 +- .../unit/empty-content-stopreason-3572.test.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/open-sse/services/errorClassifier.ts b/open-sse/services/errorClassifier.ts index d3765737d0..9aebe0531d 100644 --- a/open-sse/services/errorClassifier.ts +++ b/open-sse/services/errorClassifier.ts @@ -11,7 +11,7 @@ import { getProviderCategory, getRegistryEntry } from "../config/providerRegistr // NOT a silent "fake success" failure. Used to avoid rewriting a valid HTTP 200 // (e.g. a Claude Code `max_tokens: 1` connectivity ping) into a synthetic 502. const LEGIT_EMPTY_CLAUDE_STOP = new Set(["max_tokens", "tool_use"]); -const LEGIT_EMPTY_OPENAI_FINISH = new Set(["length", "tool_calls"]); +const LEGIT_EMPTY_OPENAI_FINISH = new Set(["length", "tool_calls", "content_filter"]); export function isEmptyContentResponse(responseBody: unknown): boolean { if (!responseBody || typeof responseBody !== "object") return false; diff --git a/tests/unit/empty-content-stopreason-3572.test.ts b/tests/unit/empty-content-stopreason-3572.test.ts index ea5fb4f8b1..b9676254c1 100644 --- a/tests/unit/empty-content-stopreason-3572.test.ts +++ b/tests/unit/empty-content-stopreason-3572.test.ts @@ -62,6 +62,24 @@ test("#3572 OpenAI: empty content + finish_reason=stop stays flagged (fake-succe ); }); +test("#3572 OpenAI: empty content + finish_reason=content_filter is NOT empty-failure (safety-filtered response)", () => { + assert.equal( + isEmptyContentResponse({ + choices: [{ index: 0, message: { content: "" }, finish_reason: "content_filter" }], + }), + false + ); +}); + +test("#3572 OpenAI: empty content + finish_reason=content_filter (stream chunk) is NOT empty-failure", () => { + assert.equal( + isEmptyContentResponse({ + choices: [{ index: 0, delta: { content: "" }, finish_reason: "content_filter" }], + }), + false + ); +}); + test("#3572 regression: non-empty content is never flagged", () => { assert.equal(isEmptyContentResponse({ content: [{ type: "text", text: "hi" }] }), false); assert.equal(