Files
OmniRoute/open-sse/utils/streamClaudeEmptyBody.ts
diegosouzapw 5eede28fa2 fix(sse): surface an error for a truly empty Claude stream (#12398)
Extend the Claude empty-response detector in open-sse/utils/stream.ts to
also catch an upstream connection that opens (HTTP 200) and closes having
sent literally zero bytes -- no message_start at all. The existing
hasClaudeAssistantLifecycle() gate only fired once a lifecycle event had
been observed, so this shape silently completed the client stream with a
200 and no content instead of surfacing a 502, matching the reported
symptom for claude-fable-5-max past ~1800 messages.

New streamClaudeEmptyBody.ts module keeps the frozen stream.ts file size
unchanged while adding the combined partial-lifecycle + truly-empty check.
2026-09-10 16:01:56 -03:00

35 lines
1.4 KiB
TypeScript

/**
* #12398 — decides whether a Claude-format stream must be aborted with an
* upstream error at flush time because the client got no usable content.
*
* Covers two shapes:
* - "partial lifecycle": message_start (and optionally message_delta /
* message_stop) arrived but no content block ever did — this was already
* correctly handled before #12398 and is preserved here unchanged.
* - "truly empty": the upstream connection closed having sent literally
* zero bytes (HTTP 200, not even a message_start). The lifecycle flags
* above can never catch this shape since none of them are ever set — the
* caller must additionally know whether ANY upstream chunk ever arrived.
*
* Callers must additionally require a Claude-format client (this function
* does not take that flag — both call sites in stream.ts only ever reach
* here already scoped to a Claude-format response).
*/
type ClaudeEmptyLifecycleLike = {
hasError: boolean;
hasContentBlock: boolean;
hasMessageStart: boolean;
hasMessageDelta: boolean;
hasMessageStop: boolean;
};
export function shouldAbortEmptyClaudeStream(
lifecycle: ClaudeEmptyLifecycleLike,
sawAnyUpstreamPayload: boolean
): boolean {
if (lifecycle.hasError || lifecycle.hasContentBlock) return false;
const hasPartialLifecycle =
lifecycle.hasMessageStart || lifecycle.hasMessageDelta || lifecycle.hasMessageStop;
return hasPartialLifecycle || !sawAnyUpstreamPayload;
}