fix(stream): skip [DONE] terminator for Claude SSE clients - #2190
diegosouzapw merged 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request modifies the SSE stream utility to prevent emitting the [DONE] terminator for Claude-formatted streams, which avoids parser errors in specific SDKs. The reviewer suggests consolidating the logic for emitting the terminator into a single variable, shouldEmitDoneTerminator, to improve code readability and simplify the conditional checks in multiple locations.
| await emitFinalSseMetadata(controller, usage); | ||
| doneSent = true; | ||
| if (!clientExpectsResponsesStream) { | ||
| if (!clientExpectsResponsesStream && !clientExpectsClaudeStream) { |
| await emitFinalSseMetadata(controller, state?.usage as Record<string, unknown> | null); | ||
| doneSent = true; | ||
| if (!clientExpectsResponsesStream) { | ||
| if (!clientExpectsResponsesStream && !clientExpectsClaudeStream) { |
Anthropic SSE streams terminate naturally on message_stop — there is no `data: [DONE]` line. OmniRoute was unconditionally appending one at end of every stream (gated only on OPENAI_RESPONSES), which: - Capy (Anthropic SDK) sees an extra unparseable line after message_stop. Result: text content gets rendered in the "Thought" area of the UI, follow-up turns retry from a corrupt state. - Native claude-cli, claude-code, and other Anthropic SDK consumers hit the same parse hiccup but tolerate it differently. Add `clientExpectsClaudeStream` gate alongside the existing `clientExpectsResponsesStream`. Both the passthrough and translate finalization branches now check both flags before emitting `[DONE]`. For Claude clients: stream ends after message_stop, with the trailing `: x-omniroute-*` metadata comments. Standards-compliant SSE — no terminator line needed. Tested with Capy BYOK → Opus 4.7: first-turn thinking renders in the correct UI section; followup turns no longer trigger a retry loop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7b06630 to
330bb5e
Compare
|
Addressed @gemini-code-assist's review — refactored to consolidate the gate into a single |
|
Thanks for the update, NomenAK. Consolidating the gate into |
|
Thanks @NomenAK. I synced this branch with release/v3.8.0, added stream coverage proving Claude SSE clients do not receive a |
…apw#2190) Integrated into release/v3.8.0 after syncing the contributor branch and validating tests/unit/stream-utils.test.ts locally.
…apw#2190) Integrated into release/v3.8.0 after syncing the contributor branch and validating tests/unit/stream-utils.test.ts locally.
Summary
Anthropic SSE streams terminate naturally on the
event: message_stopframe — they do not emit an OpenAI-styledata: [DONE]\n\nterminator. Emitting one to Claude clients (Capy, claude-cli, Anthropic SDK) causes parser errors and observable UI artifacts (assistant text rendered in the wrong panel, followups looping with retries).Gate the
[DONE]emission so it only fires for OpenAI clients (Chat Completions / Responses). Symmetric to the existingclientExpectsResponsesStreamgate that already silences[DONE]for the Responses API.Related Issues
Motivation
Captured artifact for a
/v1/messagesBYOK call to Claude Opus through OmniRoute :data: [DONE]is not part of the Anthropic Messages SSE spec (docs). When the client encounters it aftermessage_stop, the strict parsers in the Anthropic SDKs raiseUnexpected event "[DONE]"and the client either drops the response or misclassifies the trailing text as a stream error, surfacing as : "the assistant's reply is rendered in the wrong UI section / retries the request".The OpenAI Responses API has the same constraint — it terminates on
response.completed, not[DONE]— and the codebase already has aclientExpectsResponsesStreamgate for that case. This PR is the symmetric gate for Claude clients.Changes
open-sse/utils/stream.ts:clientExpectsClaudeStreamalongside the existingclientExpectsResponsesStreamflag.[DONE]emission sites (post-stream end and post-error) now check!clientExpectsResponsesStream && !clientExpectsClaudeStreambefore writing the terminator.Validation
[DONE]no longer appears aftermessage_stopon/v1/messagescalls.[DONE](regression check).[DONE](existing behavior preserved).🤖 Generated with Claude Code