Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[Bug]: Codex still retries an Anthropic refusal five times because response.incomplete ignores retryable:false (follow-up to #4312)

Closed
#7,148 0 comments 0 reactions 0 assignees View on GitHub

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
Domain
api, backend

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

bug streaming
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
  1. 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.
  2. From Codex (App or CLI), send one turn routed to that provider through /v1/responses.
  3. OpenCodex streams response.incomplete with incomplete_details.reason = "content_filter" and retryable: false.
  4. 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from lidge-jun/opencodex

All issues in lidge-jun/opencodex

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.