[Bug]: Codex still retries an Anthropic refusal five times because response.incomplete ignores retryable:false (follow-up to #4312)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- rust, typescript
Research direction
Start in src/lib/errors.ts, where TERMINAL_REFUSAL_CODES already lists the refusal subset, then find the adapter-to-Responses bridge that emits response.incomplete. The codex-rs side is where the stream retry loop turns that event into a retryable error, so check how it handles incomplete_details. Done means a refused turn ends after one attempt with a readable error, and a test with a mock Messages endpoint returning stop_reason: "refusal" shows a single send.
Written by the indexing model from the issue text.
Description
Client or integration
Codex App
Area
Streaming
Summary
When an Anthropic upstream ends a turn with stop_reason: "refusal" (or content_filter), OpenCodex still sends Codex into its full stream-retry loop, so the same refused request is sent six times for one user turn. This is a follow-up to #4312: that fix made the Anthropic adapter emit an explicit non-retryable incomplete event, which the Responses bridge relays as response.incomplete with incomplete_details: { reason: "content_filter", retryable: false }.
codex-rs never reads incomplete_details.retryable. Every response.incomplete becomes a retryable stream error (stream disconnected before completion: Incomplete response returned, reason: content_filter), so the refused request is sent once and then re-sent through the whole stream retry budget (five more times). Each resend replays the full conversation context; on a long thread that was about 500k input tokens per attempt with zero output tokens.
Expected: a permanent upstream refusal ends the Codex turn on the first attempt with a readable error. Codex ends a turn without retrying only on response.failed whose error.code is in its fatal set (context_length_exceeded, insufficient_quota, usage_not_included, cyber_policy, misalignment_policy_violation, invalid_prompt, bio_policy), which is the same mapping confirmed while fixing #5176 in #5200. src/lib/errors.ts already lists the refusal subset as TERMINAL_REFUSAL_CODES, but the adapter-to-Responses bridge never uses it for an incomplete.
#7122 (normalizing incomplete reasons) is related but keeps retryability unchanged, so it does not stop the retries.
Reproduction
- Configure an Anthropic-compatible provider whose upstream ends a streamed turn with
message_delta.delta.stop_reason = "refusal". A local mock Messages endpoint that returns that stop reason reproduces it deterministically; in production it happened with real refusals on long threads. - From Codex (App or CLI), send one turn routed to that provider through
/v1/responses. - OpenCodex streams
response.incompletewithincomplete_details.reason = "content_filter"andretryable: false. - Codex shows the reconnect counter and finally
stream disconnected before completion: Incomplete response returned, reason: content_filter. The request log shows six sends of the same request for that single user turn.
Version
2.76.0 (also observed on 2.64.0); the code path is unchanged on dev at 7640a942b
Operating system
macOS 26 (Darwin 25.6)
Provider and model
anthropic / claude-opus-5-5 and claude-opus-5
Logs or error output
stream disconnected before completion: Incomplete response returned, reason: content_filter
# Request log for ONE user turn (redacted): 6 rows within about 40 seconds, each
# status=502 terminalStatus=incomplete closeReason=terminal
# upstreamError="upstream ended the turn with stop_reason \"refusal\""
# inputTokens≈499,850 outputTokens=0
# A second occurrence on claude-opus-5: 12 rows for 2 user turns (6 per turn).
Screenshots and supporting files
No response
Redacted configuration
No response
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
- Dominant language
- TypeScript
- Stars
- 16.9k
- Forks
- 1.3k
- Avg merge
- 9h 16m
- Merged PRs (30d)
- 623
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from lidge-jun/opencodex
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
catalog
Difficulty 4/5 3-5 days Newbie friendliness 43/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 42/100
lidge-jun/opencodex#7677 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
bug catalog proxy
Difficulty 4/5 3-5 days Newbie friendliness 48/100
Maintainers usually reply within 1 day
-
bug tools
Difficulty 3/5 1-2 days Newbie friendliness 55/100
Maintainers usually reply within 1 day
All issues in lidge-jun/opencodex
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
farion1231/cc-switch#8829 ·
Maintainers usually reply within 1 day
-
[core-client] Remove unused hand-maintained transport helpers after the definition-table cutoverOpendoc-audit enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
Snapshot capture failure (EPERM spawning git) fails the whole step despite the best-effort contractOpenreproduced
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
anomalyco/opencode#58685 · 1 comment ·
Maintainers usually reply within 1 day