Skip to content

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

Closed

Description

@nekonade

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 7640a94

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.

Activity

  1. added
    bugSomething isn't working
    streamingSSE, WebSocket, terminal stream frames
    on Oct 15, 2026
  2. Ingwannu commented on Oct 16, 2026

    @Ingwannu
    Owner

    The concrete refusal/retry gap is addressed by #7152, now carried into #7218. This is separate from #7122's incomplete-reason vocabulary normalization: an explicit permanent refusal needs a terminal failure contract that the client treats as fatal.

    Neither source/carry is merged at the inspected snapshot. Keeping the issue open until that change lands on dev; current-head verification and the client retry contract remain required. No live refusal request was replayed in this pass.

  3. Ingwannu commented on Oct 16, 2026

    @Ingwannu
    Owner

    The previously pending integration is now on dev: #7218, merge commit 3785915d780fe3b8b13c551c253cb478c504840e. I verified ancestry against dev@c03eafd46b; the carry contains the fix linked in the earlier triage comment.

    Closing as completed on dev. The bundle contains its own verification and documented limits; I did not rerun those runtime tests or confirm the reporter's live client. This does not assert release/package availability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstreamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions