Skip to content

feat(proxy): opt-in same-target 429 wait-and-retry before key failover (#487) - #865

Open
harryzhou2000 wants to merge 14 commits into
lidge-jun:devfrom
harryzhou2000:feat/429-same-target-retry
Open

feat(proxy): opt-in same-target 429 wait-and-retry before key failover (#487)#865
harryzhou2000 wants to merge 14 commits into
lidge-jun:devfrom
harryzhou2000:feat/429-same-target-retry

Conversation

@harryzhou2000

@harryzhou2000 harryzhou2000 commented Aug 1, 2026

Copy link
Copy Markdown

Summary

Adds an opt-in, provider-level retryOn429 policy: on HTTP 429 the proxy waits (upstream Retry-After or a fixed interval, capped) and replays the identical pre-stream request on the same key before any multi-key failover.

Why

Behavior

  • Config: providers.<name>.retryOn429 = { enabled?, attempts?, intervalMs?, maxIntervalMs?, respectRetryAfter? } (defaults: enabled=true, attempts=3, intervalMs=5000, maxIntervalMs=60000, respectRetryAfter=true). Default off when absent → zero behavior change.
  • API-key providers only (authMode: "key", or the documented omitted default for custom API-key providers). Fail closed: OAuth/forward credentials are never replayed on the same token; local runtimes (Ollama etc.) have no remote key to preserve; unknown/custom auth modes are rejected rather than guessed at. providerConfigSeed now preserves the registry auth kind (including "local") so the gate survives the seed round-trip.
  • Pre-stream only (429 arrives before any bytes are relayed → replay is lossless). Runs before key failover; failover still works after attempts exhaust; final 429 keeps Retry-After.
  • Retry budget is scoped per request and lives outside the recovery loop, so a 413/401 replay that comes back 429 cannot re-arm a fresh budget (bounded to attempts).
  • Covers /v1/responses, /v1/chat/completions, and routed /v1/messages (all enter handleResponses), plus the other key-auth surfaces that bypass that loop: the Responses passthrough wire (openai-responses key-auth gateways, e.g. the built-in DeepSeek preset), the image/video bridge and web-search sidecar loops (before their on429 key rotation), and Anthropic terminal-guard continuations (before key/account failover).
  • Abort during the wait: the sleep is abort-aware. Once the server observes the client disconnect (Bun propagates it asynchronously, observed 1–10s), the unread 429 body is released, the upstream fetch is aborted, and the request is cancelled with 499 before any replay. Because propagation is async, a replay may precede the cancel if the interval elapses first — bounded by the same attempts budget.
  • Any single wait — Retry-After or the fixed fallback — is capped at maxIntervalMs; the schema caps maxIntervalMs at 600000 (the effective per-wait cooldown ceiling). An already-expired HTTP-date Retry-After retries immediately (like Retry-After: 0).
  • Every surface releases the unread 429 body BEFORE the backoff and records the rate-limit-429 recovery kind on replay sends; the image/video and web-search bridge loops restart their response-header deadline after each deliberate wait, so backoffs never consume the connect budget or surface as a 504.
  • Config load degrades invalid optional retryOn429 fields with a warning instead of tripping the whole schema (which would hide every provider/key behind a default config); the management write boundary still rejects invalid policies.

Files

  • src/types.tsRateLimitRetryPolicy + OcxProviderConfig.retryOn429
  • src/config.ts — zod validation (outer provider schema stays passthrough; a typo inside retryOn429 degrades, never rejects the whole config), maxIntervalMs ≤ 600000
  • src/providers/key-failover.ts — policy normalization gated to key-auth + delay computation (Retry-After seconds/HTTP-date/0, capped)
  • src/providers/derive.tsproviderConfigSeed preserves the registry auth kind (incl. "local")
  • src/server/responses/core.ts — recovery-loop replay before the multi-key failover while; passthrough-wire replay before the forward-pool logic; terminal-guard continuation replay before key/account failover; abort path cancels the unread 429 body first
  • src/images/loop.ts, src/web-search/loop.ts — same-target replay before their on429 key rotation (new retryOn429Policy dep)
  • src/usage/log.tsAttemptRecoveryKind member rate-limit-429 and its persisted-usage whitelist entry
  • gui/src/pages/Logs.tsx — recovery-kind union includes rate-limit-429 (and pre-existing anthropic-oauth-429)
  • docs-site configuration reference (all five locales), structure/04 transport note, devlog/_plan/260802_429_same_target_retry/

Test plan

  • bun test tests/rate-limit-retry.test.ts tests/server-rate-limit-retry-e2e.test.ts tests/usage-log.test.ts tests/key-failover.test.ts tests/retry-after-429.test.ts — 79 pass (policy/delay units incl. fail-closed auth gating, expired HTTP-date, Retry-After: 0, fallback cap; persisted rate-limit-429 recovery kind; deterministic direct-handler abort test; e2e: replay-to-success with byte-identical bodies, opt-in passthrough, exhaustion, retry-before-failover ordering, key-auth openai-responses passthrough replay with identical body/auth, per-request budget across a 2-key pool)
  • New surface tests: terminal-guard continuation 429 replay with byte-identical requests + per-request budget across a 2-key pool (tests/terminal-guard-server.test.ts); image bridge + web-search loop same-key replay before rotation with rate-limit-429 telemetry (tests/images/loop.test.ts, tests/web-search.test.ts); config load degradation (tests/config-user-edits.test.ts); registry auth-kind preservation (tests/provider-registry-parity.test.ts)
  • Full suite: 6805 pass / 6 skip / 8 fail (baseline environmental failures; stalled-400 test flaked once under load in one run and passed in the other two)
  • bun run typecheck
  • bun run privacy:scan
  • cd gui && bun run lint
  • Full suite: 6711 pass; remaining failures reproduce identically on pristine dev in this environment (WS/auth/connection-refused) plus GUI tests that pass once gui/ deps are installed

Commits

  • 667ad08f — feature implementation
  • 2efb887b — audit round-1 fixes (usage-log whitelist, key-auth gating, budget scope, abort body cancel, schema cap, GUI union, docs)
  • a06a4160 — audit round-3 xhigh fixes: coverage extended to passthrough wire / image+web-search bridges / terminal continuations, fixed-fallback cap, local-mode gating, docs + tests
  • 58c36c9e — review-bot round: fail-closed auth (registry preserves local), expired Retry-After → immediate, release 429 bodies before backoff, rate-limit-429 recovery on every surface, bridge header-deadline restart, continuation budget hoisted + upstream-signal sleep, config load degradation, identical-replay + budget regression tests, provider/adapter docs (5 locales)
  • 568e565c — review-bot round 2: awaited body-cancel before backoff, stale-deadline cleared pre-sleep + 499 re-check post-sleep, misnamed retryOn429 keys warn at load, opt-in + ordering wording in provider/adapter guides (5 locales)
  • 3c19337e — docstring coverage pass on the diff (JSDoc for seeded provider config, responses core helpers, image/web-search iteration prep, terminal-guard continuation, and test helpers)
  • 1af83c39 — attach JSDoc directly to the remaining diff-touched declarations (terminal-guard continuation, image/web-search fetchOnce, cooldown check, provider-config interface, persisted-usage helper)
  • d4e19788 — review round 3: retry budget hoisted per request across bridge iterations, single dispatch-time attempt telemetry (recovery kind passed through), invalid enabled master switch discards the whole policy, secret-safe config warnings (path + type only)
  • 58bf694f — local audit round (gpt-5.6-terra + DeepSeek-V4-Flash): one request-wide 429 budget shared between the main recovery loop and the terminal-guard continuation; awaited 429-body cancellation before every backoff (all surfaces); post-sleep client-abort re-check before dispatching every replay; xAI x-grok-req-id pinned per logical request so same-key replays are byte-identical; new regression tests (shared continuation budget, continuation abort-during-wait, bridge budget not re-armed after rotation, far-future HTTP-date cap, stable xAI request id)
  • 4fee87b5 — review round 4: unrecognized retryOn429 field NAMES are redacted before logging (secret-shaped property names become [REDACTED], ordinary typos stay readable)
  • 5945711b — review round 4 cleanup: rename the xAI request-id param to pinnedRequestId (fallback only at transport resolution); anchor the secret-warning test assertion to the exact field+type diagnostic
  • e65106f0 — review round 4 follow-up: JSON-escape the redacted field name in load warnings so control-character property names (newline/ANSI) cannot forge log lines
  • 89535fb7 — review round 5: redact and JSON-escape the PROVIDER name in all retryOn429 load warnings (sanitizer runs pre-validation, so the name is untrusted; secret-shaped names log as [REDACTED], control characters escaped)

Draft — opened for review, not for merge.

lidge-jun#487)

Codex never retries HTTP 429 (openai/codex#30471 keeps retry_429=false and the misleading 'exceeded retry limit' error), and single-key pools have no failover, so provider-level retryOn429 waits (Retry-After or fixed interval) and replays the identical pre-stream request on the same key before any key rotation.

- types.ts: RateLimitRetryPolicy + OcxProviderConfig.retryOn429
- config.ts: lenient zod validation (strip unknown; typo degrades, never rejects config)
- key-failover.ts: rateLimitRetryPolicyFor + rateLimitRetryDelayMs (Retry-After capped at maxIntervalMs)
- responses/core.ts: recovery-loop replay before multi-key failover; abort-aware sleep; covers Responses, chat completions, and routed Claude messages
- usage/log.ts: AttemptRecoveryKind 'rate-limit-429'
- docs-site configuration reference + structure/04 transport note + devlog plan unit
- tests: policy unit tests + e2e (single-key replay, passthrough without knob, exhaustion, retry-before-failover ordering)

Verified: typecheck, privacy scan, 43/43 retry-related tests; full suite failures are pre-existing on pristine dev in this environment (WS/auth/connection-refused) plus missing-gui-deps artifacts that pass once installed.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds configurable, API-key-only HTTP 429 retries. Requests replay on the same key before failover, use bounded Retry-After or fixed delays, support cancellation, and cover response, passthrough, image, web-search, and terminal-continuation paths.

Changes

Rate-limit retry policy

Layer / File(s) Summary
Retry policy contract and configuration
src/types.ts, src/config.ts, src/providers/key-failover.ts, src/providers/derive.ts, src/usage/log.ts, gui/src/pages/Logs.tsx, docs-site/src/content/docs/*, structure/04-transports-and-sidecars.md, devlog/_plan/260802_429_same_target_retry/*
Adds RateLimitRetryPolicy, provider-level retryOn429, validation, delay calculation, API-key eligibility checks, authentication-mode preservation, recovery logging, and documentation.
Same-target request replay and failover wiring
src/server/responses/core.ts, src/images/loop.ts, src/web-search/loop.ts, src/providers/xai-transport.ts
Retries 429 responses on the current key or adapter before failover. Replays requests, honors Retry-After, cancels response bodies, resets bridge deadlines, preserves request identity, and handles client cancellation.
Retry behavior and integration coverage
tests/rate-limit-retry.test.ts, tests/server-rate-limit-retry-e2e.test.ts, tests/images/loop.test.ts, tests/web-search.test.ts, tests/terminal-guard-server.test.ts, tests/usage-log.test.ts, tests/config-user-edits.test.ts, tests/provider-registry-parity.test.ts, tests/xai-transport.test.ts
Covers policy normalization, delay parsing, cancellation, replay, exhaustion, failover ordering, passthrough, bridge paths, terminal continuations, configuration cleanup, authentication parity, recovery-log deduplication, and stable transport request IDs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ResponsesCore
  participant UpstreamProvider
  participant KeyFailover
  Client->>ResponsesCore: Send request
  ResponsesCore->>UpstreamProvider: Send request with current API key
  UpstreamProvider-->>ResponsesCore: Return HTTP 429
  ResponsesCore->>ResponsesCore: Wait using retry policy
  ResponsesCore->>UpstreamProvider: Replay identical request on same key
  UpstreamProvider-->>ResponsesCore: Return success or HTTP 429
  ResponsesCore->>KeyFailover: Fail over after retry attempts
  KeyFailover-->>ResponsesCore: Provide next key
  ResponsesCore-->>Client: Return final response
Loading

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun, wibias

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 82.61% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the opt-in same-target HTTP 429 retry behavior before key failover.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 1, 2026
- usage/log: whitelist the rate-limit-429 recovery kind so persisted usage rows and
  post-restart /api/logs keep the reason
- key-failover: gate retryOn429 to key-auth providers (no same-token OAuth replays,
  no silent no-op on forward passthrough); accept Retry-After 0 as immediate
- config: cap maxIntervalMs at the effective 10-minute cooldown ceiling
- core: hoist the retry budget outside the recovery loop so 413/401 replays cannot
  re-arm it; cancel the unread 429 body before aborting on client cancel
- gui: add rate-limit-429 (and pre-existing anthropic-oauth-429) to the Logs union
- tests: OAuth/forward gating, HTTP-date Retry-After, Retry-After 0, persisted
  recovery-kind, and a deterministic direct-handler abort test (real-socket
  disconnect propagation is async in Bun, so the e2e version was timing-flaky)
- docs: key-auth-only note, maxIntervalMs cap, abort-propagation nuance, locale rows
… xhigh)

- core: the Responses passthrough wire (openai-responses key-auth gateways, e.g.
  the built-in DeepSeek preset) now replays 429 on the same key pre-relay, before
  the forward-pool logic; Anthropic terminal-guard continuations replay before
  key/account failover
- images/loop + web-search/loop: same-target replay before on429 key rotation via
  a new retryOn429Policy dep (abort-aware, heartbeat seams preserved)
- key-failover: the fixed fallback is now capped at maxIntervalMs (a single wait
  never exceeds the cap); authMode local runtimes are gated out alongside
  oauth/forward, so the knob matches the documented API-key scope exactly
- tests: key-auth openai-responses passthrough e2e, terminal-guard continuation
  replay, image/web-search same-key replay (rotation stays zero), fallback cap,
  local-mode gating
- docs: coverage and cap wording in devlog 010, structure/04, and all five
  configuration locale rows
@harryzhou2000
harryzhou2000 marked this pull request as ready for review August 1, 2026 18:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 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 `@devlog/_plan/260802_429_same_target_retry/010_design.md`:
- Around line 57-67: Update the latency and concurrency sections of the retry
design to distinguish retry-wait time from total request latency, noting that
connection, response, and configured timeout durations also contribute. Revise
the request-volume bound to account for multi-key failover, including the
possibility of exhausting the retry budget on one key before attempting another,
and document the combined bound alongside the existing failover behavior.
- Around line 7-9: Update same-key retry handling around the
continuation-request rebuild in core response processing so every retry replays
a cached, body-safe representation of the identical upstream request, including
serialized body and authentication headers, rather than rebuilding it each time.
Apply this to passthrough Responses and Anthropic terminal continuations; only
rebuild after the target or adapter changes, defining deterministic equivalence
where adapters must rebuild. Extend the rate-limit retry and terminal-guard
tests to assert body and authentication/header equality, not just send counts.
- Around line 32-34: Update rate-limit retry authentication handling to fail
closed: normalize omitted key-provider auth modes to "key", preserve "local" in
providerConfigSeed (including the mapping in derive), and make
rateLimitRetryPolicyFor allow retries only when authMode === "key". Add
regression coverage for omitted, local, and unknown authentication modes while
preserving existing key-provider behavior.

In `@docs-site/src/content/docs/reference/configuration.md`:
- Line 353: Update the retryOn429 documentation across the English, Japanese,
Korean, Russian, and Chinese provider and adapter reference pages. State that it
applies only to authMode: "key" and excludes oauth, forward, and local
providers; document same-key replay, raw openai-responses passthrough,
translated openai-chat/Anthropic requests, and that custom runTurn transports
are excluded. Keep the existing defaults and behavior description consistent
across all pages.

In `@src/images/loop.ts`:
- Around line 467-490: The 429 retry waits must not consume the cumulative
header deadline in either loop. In src/images/loop.ts lines 467-490, update the
flow around the headerDeadline and rateLimitRetryPolicy retry loop so each
deliberate wait is excluded, while preserving the configured retry outcome and
preventing the 504 header-timeout path from firing due to that wait; apply the
identical change in src/web-search/loop.ts lines 361-384 around its
headerDeadline and retry loop.

In `@src/providers/key-failover.ts`:
- Around line 76-96: Ensure local providers cannot enter the key-failover retry
path when authMode is undefined: preserve authMode: "local" through the provider
derivation/routing flow or pass the registry auth kind into
rateLimitRetryPolicyFor, while retaining undefined as the default for custom
API-key providers. Add a regression test covering a local provider configured
with retryOn429: {}.

In `@src/server/responses/core.ts`:
- Around line 1631-1671: Update the retry closure in the passthrough 429 loop to
pass `"rate-limit-429"` to `noteAttemptSend` whenever `fetchWithTransientRetry`
provides no recovery kind, while preserving any recovery kind it does provide.
Keep the existing `rateLimitPolicy` retry flow unchanged.
- Around line 2629-2665: Hoist the rateLimitPolicy and rateLimitRetries
declarations out of the terminal continuation while loop, placing them beside
imageTierBias so the retry budget persists across key rotation, account
rotation, and image-tier 413 continues. Resolve rateLimitPolicy once from the
initial route.provider and preserve the existing retry loop behavior while
preventing the budget from resetting per iteration.

In `@tests/rate-limit-retry.test.ts`:
- Around line 99-159: The retry tests lack coverage proving the retry budget is
scoped per request rather than reset during failover. Add focused regression
coverage near the existing retry-loop tests using a two-key provider pool,
retryOn429 with attempts set to 1, and upstream responses that always return
429; assert the total upstream send count remains within the request-wide bound,
covering both the main recovery loop and terminal-continuation path.

In `@tests/usage-log.test.ts`:
- Around line 39-62: Replace the as never cast in the
persists-the-rate-limit-429-recovery-kind-on-attempts test with a
PersistedUsageEntry-typed object. Keep the recoveryKinds literal type-checked
against the recovery-kind union, and cast only individual fields that genuinely
require it while preserving the existing round-trip assertion.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a0cd7539-4937-4e9d-bf2a-e78d059e114a

📥 Commits

Reviewing files that changed from the base of the PR and between aae9426 and a06a416.

📒 Files selected for processing (22)
  • devlog/_plan/260802_429_same_target_retry/000_research.md
  • devlog/_plan/260802_429_same_target_retry/010_design.md
  • docs-site/src/content/docs/ja/reference/configuration.md
  • docs-site/src/content/docs/ko/reference/configuration.md
  • docs-site/src/content/docs/reference/configuration.md
  • docs-site/src/content/docs/ru/reference/configuration.md
  • docs-site/src/content/docs/zh-cn/reference/configuration.md
  • gui/src/pages/Logs.tsx
  • src/config.ts
  • src/images/loop.ts
  • src/providers/key-failover.ts
  • src/server/responses/core.ts
  • src/types.ts
  • src/usage/log.ts
  • src/web-search/loop.ts
  • structure/04_transports-and-sidecars.md
  • tests/images/loop.test.ts
  • tests/rate-limit-retry.test.ts
  • tests/server-rate-limit-retry-e2e.test.ts
  • tests/terminal-guard-server.test.ts
  • tests/usage-log.test.ts
  • tests/web-search.test.ts

Comment thread devlog/_plan/260802_429_same_target_retry/010_design.md
Comment thread devlog/_plan/260802_429_same_target_retry/010_design.md Outdated
Comment thread devlog/_plan/260802_429_same_target_retry/010_design.md Outdated
Comment thread docs-site/src/content/docs/reference/configuration.md
Comment thread src/images/loop.ts
Comment thread src/providers/key-failover.ts
Comment thread src/server/responses/core.ts
Comment thread src/server/responses/core.ts
Comment thread tests/rate-limit-retry.test.ts
Comment thread tests/usage-log.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a06a416024

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server/responses/core.ts Outdated
Comment thread src/server/responses/core.ts Outdated
Comment thread src/images/loop.ts
Comment thread src/providers/key-failover.ts
Comment thread src/config.ts
Comment thread src/server/responses/core.ts
Comment thread src/server/responses/core.ts Outdated
… deadlines, recovery labels)

- key-failover: fail closed - only authMode key (or the documented omitted
  default) may replay; unknown modes rejected; an already-expired HTTP-date
  Retry-After retries immediately (like Retry-After: 0)
- derive: providerConfigSeed preserves the registry auth kind (incl. local) so
  the gate survives the seed round-trip and routing
- core: release the unread 429 body BEFORE every backoff (main loop, passthrough,
  continuations); passthrough and continuation replay sends record the
  rate-limit-429 recovery kind; continuation budget hoisted outside the failover
  loop so rotation/413 cannot re-arm it; continuation waits sleep on the upstream
  signal so an SSE body-cancel aborts them too
- images/web-search loops: release the 429 body before the wait, restart the
  response-header deadline after each deliberate wait (backoffs never consume the
  connect budget or surface as 504), and record rate-limit-429 on replay sends
- config: load-time degradation for invalid optional retryOn429 fields (warn +
  drop the field) instead of tripping the whole schema and hiding all providers
  behind a default config; the management write boundary still rejects
- tests: fail-closed auth modes (incl. unknown), expired HTTP-date, per-request
  budget across 2-key pools (e2e + terminal continuation), byte-identical replay
  bodies/auth (passthrough + continuation), recovery telemetry in both loops,
  config load degradation, registry auth-kind preservation, typed usage-log entry
- docs: retry-wait vs total-latency and attempts+poolKeys volume bounds,
  identical-replay equivalence, deadline/backoff behavior in devlog 010 and
  structure/04; retryOn429 boundary notes in the provider guide and adapter
  reference (English + ja/ko/ru/zh-cn)
@harryzhou2000

Copy link
Copy Markdown
Author

All 17 inline review comments are addressed in 58c36c9 (plus docstrings in 0feede1 for the coverage check). Highlights: fail-closed auth (registry now preserves authMode "local"), expired Retry-After dates retry immediately, unread 429 bodies are released before every backoff, rate-limit-429 is recorded on every retry surface, bridge loops restart their header deadline after each wait, the continuation budget is per-request, invalid optional retryOn429 fields degrade at load instead of discarding the config, and replay identity is asserted byte-for-byte in tests. Replies were posted on each thread; the 7 open threads are resolved. The PR remains a draft.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/server/responses/core.ts (1)

2647-2685: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Emit upstream heartbeats during 429 backoff

The bridge checks upstream activity every 2,000 ms. It increments stallTicks when no adapter event arrives and aborts at ceil(stallTimeoutSec * 1000 / 2000) ticks. Wire heartbeats do not reset this counter.

The retry waits can be silent for up to 600,000 ms. This exceeds the default 300-second core budget and the default 330-second web-search budget.

Apply the fix to these live generators:

  • src/server/responses/core.ts:2647-2685
  • src/web-search/loop.ts:363-393
  • src/images/loop.ts:469-498

Split each sleepWithAbort call into chunks shorter than 2,000 ms, such as 1,000 ms. Yield { type: "heartbeat" } after every chunk, including the final chunk. Add regression coverage for a backoff longer than stallTimeoutSec. The pre-stream retry loops in src/server/responses/core.ts do not require this change.

Update the web-search timeout documentation and tests to state that retry backoff remains live through these upstream heartbeats.

🤖 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 `@src/server/responses/core.ts` around lines 2647 - 2685, Update the live
retry-backoff loops in src/server/responses/core.ts:2647-2685,
src/web-search/loop.ts:363-393, and src/images/loop.ts:469-498 to split
sleepWithAbort waits into sub-2,000 ms chunks, yielding a heartbeat after every
chunk including the final one, while preserving abort handling; leave pre-stream
retry loops unchanged. Add regression coverage for backoffs exceeding
stallTimeoutSec, and update web-search timeout documentation and tests to
describe continued liveness through upstream heartbeats.
src/images/loop.ts (1)

469-476: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Keep the retry budget scoped consistently.

src/images/loop.ts initializes rateLimitRetries inside prepareIterationEvents, while one runWithImageBridge request can execute multiple image-loop iterations. This can re-arm the same-key budget and exceed the documented per-request send bound.

  • src/images/loop.ts#L469-L476: move the policy and counter to request scope, or explicitly define the budget per iteration.
  • structure/04_transports-and-sidecars.md#L235-L235: update the attempts + poolKeys bound if per-iteration scope is intentional.
  • devlog/_plan/260802_429_same_target_retry/010_design.md#L95-L110: add a multi-iteration image-bridge regression test.
#!/usr/bin/env bash
set -euo pipefail
rg -n -C 8 'prepareIterationEvents|rateLimitRetries|rateLimitRetryPolicy|HARD_CAP|retryOn429Policy' \
  src/images/loop.ts tests/images/loop.test.ts

As per path instructions, add a focused regression in the flat Bun tests for this src/** behavior change.

🤖 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 `@src/images/loop.ts` around lines 469 - 476, Keep the 429 retry policy and
counter request-scoped in prepareIterationEvents/runWithImageBridge so multiple
image-loop iterations cannot reset the same-key retry budget; preserve the
documented per-request send bound. Update
structure/04_transports-and-sidecars.md:235 to reflect the chosen request-scoped
bound, and extend devlog/_plan/260802_429_same_target_retry/010_design.md:95-110
with a multi-iteration image-bridge regression scenario. Add a focused
regression to the flat Bun tests covering this behavior.

Source: Path instructions

🤖 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 `@devlog/_plan/260802_429_same_target_retry/010_design.md`:
- Around line 88-89: Align the design and associated tests with the current
implementation: update the expired HTTP-date and numeric Retry-After: 0 cases to
expect the configured fallback interval when the parser returns undefined,
unless the parser and rateLimitRetryDelayMs are intentionally changed to
propagate a zero delay. Keep the documented behavior, parser expectations, and
tests consistent.

In `@docs-site/src/content/docs/ja/reference/adapters.md`:
- Around line 41-43: Update the retry descriptions in
docs-site/src/content/docs/ja/reference/adapters.md:41-43,
docs-site/src/content/docs/ko/reference/adapters.md:48-50,
docs-site/src/content/docs/ru/reference/adapters.md:51-53, and
docs-site/src/content/docs/zh-cn/reference/adapters.md:46-48 to state that
same-key 429 replay occurs before other handling or failover, while preserving
the existing translated behavior and custom runTurn exception.

In `@docs-site/src/content/docs/zh-cn/guides/providers.md`:
- Around line 37-40: Update the Chinese provider guide passage describing
retryOn429 to explicitly state that it is opt-in and disabled by default unless
configured, while preserving the existing API-key-only restriction and OAuth,
forward, and local exclusions. Keep the wording consistent with the
corresponding English provider guide.

In `@src/config.ts`:
- Around line 1135-1149: Update the retryOn429 sanitization around the fields
definition and loop to iterate over all keys in policy, warning and ignoring any
key not in the recognized field set. Preserve the existing validators and
warning behavior for known fields with invalid values, and continue rebuilding
p.retryOn429 from only accepted entries.

In `@src/images/loop.ts`:
- Around line 479-486: Await the unread 429 response body cancellation in the
retry flow around sleepWithAbort, while preserving handling for already-closed
bodies and cancellation failures. Update
devlog/_plan/260802_429_same_target_retry/010_design.md lines 53-55 and
structure/04_transports-and-sidecars.md lines 236-237 only as needed to keep
their “release before backoff” claims accurate after the implementation change.
- Around line 482-498: The retry flow around sleepWithAbort in
src/images/loop.ts lines 482-498 must clear headerDeadline before sleeping,
check signal.aborted immediately afterward and throw the existing 499 LoopError
before telemetry or replay, then create the replacement deadline before
onRateLimitRetrySend and fetchOnce. Update
structure/04_transports-and-sidecars.md lines 238-242 to preserve and document
the 499-before-replay guarantee; no other behavior needs changing.

---

Outside diff comments:
In `@src/images/loop.ts`:
- Around line 469-476: Keep the 429 retry policy and counter request-scoped in
prepareIterationEvents/runWithImageBridge so multiple image-loop iterations
cannot reset the same-key retry budget; preserve the documented per-request send
bound. Update structure/04_transports-and-sidecars.md:235 to reflect the chosen
request-scoped bound, and extend
devlog/_plan/260802_429_same_target_retry/010_design.md:95-110 with a
multi-iteration image-bridge regression scenario. Add a focused regression to
the flat Bun tests covering this behavior.

In `@src/server/responses/core.ts`:
- Around line 2647-2685: Update the live retry-backoff loops in
src/server/responses/core.ts:2647-2685, src/web-search/loop.ts:363-393, and
src/images/loop.ts:469-498 to split sleepWithAbort waits into sub-2,000 ms
chunks, yielding a heartbeat after every chunk including the final one, while
preserving abort handling; leave pre-stream retry loops unchanged. Add
regression coverage for backoffs exceeding stallTimeoutSec, and update
web-search timeout documentation and tests to describe continued liveness
through upstream heartbeats.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9881d8f6-9808-4382-a953-a3a067cfc38b

📥 Commits

Reviewing files that changed from the base of the PR and between a06a416 and 0feede1.

📒 Files selected for processing (26)
  • devlog/_plan/260802_429_same_target_retry/010_design.md
  • docs-site/src/content/docs/guides/providers.md
  • docs-site/src/content/docs/ja/guides/providers.md
  • docs-site/src/content/docs/ja/reference/adapters.md
  • docs-site/src/content/docs/ko/guides/providers.md
  • docs-site/src/content/docs/ko/reference/adapters.md
  • docs-site/src/content/docs/reference/adapters.md
  • docs-site/src/content/docs/ru/guides/providers.md
  • docs-site/src/content/docs/ru/reference/adapters.md
  • docs-site/src/content/docs/zh-cn/guides/providers.md
  • docs-site/src/content/docs/zh-cn/reference/adapters.md
  • src/config.ts
  • src/images/loop.ts
  • src/providers/derive.ts
  • src/providers/key-failover.ts
  • src/server/responses/core.ts
  • src/web-search/loop.ts
  • structure/04_transports-and-sidecars.md
  • tests/config-user-edits.test.ts
  • tests/images/loop.test.ts
  • tests/provider-registry-parity.test.ts
  • tests/rate-limit-retry.test.ts
  • tests/server-rate-limit-retry-e2e.test.ts
  • tests/terminal-guard-server.test.ts
  • tests/usage-log.test.ts
  • tests/web-search.test.ts

Comment thread devlog/_plan/260802_429_same_target_retry/010_design.md
Comment thread docs-site/src/content/docs/ja/reference/adapters.md Outdated
Comment thread docs-site/src/content/docs/zh-cn/guides/providers.md
Comment thread src/config.ts
Comment thread src/images/loop.ts
Comment thread src/images/loop.ts Outdated
…ne race, config warnings, locale docs)

- images/web-search loops: AWAIT the unread 429 body cancellation before the
  backoff; clear the old header deadline BEFORE the sleep; re-check client
  cancellation after the wait so 499 wins over stale-deadline edges; start the
  fresh deadline before telemetry and replay
- config: sanitizeRetryOn429ForLoad warns about misnamed keys (e.g. attempt)
  instead of silently dropping them
- docs: locale adapter pages state same-key replay runs before other
  handling/failover; provider guide (English + ja/ko/ru/zh-cn) states retryOn429
  is opt-in (absent = off); devlog and structure/04 note the awaited
  cancellation and the 499-before-replay guarantee
- test: config-user-edits covers the misnamed-key drop
@harryzhou2000
harryzhou2000 marked this pull request as draft August 1, 2026 19:50
@harryzhou2000
harryzhou2000 marked this pull request as ready for review August 1, 2026 19:57
@harryzhou2000
harryzhou2000 marked this pull request as draft August 1, 2026 19:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
devlog/_plan/260802_429_same_target_retry/010_design.md (1)

47-53: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the runTurn exception for bridge retries.

src/images/loop.ts enters the adapter.runTurn branch at Lines 355-433 and returns before the HTTP 429 retry loop at Lines 477-513. A key-auth custom runTurn adapter therefore does not receive this HTTP retry policy. State that bridge retries apply to HTTP adapters only and exclude custom runTurn transports.

🤖 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 `@devlog/_plan/260802_429_same_target_retry/010_design.md` around lines 47 -
53, Update the retry-policy documentation near the bridge retry references to
state that image/video bridge retries apply only to HTTP adapters. Explicitly
exclude custom adapters using the adapter.runTurn path in src/images/loop.ts,
which returns before the HTTP 429 retry loop; do not imply that runTurn
transports receive the same wait-and-replay behavior.
src/server/responses/core.ts (1)

2668-2707: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Await terminal-continuation body cancellation before the backoff.

At Line 2679, response.body.cancel() is detached with void. sleepWithAbort() then starts at Line 2681 while cancellation can still be pending. Under repeated 429 responses, unread bodies can remain active through the retry wait and replay.

Await cancellation before sleeping.

Proposed fix
-        try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
+        try { await response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
🤖 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 `@src/server/responses/core.ts` around lines 2668 - 2707, In the
terminal-continuation retry loop, update the response body cleanup before
sleepWithAbort to await response.body cancellation rather than detaching the
promise. Preserve the existing safe handling for absent or already-closed
bodies, and ensure the backoff starts only after cancellation has settled.
🤖 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.

Outside diff comments:
In `@devlog/_plan/260802_429_same_target_retry/010_design.md`:
- Around line 47-53: Update the retry-policy documentation near the bridge retry
references to state that image/video bridge retries apply only to HTTP adapters.
Explicitly exclude custom adapters using the adapter.runTurn path in
src/images/loop.ts, which returns before the HTTP 429 retry loop; do not imply
that runTurn transports receive the same wait-and-replay behavior.

In `@src/server/responses/core.ts`:
- Around line 2668-2707: In the terminal-continuation retry loop, update the
response body cleanup before sleepWithAbort to await response.body cancellation
rather than detaching the promise. Preserve the existing safe handling for
absent or already-closed bodies, and ensure the backoff starts only after
cancellation has settled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 72915daa-eace-482c-83cd-3c1d098cc169

📥 Commits

Reviewing files that changed from the base of the PR and between 0feede1 and 1af83c3.

📒 Files selected for processing (24)
  • devlog/_plan/260802_429_same_target_retry/010_design.md
  • docs-site/src/content/docs/guides/providers.md
  • docs-site/src/content/docs/ja/guides/providers.md
  • docs-site/src/content/docs/ja/reference/adapters.md
  • docs-site/src/content/docs/ko/guides/providers.md
  • docs-site/src/content/docs/ko/reference/adapters.md
  • docs-site/src/content/docs/ru/guides/providers.md
  • docs-site/src/content/docs/ru/reference/adapters.md
  • docs-site/src/content/docs/zh-cn/guides/providers.md
  • docs-site/src/content/docs/zh-cn/reference/adapters.md
  • src/config.ts
  • src/images/loop.ts
  • src/providers/derive.ts
  • src/providers/key-failover.ts
  • src/server/responses/core.ts
  • src/types.ts
  • src/web-search/loop.ts
  • structure/04_transports-and-sidecars.md
  • tests/config-user-edits.test.ts
  • tests/images/loop.test.ts
  • tests/server-rate-limit-retry-e2e.test.ts
  • tests/terminal-guard-server.test.ts
  • tests/usage-log.test.ts
  • tests/web-search.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1af83c39cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/images/loop.ts Outdated
Comment thread src/config.ts
Comment thread src/images/loop.ts Outdated
Comment thread src/config.ts Outdated
… send telemetry, invalid master switch, secret-safe warnings)
@harryzhou2000
harryzhou2000 marked this pull request as ready for review August 1, 2026 20:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4e1978821

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/config.ts Outdated
Comment thread src/server/responses/core.ts Outdated
…ncel, post-sleep abort re-checks, pinned xAI req-id)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/server/responses/core.ts (1)

2617-2666: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reduce the duplicated recovery-label expression in fetchContinuation.

Lines 2638 and 2647 both derive the same label from replay. The second one additionally merges the transient-retry kind. The logic is correct, but the label rule now lives in two places, so a future change to the label (for example a distinct kind for continuation replays) can easily update only one branch.

♻️ Optional consolidation
       if (continuationEstimate !== undefined) logCtx.usageLogInputTokens = continuationEstimate;
+      const replayKind: AttemptRecoveryKind | undefined = replay ? "rate-limit-429" : undefined;
       try {
         if (activeAdapter.fetchResponse) {
-          noteAttemptSend(logCtx.activeAttempt, continuationEstimate, replay ? "rate-limit-429" : undefined);
+          noteAttemptSend(logCtx.activeAttempt, continuationEstimate, replayKind);
           return await activeAdapter.fetchResponse(continuationRequest, {
@@
         return await fetchWithResetRetry(
           recovery => {
-            noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? (replay ? "rate-limit-429" : undefined));
+            noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind);
🤖 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 `@src/server/responses/core.ts` around lines 2617 - 2666, Consolidate the
replay-derived recovery label in fetchContinuation by computing it once before
the fetch branches, then reuse that value in both noteAttemptSend calls while
preserving the fetchWithResetRetry recovery override behavior.
src/config.ts (1)

1144-1158: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Include the accepted range in the warning for out-of-range numbers.

The validators at Lines 1146-1148 reject both wrong types and out-of-range numbers, but the warning at Line 1157 reports only typeof value. A user who writes "attempts": 999 sees providers.x.retryOn429.attempts (number) is invalid, which gives no hint about the accepted bound. Numbers and booleans cannot carry secret material, so the range can be stated safely while the value itself stays unlogged.

♻️ Proposed diagnostic improvement
-    const fields: Array<[string, (value: unknown) => boolean]> = [
-      ["enabled", value => typeof value === "boolean"],
-      ["attempts", value => typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 20],
-      ["intervalMs", value => typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 600_000],
-      ["maxIntervalMs", value => typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 600_000],
-      ["respectRetryAfter", value => typeof value === "boolean"],
+    const fields: Array<[string, (value: unknown) => boolean, string]> = [
+      ["enabled", value => typeof value === "boolean", "expected boolean"],
+      ["attempts", value => typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 20, "expected integer 1-20"],
+      ["intervalMs", value => typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 600_000, "expected integer 100-600000"],
+      ["maxIntervalMs", value => typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 600_000, "expected integer 100-600000"],
+      ["respectRetryAfter", value => typeof value === "boolean", "expected boolean"],
     ];
     const cleaned: Record<string, unknown> = {};
-    for (const [key, isValid] of fields) {
+    for (const [key, isValid, expectation] of fields) {
       const value = policyRecord[key];
       if (value === undefined) continue;
       if (isValid(value)) cleaned[key] = value;
       // Log only the received type, never the value (provider config can hold secrets).
-      else console.warn(`⚠️  config.json providers.${name}.retryOn429.${key} (${typeof value}) is invalid — ignoring the field`);
+      else console.warn(`⚠️  config.json providers.${name}.retryOn429.${key} (${typeof value}) is invalid — ${expectation}; ignoring the field`);
     }
🤖 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 `@src/config.ts` around lines 1144 - 1158, Update the validation warning in the
retry policy field loop around the `fields` validators to include the accepted
ranges for numeric keys such as `attempts`, `intervalMs`, and `maxIntervalMs`,
while retaining type-only reporting and never logging the received value. Keep
boolean warnings unchanged and preserve the existing validation and
field-cleaning behavior.
src/images/loop.ts (1)

483-515: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound pre-header 429 retries before creating SSE

For non-runTurn image requests and all web-search requests, prepareIterationDrained consumes the retry heartbeat before bridgeToResponsesSSE is created. The client receives no response headers while same-target 429 retries wait. The default policy can wait 3 minutes; valid configuration can wait up to 200 minutes (20 × 600_000ms). Bound the aggregate eager-phase wait to the request header budget, or move these retries into the live produce() phase. Apply the fix to both loops. runTurn image requests already skip the eager drain.

🤖 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 `@src/images/loop.ts` around lines 483 - 515, Bound same-target 429 retry
waiting during the eager preparation phase so it cannot exceed the request
header-timeout budget before SSE creation, or defer the retries into the live
produce phase. Apply the corresponding fix to the retry loop in
src/images/loop.ts lines 483-515 and src/web-search/loop.ts lines 380-412;
preserve the existing runTurn image behavior, which already skips eager
draining.
🤖 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 `@src/providers/xai-transport.ts`:
- Around line 135-138: Rename the second parameter of withGeneratedRequestId to
reflect that it receives the already-resolved request ID, whether configured or
generated. Update all references within the helper while preserving the single
fallback in the request setup where configuredRequestId ?? randomUUID() is
computed, ensuring retries continue using the same ID.

In `@tests/config-user-edits.test.ts`:
- Around line 183-185: In the test assertion for the retryOn429 diagnostic,
replace the loose "string" substring check with an assertion anchored to the
retryOn429 field and its parenthesized received type. Keep the secret-redaction
assertion unchanged and ensure the expectation specifically verifies the
type-only warning from loadConfig().

---

Outside diff comments:
In `@src/config.ts`:
- Around line 1144-1158: Update the validation warning in the retry policy field
loop around the `fields` validators to include the accepted ranges for numeric
keys such as `attempts`, `intervalMs`, and `maxIntervalMs`, while retaining
type-only reporting and never logging the received value. Keep boolean warnings
unchanged and preserve the existing validation and field-cleaning behavior.

In `@src/images/loop.ts`:
- Around line 483-515: Bound same-target 429 retry waiting during the eager
preparation phase so it cannot exceed the request header-timeout budget before
SSE creation, or defer the retries into the live produce phase. Apply the
corresponding fix to the retry loop in src/images/loop.ts lines 483-515 and
src/web-search/loop.ts lines 380-412; preserve the existing runTurn image
behavior, which already skips eager draining.

In `@src/server/responses/core.ts`:
- Around line 2617-2666: Consolidate the replay-derived recovery label in
fetchContinuation by computing it once before the fetch branches, then reuse
that value in both noteAttemptSend calls while preserving the
fetchWithResetRetry recovery override behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3f71039b-4261-460f-853e-cf5dc91ff470

📥 Commits

Reviewing files that changed from the base of the PR and between 1af83c3 and 58bf694.

📒 Files selected for processing (11)
  • src/config.ts
  • src/images/loop.ts
  • src/providers/xai-transport.ts
  • src/server/responses/core.ts
  • src/web-search/loop.ts
  • tests/config-user-edits.test.ts
  • tests/images/loop.test.ts
  • tests/rate-limit-retry.test.ts
  • tests/terminal-guard-server.test.ts
  • tests/web-search.test.ts
  • tests/xai-transport.test.ts

Comment thread src/providers/xai-transport.ts
Comment thread tests/config-user-edits.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/config.ts (1)

1384-1384: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Sanitize configDiagnosticsFromRaw before schema validation. loadConfig sanitizes before both parses, but src/config.ts:1540-1546 does not. An invalid retryOn429 then returns the default fallback, and src/cli/config-command.ts:101-105 can save that fallback over the user's providers and keys. Call sanitizeRetryOn429ForLoad(parsed) after JSON.parse and add a regression test.

🤖 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 `@src/config.ts` at line 1384, Update the config loading path around
configDiagnosticsFromRaw so sanitizeRetryOn429ForLoad(parsed) runs immediately
after JSON.parse and before schema validation, matching loadConfig’s
sanitization order. Preserve the existing validation and fallback behavior
otherwise, and add a regression test confirming invalid retryOn429 does not
cause the fallback to overwrite user providers or keys when saving via the
config command.
🤖 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 `@src/config.ts`:
- Around line 1163-1166: Update the warning in the retryOn429 unknown-field
handling to apply a log-safe encoding to the result of redactSecretString(key)
before interpolation. Preserve the existing secret redaction and readable
ordinary field names while escaping control and ANSI characters in the logged
field name.

---

Outside diff comments:
In `@src/config.ts`:
- Line 1384: Update the config loading path around configDiagnosticsFromRaw so
sanitizeRetryOn429ForLoad(parsed) runs immediately after JSON.parse and before
schema validation, matching loadConfig’s sanitization order. Preserve the
existing validation and fallback behavior otherwise, and add a regression test
confirming invalid retryOn429 does not cause the fallback to overwrite user
providers or keys when saving via the config command.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8b7029ea-0a8e-4f20-88be-326c30edde53

📥 Commits

Reviewing files that changed from the base of the PR and between 58bf694 and 4fee87b.

📒 Files selected for processing (2)
  • src/config.ts
  • tests/config-user-edits.test.ts

Comment thread src/config.ts Outdated
@harryzhou2000

Copy link
Copy Markdown
Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 `@src/config.ts`:
- Around line 1163-1167: Update sanitizeRetryOn429ForLoad() to create one safe
provider-name value using JSON.stringify(redactSecretString(name)), then use
that value in every warning emitted by the function instead of interpolating
name directly. Add coverage verifying secret-shaped provider names are redacted
and control-character names are safely JSON-escaped.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 350a5f0d-56af-496c-a07b-844d73515bd6

📥 Commits

Reviewing files that changed from the base of the PR and between 58bf694 and e65106f.

📒 Files selected for processing (3)
  • src/config.ts
  • src/providers/xai-transport.ts
  • tests/config-user-edits.test.ts

Comment thread src/config.ts Outdated
@lidge-jun

Copy link
Copy Markdown
Owner

Maintainer architecture review on head 89535fb7. This capability is strategically important to us — we intend to drive it to landing — so here is the exact verified state and what must change.

Verified FIXED: (a) backoff no longer consumes the header deadline in images/web-search loops; (c) retry budget is request-wide across key/account rotation and image-tier continues; (d) auth gating rejects oauth/forward/local and registry preserves local; (f) passthrough telemetry falls back to rate-limit-429 correctly. Local head verification: 196 focused tests + typecheck green.

Still blocking:

  1. Bridge stall watchdog vs long backoff (NOT FIXED). Terminal continuation awaits the full backoff without yielding any adapter event (src/server/responses/core.ts:2682-2715); the bridge only records upstream activity when iterator.next() returns (src/bridge.ts:694-703) and aborts at the stall limit (default 300s, src/stall-timeout.ts:8-19) while retry waits can reach 600s (src/config.ts:490-493). Image/web-search yield their heartbeat only after sleeping. During deliberate waits, periodically yield adapter heartbeats or explicitly suspend/rebase the watchdog — wire response.heartbeat frames do not reset upstream activity. Add a deterministic test where the 429 wait exceeds stallTimeoutSec and the replay still succeeds.
  2. Same-target replay still rebuilds (PARTIAL). Passthrough correctly reuses one built request (core.ts:1588-1639), but the main recovery path (core.ts:2384-2408), terminal continuation (core.ts:2625-2665), and both sidecar loops rebuild per attempt. Cache an immutable body-safe outbound request per target (URL, serialized body, auth headers, generated compat headers); rebuild only after key/account/adapter/target changes. Assert full header/body equality and that the request builder runs once per same-target sequence.
  3. Header-deadline regression coverage for image/web-search: wait longer than connectTimeoutMs, prove the replay succeeds instead of 504.
  4. Full CI never ran on the current head (only enforce-target/label/CodeRabbit contexts exist). Merge policy requires the full matrix green on the final SHA.

Items 1-2 are the architectural core; everything else is close. If you are short on cycles, say so — a maintainer will take over the remaining items on top of your branch. Thank you for the substantial work here; the resolved set shows the design is sound.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants