refactor(runtime): carve rotation-proxy seams (phase 1) - #532
Conversation
…ate-limit decisions Audit roadmap §4.1.3 phase 1: split lib/runtime-rotation-proxy.ts (2,498 -> 2,099 lines) along its natural seams with zero behavior change. All code moved verbatim with its private helpers/constants. - lib/runtime/rotation-server-types.ts (69 lines): server/request-context type definitions — RuntimeRotationProxyServer, RuntimeRotationProxyStatus, RuntimeRotationProxyOptions, RequestContext, ExhaustionReason, RuntimeProxyHttpError, RuntimeRotationAccountIdentity. Type-only module (imports only type names from accounts.js / prompts/codex.js). - lib/request/stream-failover-runtime.ts (170 lines): stream-failover orchestration — forwardStreamingResponse, readErrorBody, withTimeout, responseHeadersForClient, plus the HOP_BY_HOP / private-response / decoded-response header sets they filter on. All inputs explicit (upstream Response, ServerResponse, status object, stall timeout). - lib/request/rate-limit-decision.ts (211 lines): rate-limit decision tree — parseRetryAfterHeaderMs/BodyMs, getQuotaNearExhaustionWaitMs (+ private getQuotaWindowWaitMs), isTokenInvalidationError (+ phrase list), isTokenRefreshRetryable, buildTokenInvalidationBody, extractErrorCodeFromBody, normalizeExhaustionStatus, buildPinnedUnavailableErrorBody (+ PinnedUnavailableErrorBody). Pure functions over explicit inputs only. Importer compatibility preserved: every previously exported moved symbol (the three proxy interfaces, buildTokenInvalidationBody, PinnedUnavailableErrorBody, buildPinnedUnavailableErrorBody) is re-exported from runtime-rotation-proxy.ts, so lib/index.ts's star export, scripts/codex.js, scripts/codex-app-router.js, and all test imports keep working unchanged. New modules do not import the proxy module (no cycles). Left for phase 2 (entangled with the server closure / module state): the rotation loop in handleRequestInner, chooseAccount + linear-scan fallback, ensureFreshAccessToken + refresh-commit dedupe (WeakMap), storage-meta cache (STORAGE_META_CACHE), session-affinity invalidation, thread-goal fallback handling, and auth-cooldown mutators — threading that state through parameter lists would change call semantics. Verified: typecheck, eslint (--max-warnings=0), and the eight proxy test suites; the only failures (2x IPv6 ::1 bind, 3x Windows codex path) reproduce identically on a clean origin/main tree and are container environment failures, not regressions. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 WalkthroughSeverity: Minor (mechanical refactoring) This is phase 1 of a multi-phase extraction refactoring that carves ~400 lines from New modules:
Architectural note: This PR extracts only stateless, pure-function helpers and type definitions; closure state (status, affinity store, stale-reload dedupe), account selection logic, and token-refresh deduplication are intentionally deferred to phase 2 per the plan. No logging changes; new modules reference no proxy internals. Test coverage: 38 new deterministic tests added (commit 2) cover retry-after parsing, token-invalidation envelopes, quota waits, pinned-unavailable bodies, client header filtering, timeout behavior, error-body reading, and stream forwarding—including an explicit pinned bug expectation (stalled stream handling) deferred to follow-up. Greptile-recommended module header note on token-redaction for Known issue: A latent pre-existing bug in Walkthroughextracts token invalidation detection, retry-after parsing, quota exhaustion calculation, and streaming response logic from ChangesHelper Module Extraction
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes rationale: moderate heterogeneity across three new modules (decision logic, streaming I/O, types) plus refactoring of a large file, with dense parsing/timeout/streaming logic requiring careful attention to edge cases (malformed headers, timeout cleanup, stall detection, reader cancellation). test coverage is comprehensive but requires cross-checking implementation paths. the proxy refactoring itself is lower-friction—mostly imports—but depends on correctly understanding the extracted helpers. Possibly related PRs
key review notesedge cases and gaps:
concurrent/async concerns:
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
test/rate-limit-decision.test.tsOops! Something went wrong! :( ESLint: 10.0.0 Error: The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it. test/stream-failover-runtime.test.tsOops! Something went wrong! :( ESLint: 10.0.0 Error: The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Addresses the review note that the phase-1 extractions left
lib/request/rate-limit-decision.ts and lib/request/stream-failover-runtime.ts
covered only through proxy integration. 38 deterministic unit tests pin the
retry-after parsing (header + body), token-invalidation envelope, error-code
extraction, quota-near-exhaustion waits, pinned-unavailable body, client
header filtering, withTimeout, readErrorBody bounds, and the streaming
forwarder.
Writing these surfaced a latent pre-existing bug, pinned (not fixed) here:
on an upstream stream stall, withTimeout's onTimeout cancels the reader,
which settles the pending read() with {done: true} before the rejection,
so Promise.race resolves and the stall is treated as a clean end-of-stream
(truncated body, clean end(), no lastError, onStreamError never fires).
The fix lands in a stacked follow-up PR so this branch stays zero-behavior-
change.
https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/request/rate-limit-decision.ts`:
- Around line 198-203: The message always uses displayIndex =
(normalizedPinnedIndex ?? 0) + 1 which renders null as "1" while
pinnedAccountIndex remains null; change the message construction to only compute
and inject a numeric index when normalizedPinnedIndex is not null (e.g. when
normalizedPinnedIndex !== null use displayIndex = normalizedPinnedIndex + 1 and
include "Pinned account X", otherwise use a generic phrase like "A pinned
account" or omit the index), keeping the returned pinnedAccountIndex field as
normalizedPinnedIndex; update the message/template generation where message,
displayIndex, normalizedPinnedIndex, and pinnedAccountIndex are referenced so
text and payload stay consistent.
In `@lib/request/stream-failover-runtime.ts`:
- Around line 16-21: The current PRIVATE_CLIENT_RESPONSE_HEADERS list only
blocks four exact header names and can miss future sensitive headers under the
same prefix; update the header-filtering logic in
lib/request/stream-failover-runtime.ts to treat any header that starts with the
prefix "x-codex-multi-auth-account-" as private (in addition to the existing
exact keys). Replace or augment the current Set PRIVATE_CLIENT_RESPONSE_HEADERS
check with a small helper (e.g., isPrivateResponseHeader(headerName)) used where
headers are filtered (the code around the existing usage at lines referencing
PRIVATE_CLIENT_RESPONSE_HEADERS) so it returns true for exact matches or when
headerName.toLowerCase().startsWith("x-codex-multi-auth-account-"). Ensure all
places that previously referenced PRIVATE_CLIENT_RESPONSE_HEADERS use this
helper so future headers with that prefix are automatically blocked.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 02759a11-26d2-40ce-88f9-a1ce75ad94b5
📒 Files selected for processing (6)
lib/request/rate-limit-decision.tslib/request/stream-failover-runtime.tslib/runtime-rotation-proxy.tslib/runtime/rotation-server-types.tstest/rate-limit-decision.test.tstest/stream-failover-runtime.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (10)
lib/**/*.ts
📄 CodeRabbit inference engine (lib/AGENTS.md)
lib/**/*.ts: All public exports should flow throughlib/index.tsor documented package subpaths
Never import fromdist/in source tests or library code
Never suppress type errors
Files:
lib/runtime/rotation-server-types.tslib/request/stream-failover-runtime.tslib/request/rate-limit-decision.tslib/runtime-rotation-proxy.ts
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Do not use
as any,@ts-ignore, or@ts-expect-errortype assertions
Files:
lib/runtime/rotation-server-types.tslib/request/stream-failover-runtime.tstest/stream-failover-runtime.test.tstest/rate-limit-decision.test.tslib/request/rate-limit-decision.tslib/runtime-rotation-proxy.ts
**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use ESM module syntax exclusively; the project is ESM-only with
"type": "module"
Files:
lib/runtime/rotation-server-types.tslib/request/stream-failover-runtime.tstest/stream-failover-runtime.test.tstest/rate-limit-decision.test.tslib/request/rate-limit-decision.tslib/runtime-rotation-proxy.ts
lib/**
⚙️ CodeRabbit configuration file
focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.
Files:
lib/runtime/rotation-server-types.tslib/request/stream-failover-runtime.tslib/request/rate-limit-decision.tslib/runtime-rotation-proxy.ts
**
⚙️ CodeRabbit configuration file
**: # PROJECT KNOWLEDGE BASEGenerated: 2026-04-25
Commit: a87e005
Branch: main
Package version: 2.2.0OVERVIEW
codex-multi-authis a Codex CLI-first OAuth account manager and optional forwarding wrapper for the official Codex CLI. The installedcodex-multi-authentrypoint handles account-management commands locally,codex-multi-auth-codexforwards official Codex commands through this package's wrapper when explicitly used, and runtime rotation can route live Responses traffic through a localhost account-rotation proxy by default. The plugin-host entrypoint remains exported for compatibility, but the primary product surface is the account manager, optional wrapper, storage, runtime proxy, and repair tooling.STRUCTURE
./ ├── scripts/ │ ├── codex.js # codex-multi-auth-codex wrapper, official CLI forwarder, shadow CODEX_HOME/runtime proxy setup │ ├── codex-multi-auth.js # standalone package CLI entrypoint │ ├── codex-routing.js # auth command and compatibility alias routing │ ├── codex-bin-resolver.js # official Codex binary discovery │ ├── codex-app-router.js # persistent localhost router for packaged Codex app bind │ └── codex-app-launcher.js # reversible user-level app launcher routing helper ├── index.ts # optional plugin-host runtime entry ├── lib/ # core runtime logic (see lib/AGENTS.md) │ ├── auth/ # OAuth flow, PKCE, callback server │ ├── runtime/ # Codex CLI/app integration helpers, app bind, live sync, runtime observability │ ├── request/ # request transform, SSE, failover, backoff │ ├── storage/ # path resolution, migrations, backups, restore, import/export │ ├── codex-cli/ # Codex CLI state sync and writer helpers │ ├── codex-manager/ # command modules and settings panels │ ├── prompts/ # model-family prompts, GitHub ETag cache │ ├── recovery/ # conve...
Files:
lib/runtime/rotation-server-types.tslib/request/stream-failover-runtime.tstest/stream-failover-runtime.test.tstest/rate-limit-decision.test.tslib/request/rate-limit-decision.tslib/runtime-rotation-proxy.ts
lib/request/*.ts
📄 CodeRabbit inference engine (lib/AGENTS.md)
Node fetch returns decoded response bytes while preserving upstream
content-encoding; do not forward stale decoded encoding metadata to local clients
Files:
lib/request/stream-failover-runtime.tslib/request/rate-limit-decision.ts
test/**/*.test.ts
📄 CodeRabbit inference engine (test/AGENTS.md)
test/**/*.test.ts: Vitest globals (describe,it,expect) are enabled and should be used without explicit imports
Maintain 80% coverage threshold across statements, branches, functions, and lines
UseremoveWithRetryfor Windows filesystem cleanup instead of barefs.rmto handle EBUSY/EPERM/ENOTEMPTY backoff
Use source files in tests, not compileddist/files; test the source directly
Do not skip tests without justification; include rationale if a test must be skipped
Relax ESLint rules for test files as specified ineslint.config.js
Files:
test/stream-failover-runtime.test.tstest/rate-limit-decision.test.ts
test/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Windows filesystem operations must include retry handling for transient
EBUSY,EPERM, andENOTEMPTYerrors where tests cover Windows locks
Files:
test/stream-failover-runtime.test.tstest/rate-limit-decision.test.ts
test/**
⚙️ CodeRabbit configuration file
tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.
Files:
test/stream-failover-runtime.test.tstest/rate-limit-decision.test.ts
lib/runtime-rotation-proxy.ts
📄 CodeRabbit inference engine (lib/AGENTS.md)
lib/runtime-rotation-proxy.ts: Runtime rotation code must preserve pass-through semantics except for auth/provider headers that intentionally change
Runtime proxy client-facing headers must not expose account emails or tokens
Runtime rotation should fail open to normal official Codex forwarding when startup helpers are unavailable
Never add account emails/tokens to runtime proxy client responsesDo not expose account emails or tokens in runtime proxy client response headers or logs
Files:
lib/runtime-rotation-proxy.ts
🧠 Learnings (2)
📚 Learning: 2026-06-04T06:14:18.093Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/scheduling-strategy-config.test.ts:1-1
Timestamp: 2026-06-04T06:14:18.093Z
Learning: In ndycode/codex-multi-auth, do not flag explicit imports from "vitest" (e.g., describe, it, expect, beforeEach/afterEach, etc.) in test files as issues—even if the Vitest config sets `globals: true`. The repo’s established convention is to keep these imports for consistency with neighboring tests; removing them would make files outliers.
Applied to files:
test/stream-failover-runtime.test.tstest/rate-limit-decision.test.ts
📚 Learning: 2026-06-04T06:14:24.975Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/runtime-rotation-proxy.test.ts:2478-2491
Timestamp: 2026-06-04T06:14:24.975Z
Learning: In ndycode/codex-multi-auth test files (e.g. `test/*.test.ts`), when creating V3 storage fixtures for accounts, it’s an intentional convention to use `as never` for deliberately minimal stored-account objects that only include `refreshToken`, `addedAt`, and `lastUsed`. Do not treat `as never` here as a type-safety problem: optional/other fields are expected to be populated by the runtime during execution, and the cast is used solely to keep the fixture minimal and consistent across existing tests.
Applied to files:
test/stream-failover-runtime.test.tstest/rate-limit-decision.test.ts
🔇 Additional comments (4)
test/rate-limit-decision.test.ts (1)
1-287: LGTM!test/stream-failover-runtime.test.ts (1)
1-282: LGTM!lib/request/stream-failover-runtime.ts (1)
146-158: Respect response backpressure during stream forwarding
- In
lib/request/stream-failover-runtime.ts, gate forwarding onres.write(...)return value: whenwrite()returnsfalse, wait for thedrainevent before continuing.- In
test/stream-failover-runtime.test.ts, add a deterministic regression that makesres.write()returnfalseand asserts the forwarder pauses untildrain(and that abort/cleanup stops any upstream reads).lib/runtime-rotation-proxy.ts (1)
54-99: LGTM!
| const displayIndex = (normalizedPinnedIndex ?? 0) + 1; | ||
| return { | ||
| message: `Pinned account ${displayIndex} is currently unavailable${reasonSuffix}; run \`codex-multi-auth status\` for details, or \`codex-multi-auth unpin\` to allow rotation.`, | ||
| code: "codex_pinned_account_unavailable", | ||
| pinnedAccountIndex: normalizedPinnedIndex, | ||
| reason: skipReason, |
There was a problem hiding this comment.
null pinned index is rendered as account 1 in the error message.
lib/request/rate-limit-decision.ts:198-203 reports pinnedAccountIndex: null but still emits Pinned account 1.... this makes desync diagnostics contradictory for clients and operators.
proposed fix
- const displayIndex = (normalizedPinnedIndex ?? 0) + 1;
+ const displayIndex =
+ normalizedPinnedIndex === null ? null : normalizedPinnedIndex + 1;
return {
- message: `Pinned account ${displayIndex} is currently unavailable${reasonSuffix}; run \`codex-multi-auth status\` for details, or \`codex-multi-auth unpin\` to allow rotation.`,
+ message:
+ displayIndex === null
+ ? `Pinned account is currently unavailable${reasonSuffix}; run \`codex-multi-auth status\` for details, or \`codex-multi-auth unpin\` to allow rotation.`
+ : `Pinned account ${displayIndex} is currently unavailable${reasonSuffix}; run \`codex-multi-auth status\` for details, or \`codex-multi-auth unpin\` to allow rotation.`,
code: "codex_pinned_account_unavailable",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/request/rate-limit-decision.ts` around lines 198 - 203, The message
always uses displayIndex = (normalizedPinnedIndex ?? 0) + 1 which renders null
as "1" while pinnedAccountIndex remains null; change the message construction to
only compute and inject a numeric index when normalizedPinnedIndex is not null
(e.g. when normalizedPinnedIndex !== null use displayIndex =
normalizedPinnedIndex + 1 and include "Pinned account X", otherwise use a
generic phrase like "A pinned account" or omit the index), keeping the returned
pinnedAccountIndex field as normalizedPinnedIndex; update the message/template
generation where message, displayIndex, normalizedPinnedIndex, and
pinnedAccountIndex are referenced so text and payload stay consistent.
There was a problem hiding this comment.
Valid finding, but it's pre-existing behavior this PR moved verbatim (the same (normalizedPinnedIndex ?? 0) + 1 lives on main inside runtime-rotation-proxy.ts), so the fix lands in the stacked PR #546 to keep this one zero-behavior-change: the null-index desync path now says "The pinned account is currently unavailable…" and the previously-pinned contradictory expectation in test/issue-474-pin-honored.test.ts is updated there.
Generated by Claude Code
There was a problem hiding this comment.
@ndycode acknowledged — that's the right call for a mechanical-move PR. keeping the revert story clean outweighs fixing a latent bug inline here.
tracking confirmed: lib/request/rate-limit-decision.ts:200 carries the pre-existing (normalizedPinnedIndex ?? 0) + 1 logic moved verbatim from lib/runtime-rotation-proxy.ts on main; #546 closes the desync (null index → "the pinned account is currently unavailable…" + test/issue-474-pin-honored.test.ts expectation updated).
nothing blocking on this end. lgtm for the phase 1 extraction.
✏️ Learnings added
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 532
File: lib/request/rate-limit-decision.ts:198-203
Timestamp: 2026-06-10T11:14:03.257Z
Learning: In `lib/request/rate-limit-decision.ts` (PR `#532`, repo ndycode/codex-multi-auth), the expression `(normalizedPinnedIndex ?? 0) + 1` in `buildPinnedUnavailableErrorBody` is pre-existing behavior moved verbatim from `lib/runtime-rotation-proxy.ts` on `main`. When `pinnedIndex` is null/undefined, `pinnedAccountIndex` is set to null but the message still says "Pinned account 1 is currently unavailable…". The fix (null index renders as "The pinned account is currently unavailable…" with no numeric index) is tracked in stacked PR `#546` alongside an update to the contradictory expectation in `test/issue-474-pin-honored.test.ts`.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
Phase 2 of the §4.1.3 rotation-proxy decomposition (follows the phase-1 carve, PR ndycode#532). The closure state inside startRuntimeRotationProxy is now an explicit RotationProxyState container created once at startup and passed to plain module-level functions; function bodies are unchanged apart from closure-variable -> state-field references. Module map: - lib/runtime/rotation-proxy-state.ts (116 lines): RotationProxyState container (status, session-affinity store, thread-goal fallbacks, rotation stickiness, active AccountManager) plus the deduped recoverStaleRuntimeState reload. - lib/runtime/rotation-account-selection.ts (257 lines): chooseAccount and the shared linear-scan fallback. - lib/runtime/rotation-token-refresh.ts (141 lines): ensureFreshAccessToken, the per-manager refresh-commit dedupe WeakMap, and applyMonotonicAuthCooldown. - lib/runtime/rotation-storage-meta.ts (206 lines): the content-hash storage-meta cache (readStorageMetaFromDisk and friends). lib/runtime-rotation-proxy.ts: 2,099 -> 1,505 lines. Every previously exported symbol is still exported from lib/runtime-rotation-proxy.ts via re-exports, so lib/index.ts, scripts/codex.js, scripts/codex-app-router.js, and all test imports are untouched. The HTTP plumbing helpers (request parsing/forwarding, writeJson and friends) deliberately stay in runtime-rotation-proxy.ts: moving them would relocate the entire module rather than break up the closure state. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Summary
Phase 1 of carving
lib/runtime-rotation-proxy.tsalong its natural seams — audit roadmap §4.1.3 (docs/audits/AUDIT_2026-06-10.md, PR #522). All code moved verbatim; every previously-exported symbol re-exported, so the 3 importing call sites (lib/index.ts,scripts/codex.js,scripts/codex-app-router.js) and all 8 test files are untouched. Zero behavior change; the proxy shrinks 2,498 → 2,099 lines.Changes
lib/runtime/rotation-server-types.tsRuntimeRotationProxyServer/Status/Options, plus previously-privateRequestContext,ExhaustionReason,RuntimeProxyHttpError, account-identity type — type-only imports, no cycle risklib/request/stream-failover-runtime.tsforwardStreamingResponse,readErrorBody,withTimeout, client-response header filtering (HOP_BY_HOP_HEADERS+ private/decoded-header sets) — all inputs explicit, no closure statelib/request/rate-limit-decision.tsDeliberately deferred to phase 2 (entangled with closure/module state; extracting now would change call semantics): the rotation loop and
startRuntimeRotationProxyclosure (status, affinity store, stale-reload dedupe),chooseAccount/linear-scan fallback (mutate AccountManager cursors),ensureFreshAccessToken+ refresh-dedupe WeakMap, and the storage-meta cache helpers.No logging was touched (token/email-redaction rules unaffected); new modules contain zero references back to the proxy (no cycles).
Validation
npm run typecheck; eslint on all 4 files--max-warnings=0::1bind tests, reproduced identically on a clean origin/main tree (environment-only)Risk / Rollback
Mechanical move with re-exports; revert the single commit. Phase 2 (closure-state extraction) is a separate, riskier PR by design.
https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Generated by Claude Code
note: greptile review for oc-chatgpt-multi-auth. cite files like
lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.Greptile Summary
phase 1 of the
runtime-rotation-proxy.tscarve — mechanical extraction of ~400 lines into three focused modules (rotation-server-types.ts,stream-failover-runtime.ts,rate-limit-decision.ts) with verbatim code moves and full re-exports at the original import path. all 3 call sites and 8 proxy test files are untouched.lib/runtime/rotation-server-types.ts: type-only extraction (server interfaces + previously-private types); no runtime code, no cycleslib/request/rate-limit-decision.ts+lib/request/stream-failover-runtime.ts: pure-function extraction with explicit inputs; account email/token headers remain correctly stripped from client responses viaPRIVATE_CLIENT_RESPONSE_HEADERStest/stream-failover-runtime.test.tspins a known pre-existing bug where a stalled upstream stream resolves as a clean end rather than triggering theonStreamErrorfailover hook (documented for a follow-up PR)Confidence Score: 5/5
safe to merge — purely mechanical moves, all previously-exported symbols re-exported, no behavior change
every function and type moved verbatim; the re-export block in runtime-rotation-proxy.ts covers the complete prior public surface; token-safety header filtering is unchanged and verified by the new test suite; the only noteworthy item is two stall tests using real 25 ms timers rather than fake timers, which is a test-convention nit and not a correctness issue
test/stream-failover-runtime.test.ts — stall tests for readErrorBody and forwardStreamingResponse rely on real wall-clock timeouts
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["lib/runtime-rotation-proxy.ts\n(main proxy — rotation loop, startRuntimeRotationProxy)"] B["lib/runtime/rotation-server-types.ts\n(types: Server, Status, Options,\nRequestContext, ExhaustionReason, ...)"] C["lib/request/rate-limit-decision.ts\n(parseRetryAfter*, isTokenInvalidation*,\nbuildTokenInvalidation*, getQuotaWait*, ...)"] D["lib/request/stream-failover-runtime.ts\n(forwardStreamingResponse, readErrorBody,\nwithTimeout, responseHeadersForClient)"] E["lib/index.ts\n(barrel — star-exports proxy)"] F["scripts/codex.js\nscripts/codex-app-router.js"] A -->|"import type"| B A -->|"import"| C A -->|"import"| D A -->|"re-export type (Server/Status/Options)"| B A -->|"re-export (buildPinnedUnavailable*, buildTokenInvalidation*)"| C E -->|"star re-export"| A F -->|"import"| E C -->|"import type ExhaustionReason"| B D -->|"import type RuntimeRotationProxyStatus"| BPrompt To Fix All With AI
Reviews (2): Last reviewed commit: "test: add unit suites for the extracted ..." | Re-trigger Greptile
Context used: