Codex Responses: CRLF event streams parse as an empty successful turn, the final event is dropped, and non-retryable 4xx are retried #1553
Jiaaqiliu
started this conversation in
Bug reports
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Environment. prime-agent 0.7.3,
mainat f8f0036, Node 22.17.0, macOS 15 (darwin 25.5.0).Summary
Three defects in the OpenAI Codex Responses provider, all on the same request/parse path. The first one is the concerning one: on a CRLF event stream the provider yields a successful, empty, zero-usage assistant turn instead of an error.
1.
parseSSEframes events on a literal"\n\n"providers/openai-codex-responses.ts:517:SSE separates events with a blank line, which on the wire is CRLF for a spec-compliant server and for anything behind a proxy that rewrites line endings.
\r\n\r\ncontains no two consecutive\n, soindexOfis permanently-1. No event is ever yielded,buffergrows for the whole response, andprocessResponsesStreamsees an empty iterator — sooutput.stopReasonkeeps its"stop"initializer and nothing raises.Feeding the repo's own event list from
test/openai-codex-stream.test.tsCRLF-delimited producescontent: [],usage: {..., totalTokens: 0},stopReason: "stop".providers/anthropic.ts:298-325already handles\r,\n, and\r\nvianextLineBreakIndex/consumeLine.parseSSEdoes not.2. The final event is dropped when the body does not end with a blank line
The read loop only emits what
indexOfcan carve out, thenbreaks straight into thefinally. There is no trailing flush ofbufferand no finaldecoder.decode(). For Codex the last event isresponse.completed, which sets usage, cost, and thetoolUsepromotion inopenai-responses-shared.ts:472. So the turn reports zero tokens and zero cost, and a tool-calling turn is reported asstop— the agent loop stops instead of running the tools.anthropic.ts:359-380does this correctly.3. The retry loop retries every non-retryable status
openai-codex-responses.ts:238:The throw that expresses "this status is not retryable" sits inside the same
try, so the genericcatchswallows it and retries anyway.isRetryableErroronly decides which branch does the sleeping. Mocking a400 {"error":{"code":"invalid_request_error","message":"bad model"}}produces 4 requests and about 7s of backoff. The same applies to401, which re-sends a known-bad token three extra times.Suggested fix
done.catch, soisRetryableErroractually decides.I have a fix with a regression test on a branch:
fix/codex-sse-framing-and-retries.npm run checkpasses and the surrounding suites still pass. I opened it as a PR first and the contribution gate closed it, which is what CONTRIBUTING.md says should happen, so I am bringing it here instead. Happy to leave it as is, adjust it, or drop it entirely if you would rather fix this differently.All reactions