Skip to content

xAI policy 403 aborts Codex turns and retry loops the same user message #5277

Closed as not planned

Description

@sh940701

Summary

xAI sometimes refuses a turn with HTTP 403 and the body I can't help with that request. instead of HTTP 200 + finish_reason: content_filter.

Codex treats that 403 as a transport failure. The user message is never recorded as a completed assistant turn. Retrying the same thread resends the same pending user text and the 403 repeats. A new Codex thread on the same account and xai/grok-4.6 succeeds.

Grok Build (native Grok CLI) does not get stuck: the refusal lands as a normal assistant message and the next user turn proceeds.

This is not a plan/subscription gate. Subscription 403s use different wording (need a Grok subscription, credits, not allowed to use this model). Those should stay errors.

Reproduction

  1. Codex → OpenCodex → xai/grok-4.6 (OAuth or key).
  2. In a long thread, send a request xAI's policy classifier refuses.
  3. Observe unexpected status 403 Forbidden: Provider error 403: I can't help with that request., url: http://127.0.0.1:10100/v1/responses.
  4. Retry / “continue” in the same thread → same 403 (~1s, no tokens).
  5. New thread, trivial prompt → 200.

OpenCodex usage log for the failing request: status: 403, errorCode: permission_denied, upstreamError: Provider error 403: I can't help with that request., closeReason: non_stream.

Dashboard copy currently labels this as a plan/subscription gate. That label is wrong for this body.

Expected

The refusal is recorded as a completed (incomplete/content_filter) assistant turn. The next, different user message proceeds.

Actual

Codex surfaces a 403 transport error. The user message stays pending. Retries loop.

Version

OpenCodex 2.31.0 (reproduced). Latest 2.60.0 still maps this 403 through the generic error path (#5200 makes refusals terminal errors, which is correct for retry, but does not rewrite them into an in-band turn).

Proposed fix

Allowlist the refusal sentence. On HTTP 403 with that body, rewrite to a Chat Completions 200 with finish_reason: content_filter so the existing openai-chat adapter emits incomplete_details.reason = content_filter. Leave subscription/entitlement/model-access 403s unchanged.

Activity

  1. github-actions commented on Sep 20, 2026

    @github-actions
    Contributor

    Issue closed: use an issue template

    Thanks for taking the time to submit this issue. It was closed automatically because it was not opened with a recognized issue template (Bug report, Feature request, Documentation, or Provider compatibility). Freeform and API-opened issues cannot skip this check.

    • This looks like a bug report but it does not use the Bug report template headings (for example Description/Log entry instead of Summary).

    Please edit the issue to add:

    • Use the Bug report template, or edit this issue to include: Client or integration, Summary, Reproduction, Version, and Operating system.
    • Retitling with [Bug]: and applying the bug label alone is not enough without those section headings filled in.

    See the Contributing guide for details.

    Once the report passes the automated checks, it will be reopened automatically unless a maintainer has changed its state.

  2. sh940701 commented on Sep 20, 2026

    @sh940701
    ContributorAuthor

    Reopened until the fix lands on dev. The PR should close this on merge.

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

    account-poolOAuth, credentials, Codex pool, quota, failover, plans

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions