fix(codex): unify live stream commit and terminal semantics - #103
Conversation
b0bc4fb to
ed725c9
Compare
|
Rebased this PR onto current The keepalive and stream-state changes are complementary. The single textual conflict was in adjacent tests; the rebased branch preserves both the keepalive retry coverage and the shared terminal/failure classifier coverage. I also fixed the cross-provider regression found during review:
Verification after the rebase:
Keeping the PR in Draft for now, as requested, while the expanded compatibility results are reviewed. |
|
Follow-up: the expanded compatibility review is complete, so the previous Draft note is no longer current.
The focused regression suite, full Rust test suite, clippy, formatting, release build, and CI checks are green. The PR is now marked ready for review. |
Summary
response.output_item.addedframes private until actual text, reasoning, tool arguments, or a finalized tool item makes replay unsafe;response.incomplete, while allowing only OpenCode Go Responses to translate the exactmax_output_tokensterminal into Claude's normalmax_tokensstop;response.failedandresponse.incompleteas typed failures instead of successful completion, while preserving permanent policy, quota, permission, and context-window errors;codex.rate_limitsas telemetry while the response stream continues;message_stop; the existing WebSocket missing-terminal compatibility path remains intact.Reproduced failures
The regression fixtures use raw TCP/SSE and WebSocket upstreams, including an inflated
Content-Lengthfollowed by an early socket close.502, no replayevent: errorresponse.incompleteafter partial textmax_tokenscompletionevent: error, without syntheticmessage_stopresponse.incomplete/max_output_tokens502after the shared Codex fixstop_reason=max_tokensplusmessage_stop; other incomplete reasons remain errors502 api_error502413 request_too_large,429 rate_limit_error, or403 permission_errorcodex.rate_limitsfollowed by a valid completion429tool_usecompletionevent: errorWhy these boundaries
Pinned native Codex
rust-v0.146.0(e363b08c) treatsOutputItemAddedas stream-state initialization. Tool execution is queued only from the finalizedOutputItemDone: turn lifecycle, tool queue. That makes an added-only item safe to discard and replay as long as no semantic output has been exposed downstream.The same Codex parser returns errors for both
response.failedand everyresponse.incomplete; onlyresponse.completedbecomes a successful completion. It also gives special fatal handling to context-window, quota, usage, cyber-policy,invalid_prompt, andbio_policycodes: Responses event parser, retry classification.That strict policy is provider-scoped. OpenCode Go's Responses-compatible lane can legitimately terminate with
response.incompleteandincomplete_details.reason=max_output_tokens; the opt-in is therefore limited to that exact event/status/reason tuple. Missing, malformed,content_filter, duplicate, and post-terminal events still fail. OpenAI documentsresponse.incompleteas the terminal event for an incomplete response and exposesincomplete_detailson the response object: Responses streaming event, Responses object.Codex forwards rate-limit snapshots as
ResponseEvent::RateLimitsand continues consuming the response instead of treating the snapshot itself as a terminal API error: WebSocket reader, turn handling.CCP pins
reqwest 0.12.28. Its response-body APIs wrap underlying body-stream failures asDecode, which is why a truncated HTTP response can surface aserror decoding response bodyrather thanis_body(): body implementation, error classification and source chain, Decode construction.Anthropic streams distinguish normal
message_stopcompletion from an in-streamevent: error; the SDK raises on the latter instead of synthesizing success. Transparent retry after already-yielded semantic output is also intentionally avoided because it can duplicate content: streaming contract, Python SDK parser, maintainer clarification.Compatibility notes
response.incomplete -> max_tokenschoice retained in #70 / #71, because the pinned native Codex parser treats every incomplete response as an error. OpenCode Go Responses opts into only the exactmax_output_tokenscase, without weakening Codex semantics.Verification
cargo test --offline --all-targets --quiet— 987 passed, 0 failed;cargo +1.96.0 clippy --offline --all-targets -- -D warnings;cargo +1.96.0 fmt --all -- --check;cargo build --release --locked --offline;gpt-5.4) passed;response.incomplete/max_output_tokensand downstreamstop_reason=max_tokensplusmessage_stop, withoutevent:error;qwen3.8-max) completed Read, Edit, Bash/classifier, thinking, and PNG input through the rebased branch.