From 3bd6873c863d58f6a9a3174a27734d973462356a Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sun, 30 Aug 2026 19:26:49 +0900 Subject: [PATCH 1/2] fix(responses): keep the terminal-guard continuation on the shared transient budget 607042b0 introduced `transientRetryOn5xx` as ONE request-wide total-send budget and threaded a shared counter through the initial send and the 429/account-rotation refetches. The terminal-guard continuation was left on the raw policy value, so a request that reached it received a fresh full `attempts` allowance: with `attempts: 3` an initial send that spent its budget could still emit three more upstream sends on the continuation leg, which is exactly the multiplication the shared budget exists to prevent. Pass the remaining budget and `onSendsConsumed` at the continuation call site, so every leg of one request draws from the same ceiling. Also repairs a docs structure regression from 124a2b148: the new native-quota section was inserted directly after the `## Integration path` heading, leaving that heading with no body and stranding its paragraph at the end of the quota section. Only the English source was affected; all seven locales already place the quota section last. Verification: bun x tsc --noEmit clean; bun test tests/upstream-transient-retry.test.ts 15 pass. Local full suite deliberately not run (hosted exact-SHA CI is the gate). --- docs-site/src/content/docs/guides/codex-app-models.md | 10 +++++----- src/server/responses/core.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-app-models.md b/docs-site/src/content/docs/guides/codex-app-models.md index c84f8f6407..053fba1669 100644 --- a/docs-site/src/content/docs/guides/codex-app-models.md +++ b/docs-site/src/content/docs/guides/codex-app-models.md @@ -105,6 +105,11 @@ them by ignoring `visibility`. See [Codex Desktop native-allowlist compatibility for the command, disable-key semantics, and safety constraints. ## Integration path + +`ocx init`, `ocx start`, and `ocx sync` wire the shared Codex config and catalog into the proxy; see +[Codex Integration](/guides/codex-integration/) for config injection, catalog sync, shims, WebSocket +fallback, and restore mechanics. + ## Native quota fallback limitation When the Codex app exhausts its native five-hour quota it can switch to a reserve @@ -137,11 +142,6 @@ mode is active; if the client rewrites or refuses it before the request leaves, setting changes that. Treat the explicit-selection route as worth trying rather than a confirmed workaround. - -`ocx init`, `ocx start`, and `ocx sync` wire the shared Codex config and catalog into the proxy; see -[Codex Integration](/guides/codex-integration/) for config injection, catalog sync, shims, WebSocket -fallback, and restore mechanics. - ## Why routed models show up Codex's model picker expects Codex-shaped catalog entries. opencodex builds routed entries by cloning diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 96bb1f8ce5..1f56ed979a 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -6144,7 +6144,15 @@ async function handleResponsesInner( { abortSignal: upstream.signal, label: safeHostLabel(builtContinuationRequest.url), - ...(continuationTransientPolicy ? { attempts: continuationTransientPolicy.attempts } : {}), + // Same request-scoped budget as the initial send and the 429/rotation refetches: + // a terminal-guard continuation is another leg of ONE request, so handing it a + // fresh `attempts` would let one request exceed the configured total-send ceiling. + ...(continuationTransientPolicy + ? { + attempts: remainingTransientSendBudget(continuationTransientPolicy.attempts), + onSendsConsumed: noteTransientSends, + } + : {}), }, ); } finally { From 3998bb4390e260012b516023a9306f73d97a2740 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sun, 30 Aug 2026 19:29:30 +0900 Subject: [PATCH 2/2] test(responses): pin every transient-retry leg to the shared send budget The runtime tests prove `fetchWithTransientRetry` reports what it spent and honors a remainder, but they cannot prove every call site asks for one: a leg that forgets simply passes a larger number and stays green. This is a source oracle over the three legs in core.ts (initial send, 429/rotation refetch, terminal-guard continuation) asserting the shared counter is declared once and that both post-initial legs request the remainder. Driven red against the exact regressed shape (`attempts: continuationTransientPolicy.attempts` with no `onSendsConsumed`): 1 fail, then green once the leg draws from the counter. Verification: bun test tests/transient-budget-scope-source.test.ts tests/upstream-transient-retry.test.ts 17 pass; bun x tsc --noEmit clean. --- tests/transient-budget-scope-source.test.ts | 52 +++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/transient-budget-scope-source.test.ts diff --git a/tests/transient-budget-scope-source.test.ts b/tests/transient-budget-scope-source.test.ts new file mode 100644 index 0000000000..437973ba32 --- /dev/null +++ b/tests/transient-budget-scope-source.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const source = (relative: string): string => + readFileSync(join(import.meta.dir, "..", "src", ...relative.split("/")), "utf8"); + +/** + * `transientRetryOn5xx.attempts` is ONE request-wide total-send budget, not a per-leg + * allowance. A Responses request can reach upstream on several legs — the initial send, a + * 429/account-rotation refetch, and the terminal-guard continuation — and each leg calls + * `fetchWithTransientRetry` separately. The budget only holds if every leg draws from the + * shared request-scoped counter. + * + * The continuation leg shipped on the raw policy value instead, so a request that reached it + * received a fresh full `attempts` allowance: with `attempts: 3` an initial send that had + * already spent its budget could still emit three more upstream sends. Runtime coverage in + * `tests/upstream-transient-retry.test.ts` proves the helper reports and honors a remainder; + * it cannot prove that every call site asks for one, because a site that forgets simply + * passes a larger number. This asserts the wiring at the source, which is the only place the + * omission is visible. + */ +describe("transient send budget stays request-scoped", () => { + test("every transient-retry call site draws from the shared counter", () => { + const core = source("server/responses/core.ts"); + + // One owner per request, declared before any leg can send. + expect(core.match(/let transientSendsUsed = 0;/g)).toHaveLength(1); + expect(core.match(/const remainingTransientSendBudget = \(budget: number\): number =>/g)).toHaveLength(1); + + // Initial send, 429/rotation refetch, and terminal-guard continuation: three legs, three + // reports into the same counter. + expect(core.match(/onSendsConsumed: noteTransientSends/g)).toHaveLength(3); + + // The refetch and continuation legs must ask for the REMAINDER. Only the initial send may + // pass a policy value directly, because nothing has been spent yet. + expect(core.match(/attempts: remainingTransientSendBudget\(/g)).toHaveLength(2); + expect(core).toContain("attempts: remainingTransientSendBudget(refetchTransientPolicy.attempts)"); + expect(core).toContain("attempts: remainingTransientSendBudget(continuationTransientPolicy.attempts)"); + + // The regressed shape: a leg handing itself a fresh full budget. + expect(core).not.toContain("attempts: continuationTransientPolicy.attempts }"); + expect(core).not.toContain("attempts: refetchTransientPolicy.attempts }"); + }); + + test("the helper still exposes the seam those call sites depend on", () => { + const retry = source("lib/upstream-retry.ts"); + expect(retry).toContain("onSendsConsumed?: (sends: number) => void;"); + // Reported in `finally` so every exit path — return, throw, abort — feeds the counter. + expect(retry).toMatch(/} finally \{\n\s*opts\.onSendsConsumed\?\.\(sent\);/); + }); +});