Files
OmniRoute/tests/unit
initguru d70f43d4b4 fix(sse): retry empty_response 502 + reasoning-aware direct response-start timeout (#12906)
* fix(sse): retry 0-byte empty_response 502 like STREAM_EARLY_EOF to stop autocompact 502

A genuine 0-byte upstream empty response (GLM-5.2 on a huge autocompact
context returns ONLY reasoning_content or nothing, then closes) reaches
stream.ts::emitClaudeEmptyStreamErrorAndAbort which emits a 502 with
code "empty_response" via the onFailure callback AND propagates the
failure down the pipeline as controller.error(new Error(msg)). The plain
Error carries no .code, so getUpstreamErrorIdentifier (reads only
error.code) returns undefined, result.errorCode/result.errorType become
undefined, and the single-model retry guard (chat.ts) only matches
errorType === "stream_early_eof" / errorCode === "STREAM_EARLY_EOF".
The 502 surfaces to the client with no re-attempt (call logs
1788132529140-96ef4a / 1788142914004-062cf6, ~48s, tokens out=0).

This is the same class of transient upstream glitch STREAM_EARLY_EOF was
built for (HTTP 200 then zero useful frames — #3758), but empty_response
was never wired into the retry path.

Fix (three chokepoints, all required for consistency):
- stream.ts: emitClaudeEmptyStreamErrorAndAbort now propagates an Error
  carrying code="empty_response" so a downstream classifier can identify
  it (plain new Error(msg) dropped it).
- chatHelpers.ts: shouldRetryStreamEarlyEof now treats "empty_response"
  the same as "STREAM_EARLY_EOF" via RETRYABLE_STREAM_EMPTY_CODES Set —
  ONE bounded same-connection re-attempt, never a loop
  (STREAM_EARLY_EOF_MAX_RETRIES=1 unchanged).
- chat.ts: the single-model retry guard now also enters on
  errorCode === "empty_response".

The bounded retry never marks the account unavailable (an empty response
is a transient upstream glitch, not a bad key), mirroring #3758.

Tests: 5/5 (stream-empty-response-retry-96ef4a). Existing 3758 regression
guard stays green (5/5). typecheck:core clean.

* fix(sse): make direct response-start timeout reasoning-aware to stop 504 on high-effort TTFB

Reasoning models (GLM-5.2/5.3 reasoning.effort=high/max, codex-gpt-5.x-high,
third-party Claude-format replicas) warm up with a ~78s+ TTFB before
emitting the first byte. The stream-readiness layer (streamReadinessPolicy)
already budgets 180s for this class, but the fetch-layer guard
(resolveDirectHeadersTimeoutMs) was a flat 30s — it pre-empted a warm
reasoning response the readiness layer would have permitted, surfacing a
504 (regression introduced by 142ae9349).

Fix: resolveDirectHeadersTimeoutMs now accepts the request body and, when
hasHighReasoningEffort(body) matches a quoted "reasoning_effort" or nested
"effort" field with value high/max, raises the budget to
REASONING_READINESS_CEILING_MS (180_000) — aligning to the same ceiling the
readiness layer uses. The operator env override (OMNIROUTE_DIRECT_HEADERS
TIMEOUT_MS) is treated as a FLOOR: reasoning awareness only raises the
budget, never lowers it; an override above the ceiling (e.g. 240s) is
preserved.

proxyFetch.ts passes the request body (when it is a string) to
resolveDirectHeadersTimeoutMs so the budget is per-request.

The HIGH_REASONING_EFFORT_PATTERN is a bounded, non-overlapping regex
(no variable-length quantifier overlap) — no ReDoS surface (PII rule #1).

Tests: 7/7 (direct-response-start-timeout-reasoning-504 — flat default,
env override, high/max ceiling bump, floor semantics, non-reasoning
pass-through). typecheck:core clean.

* docs(changelog): add fragments for empty_response 502 retry + reasoning-aware timeout

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Jihyun Son <jihyun.son@sk.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 02:30:15 -03:00
..
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00
2026-08-26 14:25:01 -03:00