Repository navigation
[Bug]: translated Responses→chat dispatch never wires claimAmbiguousResend — providers.<name>.retryOnReset cannot take effect and a pre-header reset always answers 429 #6510
Description
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} : {}) }
- 2.75.0 stock: md5
- Contrast, same gate wired elsewhere:
src/server/responses/passthrough-dispatch.ts:781-787definesclaimPreHeaderResend = () => authorizeResendForRecovery("pre-header", "connection-reset", ambiguousResend()).allowedand passes it asclaimAmbiguousResendat five call sites (L996, L1098, L1227, L1355, L1490).src/server/chat-native.ts:693wires the headers-only row viaauthorizeResendForRecovery("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):
- import
authorizeResendForRecoveryfrom../../lib/request-resend-gateandambiguousResendAllowanceFor/selfContainedResponsesBodyfrom./reset-replay; - take
claimAmbiguousResendout of the send budget (samePick<...>/ destructure shape as passthrough); - 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;- pass
claimAmbiguousResend: claimPreHeaderResendinto bothfetchWithResetRetryoption 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.
- 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: {} } }. - 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. - 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_refusedwithx-should-retry: false. - 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
github-actions commentedon Oct 3, 2026
Issue reopened
The report now contains the information required by the automated check. Thanks for updating it.
fix(responses): share reset replay grants across translated sends (#6555
lidge-jun commentedon Oct 4, 2026
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.
Update from upstream 2.80.0 while preserving fork integrations (#9)