Repository navigation
fix(proxy): return the upstream error when a collapsed Responses stream fails - #148
Conversation
…am fails When the ChatGPT sign-in path forces a streamed Responses request for a non-streaming call, the stream is collapsed back into one message. Only response.completed and response.incomplete were read; response.failed and error events were dropped, so a failed generation came back as an empty end_turn reply with HTTP 200. The streaming translator already turns both events into an error event. The collapse now throws OpenAIResponsesStreamError for them when no final response arrived, and the fetch wrapper answers with an api_error body and status 502 like its other upstream failures. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GoDiao
left a comment
There was a problem hiding this comment.
Thanks again! The fix is right. fetch.ts is the only caller, so the new throw stays contained, the 502 api_error body matches the other upstream failures there, and a later response.completed still wins. The tests cover each case.
A few small things before we merge:
-
Share the error lookup instead of copying it.
upstreamErrorMessageis a copy of the lookup inside theresponse.failed/errorcase inopenaiResponsesStreamToAnthropic.ts. If one copy changes later, the streaming and non-streaming paths will report different messages. Please extract it into an exported function there and call it from both places:// openaiResponsesStreamToAnthropic.ts export function upstreamErrorMessage(data: Record<string, unknown>): string { const response = data.response as Record<string, unknown> | undefined const error = (response?.error ?? data.error ?? data) as Record<string, unknown> | undefined return typeof error?.message === 'string' && error.message ? error.message : 'Upstream Responses stream failed' }
Then
const message = upstreamErrorMessage(data)in that case, and import it inopenaiResponsesStreamToAnthropicResponse.ts. This touches one more file than you intended, but it's a small extraction and moves with the #119 rework just as easily. -
No comments in new code. The project keeps new code free of comments, including JSDoc. This isn't written down in CONTRIBUTING yet, which is on us. Please drop the JSDoc on
OpenAIResponsesStreamErrorand the "Same message lookup…" comment; the shared function makes the second one unnecessary anyway. -
Rebase on main.
CHANGELOG.mdconflicts, because[Unreleased]on main already has a### Fixedsection. Moving your entry into it is enough.
Thanks for keeping these proxy fixes coming!
…g and non-streaming converters Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thanks for the review, and sorry for the delay. All three points are addressed in the pushed commits:
This update was prepared with Claude Code. |
GoDiao
left a comment
There was a problem hiding this comment.
Thanks! All three points are in, and the tests cover each case.
|
Merged, thanks @pcbeingused333! Sharing the error lookup keeps the streaming and non-streaming paths reporting the same message, and the late-completion test pins the one case that's easy to get wrong. That's your third proxy fix here, and each one has come with tests that fail on the old code, which makes them easy to review. Your CHANGELOG entry is in. If DreamCoder is useful to you, a ⭐ on the repo would mean a lot. Looking forward to the next one! |
When the ChatGPT sign-in path gets a non-streaming request, it forces a streamed Responses call and collapses the stream back into one message (
openaiResponsesStreamToAnthropicResponse). That function only readresponse.completedandresponse.incomplete. A stream that ends withresponse.failedorerrorfell through to the text-only fallback, so the caller got an emptyend_turnmessage with HTTP 200 and never saw the upstream error. The streaming translator already turns both events into an Anthropicerrorevent.The collapse now remembers the message of a
response.failedorerrorevent (same lookup as the streaming translator, with the same fallback text) and throwsOpenAIResponsesStreamErrorwhen no final response arrived. Aresponse.completedorresponse.incompletethat comes later still wins.buildOpenAICodexFetchcatches that error and answers with{ type: 'error', error: { type: 'api_error', message } }and status 502, the shape its other upstream failures use.This is the same collapse helper as #143, on the ChatGPT sign-in path that #119 is reworking; the change stays inside that helper and its one caller, so it carries over or drops out with that rework.
Tests:
proxy-streaming.test.tschecks thatresponse.failedanderrorevents (with and without a message) reject with the upstream message, and that a completed response after an earlier error is kept.fetch.test.tschecks the 502 body for a failed stream on a non-streaming call. On the previous code the fetch test gets HTTP 200 and the streaming test file does not compile its import.proxy-streaming,fetchandproxy-transformtests pass (75). This was only exercised with synthetic event streams, not against a live ChatGPT/Codex upstream. The CHANGELOG line is under Unreleased.This change was made with Claude Code.
🤖 Generated with Claude Code