You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
keep provisional Codex function-call events private until an authoritative response.output_item.done arrives with the complete call;
retry a truncated HTTP response when only provisional tool arguments were received, without leaking the abandoned tool ID, name, or partial JSON to Claude Code;
translate a completed function call into one atomic Anthropic tool block, including standalone output_item.done events without output_index or a preceding output_item.added;
validate authoritative function arguments as a bounded JSON object before emitting content_block_stop, normalizing empty input to {};
preserve completed tool calls while dropping any unrelated provisional call left behind by a later body failure or terminal response;
prohibit exact request replay after non-empty reasoning/text or a completed hosted/function tool;
keep the stalled-Read recovery as a narrow bounded compatibility exception;
map policy, quota, permission, context-window, and payload-size failures into stable Anthropic client statuses/types while preserving the upstream message and applicable retry metadata.
Problem
The incremental Codex HTTP reader previously treated every non-empty response.function_call_arguments.delta as committed semantic output.
If the upstream HTTP body was reset while a function-call JSON object was still being streamed, CCP had already emitted an Anthropic tool_use start and partial input_json_delta. The retry window was therefore closed even though Claude Code could not execute the incomplete tool. The request ended with an in-stream API error and the agent terminated early.
The deterministic raw-TCP fixture reproduces this by advertising a larger Content-Length, sending a partial function call, and closing the first connection before the body is complete.
Scenario
Before
After
body reset during Bash arguments
one attempt, partial failed tool leaks, then event: error
failed attempt is discarded; second attempt produces one complete tool block
two provisional calls, then reset
both partial calls can become visible
neither failed call is visible; retry starts from a clean attempt
completed tool B while call A remains provisional
A can block or corrupt B's lifecycle
B is emitted atomically and becomes the commit boundary; A remains hidden
terminal response with an unfinished provisional call
incomplete tool can be synthesized as success or protocol noise
provisional call is dropped; completed text/tools retain normal terminal semantics
standalone function output_item.done without index
item can be lost or collide with output index 0
complete tool is emitted once, in arrival order
Protocol boundary
This change follows the current native Codex lifecycle rather than treating argument deltas as authoritative:
the Codex Responses parser does not require output_index in its stream event structure;
response.function_call_arguments.delta and .done are not used as the executable function call;
response.output_item.done is deserialized as the final ResponseItem;
native Codex persists and schedules the tool from that completed item before response.completed;
official function-call fixtures contain a standalone output_item.done with full call_id, name, and arguments.
The downstream side uses Anthropic's normal stream contract: an incomplete input_json_delta is not a complete tool, while the emitted content_block_stop finalizes the tool block. CCP therefore releases the start, complete JSON delta, and stop atomically with respect to the replay decision.
This implementation deliberately uses a stricter recovery boundary than the other current open-source Codex/Claude proxy paths we reviewed.
Behavior
This change
Other implementations
provisional function arguments
remain private and replayable
CLIProxyAPI and auth2api start an Anthropic tool block on output_item.added and forward argument deltas immediately
authoritative tool boundary
full output_item.done.item.arguments only
OpenCode uses the same completed-item boundary; CLIProxyAPI/auth2api can complete from accumulated deltas
malformed final arguments
rejected before a tool block is committed; empty input becomes {}
OpenCode also parses final input as JSON; CLIProxyAPI/auth2api expose partial argument strings before final validation
body reset after HTTP 200
bounded retry while no semantic/tool commit exists
CLIProxyAPI normally returns an incomplete-stream 408; auth2api and Claude Code Router do not restart the body after a successful response object was returned
replay after visible output
forbidden after thinking, text, hosted tool, or function tool commit
OpenCode's session retry can preserve partial output and run the stream again
EOF and terminal validation
explicit terminal required; malformed/incomplete frames are distinct errors
the inspected auth2api and Claude Code Router relay paths derive completion from body/connection lifecycle rather than an equivalent semantic-terminal validator
no equivalent aggregate byte cap was found in the inspected OpenCode/auth2api translator paths; CLIProxyAPI bounds an individual scanner token
ambiguous tool identity
optional aliases are matched only when unambiguous
CLIProxyAPI falls back to the last call when event identity is missing
Primary-source references:
CLIProxyAPI emits tool_use at item-added time and forwards partial arguments immediately: item added, argument deltas.
CLIProxyAPI turns a truncated Codex stream into a request-scoped incomplete error instead of replaying the pre-commit attempt: terminal error, retry exclusion.
OpenCode independently confirms that a tool becomes executable only from the completed call and parses final input as JSON: Responses completion, tool finalization.
Claude Code Router's own retry/fallback loop ends once a successful response object has been returned; body-level conversion is delegated to an external gateway package: executor, stream EOF handling, gateway dependency.
CLIProxyAPI has a useful stronger ordering policy for interleaved blocks: it preserves first-added order by delaying a completed later tool behind an older partial one. This change intentionally follows native Codex completion order instead: an authoritative completed tool is released immediately so an unrelated stalled provisional call cannot block it. CLIProxyAPI tests its first-added ordering; this change separately tests its completion-order policy: CLIProxyAPI parallel-order tests.
Retry and safety invariants
Provisional function-call events never reach Claude Code.
A body/decode/EOF/idle failure may restart the exact HTTP request only while no non-empty reasoning/text or completed function/hosted-tool item has been committed.
A complete function call is emitted as a single valid Anthropic block and immediately closes the replay window.
Non-empty reasoning and text retain the existing no-replay boundary. Native Codex can rebuild a retry from its persisted turn history; CCP cannot safely emulate that by replaying the same request body.
Completed tools remain visible if a later provisional tool is truncated; the incomplete tool remains invisible.
Provisional state is attempt-local and cleared on retry or terminal response.
Buffered state is bounded by a combined byte budget. Large ordinary Bash deltas are discarded rather than accumulated; only the special Read repair keeps bounded argument state.
Missing or ambiguous optional aliases cannot merge two provisional calls.
Authoritative arguments must be a bounded JSON object. Malformed or non-object values cannot cross content_block_stop; empty input is normalized to {}.
Upstream failures preserve their message; status and error type are normalized into the stable Anthropic client contract (including the intentional context-window 413 mapping). Retryable errors retain Retry-After while an HTTP response can still carry that header.
Downstream cancellation still aborts active reads and retry backoff.
cargo test --locked --offline --all-targets -- --skip cancellation_while_replacement_startup_is_blocked_aborts_request_state
focused Codex HTTP, live translator, and continuation tests
cargo build --release --locked --offline
The full test suite completed with 1,067 passed, 0 failed, and the one known baseline timing test filtered as described below.
A live Core compatibility matrix also exercised every currently listed model: 9 Codex models and 22 OpenCode Go models. Of the 31 routes:
14 passed the strict harness (including three models whose only distinction was that no visible thinking block was emitted);
8 completed the full file/classifier/tool flow but reported the bare upstream model ID in Claude assistant events;
4 completed the full flow and final marker but made additional diagnostic or repeated Bash calls beyond the harness's exact-one-call expectation;
5 were rejected before agent execution for provider-side reasons: two legacy Codex IDs are unsupported with ChatGPT accounts, two DeepSeek routes require the provider's China opt-in, and Grok returned a temporary endpoint-unavailable 502.
Every model that reached agent execution completed the file mutation/readback and final marker. The expensive final group was run last: kimi-k3 passed the strict harness, qwen3.8-max completed the full flow with the bare-ID reporting distinction above, and grok-4.5 hit the provider-side 502.
The skipped WebSocket cancellation timing test also times out on the unchanged PR base when run in isolation; it is not affected by this HTTP change.
The WebSocket transport keeps its existing retry policy.
Codex's default terminal and post-terminal validation remains strict.
OpenCode Responses reuses the shared live translator, so its completed-call path also receives the call-ID-aware standalone output_item.done handling; the existing OpenCode Responses compatibility tests remain green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
Summary
response.output_item.donearrives with the complete call;output_item.doneevents withoutoutput_indexor a precedingoutput_item.added;content_block_stop, normalizing empty input to{};Readrecovery as a narrow bounded compatibility exception;Problem
The incremental Codex HTTP reader previously treated every non-empty
response.function_call_arguments.deltaas committed semantic output.If the upstream HTTP body was reset while a function-call JSON object was still being streamed, CCP had already emitted an Anthropic
tool_usestart and partialinput_json_delta. The retry window was therefore closed even though Claude Code could not execute the incomplete tool. The request ended with an in-stream API error and the agent terminated early.The deterministic raw-TCP fixture reproduces this by advertising a larger
Content-Length, sending a partial function call, and closing the first connection before the body is complete.event: erroroutput_item.donewithout index0Protocol boundary
This change follows the current native Codex lifecycle rather than treating argument deltas as authoritative:
output_indexin its stream event structure;response.function_call_arguments.deltaand.doneare not used as the executable function call;response.output_item.doneis deserialized as the finalResponseItem;response.completed;output_item.donewith fullcall_id,name, andarguments.Sources:
OutputItemDoneThe downstream side uses Anthropic's normal stream contract: an incomplete
input_json_deltais not a complete tool, while the emittedcontent_block_stopfinalizes the tool block. CCP therefore releases the start, complete JSON delta, and stop atomically with respect to the replay decision.Comparison with other proxies
This implementation deliberately uses a stricter recovery boundary than the other current open-source Codex/Claude proxy paths we reviewed.
output_item.addedand forward argument deltas immediatelyoutput_item.done.item.argumentsonly{}ReadargumentsPrimary-source references:
tool_useat item-added time and forwards partial arguments immediately: item added, argument deltas.CLIProxyAPI's translator has richer interleaving queues but no aggregate byte bound for deferred events and accumulated arguments: parallel call resolution, unbounded argument accumulation, 50 MB scanner-token bound.
CLIProxyAPI has a useful stronger ordering policy for interleaved blocks: it preserves first-added order by delaying a completed later tool behind an older partial one. This change intentionally follows native Codex completion order instead: an authoritative completed tool is released immediately so an unrelated stalled provisional call cannot block it. CLIProxyAPI tests its first-added ordering; this change separately tests its completion-order policy: CLIProxyAPI parallel-order tests.
Retry and safety invariants
Readrepair keeps bounded argument state.content_block_stop; empty input is normalized to{}.Retry-Afterwhile an HTTP response can still carry that header.Prevented failure modes
smoke_codex_http_retries_body_error_mid_tool_arguments_without_leakagesmoke_codex_http_does_not_retry_after_reasoning_output,smoke_codex_http_body_error_after_closed_tool_is_not_successcall_id, then uses atomic fallbacksmoke_codex_http_emits_standalone_function_done_without_output_index,smoke_codex_http_emits_multiple_standalone_function_done_in_arrival_ordersmoke_codex_http_keeps_completed_tool_when_another_provisional_tool_truncates,smoke_codex_http_emits_completed_tool_before_another_provisional_tool_closesprovisional_tool_calls_tolerate_missing_and_ambiguous_aliases,standalone_function_done_does_not_close_a_different_open_toolsmoke_codex_http_rejects_invalid_authoritative_tool_before_emission_or_replay; all JSON shapes:completed_function_call_rejects_invalid_argument_shapes_before_commit{}smoke_codex_http_normalizes_empty_authoritative_tool_argssmoke_codex_http_rejects_oversized_authoritative_tool_args,completed_function_call_argument_limit_is_non_retryablesmoke_codex_http_caps_pending_events_plus_retained_read_args,pending_http_event_buffer_caps_retained_read_metadatastalled_read_probe_does_not_record_offset_rewritesmoke_codex_http_preserves_cyber_policy_behind_open_tool_barrier,smoke_codex_http_context_window_error_requests_compaction,codex_http_error_mapping_preserves_typed_client_contract,live_upstream_status_and_retry_after_are_preservedtx.closed()participates in the retry waitsmoke_codex_http_cancels_retry_backoff_when_request_dropsStalled
ReadcompatibilityThe existing whitespace-stalled
Readrepair remains intentionally separate from the normal Codex function-call lifecycle.It can finish locally only when:
Readcall with valid recoverable JSON;The speculative probe is side-effect free. Read-offset rewrite metadata is recorded only when the repaired tool is actually committed downstream.
Verification
Deterministic regressions cover:
output_index;Readcases;Executed checks:
cargo +1.96.0 fmt --all -- --checkcargo +1.96.0 clippy --locked --offline --all-targets -- -D warningscargo test --locked --offline --all-targets -- --skip cancellation_while_replacement_startup_is_blocked_aborts_request_statecargo build --release --locked --offlineThe full test suite completed with 1,067 passed, 0 failed, and the one known baseline timing test filtered as described below.
A live Core compatibility matrix also exercised every currently listed model: 9 Codex models and 22 OpenCode Go models. Of the 31 routes:
Every model that reached agent execution completed the file mutation/readback and final marker. The expensive final group was run last:
kimi-k3passed the strict harness,qwen3.8-maxcompleted the full flow with the bare-ID reporting distinction above, andgrok-4.5hit the provider-side 502.The skipped WebSocket cancellation timing test also times out on the unchanged PR base when run in isolation; it is not affected by this HTTP change.
Compatibility notes
output_item.donehandling; the existing OpenCode Responses compatibility tests remain green.