Skip to content

[Bug]: translated Responses→chat dispatch never wires claimAmbiguousResend — providers.<name>.retryOnReset cannot take effect and a pre-header reset always answers 429 #6510

Closed

Description

@zgc

Client or integration

Codex CLI

Area

Provider adapter

Summary

On the translated dispatch lane (src/server/responses/adapter-dispatch.ts, the responses-to-chat adapter used when a Codex Responses request is served by a chat-wire provider such as OpenCode Go / DeepSeek), both fetchWithResetRetry calls pass only attempts / onSendsConsumed. The pre-header row of the ambiguous-resend gate (claimAmbiguousResend, #4942 / #5342) is never wired, so an upstream connection that closes before any response byte is answered with the fail-closed refusal upstream_reset_replay_refused (429, x-should-retry: false) even when the operator has opted in with providers.<name>.retryOnReset.

Net effect for the user: Codex (Responses in, chat-wire provider out) intermittently fails a turn with exceeded retry limit, last status: 429 Too Many Requests on an upstream that is neither rate-limiting nor out of quota, and the one send the operator's opt-in paid for is never spent.

Where

  • src/server/responses/adapter-dispatch.ts
    • 2.75.0 stock: md5 ecaaf3d7b067f803af7987ac718a71c4
    • 2.76.0 stock: md5 1edd3e8725cdbf423ab7c676061abbeb
    • both: grep -c claimAmbiguousResend = 0, grep -c retryOnReset = 0
    • the two calls at L346 and L545 pass { abortSignal, label, ...(policy ? {attempts, onSendsConsumed} : {}) }
  • Contrast, same gate wired elsewhere:
    • src/server/responses/passthrough-dispatch.ts:781-787 defines claimPreHeaderResend = () => authorizeResendForRecovery("pre-header", "connection-reset", ambiguousResend()).allowed and passes it as claimAmbiguousResend at five call sites (L996, L1098, L1227, L1355, L1490).
    • src/server/chat-native.ts:693 wires the headers-only row via authorizeResendForRecovery("headers-only", "connection-reset", ambiguousResend()).allowed.

Why the client surfaces it as a hard failure

src/lib/upstream-retry.ts:667-675:

if (attempt + 1 >= attempts || opts.claimAmbiguousResend?.() !== true) {
  return replayRefusalResponse();
}

replayRefusalResponse() answers 429 on purpose: the comment on UPSTREAM_RESET_REPLAY_REFUSED_CODE (L70-78) says the Codex client runs retry_429: false, so a 5xx here would multiply the duplicate send the refusal exists to prevent. That is correct for an un-opted-in operator. On this lane it also applies to an opted-in one, because the callback is absent; the refusal is then indistinguishable from "no policy", which is the fail-closed answer by design.

Suggested fix

Mirror the passthrough wiring in adapter-dispatch.ts (5 hunks; this is what our local patch does):

  1. import authorizeResendForRecovery from ../../lib/request-resend-gate and ambiguousResendAllowanceFor / selfContainedResponsesBody from ./reset-replay;
  2. take claimAmbiguousResend out of the send budget (same Pick<...> / destructure shape as passthrough);
  3. memoize the judgment and route the decision through the gate:
let selfContainedJudgment: boolean | undefined;
const requestIsSelfContained = (): boolean =>
  selfContainedJudgment ??= selfContainedResponsesBody(parsed._rawBody);
const ambiguousResend = () =>
  ambiguousResendAllowanceFor(route.provider, requestIsSelfContained, claimAmbiguousResend);
const claimPreHeaderResend = (): boolean =>
  authorizeResendForRecovery("pre-header", "connection-reset", ambiguousResend()).allowed;
  1. pass claimAmbiguousResend: claimPreHeaderResend into both fetchWithResetRetry option objects.

One caveat for the fix: the gate still requires attempts >= 2 (attempt + 1 >= attempts refuses at the first attempt when attempts is 1), and this lane currently only passes attempts when a transient policy exists, so the opted-in allowance has to reach the send budget the same way the passthrough lane does.

Secondary observation (not verified as deliberate): adapter-dispatch.ts never calls refetchAfterProtocolSafeReset, so the post-head authorize row is unwired on this lane too.

Reproduction

The gap is deterministic and does not need the real upstream. A loopback synthetic provider that accepts the TCP connection and closes it before writing any response byte is enough.

  1. Configure a provider with retryOnReset: {} (opt-in) plus a model routed over the chat wire, e.g. providers: { "synthetic-chat": { baseUrl: "http://127.0.0.1:<port>", retryOnReset: {} } }.
  2. Send a Codex Responses request (POST /v1/responses) whose model routes to that provider through the native Responses passthrough lane: the probe observes 2 upstream hits and the client gets 200.
  3. Send the same request through the translated lane (Responses in, chat-wire provider out), same provider config: the probe observes 1 upstream hit only, and the client gets 429 upstream_reset_replay_refused with x-should-retry: false.
  4. Apply the patch from "Suggested fix" and repeat step 3: 2 upstream hits, 200.

Observed in production as well (ocx 2.75.0, Codex CLI 0.154.0-alpha.6.1, ~/.opencodex/usage.jsonl), on an upstream that was neither rate-limiting nor out of quota:

{"provider":"combo","model":"auto","inboundProtocol":"responses","status":429,
 "errorCode":"upstream_reset_replay_refused","closeReason":"non_stream",
 "attempts":[{"ordinal":1,"adapter":"openai-chat","sendCount":1}],
 "upstreamError":"Provider error 429: The upstream exchange did not complete reliably. The request may already have been processed; automatic replay was stopped."}

In production the trigger is intermittent because it needs the upstream to close before the first byte, but whenever it happens the turn dies with exceeded retry limit, last status: 429 Too Many Requests instead of spending the opted-in resend.

Version

2.75.0 (2.76.0 checked: same gap)

Operating system

Ubuntu 24.04 x64 (Linux), bun runtime

Provider and model

OpenCode Go (chat wire) / deepseek-v4.1-flash. Client: Codex CLI 0.154.0-alpha.6.1. The gap is adapter-level, so any chat-wire provider is affected.

Logs or error output

exceeded retry limit, last status: 429 Too Many Requests

plus the usage.jsonl record quoted under Reproduction.

Redacted configuration

{
  "providers": {
    "opencode-go": { "retryOnReset": {} }
  }
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. github-actions commented on Oct 3, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  2. added
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Oct 3, 2026
  3. lidge-jun commented on Oct 4, 2026

    @lidge-jun
    Owner

    Fixed on dev by #6555, merged as 16d5dae.

    Translated generic Responses dispatch now uses the existing operator opt-in and shared replacement grant on both initial and rebuilt pre-header sends, with exact request/workflow accounting and preserved prepaid recovery capacity. Original regressions remain, and real loopback socket QA demonstrated two sends only for the eligible opt-in case; absent opt-in and stored requests stop after one with the non-replayable marker. The compaction regression found in CI was repaired without changing its original assertion, and final-head CI37178553374 passed before integration.

    This closure covers the reported pre-header wiring defect. Adapter-owned transports and translated failures after headers retain their existing policy; no live-provider or installed-runtime verification is claimed. The change is on dev, not part of the already-published2.77.0 release.

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 workingproviderProvider adapters, OpenAI-compat presets, upstream API quirks

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions