Files
OmniRoute/changelog.d/fixes/1382-streaming-empty-content-block.md
Diego Rodrigues de Sa e Souza dedf680231 fix(combo): detect empty content_block in streaming SSE peek (#7121)
* fix(combo): detect empty content_block in streaming SSE peek (port from 9router#1382)

The bounded SSE peek in validateResponseQuality() treated ANY content_block_start/delta/stop event as proof of real output and stopped buffering immediately, without checking whether the block actually carried text/tool_use content. Some upstreams (reported: DeepSeek, GLM via claude→openai translation) can open and close a text content_block with empty text and no tool_use on tool-heavy requests — the gateway logged success and forwarded a client-visible empty completion, and combo routing never failed over to the next model.

Track real content separately from 'a content_block_* event was seen': a tool_use/redacted_thinking block start is self-evidently real signal, a text/thinking block start is not (real content only confirmed via a subsequent delta carrying non-empty text/thinking, or an input_json_delta streaming tool arguments). A completed lifecycle (message_start + message_delta/stop) that never produced real content now fails validateResponseQuality(), matching the existing content_filter empty-stream detection path (#3685).

Reported-by: heishen6 (https://github.com/decolua/9router/issues/1382)

* refactor(combo): extract SSE lifecycle applier to keep the complexity ratchets at baseline

The #1382 empty-content_block peek added a branchy switch inline in
parseAccumulatedSse, pushing check:complexity to 2057 > baseline 2056.

Move the switch to a module-level applySseLifecycleEvent() and hold the
four lifecycle booleans in a single SseLifecycleFlags object threaded
through it, so the closure no longer copies flags in and out per event.
The per-event predicates (content_block_start / content_block_delta /
message_delta) are split into small guard helpers, which keeps the
applier flat — cognitive complexity punishes nesting, and an earlier
switch-only extraction traded the cyclomatic ratchet for a cognitive
regression at 891 > 890.

Logic is unchanged; both ratchets are now green (complexity 2055,
cognitive-complexity 890) and the #1382 regression tests still pass.
2026-07-16 14:14:12 -03:00

383 B

  • fix(combo): streaming Claude responses whose content block opens (content_block_start) and closes with no usable text/tool_use — a shape some upstreams return for tool-heavy requests on HTTP 200 — are now detected by validateResponseQuality's SSE peek and trigger combo failover instead of being forwarded to the client as a silent empty completion (thanks @heishen6).