Skip to content

Complete coderouter private-beta routing reliability - #9692

Merged
lawrencecchen merged 1 commit into
mainfrom
feat/coderouter-private-beta-100
Aug 6, 2026
Merged

lawrencecchen merged 1 commit into
mainfrom
feat/coderouter-private-beta-100

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Adds manager-scoped account removal with encrypted-credential cascade and final-account route-token revocation, transient-safe provider reads, multi-account failure routing, refresh/KMS/RDS race tests, scrubbed alert signals, usage freshness metadata, Server-Timing, and accurate landing-page support states. Also includes the production backend needed by experimental cr pi.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Improves CodeRouter private beta reliability with safe, retried provider reads, multi-account failover, and secure manager-scoped deletion that cascades encrypted credentials and revokes route tokens when a team loses its last account. Adds usage freshness fields with Server-Timing mirrored in headers, rate-limit telemetry, and updates the landing page to reflect private beta and experimental cr pi.

  • New Features

    • DELETE api/coderouter/accounts/[accountId] (manage scope, team-scoped) with encrypted-credential cascade and final route-token revocation; returns legacyCleanupPending when rollback cleanup fails.
    • Account listing now returns usageAsOf, usageAgeSeconds, cacheMaxAgeSeconds, and Server-Timing; mirrors timing in x-coderouter-server-timing. Landing page clarifies: Codex and OpenCode Go available, Pi experimental, Claude Max and API-key routing coming soon.
  • Bug Fixes

    • Adds fetchProviderRead for one safe retry of read-only provider calls; used in Codex models, usage reads, and OpenCode config; reports rate limits and applies cooldowns; routes around refreshing/broken accounts.
    • Hardens refresh with lease coordination, rotated-token persistence via CAS, terminal error classification, and scrubbed error reporting via reportCoderouterFailure; coalesces Vercel AWS role exchanges for KMS.

Written for commit db6cc10. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added permission- and team-scoped CodeRouter account removal with cleanup status reporting.
    • Account listings now include usage metadata, cache age, request timing, and improved cache handling.
    • Improved provider routing with transient-failure retries and fallback to healthy accounts.
    • Added shared credential refresh handling for more reliable encrypted provider access.
  • Bug Fixes

    • Improved credential refresh recovery and temporary provider failure handling.
    • Enhanced error reporting while protecting sensitive credentials.
  • Documentation

    • Updated CodeRouter messaging to reflect private beta availability and supported providers.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds team-scoped CodeRouter account deletion, provider-read retries, credential-refresh observability, proxy account failover, usage timing metadata, updated credential-storage documentation, landing-page status text, and expanded tests.

Changes

CodeRouter reliability and account management

Layer / File(s) Summary
Team-scoped account removal
web/services/coderouter/repository.ts, web/services/coderouter/accounts.ts, web/app/api/coderouter/accounts/[accountId]/route.ts, web/tests/coderouter-account-removal.test.ts
The deletion flow validates UUIDs, checks management authorization, removes runtime data, performs leased legacy cleanup, and reports pending cleanup failures.
Credential refresh coordination
web/services/coderouter/refresh.ts, web/services/coderouter/encryption.ts, web/services/coderouter/observability.ts, web/tests/coderouter-refresh.test.ts, web/tests/coderouter-encryption.test.ts
Credential refresh uses injected dependencies, lease coordination, failure reporting, and terminal-error handling. AWS credentials use cached, coalesced refreshes.
Provider-read retries and proxy routing
web/services/coderouter/providerFetch.ts, web/services/coderouter/codexProxy.ts, web/services/coderouter/opencodeProxy.ts, web/tests/coderouter-provider-fetch.test.ts, web/tests/coderouter-models-proxy.test.ts, web/tests/coderouter-opencode-proxy.test.ts
Provider reads retry transient failures. Codex and OpenCode routing use injected services and fail over when credentials are unavailable.
Usage timing and account API
web/services/coderouter/usage.ts, web/app/api/coderouter/accounts/route.ts
Usage responses include timing and cache metadata. The accounts API emits serialized JSON, timing headers, and no-store cache headers.
Status and storage documentation
web/app/coderouter/page.tsx, web/db/schema.ts
The landing page describes provider availability. The schema documentation identifies encrypted credential storage.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant DeleteAccountRoute
  participant AccountRemover
  participant Repository
  participant LegacyVault
  Client->>DeleteAccountRoute: DELETE account
  DeleteAccountRoute->>AccountRemover: removeAccount(teamId, accountId)
  AccountRemover->>Repository: deleteAccount(teamId, accountId)
  Repository-->>AccountRemover: removal metadata
  AccountRemover->>LegacyVault: delete legacy credentials under lease
  AccountRemover-->>DeleteAccountRoute: cleanup status
  DeleteAccountRoute-->>Client: deletion response
Loading

Possibly related PRs


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error web/app/coderouter/page.tsx adds and changes visible English copy, but has no next-intl source and no matching entries in any of the 20 supported locale files. Move the CodeRouter page copy and metadata to next-intl keys, then add translated entries to all 20 files in web/messages/.
Docstring Coverage ⚠️ Warning Docstring coverage is 7.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description summarizes the changes but omits the required Testing, Demo Video, review trigger, and Checklist sections. Add the required Testing, Demo Video, Review Trigger, and Checklist sections, including test results and the requested review status.
✅ Passed checks (22 passed)
Check name Status Explanation
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.
Cmux Swift Actor Isolation ✅ Passed The PR changes no Swift files; all 19 changed paths are TypeScript, TSX, schema, or tests, so it introduces no Swift actor-isolation issue.
Cmux Swift Blocking Runtime ✅ Passed The PR changes 19 TypeScript/TSX/schema/test files and no Swift files, so it introduces no production Swift blocking runtime primitive.
Cmux Browser Automation Off-Main ✅ Passed The PR changes only 19 web/CodeRouter files; it has no Swift/native paths, browser.* commands, WebKit waits, or socket-worker routing changes covered by the rule.
Cmux Expensive Synchronous Load ✅ Passed The commit changes only 18 TypeScript and one TSX file; it adds no Swift paths or expensive Swift loader calls, so this check is not applicable.
Cmux Cache Substitution Correctness ✅ Passed The diff adds no persistence, history, undo, or snapshot path. The existing usage cache is unchanged, while provider reads remain no-store and only retry fresh requests.
Cmux No Hacky Sleeps ✅ Passed The production diff adds no sleep, setTimeout, setInterval, polling delay, or delayed dispatch; fetchProviderRead retries immediately and its tests cover the retry behavior. Existing AbortSignal.ti...
Cmux Algorithmic Complexity ✅ Passed Added account-routing loops are explicitly bounded to 8; provider retries and fixed lists are constant-size. The usage join and OpenCode nested provider/model traversal predate this diff, and delet...
Cmux Swift Concurrency ✅ Passed The pull request changes 19 web files only; the parent-to-HEAD diff contains no Swift paths or Swift content, so it introduces no cmux Swift concurrency patterns.
Cmux Swift @Concurrent ✅ Passed The PR diff contains only 18 TypeScript and 1 TSX file, with no Swift files or Swift concurrency changes; the Swift-specific check is not applicable.
Cmux Swift Package Boundaries ✅ Passed The PR diff contains 19 TypeScript files and zero Swift or Package.swift changes, so the Swift package-boundary rule is not applicable.
Cmux Swiftpm Lockfiles ✅ Passed The PR diff contains only web CodeRouter TypeScript, schema, page, and test changes; it changes no Package.swift, Package.resolved, Xcode project, .gitignore, workflow, or dependency files.
Cmux Swift Logging ✅ Passed The PR diff changes only web TypeScript and schema/test files; no Swift files are changed, so the Swift logging check is not applicable.
Cmux User-Facing Error Privacy ✅ Passed Changed API errors use generic codes; usage failures expose only HTTP status or “unavailable”; telemetry reports generic errors with scalar context and no raw messages or credentials.
Cmux Swiftui State Layout ✅ Passed The PR diff contains only TypeScript and TSX files; it has no Swift-family paths or added SwiftUI patterns, so this check is not applicable.
Cmux Architecture Rethink ✅ Passed The PR diff changes only 19 web files and contains no Swift, Objective-C, or UI lifecycle files; the Swift architectural rule is not applicable.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR changes 19 files, all TypeScript/TSX; the diff contains no Swift files or NSWindow/WindowGroup code, so the auxiliary-window rule is not applicable.
Cmux Source Artifacts ✅ Passed All 19 changed paths are regular TypeScript/TSX source, schema, or test files under web; no artifact directories, binaries, logs, screenshots, caches, or build output were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The PR diff contains no Swift files or Swift diff hunks; all 19 changed files are web TypeScript/TSX files, so no production Sources/ test/debug seam was added.
Cmux No Ambient Global State ✅ Passed The pull request changes only web TypeScript/TSX files; it contains no production Swift changes, so the Swift ambient-global-state rule does not apply.
Title check ✅ Passed The title clearly summarizes the primary changes that improve CodeRouter private-beta routing reliability.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/coderouter-private-beta-100

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.

❤️ Share

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

@lawrencecchen
lawrencecchen force-pushed the feat/coderouter-private-beta-100 branch from 4c179d2 to 4bc5712 Compare August 6, 2026 04:47
@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@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: 9

🤖 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 `@web/app/coderouter/page.tsx`:
- Around line 38-45: Move the landing-page strings in the coderouter page,
including “private beta” and the availability paragraph, to the established
next-intl locale source. Add keys and matching translations for every locale
listed in web/i18n/routing.ts under web/messages/, then read them through the
page’s existing localization mechanism instead of hardcoded English text.

In `@web/services/coderouter/accounts.ts`:
- Around line 76-88: Update removeAccount() so a failed deleteLegacy cleanup
remains recoverable when deleteRuntime reports the account is already absent:
persist an idempotent legacy-cleanup job or invoke cleanup during the retry path
before returning 404. Preserve idempotency, ensure successful retries clear
legacyCleanupPending, and add a recovery test covering failure followed by a
retry DELETE.

In `@web/services/coderouter/codexProxy.ts`:
- Around line 129-138: Extract the routing-skip tag predicate currently used by
proxyCodexRequest into a shared CodeRouter helper, then use it in
proxyCodexRequest and both account-selection loops. In codexProxy.ts at lines
129-138 and opencodeProxy.ts at lines 90-92, skip only CodeRouterRefreshBusy and
CodeRouterCredentialBroken errors; report or rethrow all other credential
failures so infrastructure errors reach the existing failure handling.
- Around line 142-150: Update the upstream fetch inside the providerRead
callback in the codex proxy to create a fresh AbortSignal.timeout(5_000) for
each retry attempt and pass it as the fetch signal, matching the bounded reads
used by sibling providers.

In `@web/services/coderouter/providerFetch.ts`:
- Around line 7-12: Update the retryable-status branch in the request flow
around `request()` and `RETRYABLE_STATUS` to cancel the discarded first
`Response` body before issuing the second request. Preserve returning the first
response for non-retryable statuses and the existing retry behavior for thrown
requests.

In `@web/services/coderouter/refresh.ts`:
- Around line 107-118: Separate the `dependencies.refresh` error handling from
the surrounding lease operations in the refresh flow. Report `provider_refresh`
with the provider tag only when `dependencies.refresh` fails; report failures
from `dependencies.read`, `dependencies.release`, `dependencies.encrypt`, or
`dependencies.complete` under the existing `rds` category without a provider
tag. Keep the existing `dependencies.fail` lease-failure recording unchanged.

In `@web/services/coderouter/repository.ts`:
- Around line 100-112: The final-account check in the account deletion flow of
web/services/coderouter/repository.ts (lines 100-112) must be serialized per
team: add a transaction-scoped per-team lock shared by both account creation and
deletion, then perform the remaining-account check and active route-token
revocation under that boundary. Extend
web/tests/coderouter-account-removal.test.ts (lines 83-124) with a database
regression test that concurrently deletes two accounts from the same team and
verifies every active route token is revoked.

In `@web/tests/coderouter-models-proxy.test.ts`:
- Around line 22-27: Update the fake select implementation in the proxy test to
inspect its third argument and record or assert the supplied excludedAccountIds,
then verify that createCodexModelsProxy passes previously attempted account IDs
on subsequent selections. Preserve the existing queue behavior while adding an
assertion matching the OpenCode proxy test’s coverage.

In `@web/tests/coderouter-refresh.test.ts`:
- Around line 113-125: Add a test alongside the existing
createCredentialRefresher cases that makes read return an expired credential on
the initial call and a valid credential after lease acquisition. Configure
release to record invocation and refresh to fail if called, then assert the
refresher returns the existing valid token, release was invoked, and the
provider refresh path was not contacted.
🪄 Autofix

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: 8b11d05a-2616-4550-be7f-166353cf6de0

📥 Commits

Reviewing files that changed from the base of the PR and between e012389 and 4c179d2.

📒 Files selected for processing (18)
  • web/app/api/coderouter/accounts/[accountId]/route.ts
  • web/app/api/coderouter/accounts/route.ts
  • web/app/coderouter/page.tsx
  • web/db/schema.ts
  • web/services/coderouter/accounts.ts
  • web/services/coderouter/codexProxy.ts
  • web/services/coderouter/observability.ts
  • web/services/coderouter/opencodeProxy.ts
  • web/services/coderouter/providerFetch.ts
  • web/services/coderouter/refresh.ts
  • web/services/coderouter/repository.ts
  • web/services/coderouter/usage.ts
  • web/tests/coderouter-account-removal.test.ts
  • web/tests/coderouter-encryption.test.ts
  • web/tests/coderouter-models-proxy.test.ts
  • web/tests/coderouter-opencode-proxy.test.ts
  • web/tests/coderouter-provider-fetch.test.ts
  • web/tests/coderouter-refresh.test.ts

Comment on lines +38 to +45
private beta
</span>
</div>

<p className="mt-5 max-w-xl font-mono text-[11px] leading-5 text-black/40">
Codex and OpenCode Go are available now. Pi support is experimental.
Claude Max and API-key routing are coming soon.
</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Move the new landing-page copy into the locale catalogs.

Lines 38, 43, and 44 add raw English user-facing text. Read these strings from next-intl or the established locale source. Add matching translations for every supported locale.

As per coding guidelines, “User-facing web UI text ... must use next-intl or another locale-specific runtime source and be represented for every supported locale.” As per path instructions, update every supported locale in web/i18n/routing.ts and web/messages/.

🤖 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 `@web/app/coderouter/page.tsx` around lines 38 - 45, Move the landing-page
strings in the coderouter page, including “private beta” and the availability
paragraph, to the established next-intl locale source. Add keys and matching
translations for every locale listed in web/i18n/routing.ts under web/messages/,
then read them through the page’s existing localization mechanism instead of
hardcoded English text.

Sources: Coding guidelines, Path instructions

Comment on lines +76 to +88
const result = await dependencies.deleteRuntime({ teamId, accountId });
if (!result.removed) return { ...result, legacyCleanupPending: false };
try {
// Temporary rollback copy only. This call disappears after the migration
// cleanup window; failure cannot restore runtime access to the credential.
await dependencies.withLease(
teamId,
async () => await dependencies.deleteLegacy(teamId, accountId),
);
return { ...result, legacyCleanupPending: false };
} catch (error) {
dependencies.report("legacy_cleanup", error);
return { ...result, legacyCleanupPending: true };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file sizes =="
wc -l web/services/coderouter/accounts.ts web/app/api/coderouter/accounts/[accountId]/route.ts web/tests/coderouter-account-removal.test.ts web/services/coderouter/vault.ts web/services/coderouter/repository.ts 2>/dev/null || true

echo "== accounts.ts relevant section =="
sed -n '1,140p' web/services/coderouter/accounts.ts

echo "== route.ts =="
sed -n '1,90p' 'web/app/api/coderouter/accounts/[accountId]/route.ts'

echo "== tests relevant section =="
sed -n '1,180p' web/tests/coderouter-account-removal.test.ts

echo "== search legacy cleanup pending / reconciler / deleteVaultCredential =="
rg -n "legacyCleanupPending|clean.*legacy|deleteLegacy|deleteVault|legacy cleanup|legacy_cleanup|Delete.*Account|removeAccount" web -S || true

Repository: manaflow-ai/cmux

Length of output: 13763


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file sizes =="
wc -l web/services/coderouter/accounts.ts 'web/app/api/coderouter/accounts/[accountId]/route.ts' web/tests/coderouter-account-removal.test.ts web/services/coderouter/vault.ts web/services/coderouter/repository.ts 2>/dev/null || true

echo "== accounts.ts relevant section =="
sed -n '1,140p' web/services/coderouter/accounts.ts

echo "== route.ts =="
sed -n '1,90p' 'web/app/api/coderouter/accounts/[accountId]/route.ts'

echo "== tests relevant section =="
sed -n '1,180p' web/tests/coderouter-account-removal.test.ts

echo "== search legacy cleanup pending / reconciler / deleteVaultCredential =="
rg -n "legacyCleanupPending|clean.*legacy|deleteLegacy|deleteVault|legacy cleanup|legacy_cleanup|Delete.*Account|removeAccount" web -S || true

Repository: manaflow-ai/cmux

Length of output: 13763


Sensitive Data Exposure (CWE-459)

Reachability: Internal

Reachability path
● Entry
  web/app/api/coderouter/accounts/route.ts:8
  runtime
│
▼
● Hop
  web/app/api/coderouter/accounts/[accountId]/route.ts:9
  createDeleteAccountHandler
│
▼
● Sink
  web/services/coderouter/accounts.ts

Make failed legacy cleanup recoverable.

removeAccount() deletes the runtime account before the leased legacy vault cleanup, and cleanup failures return legacyCleanupPending: true. A retry DELETE reuses removeAccount(); when the runtime account is already absent, 404 is returned, so this path cannot retry the vault cleanup. Persist an idempotent cleanup job or retry legacy cleanup even after the runtime account is gone, and add a recovery test for failed legacy cleanup.

🤖 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 `@web/services/coderouter/accounts.ts` around lines 76 - 88, Update
removeAccount() so a failed deleteLegacy cleanup remains recoverable when
deleteRuntime reports the account is already absent: persist an idempotent
legacy-cleanup job or invoke cleanup during the retry path before returning 404.
Preserve idempotency, ensure successful retries clear legacyCleanupPending, and
add a recovery test covering failure followed by a retry DELETE.

Comment on lines +129 to +138
let credential;
try {
credential = await dependencies.credential({
teamId: identity.teamId,
accountId: account.id,
expectedRevision: account.vaultRevision,
});
} catch {
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unclassified credential errors turn infrastructure outages into silent 503s. Both new account-selection loops catch every error from the credential dependency and continue to the next account. Neither one distinguishes a routing-skip condition (CodeRouterRefreshBusy, CodeRouterCredentialBroken) from an infrastructure fault such as a KMS outage, an RDS failure, or a decryption error. The caller receives no_usable_account (503) after the attempt bound, and reportCoderouterFailure never runs, so the outage produces no alert. proxyCodexRequest at web/services/coderouter/codexProxy.ts lines 49-56 already classifies the error correctly, so the two paths now disagree.

  • web/services/coderouter/codexProxy.ts#L129-L138: replace the bare catch with the tag check used by proxyCodexRequest, and rethrow anything that is not a routing-skip tag.
  • web/services/coderouter/opencodeProxy.ts#L90-L92: replace the bare catch in openCodeAccount with the same tag check, and report or rethrow anything else instead of skipping silently.

Extract the skip predicate into one shared helper in the CodeRouter service directory, then call it from all three loops. That keeps the entrypoints from diverging again.

As per coding guidelines: "When behavior has multiple entrypoints, implement one shared action path and verify every entrypoint rather than duplicating logic."

📍 Affects 2 files
  • web/services/coderouter/codexProxy.ts#L129-L138 (this comment)
  • web/services/coderouter/opencodeProxy.ts#L90-L92
🤖 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 `@web/services/coderouter/codexProxy.ts` around lines 129 - 138, Extract the
routing-skip tag predicate currently used by proxyCodexRequest into a shared
CodeRouter helper, then use it in proxyCodexRequest and both account-selection
loops. In codexProxy.ts at lines 129-138 and opencodeProxy.ts at lines 90-92,
skip only CodeRouterRefreshBusy and CodeRouterCredentialBroken errors; report or
rethrow all other credential failures so infrastructure errors reach the
existing failure handling.

Source: Coding guidelines

Comment on lines +7 to +12
try {
const first = await request();
return RETRYABLE_STATUS.has(first.status) ? await request() : first;
} catch {
return await request();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Release the discarded response body before you retry.

Line 9 drops the first Response when the status is retryable. The body stream is never read or cancelled. The underlying connection and buffered bytes stay allocated until garbage collection. This runs on the Codex models, OpenCode config, and usage hot paths, so 5xx bursts from a provider accumulate unreleased streams.

Cancel the body before you issue the retry.

🔧 Proposed fix to release the abandoned body
 export async function fetchProviderRead(
   request: () => Promise<Response>,
 ): Promise<Response> {
   try {
     const first = await request();
-    return RETRYABLE_STATUS.has(first.status) ? await request() : first;
+    if (!RETRYABLE_STATUS.has(first.status)) return first;
+    await first.body?.cancel().catch(() => undefined);
+    return await request();
   } catch {
     return await request();
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const first = await request();
return RETRYABLE_STATUS.has(first.status) ? await request() : first;
} catch {
return await request();
}
try {
const first = await request();
if (!RETRYABLE_STATUS.has(first.status)) return first;
await first.body?.cancel().catch(() => undefined);
return await request();
} catch {
return await request();
}
🤖 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 `@web/services/coderouter/providerFetch.ts` around lines 7 - 12, Update the
retryable-status branch in the request flow around `request()` and
`RETRYABLE_STATUS` to cancel the discarded first `Response` body before issuing
the second request. Preserve returning the first response for non-retryable
statuses and the existing retry behavior for thrown requests.

Comment on lines 107 to 118
} catch (error) {
const terminal = isTerminalRefreshError(error);
await failRefreshLease(
const terminal = dependencies.isTerminal(error);
reportCoderouterFailure("provider_refresh", error, {
provider: currentProvider(before.credential),
terminal,
});
await dependencies.fail(
input.accountId,
leaseId,
terminal,
refreshFailureCode(error),
dependencies.failureCode(error),
).catch(() => undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not report persistence failures as provider failures.

The catch at line 107 covers the whole lease block, not only dependencies.refresh. dependencies.read (line 82), dependencies.release (line 87), dependencies.encrypt (line 92), and dependencies.complete (line 99) all land here. A lost CAS race in completeRefreshLease or an RDS outage therefore emits coderouter.provider_refresh with a provider tag. On-call then investigates the upstream provider for a database fault.

observability.ts already declares an rds failure category, and this path never uses it. The PR objective is scrubbed, accurate alert signals, so the category must match the failing subsystem.

Narrow the reported category to the operation that failed. Keep the lease-failure recording unchanged.

🔧 Proposed fix to separate the provider failure from the persistence failure
     const refreshed = await dependencies.refresh(current.credential);
+    let persisted = false;
     const encrypted = await dependencies.encrypt({
       teamId: input.teamId,
       accountId: input.accountId,
       provider: refreshed.provider,
       credentialRevision: current.envelope.credentialRevision + 1,
       credential: refreshed,
     });
+    persisted = true;
     await dependencies.complete({
       accountId: input.accountId,
       leaseId,
       expectedRevision: current.envelope.credentialRevision,
       credential: refreshed,
       encrypted,
     });
     return refreshed;
   } catch (error) {
     const terminal = dependencies.isTerminal(error);
-    reportCoderouterFailure("provider_refresh", error, {
-      provider: currentProvider(before.credential),
-      terminal,
-    });
+    reportCoderouterFailure(persisted ? "rds" : "provider_refresh", error, {
+      provider: currentProvider(before.credential),
+      terminal,
+    });

A clearer alternative is to wrap only dependencies.refresh in its own try/catch and report rds from an outer catch.

🤖 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 `@web/services/coderouter/refresh.ts` around lines 107 - 118, Separate the
`dependencies.refresh` error handling from the surrounding lease operations in
the refresh flow. Report `provider_refresh` with the provider tag only when
`dependencies.refresh` fails; report failures from `dependencies.read`,
`dependencies.release`, `dependencies.encrypt`, or `dependencies.complete` under
the existing `rds` category without a provider tag. Keep the existing
`dependencies.fail` lease-failure recording unchanged.

Comment thread web/services/coderouter/repository.ts
Comment thread web/tests/coderouter-models-proxy.test.ts
Comment on lines +113 to +125
test("does not return a refreshed token when the RDS CAS fails", async () => {
let failedLease = false;
const refresh = createCredentialRefresher(fakeDependencies({
complete: async () => {
throw new Error("database unavailable");
},
fail: async () => {
failedLease = true;
},
}));
await expect(refresh(input())).rejects.toThrow("database unavailable");
expect(failedLease).toBe(true);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the post-claim re-read that releases the lease.

createCredentialRefresher re-reads the credential after it claims the lease. If that second read shows a still-valid token, it calls dependencies.release and returns the existing credential without contacting the provider. That branch is the core fix for the double-refresh race, and no test exercises it.

Add a test where read returns an expired credential first and a valid credential second. Assert that release runs, that refresh never runs, and that the returned token is the existing one.

🧪 Proposed test for the release branch
+  test("releases the lease when another refresh already rotated the token", async () => {
+    let released = false;
+    let refreshCalls = 0;
+    const reads = [
+      { envelope, credential: expired },
+      { envelope, credential: { ...expired, expiresAt: Date.now() + 3_600_000 } },
+    ];
+    const refresh = createCredentialRefresher(fakeDependencies({
+      read: async () => reads.shift() ?? { envelope, credential: expired },
+      release: async () => {
+        released = true;
+      },
+      refresh: async () => {
+        refreshCalls++;
+        return refreshed();
+      },
+    }));
+    const result = await refresh(input());
+    expect(result.accessToken).toBe("old-access");
+    expect(released).toBe(true);
+    expect(refreshCalls).toBe(0);
+  });
🤖 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 `@web/tests/coderouter-refresh.test.ts` around lines 113 - 125, Add a test
alongside the existing createCredentialRefresher cases that makes read return an
expired credential on the initial call and a valid credential after lease
acquisition. Configure release to record invocation and refresh to fail if
called, then assert the refresher returns the existing valid token, release was
invoked, and the provider refresh path was not contacted.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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.

♻️ Duplicate comments (6)
web/tests/coderouter-refresh.test.ts (1)

113-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the post-claim re-read that releases the lease.

createCredentialRefresher re-reads the credential after it claims the lease. If the second read returns a still-valid token, it calls dependencies.release and returns the existing credential without contacting the provider. That branch is the fix for the double-refresh race. No test in this file exercises it, and fakeDependencies.release is never asserted.

🧪 Proposed test for the release branch
+  test("releases the lease when another refresh already rotated the token", async () => {
+    let released = false;
+    let refreshCalls = 0;
+    const reads = [
+      { envelope, credential: expired },
+      { envelope, credential: { ...expired, expiresAt: Date.now() + 3_600_000 } },
+    ];
+    const refresh = createCredentialRefresher(fakeDependencies({
+      read: async () => reads.shift() ?? { envelope, credential: expired },
+      release: async () => {
+        released = true;
+      },
+      refresh: async () => {
+        refreshCalls++;
+        return refreshed();
+      },
+    }));
+    const result = await refresh(input());
+    expect(result.accessToken).toBe("old-access");
+    expect(released).toBe(true);
+    expect(refreshCalls).toBe(0);
+  });
🤖 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 `@web/tests/coderouter-refresh.test.ts` around lines 113 - 125, Add a test in
coderouter-refresh.test.ts covering createCredentialRefresher’s post-claim
re-read: configure the second credential read to return a still-valid token,
assert dependencies.release is called, verify the existing credential is
returned, and confirm the provider is not contacted.
web/services/coderouter/codexProxy.ts (2)

142-150: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the models fetch with a timeout.

This fetch sets no AbortSignal. The sibling provider reads both bound themselves: remoteConfig in web/services/coderouter/opencodeProxy.ts at line 101, and the usage read in web/services/coderouter/usage.ts, use AbortSignal.timeout(5_000).

dependencies.providerRead now replays the call once on a transient failure. An unresponsive provider connection therefore holds the request thread for two unbounded attempts instead of one. The retry wrapper increases the existing exposure.

Create the signal inside the callback so the retry attempt receives a fresh deadline.

🔧 Proposed fix to bound the upstream read
       upstream = await dependencies.providerRead(() => fetch(upstreamUrl, {
         headers: {
           authorization: `Bearer ${credential.accessToken}`,
           "chatgpt-account-id": credential.accountId,
           originator: "codex_cli_rs",
           "user-agent": request.headers.get("user-agent") ?? "coderouter",
         },
         cache: "no-store",
+        signal: AbortSignal.timeout(5_000),
       }));
🤖 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 `@web/services/coderouter/codexProxy.ts` around lines 142 - 150, Update the
upstream fetch callback in the models-read flow to pass a fresh
AbortSignal.timeout(5_000) on every attempt. Create the signal inside the
callback passed to dependencies.providerRead, and preserve the existing request
headers and no-store cache behavior.

129-138: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unclassified credential errors turn infrastructure outages into silent 503s. Both account-selection loops catch every error from the credential dependency and continue to the next account. Neither distinguishes a routing-skip condition (CodeRouterRefreshBusy, CodeRouterCredentialBroken) from an infrastructure fault such as a KMS outage, an RDS failure, or a decryption error. After the 8-attempt bound the caller receives no_usable_account (503), and reportCoderouterFailure never runs, so the outage produces no alert. proxyCodexRequest at web/services/coderouter/codexProxy.ts lines 49-56 classifies the error by _tag, so the entrypoints now disagree.

  • web/services/coderouter/codexProxy.ts#L129-L138: replace the bare catch with the _tag check used by proxyCodexRequest, and rethrow anything that is not a routing-skip tag.
  • web/services/coderouter/opencodeProxy.ts#L90-L92: replace the bare catch in openCodeAccount with the same check, and report or rethrow anything else instead of skipping silently.

Extract the skip predicate into one shared helper in the CodeRouter service directory, then call it from all three loops. If you narrow the catch in openCodeAccount, update web/tests/coderouter-opencode-proxy.test.ts line 46 to throw a tagged error instead of a plain Error.

As per coding guidelines: "When behavior has multiple entrypoints, implement one shared action path and verify every entrypoint rather than duplicating logic."

🤖 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 `@web/services/coderouter/codexProxy.ts` around lines 129 - 138, Unify
credential-error handling across proxy account-selection loops by extracting a
shared CodeRouter helper that identifies only CodeRouterRefreshBusy and
CodeRouterCredentialBroken as skippable. In
web/services/coderouter/codexProxy.ts lines 129-138,
web/services/coderouter/codexProxy.ts lines 49-56, and
web/services/coderouter/opencodeProxy.ts lines 90-92, use the helper in each
catch block and rethrow non-skippable infrastructure errors so reporting can
occur; update web/tests/coderouter-opencode-proxy.test.ts line 46 to throw a
tagged routing-skip error if required.

Source: Coding guidelines

web/tests/coderouter-models-proxy.test.ts (1)

22-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the proxy excludes accounts it already attempted.

The fake select ignores its third argument. Production selectAccountForRequest uses excludedAccountIds to skip accounts already tried in the same request. Nothing here verifies that createCodexModelsProxy populates attempted.

If the proxy stopped passing attempted, this test would still pass, because the fake advances its queue on every call. In production the same unusable account would be reselected until the 8-attempt bound, and the request would return no_usable_account.

The OpenCode test asserts this contract at web/tests/coderouter-opencode-proxy.test.ts line 58. Apply the same check here.

🧪 Proposed change to record and assert the excluded account IDs
 let selectedAccounts = ["account-1"];
 let unusableAccounts = new Set<string>();
+let excludedAccounts: string[] = [];
-  select: async () => {
+  select: async (_teamId, _provider, excluded) => {
+    excludedAccounts = [...(excluded ?? [])];
     const id = selectedAccounts.shift();
     return id
       ? { id, vaultRevision: 1, credentialExpiresAt: new Date() }
       : null;
   },
   beforeEach(() => {
     selectedAccounts = ["account-1"];
     unusableAccounts = new Set();
+    excludedAccounts = [];
   });
     expect(response.status).toBe(200);
+    expect(excludedAccounts).toContain("refreshing-account");
     expect(await response.json()).toEqual({
       models: [{ slug: "gpt-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 `@web/tests/coderouter-models-proxy.test.ts` around lines 22 - 27, Update the
fake select implementation in the createCodexModelsProxy test to inspect its
excludedAccountIds argument and avoid returning accounts whose IDs were already
attempted. Add an assertion that the proxy passes the accumulated attempted IDs
to selectAccountForRequest, matching the contract covered by the OpenCode proxy
test.
web/services/coderouter/providerFetch.ts (1)

7-12: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cancel the discarded response body before the retry.

Line 9 drops the first Response when the status is retryable. The body stream is never read or cancelled, so the connection and buffered bytes stay allocated until garbage collection. This helper now runs on the Codex models path, the OpenCode config path, and the usage path, so a provider 5xx burst accumulates unreleased streams.

🔧 Proposed fix to release the abandoned body
 export async function fetchProviderRead(
   request: () => Promise<Response>,
 ): Promise<Response> {
   try {
     const first = await request();
-    return RETRYABLE_STATUS.has(first.status) ? await request() : first;
+    if (!RETRYABLE_STATUS.has(first.status)) return first;
+    await first.body?.cancel().catch(() => undefined);
+    return await request();
   } catch {
     return await request();
   }
 }
🤖 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 `@web/services/coderouter/providerFetch.ts` around lines 7 - 12, Update the
retryable-response branch in the request helper to cancel or otherwise release
the discarded first Response body before issuing the second request. Preserve
the existing retry behavior for retryable statuses and the catch-path retry,
ensuring only the abandoned response stream is cleaned up.
web/services/coderouter/refresh.ts (1)

107-118: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Report persistence failures under the rds category, not provider_refresh.

The catch at line 107 covers the whole lease block. dependencies.read (line 82), dependencies.release (line 87), dependencies.encrypt (line 92), and dependencies.complete (line 99) all land here. A lost CAS race in completeRefreshLease or an RDS outage therefore emits coderouter.provider_refresh with a provider tag, and on-call investigates the upstream provider for a database fault.

observability.ts declares an rds failure category. No path in this file uses it.

The same mismatch exists at line 70. dependencies.read resolves to readCredential, which throws CodeRouterCredentialBroken("encrypted credential not found") for a missing row. That storage condition is reported as credential_decrypt.

Wrap only the provider call in its own try/catch, and report the surrounding storage operations under rds.

🔧 Proposed fix to separate the provider failure from the persistence failure
     const refreshed = await dependencies.refresh(current.credential);
+    let persisted = false;
     const encrypted = await dependencies.encrypt({
       teamId: input.teamId,
       accountId: input.accountId,
       provider: refreshed.provider,
       credentialRevision: current.envelope.credentialRevision + 1,
       credential: refreshed,
     });
+    persisted = true;
     await dependencies.complete({
       accountId: input.accountId,
       leaseId,
       expectedRevision: current.envelope.credentialRevision,
       credential: refreshed,
       encrypted,
     });
     return refreshed;
   } catch (error) {
     const terminal = dependencies.isTerminal(error);
-    reportCoderouterFailure("provider_refresh", error, {
+    reportCoderouterFailure(persisted ? "rds" : "provider_refresh", error, {
       provider: currentProvider(before.credential),
       terminal,
     });
🤖 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 `@web/services/coderouter/refresh.ts` around lines 107 - 118, Restructure the
lease flow so only the provider call is handled by the existing provider-refresh
error reporting, while storage operations including dependencies.read, release,
encrypt, complete, and fail use the rds failure category without a provider tag.
Move the provider try/catch around the provider invocation and update the outer
persistence catch accordingly, preserving the existing terminal handling and
failure-code propagation.
🤖 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.

Duplicate comments:
In `@web/services/coderouter/codexProxy.ts`:
- Around line 142-150: Update the upstream fetch callback in the models-read
flow to pass a fresh AbortSignal.timeout(5_000) on every attempt. Create the
signal inside the callback passed to dependencies.providerRead, and preserve the
existing request headers and no-store cache behavior.
- Around line 129-138: Unify credential-error handling across proxy
account-selection loops by extracting a shared CodeRouter helper that identifies
only CodeRouterRefreshBusy and CodeRouterCredentialBroken as skippable. In
web/services/coderouter/codexProxy.ts lines 129-138,
web/services/coderouter/codexProxy.ts lines 49-56, and
web/services/coderouter/opencodeProxy.ts lines 90-92, use the helper in each
catch block and rethrow non-skippable infrastructure errors so reporting can
occur; update web/tests/coderouter-opencode-proxy.test.ts line 46 to throw a
tagged routing-skip error if required.

In `@web/services/coderouter/providerFetch.ts`:
- Around line 7-12: Update the retryable-response branch in the request helper
to cancel or otherwise release the discarded first Response body before issuing
the second request. Preserve the existing retry behavior for retryable statuses
and the catch-path retry, ensuring only the abandoned response stream is cleaned
up.

In `@web/services/coderouter/refresh.ts`:
- Around line 107-118: Restructure the lease flow so only the provider call is
handled by the existing provider-refresh error reporting, while storage
operations including dependencies.read, release, encrypt, complete, and fail use
the rds failure category without a provider tag. Move the provider try/catch
around the provider invocation and update the outer persistence catch
accordingly, preserving the existing terminal handling and failure-code
propagation.

In `@web/tests/coderouter-models-proxy.test.ts`:
- Around line 22-27: Update the fake select implementation in the
createCodexModelsProxy test to inspect its excludedAccountIds argument and avoid
returning accounts whose IDs were already attempted. Add an assertion that the
proxy passes the accumulated attempted IDs to selectAccountForRequest, matching
the contract covered by the OpenCode proxy test.

In `@web/tests/coderouter-refresh.test.ts`:
- Around line 113-125: Add a test in coderouter-refresh.test.ts covering
createCredentialRefresher’s post-claim re-read: configure the second credential
read to return a still-valid token, assert dependencies.release is called,
verify the existing credential is returned, and confirm the provider is not
contacted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8aa14d1a-fe32-4be0-9f82-627216916f8b

📥 Commits

Reviewing files that changed from the base of the PR and between e012389 and 4bc5712.

📒 Files selected for processing (18)
  • web/app/api/coderouter/accounts/[accountId]/route.ts
  • web/app/api/coderouter/accounts/route.ts
  • web/app/coderouter/page.tsx
  • web/db/schema.ts
  • web/services/coderouter/accounts.ts
  • web/services/coderouter/codexProxy.ts
  • web/services/coderouter/observability.ts
  • web/services/coderouter/opencodeProxy.ts
  • web/services/coderouter/providerFetch.ts
  • web/services/coderouter/refresh.ts
  • web/services/coderouter/repository.ts
  • web/services/coderouter/usage.ts
  • web/tests/coderouter-account-removal.test.ts
  • web/tests/coderouter-encryption.test.ts
  • web/tests/coderouter-models-proxy.test.ts
  • web/tests/coderouter-opencode-proxy.test.ts
  • web/tests/coderouter-provider-fetch.test.ts
  • web/tests/coderouter-refresh.test.ts

@lawrencecchen
lawrencecchen force-pushed the feat/coderouter-private-beta-100 branch from 4bc5712 to 138dc42 Compare August 6, 2026 04:56
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@lawrencecchen
lawrencecchen force-pushed the feat/coderouter-private-beta-100 branch from 138dc42 to e61c2fe Compare August 6, 2026 05:01

@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: 3

♻️ Duplicate comments (1)
web/tests/coderouter-models-proxy.test.ts (1)

22-27: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert that the proxy excludes accounts it already attempted.

The fake select at line 22 takes no parameters, so it discards excludedAccountIds. Production selectAccountForRequest uses that argument to skip accounts already tried in the same request. Nothing here verifies that createCodexModelsProxy populates attempted.

If the proxy stopped passing attempted, this test would still pass, because the fake advances its queue on every call. In production the same unusable account would be reselected until the 8-attempt bound, and the request would return no_usable_account.

The OpenCode test asserts this contract at web/tests/coderouter-opencode-proxy.test.ts line 58. A previous review thread marked this as fixed, but the current code does not read the argument.

🧪 Proposed change to record and assert the excluded account IDs
 let selectedAccounts = ["account-1"];
 let unusableAccounts = new Set<string>();
+let excludedAccounts: string[] = [];
-  select: async () => {
+  select: async (_teamId, _provider, excluded) => {
+    excludedAccounts = [...(excluded ?? [])];
     const id = selectedAccounts.shift();
     return id
       ? { id, vaultRevision: 1, credentialExpiresAt: new Date() }
       : null;
   },
   beforeEach(() => {
     selectedAccounts = ["account-1"];
     unusableAccounts = new Set();
+    excludedAccounts = [];
   });
     expect(response.status).toBe(200);
+    expect(excludedAccounts).toContain("refreshing-account");
     expect(await response.json()).toEqual({
       models: [{ slug: "gpt-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 `@web/tests/coderouter-models-proxy.test.ts` around lines 22 - 27, Update the
fake select implementation in the proxy test to accept and record the
excludedAccountIds argument, then assert that createCodexModelsProxy passes
previously attempted account IDs on subsequent selections. Mirror the existing
contract assertion from the OpenCode proxy test, while preserving the
selectedAccounts queue behavior.
🤖 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 `@web/app/api/coderouter/accounts/route.ts`:
- Around line 32-37: Update the Server-Timing construction in the accounts route
to distinguish timings from the current request versus a cached refresh: have
accountsWithUsage return cache-hit/refresh metadata separately, emit rds and
provider timings only when this request performed the refresh, and emit a cache
metric for cache hits while preserving auth, serialize, and total timings.

In `@web/services/coderouter/codexProxy.ts`:
- Around line 142-150: Update the fetch call in fetchProviderRead to pass a
fresh AbortSignal.timeout(5_000) inside the providerRead callback, ensuring
every retry gets its own deadline. Wrap this provider read in the account
iteration’s existing failure handling so rejected transport errors are treated
like rate-limited accounts and processing continues to the next account.

In `@web/services/coderouter/opencodeProxy.ts`:
- Around line 198-200: Move the shared `isRecord` type guard into a common
CodeRouter helper module, then remove the local definitions from
`opencodeProxy.ts`, `refresh.ts`, `accounts.ts`, `usage.ts`, `vault.ts`, and
`encryption.ts` and import the shared implementation consistently. Use one
uniform `Record<string, unknown>` signature, including replacing the differing
`vault.ts` variant.

---

Duplicate comments:
In `@web/tests/coderouter-models-proxy.test.ts`:
- Around line 22-27: Update the fake select implementation in the proxy test to
accept and record the excludedAccountIds argument, then assert that
createCodexModelsProxy passes previously attempted account IDs on subsequent
selections. Mirror the existing contract assertion from the OpenCode proxy test,
while preserving the selectedAccounts queue behavior.
🪄 Autofix

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: fd9008f5-e763-47bf-8de7-258ffd75964f

📥 Commits

Reviewing files that changed from the base of the PR and between e012389 and 138dc42.

📒 Files selected for processing (18)
  • web/app/api/coderouter/accounts/[accountId]/route.ts
  • web/app/api/coderouter/accounts/route.ts
  • web/app/coderouter/page.tsx
  • web/db/schema.ts
  • web/services/coderouter/accounts.ts
  • web/services/coderouter/codexProxy.ts
  • web/services/coderouter/observability.ts
  • web/services/coderouter/opencodeProxy.ts
  • web/services/coderouter/providerFetch.ts
  • web/services/coderouter/refresh.ts
  • web/services/coderouter/repository.ts
  • web/services/coderouter/usage.ts
  • web/tests/coderouter-account-removal.test.ts
  • web/tests/coderouter-encryption.test.ts
  • web/tests/coderouter-models-proxy.test.ts
  • web/tests/coderouter-opencode-proxy.test.ts
  • web/tests/coderouter-provider-fetch.test.ts
  • web/tests/coderouter-refresh.test.ts

Comment on lines +32 to +37
const serverTiming = [
timing("auth", authMs),
timing("rds", result.timing.rdsMs),
timing("provider", result.timing.providerMs),
timing("serialize", serializeMs),
timing("total", performance.now() - startedAt),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not report cached refresh time as current request time.

accountsWithUsage can return a cached result whose rdsMs and providerMs were captured during an earlier refresh. These lines emit those values in the current Server-Timing response, while total measures only the current request. A cache hit can therefore report RDS or provider time greater than total time.

Return cache-hit metadata separately. Emit refresh timings only when this request performed the refresh. Emit a cache metric for cache hits.

🤖 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 `@web/app/api/coderouter/accounts/route.ts` around lines 32 - 37, Update the
Server-Timing construction in the accounts route to distinguish timings from the
current request versus a cached refresh: have accountsWithUsage return
cache-hit/refresh metadata separately, emit rds and provider timings only when
this request performed the refresh, and emit a cache metric for cache hits while
preserving auth, serialize, and total timings.

Comment thread web/services/coderouter/codexProxy.ts
Comment on lines +198 to 200
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare every isRecord definition in the coderouter service directory.
set -euo pipefail

fd . --type f --extension ts | xargs rg -nP -A2 'function\s+isRecord\s*\('

Repository: manaflow-ai/cmux

Length of output: 4130


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== coderouter isRecord definitions =="
for f in \
  web/services/coderouter/opencodeProxy.ts \
  web/services/coderouter/refresh.ts \
  web/services/coderouter/accounts.ts \
  web/services/coderouter/usage.ts \
  web/services/coderouter/vault.ts \
  web/services/coderouter/encryption.ts
do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n -A2 'function\s+isRecord\s*\(' "$f"
  else
    echo "--- missing $f"
  fi
done

echo "== imports of helper locations / nearby isRecord usage in coderouter =="
for f in \
  web/services/coderouter/opencodeProxy.ts \
  web/services/coderouter/refresh.ts \
  web/services/coderouter/accounts.ts \
  web/services/coderouter/usage.ts \
  web/services/coderouter/vault.ts \
  web/services/coderouter/encryption.ts
do
  if [ -f "$f" ]; then
    echo "--- $f"
    python3 - <<'PY' "$f"
import sys, re
p=sys.argv[1]
txt=open(p).read()
lines=txt.splitlines()
for i,l in enumerate(lines,1):
    if re.search(r'\bisRecord\s*\(', l):
        print(f"{i}: {l.strip()}")
PY
  fi
done

Repository: manaflow-ai/cmux

Length of output: 3192


Share one isRecord definition across CodeRouter files.

isRecord is defined locally in web/services/coderouter/opencodeProxy.ts, web/services/coderouter/refresh.ts, web/services/coderouter/accounts.ts, web/services/coderouter/usage.ts, web/services/coderouter/vault.ts, and web/services/coderouter/encryption.ts. Several definitions differ in signature, including the Record<string, any> return type in vault.ts. Move one definition into a shared CodeRouter helper module and import it consistently.

🤖 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 `@web/services/coderouter/opencodeProxy.ts` around lines 198 - 200, Move the
shared `isRecord` type guard into a common CodeRouter helper module, then remove
the local definitions from `opencodeProxy.ts`, `refresh.ts`, `accounts.ts`,
`usage.ts`, `vault.ts`, and `encryption.ts` and import the shared implementation
consistently. Use one uniform `Record<string, unknown>` signature, including
replacing the differing `vault.ts` variant.

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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: 3

♻️ Duplicate comments (7)
web/tests/coderouter-refresh.test.ts (1)

113-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the post-claim re-read that releases the lease.

createCredentialRefresher re-reads the credential after it claims the lease at line 82 of web/services/coderouter/refresh.ts. If the second read shows a still-valid token, it calls dependencies.release and returns the existing credential without contacting the provider. That branch is the core fix for the double-refresh race and no test exercises it.

Add a test where read returns an expired credential first and a valid credential second. Assert release runs, refresh never runs, and the returned token is the existing one.

🤖 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 `@web/tests/coderouter-refresh.test.ts` around lines 113 - 125, Add a test
alongside the existing createCredentialRefresher tests where read first returns
an expired credential and then a valid credential after the lease claim. Assert
the post-claim re-read path invokes release, does not invoke the provider
refresh operation, and returns the existing valid token.
web/tests/coderouter-models-proxy.test.ts (1)

22-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the proxy excludes accounts it already attempted.

The fake select still ignores its third argument. Production selectAccountForRequest uses excludedAccountIds to skip accounts already tried in this request. If createCodexModelsProxy stopped passing attempted, the test at lines 68-80 would still pass, because the fake advances its queue on every call. In production the same unusable account would be reselected until the 8-attempt bound and the request would return no_usable_account.

The OpenCode suite asserts this contract at web/tests/coderouter-opencode-proxy.test.ts line 58. Apply the same check here.

🧪 Proposed change
-  select: async () => {
+  select: async (_teamId, _provider, excluded) => {
+    excludedAccounts = [...(excluded ?? [])];
     const id = selectedAccounts.shift();
     return id
       ? { id, vaultRevision: 1, credentialExpiresAt: new Date() }
       : null;
   },
     expect(response.status).toBe(200);
+    expect(excludedAccounts).toContain("refreshing-account");
🤖 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 `@web/tests/coderouter-models-proxy.test.ts` around lines 22 - 27, Update the
fake select implementation in the proxy test to honor its excludedAccountIds
argument by skipping already-attempted account IDs before returning an account.
Ensure the test verifies createCodexModelsProxy passes attempted IDs through
select, matching the contract covered by the OpenCode proxy test.
web/services/coderouter/refresh.ts (1)

107-118: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not report persistence failures as provider failures.

The catch at line 107 covers the whole lease block, not only dependencies.refresh. dependencies.read (line 82), dependencies.release (line 87), dependencies.encrypt (line 92), and dependencies.complete (line 99) all land here. A lost CAS race in completeRefreshLease or an RDS outage therefore emits coderouter.provider_refresh with a provider tag. observability.ts already declares an rds category and this path never uses it.

Wrap only dependencies.refresh in its own try/catch and report rds from the outer catch. Keep the lease-failure recording unchanged.

🤖 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 `@web/services/coderouter/refresh.ts` around lines 107 - 118, The catch
surrounding the lease workflow currently reports every failure as a provider
refresh failure. In the refresh flow, isolate dependencies.refresh in its own
try/catch so only provider refresh errors call reportCoderouterFailure with the
provider context; change the outer catch to report the rds category for
persistence failures while keeping the existing dependencies.fail lease-failure
recording unchanged.
web/services/coderouter/opencodeProxy.ts (1)

198-200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share one isRecord definition across CodeRouter files.

isRecord is defined locally in opencodeProxy.ts, refresh.ts, accounts.ts, usage.ts, vault.ts, and encryption.ts. Move one definition into a shared CodeRouter helper module and import it. This change already narrows the local copy to Record<string, unknown>, so it is the right moment to consolidate.

🤖 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 `@web/services/coderouter/opencodeProxy.ts` around lines 198 - 200, Move the
local isRecord definition from opencodeProxy.ts into a shared CodeRouter helper
module, then remove the duplicate definitions from opencodeProxy.ts, refresh.ts,
accounts.ts, usage.ts, vault.ts, and encryption.ts and import the shared type
guard wherever used. Preserve its current Record<string, unknown> narrowing and
runtime checks.
web/services/coderouter/providerFetch.ts (1)

7-12: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cancel the discarded response body before the retry.

Line 9 drops the first Response when the status is retryable. The body stream is never read or cancelled, so buffered bytes and the connection stay allocated until garbage collection. This path runs for Codex models, OpenCode config, and usage reads.

🔧 Proposed fix
   try {
     const first = await request();
-    return RETRYABLE_STATUS.has(first.status) ? await request() : first;
+    if (!RETRYABLE_STATUS.has(first.status)) return first;
+    await first.body?.cancel().catch(() => undefined);
+    return await request();
   } catch {
     return await request();
   }
🤖 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 `@web/services/coderouter/providerFetch.ts` around lines 7 - 12, Update the
retry branch in the request flow around the initial response to cancel or
otherwise dispose of first.body before issuing the second request when the
status is retryable. Preserve returning the retry response, and leave the
catch-path retry behavior unchanged.
web/services/coderouter/codexProxy.ts (2)

142-150: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the models read and handle a rejected provider read.

Two defects share this call.

First, this fetch sets no AbortSignal. The sibling provider reads bound themselves with AbortSignal.timeout(5_000): remoteConfig in web/services/coderouter/opencodeProxy.ts line 101, and the usage read in web/services/coderouter/usage.ts line 87. providerRead replays the call once, so an unresponsive provider holds the request for two unbounded attempts. Create the signal inside the callback so each attempt gets a fresh deadline.

Second, fetchProviderRead rethrows when both attempts throw. Line 142 is not inside a try, so a transport fault on one account rejects the whole handler instead of failing over to the next account.

🔧 Proposed fix
-      upstream = await dependencies.providerRead(() => fetch(upstreamUrl, {
-        headers: {
-          authorization: `Bearer ${credential.accessToken}`,
-          "chatgpt-account-id": credential.accountId,
-          originator: "codex_cli_rs",
-          "user-agent": request.headers.get("user-agent") ?? "coderouter",
-        },
-        cache: "no-store",
-      }));
+      try {
+        upstream = await dependencies.providerRead(() => fetch(upstreamUrl, {
+          headers: {
+            authorization: `Bearer ${credential.accessToken}`,
+            "chatgpt-account-id": credential.accountId,
+            originator: "codex_cli_rs",
+            "user-agent": request.headers.get("user-agent") ?? "coderouter",
+          },
+          cache: "no-store",
+          signal: AbortSignal.timeout(5_000),
+        }));
+      } catch (error) {
+        reportCoderouterFailure("provider_usage", error, { provider: "codex" });
+        upstream = null;
+        continue;
+      }
🤖 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 `@web/services/coderouter/codexProxy.ts` around lines 142 - 150, Update the
provider-read flow around fetchProviderRead so the fetch callback creates a
fresh AbortSignal.timeout(5_000) for each providerRead attempt and passes it to
fetch. Also catch a rejected fetchProviderRead call at the surrounding
account-processing boundary, treating transport failures as account-level
failures so the handler continues failing over to the next account instead of
rejecting.

129-138: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Both account-selection loops swallow infrastructure errors as routing skips. Neither loop separates a routing-skip condition (CodeRouterRefreshBusy, CodeRouterCredentialBroken) from an infrastructure fault such as a KMS outage, an RDS failure, or a decryption error. Each loop advances to the next account, the caller returns no_usable_account (503), and no alert is emitted. proxyCodexRequest at web/services/coderouter/codexProxy.ts lines 49-56 already classifies the same error correctly, so the entrypoints disagree. The shared root cause is the absence of one skip predicate.

  • web/services/coderouter/codexProxy.ts#L129-L138: replace the bare catch with the tag check used by proxyCodexRequest, and rethrow anything that is not a routing-skip tag.
  • web/services/coderouter/opencodeProxy.ts#L90-L92: replace the bare catch in openCodeAccount with the same tag check, and report or rethrow anything else.

Extract the predicate into one helper in the CodeRouter service directory and call it from all three loops.

As per coding guidelines: "When behavior has multiple entrypoints, implement one shared action path and verify every entrypoint rather than duplicating logic."

🤖 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 `@web/services/coderouter/codexProxy.ts` around lines 129 - 138, The
account-selection loops incorrectly swallow infrastructure failures as routing
skips. Add one shared CodeRouter helper that identifies only
CodeRouterRefreshBusy and CodeRouterCredentialBroken as skippable, then use it
in proxyCodexRequest, the codex account-selection loop at
web/services/coderouter/codexProxy.ts lines 129-138, and openCodeAccount at
web/services/coderouter/opencodeProxy.ts lines 90-92; continue only for those
tags and report or rethrow all other errors.

Source: Coding guidelines

🤖 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 `@web/tests/coderouter-encryption.test.ts`:
- Around line 98-120: Add a rejecting-provider test alongside the existing
coalescingCredentialsProvider coverage: make the first exchange reject, assert
the initial provider call rejects, then invoke it again and assert it succeeds
with a second exchange and the call counter equals two, verifying pending is
cleared for retries.

In `@web/tests/coderouter-opencode-proxy.test.ts`:
- Around line 45-46: Update the unavailable-account fixture’s credential
callback in the test to throw the tagged CodeRouterRefreshBusy error instead of
a plain Error, preserving the existing “refreshing” scenario and matching the
production skip predicate.

In `@web/tests/coderouter-provider-fetch.test.ts`:
- Around line 26-36: Extend the fetchProviderRead test coverage with one case
where every response is 503, asserting the returned status is 503 and the
provider callback is called exactly twice. Add another case where every provider
attempt throws, asserting fetchProviderRead rejects while preserving the retry
limit behavior.

---

Duplicate comments:
In `@web/services/coderouter/codexProxy.ts`:
- Around line 142-150: Update the provider-read flow around fetchProviderRead so
the fetch callback creates a fresh AbortSignal.timeout(5_000) for each
providerRead attempt and passes it to fetch. Also catch a rejected
fetchProviderRead call at the surrounding account-processing boundary, treating
transport failures as account-level failures so the handler continues failing
over to the next account instead of rejecting.
- Around line 129-138: The account-selection loops incorrectly swallow
infrastructure failures as routing skips. Add one shared CodeRouter helper that
identifies only CodeRouterRefreshBusy and CodeRouterCredentialBroken as
skippable, then use it in proxyCodexRequest, the codex account-selection loop at
web/services/coderouter/codexProxy.ts lines 129-138, and openCodeAccount at
web/services/coderouter/opencodeProxy.ts lines 90-92; continue only for those
tags and report or rethrow all other errors.

In `@web/services/coderouter/opencodeProxy.ts`:
- Around line 198-200: Move the local isRecord definition from opencodeProxy.ts
into a shared CodeRouter helper module, then remove the duplicate definitions
from opencodeProxy.ts, refresh.ts, accounts.ts, usage.ts, vault.ts, and
encryption.ts and import the shared type guard wherever used. Preserve its
current Record<string, unknown> narrowing and runtime checks.

In `@web/services/coderouter/providerFetch.ts`:
- Around line 7-12: Update the retry branch in the request flow around the
initial response to cancel or otherwise dispose of first.body before issuing the
second request when the status is retryable. Preserve returning the retry
response, and leave the catch-path retry behavior unchanged.

In `@web/services/coderouter/refresh.ts`:
- Around line 107-118: The catch surrounding the lease workflow currently
reports every failure as a provider refresh failure. In the refresh flow,
isolate dependencies.refresh in its own try/catch so only provider refresh
errors call reportCoderouterFailure with the provider context; change the outer
catch to report the rds category for persistence failures while keeping the
existing dependencies.fail lease-failure recording unchanged.

In `@web/tests/coderouter-models-proxy.test.ts`:
- Around line 22-27: Update the fake select implementation in the proxy test to
honor its excludedAccountIds argument by skipping already-attempted account IDs
before returning an account. Ensure the test verifies createCodexModelsProxy
passes attempted IDs through select, matching the contract covered by the
OpenCode proxy test.

In `@web/tests/coderouter-refresh.test.ts`:
- Around line 113-125: Add a test alongside the existing
createCredentialRefresher tests where read first returns an expired credential
and then a valid credential after the lease claim. Assert the post-claim re-read
path invokes release, does not invoke the provider refresh operation, and
returns the existing valid token.
🪄 Autofix

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: f826a6f8-12ec-42e7-85b7-87cdeb2350d7

📥 Commits

Reviewing files that changed from the base of the PR and between e012389 and e61c2fe.

📒 Files selected for processing (19)
  • web/app/api/coderouter/accounts/[accountId]/route.ts
  • web/app/api/coderouter/accounts/route.ts
  • web/app/coderouter/page.tsx
  • web/db/schema.ts
  • web/services/coderouter/accounts.ts
  • web/services/coderouter/codexProxy.ts
  • web/services/coderouter/encryption.ts
  • web/services/coderouter/observability.ts
  • web/services/coderouter/opencodeProxy.ts
  • web/services/coderouter/providerFetch.ts
  • web/services/coderouter/refresh.ts
  • web/services/coderouter/repository.ts
  • web/services/coderouter/usage.ts
  • web/tests/coderouter-account-removal.test.ts
  • web/tests/coderouter-encryption.test.ts
  • web/tests/coderouter-models-proxy.test.ts
  • web/tests/coderouter-opencode-proxy.test.ts
  • web/tests/coderouter-provider-fetch.test.ts
  • web/tests/coderouter-refresh.test.ts

Comment on lines +98 to +120
describe("coderouter Vercel OIDC credentials", () => {
test("coalesces parallel AWS role exchanges and refreshes before expiry", async () => {
let calls = 0;
let now = 1_000;
const provider = coalescingCredentialsProvider(async () => {
calls++;
await Promise.resolve();
return {
accessKeyId: `access-${calls}`,
secretAccessKey: "secret",
expiration: new Date(now + 10 * 60 * 1_000),
};
}, () => now);
const first = await Promise.all([provider(), provider(), provider()]);
expect(calls).toBe(1);
expect(first.map((value) => value.accessKeyId)).toEqual([
"access-1",
"access-1",
"access-1",
]);
now += 6 * 60 * 1_000;
expect((await provider()).accessKeyId).toBe("access-2");
expect(calls).toBe(2);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the rejecting-provider branch of coalescingCredentialsProvider.

The suite covers the success path only. If provider() rejects, the finally block must clear pending so the next call retries. A regression there would wedge every KMS request for the process lifetime. Add a case where the first exchange rejects, assert the call rejects, then assert the next call succeeds and performs a second exchange.

🧪 Proposed test
+  test("clears the pending exchange after a failure", async () => {
+    let calls = 0;
+    const now = 1_000;
+    const provider = coalescingCredentialsProvider(async () => {
+      calls++;
+      if (calls === 1) throw new Error("role exchange rejected");
+      return {
+        accessKeyId: "access-2",
+        secretAccessKey: "secret",
+        expiration: new Date(now + 10 * 60 * 1_000),
+      };
+    }, () => now);
+    await expect(provider()).rejects.toThrow("role exchange rejected");
+    expect((await provider()).accessKeyId).toBe("access-2");
+    expect(calls).toBe(2);
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe("coderouter Vercel OIDC credentials", () => {
test("coalesces parallel AWS role exchanges and refreshes before expiry", async () => {
let calls = 0;
let now = 1_000;
const provider = coalescingCredentialsProvider(async () => {
calls++;
await Promise.resolve();
return {
accessKeyId: `access-${calls}`,
secretAccessKey: "secret",
expiration: new Date(now + 10 * 60 * 1_000),
};
}, () => now);
const first = await Promise.all([provider(), provider(), provider()]);
expect(calls).toBe(1);
expect(first.map((value) => value.accessKeyId)).toEqual([
"access-1",
"access-1",
"access-1",
]);
now += 6 * 60 * 1_000;
expect((await provider()).accessKeyId).toBe("access-2");
expect(calls).toBe(2);
describe("coderouter Vercel OIDC credentials", () => {
test("coalesces parallel AWS role exchanges and refreshes before expiry", async () => {
let calls = 0;
let now = 1_000;
const provider = coalescingCredentialsProvider(async () => {
calls++;
await Promise.resolve();
return {
accessKeyId: `access-${calls}`,
secretAccessKey: "secret",
expiration: new Date(now + 10 * 60 * 1_000),
};
}, () => now);
const first = await Promise.all([provider(), provider(), provider()]);
expect(calls).toBe(1);
expect(first.map((value) => value.accessKeyId)).toEqual([
"access-1",
"access-1",
"access-1",
]);
now += 6 * 60 * 1_000;
expect((await provider()).accessKeyId).toBe("access-2");
expect(calls).toBe(2);
});
test("clears the pending exchange after a failure", async () => {
let calls = 0;
const now = 1_000;
const provider = coalescingCredentialsProvider(async () => {
calls++;
if (calls === 1) throw new Error("role exchange rejected");
return {
accessKeyId: "access-2",
secretAccessKey: "secret",
expiration: new Date(now + 10 * 60 * 1_000),
};
}, () => now);
await expect(provider()).rejects.toThrow("role exchange rejected");
expect((await provider()).accessKeyId).toBe("access-2");
expect(calls).toBe(2);
});
});
🤖 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 `@web/tests/coderouter-encryption.test.ts` around lines 98 - 120, Add a
rejecting-provider test alongside the existing coalescingCredentialsProvider
coverage: make the first exchange reject, assert the initial provider call
rejects, then invoke it again and assert it succeeds with a second exchange and
the call counter equals two, verifying pending is cleared for retries.

Comment on lines +45 to +46
credential: async ({ accountId }) => {
if (accountId === "busy") throw new Error("refreshing");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Throw a tagged error from the unavailable-account fixture.

Line 46 throws a plain Error. The production catch in openCodeAccount currently swallows every error, so this passes today. If the skip predicate proposed for web/services/coderouter/opencodeProxy.ts lines 90-92 lands, an untagged error must no longer be skipped, and this test will fail for the wrong reason. Throw a CodeRouterRefreshBusy error here so the fixture states the intended routing-skip condition.

🔧 Proposed change
-        if (accountId === "busy") throw new Error("refreshing");
+        if (accountId === "busy") {
+          throw new CodeRouterRefreshBusy("credential refresh already in progress");
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
credential: async ({ accountId }) => {
if (accountId === "busy") throw new Error("refreshing");
credential: async ({ accountId }) => {
if (accountId === "busy") {
throw new CodeRouterRefreshBusy("credential refresh already in progress");
}
🤖 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 `@web/tests/coderouter-opencode-proxy.test.ts` around lines 45 - 46, Update the
unavailable-account fixture’s credential callback in the test to throw the
tagged CodeRouterRefreshBusy error instead of a plain Error, preserving the
existing “refreshing” scenario and matching the production skip predicate.

Comment on lines +26 to +36
test("does not retry terminal responses or rate limits", async () => {
for (const status of [400, 401, 403, 404, 429]) {
let calls = 0;
const response = await fetchProviderRead(async () => {
calls++;
return new Response(null, { status });
});
expect(response.status).toBe(status);
expect(calls).toBe(1);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the bound on retries.

The suite proves one retry happens. It does not prove the retry stops. Add a case where every attempt returns 503 and assert calls is 2 and the returned status is 503. Add a case where every attempt throws and assert the call rejects. Both behaviors are contracts the proxy loops depend on.

🧪 Proposed tests
+  test("stops after one retry when the read stays transient", async () => {
+    let calls = 0;
+    const response = await fetchProviderRead(async () => {
+      calls++;
+      return new Response(null, { status: 503 });
+    });
+    expect(response.status).toBe(503);
+    expect(calls).toBe(2);
+  });
+
+  test("rejects when both attempts fail", async () => {
+    let calls = 0;
+    await expect(fetchProviderRead(async () => {
+      calls++;
+      throw new Error("network unavailable");
+    })).rejects.toThrow("network unavailable");
+    expect(calls).toBe(2);
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("does not retry terminal responses or rate limits", async () => {
for (const status of [400, 401, 403, 404, 429]) {
let calls = 0;
const response = await fetchProviderRead(async () => {
calls++;
return new Response(null, { status });
});
expect(response.status).toBe(status);
expect(calls).toBe(1);
}
});
test("does not retry terminal responses or rate limits", async () => {
for (const status of [400, 401, 403, 404, 429]) {
let calls = 0;
const response = await fetchProviderRead(async () => {
calls++;
return new Response(null, { status });
});
expect(response.status).toBe(status);
expect(calls).toBe(1);
}
});
test("stops after one retry when the read stays transient", async () => {
let calls = 0;
const response = await fetchProviderRead(async () => {
calls++;
return new Response(null, { status: 503 });
});
expect(response.status).toBe(503);
expect(calls).toBe(2);
});
test("rejects when both attempts fail", async () => {
let calls = 0;
await expect(fetchProviderRead(async () => {
calls++;
throw new Error("network unavailable");
})).rejects.toThrow("network unavailable");
expect(calls).toBe(2);
});
🤖 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 `@web/tests/coderouter-provider-fetch.test.ts` around lines 26 - 36, Extend the
fetchProviderRead test coverage with one case where every response is 503,
asserting the returned status is 503 and the provider callback is called exactly
twice. Add another case where every provider attempt throws, asserting
fetchProviderRead rejects while preserving the retry limit behavior.

@lawrencecchen
lawrencecchen force-pushed the feat/coderouter-private-beta-100 branch from e61c2fe to db6cc10 Compare August 6, 2026 05:12
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@lawrencecchen
lawrencecchen merged commit 2ac8401 into main Aug 6, 2026
5 of 6 checks passed
@lawrencecchen
lawrencecchen deleted the feat/coderouter-private-beta-100 branch August 6, 2026 05:22

@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.

♻️ Duplicate comments (9)
web/tests/coderouter-refresh.test.ts (1)

113-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the post-claim re-read that releases the lease.

createCredentialRefresher re-reads the credential after it claims the lease. If that second read shows a still-valid token, it calls dependencies.release and returns the existing credential without contacting the provider. That branch is the core fix for the double-refresh race, and no test exercises it.

Add a test where read returns an expired credential first and a valid credential second. Assert that release runs, that refresh never runs, and that the returned token is the existing one.

🧪 Proposed test for the release branch
+  test("releases the lease when another refresh already rotated the token", async () => {
+    let released = false;
+    let refreshCalls = 0;
+    const reads = [
+      { envelope, credential: expired },
+      { envelope, credential: { ...expired, expiresAt: Date.now() + 3_600_000 } },
+    ];
+    const refresh = createCredentialRefresher(fakeDependencies({
+      read: async () => reads.shift() ?? { envelope, credential: expired },
+      release: async () => {
+        released = true;
+      },
+      refresh: async () => {
+        refreshCalls++;
+        return refreshed();
+      },
+    }));
+    const result = await refresh(input());
+    expect(result.accessToken).toBe("old-access");
+    expect(released).toBe(true);
+    expect(refreshCalls).toBe(0);
+  });
🤖 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 `@web/tests/coderouter-refresh.test.ts` around lines 113 - 125, Add coverage in
the createCredentialRefresher tests for the post-claim re-read branch: configure
read to return an expired credential first and a valid credential second, then
assert release is called, the provider refresh is not called, and the existing
valid token is returned.
web/tests/coderouter-provider-fetch.test.ts (1)

26-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the bound on retries.

The suite proves one retry happens. It does not prove the retry stops. Add a case where every attempt returns 503 and assert calls is 2 and the returned status is 503. Add a case where every attempt throws and assert the call rejects. The proxy loops depend on both behaviors.

🧪 Proposed tests
+  test("stops after one retry when the read stays transient", async () => {
+    let calls = 0;
+    const response = await fetchProviderRead(async () => {
+      calls++;
+      return new Response(null, { status: 503 });
+    });
+    expect(response.status).toBe(503);
+    expect(calls).toBe(2);
+  });
+
+  test("rejects when both attempts fail", async () => {
+    let calls = 0;
+    await expect(fetchProviderRead(async () => {
+      calls++;
+      throw new Error("network unavailable");
+    })).rejects.toThrow("network unavailable");
+    expect(calls).toBe(2);
+  });
🤖 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 `@web/tests/coderouter-provider-fetch.test.ts` around lines 26 - 36, Add tests
alongside the existing fetchProviderRead retry tests to verify retry
termination: use an always-503 response callback and assert it is called twice
with a final 503 response, then use an always-throwing callback and assert
fetchProviderRead rejects after the bounded attempts.
web/services/coderouter/opencodeProxy.ts (1)

198-200: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Share one isRecord definition across CodeRouter files.

isRecord is defined locally in web/services/coderouter/opencodeProxy.ts, web/services/coderouter/refresh.ts, web/services/coderouter/accounts.ts, web/services/coderouter/usage.ts, web/services/coderouter/vault.ts, and web/services/coderouter/encryption.ts. This change narrows one copy to Record<string, unknown> while vault.ts still returns Record<string, any>. Move one definition into a shared CodeRouter helper module and import it in all six files.

🤖 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 `@web/services/coderouter/opencodeProxy.ts` around lines 198 - 200, Move the
local isRecord definition from opencodeProxy.ts into a shared CodeRouter helper
module, using the consistent Record<string, unknown> type guard. Remove the
duplicate definitions from opencodeProxy.ts, refresh.ts, accounts.ts, usage.ts,
vault.ts, and encryption.ts, and import the shared helper in each file; update
vault.ts to use the shared typing.
web/services/coderouter/providerFetch.ts (1)

7-12: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Release the discarded response body before you retry.

Line 9 drops the first Response when the status is retryable. The body stream is never read or cancelled. The connection and buffered bytes stay allocated until garbage collection. This helper runs on the Codex models, OpenCode config, and usage paths, so a 5xx burst from a provider accumulates unreleased streams.

🔧 Proposed fix to release the abandoned body
   try {
     const first = await request();
-    return RETRYABLE_STATUS.has(first.status) ? await request() : first;
+    if (!RETRYABLE_STATUS.has(first.status)) return first;
+    await first.body?.cancel().catch(() => undefined);
+    return await request();
   } catch {
     return await request();
   }
🤖 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 `@web/services/coderouter/providerFetch.ts` around lines 7 - 12, Update the
retry logic around the first response in the provider fetch helper: when
first.status is retryable, consume or cancel first.body before issuing the
second request, while preserving the existing return behavior for non-retryable
responses and the catch-path retry.
web/tests/coderouter-models-proxy.test.ts (1)

22-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the proxy excludes accounts it already attempted.

A previous thread marked this as addressed, but the fake select at line 22 still ignores its third argument. Production selectAccountForRequest uses excludedAccountIds to skip accounts already tried in the same request.

If createCodexModelsProxy stopped passing attempted, this test would still pass, because the fake advances its own queue on every call. In production the same unusable account would be reselected until the 8-attempt bound and the request would return no_usable_account. The OpenCode test asserts this contract at web/tests/coderouter-opencode-proxy.test.ts line 58.

🧪 Proposed change to record and assert the excluded account IDs
 let selectedAccounts = ["account-1"];
 let unusableAccounts = new Set<string>();
+let excludedAccounts: string[] = [];
-  select: async () => {
+  select: async (_teamId, _provider, excluded) => {
+    excludedAccounts = [...(excluded ?? [])];
     const id = selectedAccounts.shift();
     return id
       ? { id, vaultRevision: 1, credentialExpiresAt: new Date() }
       : null;
   },
   beforeEach(() => {
     selectedAccounts = ["account-1"];
     unusableAccounts = new Set();
+    excludedAccounts = [];
   });
     expect(response.status).toBe(200);
+    expect(excludedAccounts).toContain("refreshing-account");
     expect(await response.json()).toEqual({
       models: [{ slug: "gpt-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 `@web/tests/coderouter-models-proxy.test.ts` around lines 22 - 27, Update the
fake select implementation in the proxy test to accept and record its third
argument, excludedAccountIds, and assert that attempted account IDs are passed
to createCodexModelsProxy as exclusions on subsequent selections. Ensure the
test would fail if the proxy stopped forwarding attempted IDs, while preserving
the existing account-selection behavior.
web/services/coderouter/codexProxy.ts (2)

129-138: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unclassified credential errors turn infrastructure outages into silent 503s. Both account-selection loops catch every error from the credential dependency and continue to the next account. Neither distinguishes a routing-skip condition (CodeRouterRefreshBusy, CodeRouterCredentialBroken) from an infrastructure fault such as a KMS outage, an RDS failure, or a decryption error. The caller receives no_usable_account (503) after the attempt bound, and reportCoderouterFailure never runs, so the outage produces no alert. proxyCodexRequest at web/services/coderouter/codexProxy.ts lines 49-56 already classifies the error correctly, so the paths disagree. The shared root cause is one missing skip predicate.

  • web/services/coderouter/codexProxy.ts#L129-L138: replace the bare catch with the tag check used by proxyCodexRequest, and rethrow anything that is not a routing-skip tag.
  • web/services/coderouter/opencodeProxy.ts#L90-L92: replace the bare catch in openCodeAccount with the same tag check, and report or rethrow anything else instead of skipping silently.
  • web/tests/coderouter-opencode-proxy.test.ts#L45-L46: throw CodeRouterRefreshBusy instead of a plain Error, so the fixture states the intended routing-skip condition once the predicate lands.

Extract the skip predicate into one shared helper in the CodeRouter service directory, then call it from all three loops.

As per coding guidelines: "When behavior has multiple entrypoints, implement one shared action path and verify every entrypoint rather than duplicating logic."

🤖 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 `@web/services/coderouter/codexProxy.ts` around lines 129 - 138, Unclassified
credential errors are being silently skipped instead of propagating
infrastructure failures. Add one shared skip-predicate helper in the CodeRouter
service directory for CodeRouterRefreshBusy and CodeRouterCredentialBroken, use
it in the credential catch blocks in
web/services/coderouter/codexProxy.ts#L129-L138 and
web/services/coderouter/opencodeProxy.ts#L90-L92, and rethrow non-skippable
errors so proxyCodexRequest/reportCoderouterFailure can handle them; update
web/tests/coderouter-opencode-proxy.test.ts#L45-L46 to throw
CodeRouterRefreshBusy rather than a plain Error, with no other direct change
required there.

Source: Coding guidelines


142-150: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the models read and handle a rejected provider read.

A previous thread marked this as fixed, but the current code still shows both defects.

First, this fetch sets no AbortSignal. Both sibling provider reads bound themselves: remoteConfig in web/services/coderouter/opencodeProxy.ts at line 101 and the usage read in web/services/coderouter/usage.ts use AbortSignal.timeout(5_000). dependencies.providerRead replays the call once, so an unresponsive provider holds the request thread for two unbounded attempts. Create the signal inside the callback so each attempt receives a fresh deadline.

Second, fetchProviderRead rethrows when both attempts throw. Line 142 is not inside a try, so a transport fault on one account rejects the whole handler instead of failing over to the next account. The loop already tolerates a rate-limited account.

🔧 Proposed fix to bound the read and continue on transport failure
-      upstream = await dependencies.providerRead(() => fetch(upstreamUrl, {
-        headers: {
-          authorization: `Bearer ${credential.accessToken}`,
-          "chatgpt-account-id": credential.accountId,
-          originator: "codex_cli_rs",
-          "user-agent": request.headers.get("user-agent") ?? "coderouter",
-        },
-        cache: "no-store",
-      }));
+      try {
+        upstream = await dependencies.providerRead(() => fetch(upstreamUrl, {
+          headers: {
+            authorization: `Bearer ${credential.accessToken}`,
+            "chatgpt-account-id": credential.accountId,
+            originator: "codex_cli_rs",
+            "user-agent": request.headers.get("user-agent") ?? "coderouter",
+          },
+          cache: "no-store",
+          signal: AbortSignal.timeout(5_000),
+        }));
+      } catch (error) {
+        reportCoderouterFailure("provider_usage", error, { provider: "codex" });
+        upstream = null;
+        continue;
+      }
🤖 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 `@web/services/coderouter/codexProxy.ts` around lines 142 - 150, Update the
provider read around fetchProviderRead so the fetch callback creates and passes
a fresh AbortSignal.timeout(5_000) for each retry attempt. Also catch a rejected
providerRead call at the surrounding account-processing flow and continue to the
next account on transport failure, preserving the existing rate-limit failover
behavior.
web/services/coderouter/refresh.ts (1)

107-118: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Report persistence failures under the rds category.

The catch at line 107 still covers the whole lease block. dependencies.read, dependencies.release, dependencies.encrypt, and dependencies.complete all report provider_refresh with a provider tag. A lost CAS race in completeRefreshLease or an RDS outage therefore pages on-call for the upstream provider.

One additional case shares the same root cause: line 118 discards a rejection from dependencies.fail. If the lease release fails, the account stays in refreshing until the lease expires and no alert fires. Report that rejection under rds too.

🔧 Proposed fix to separate the provider failure from the persistence failure
-    const refreshed = await dependencies.refresh(current.credential);
+    let refreshed;
+    try {
+      refreshed = await dependencies.refresh(current.credential);
+    } catch (error) {
+      reportCoderouterFailure("provider_refresh", error, {
+        provider: currentProvider(before.credential),
+        terminal: dependencies.isTerminal(error),
+      });
+      throw error;
+    }
     const encrypted = await dependencies.encrypt({
   } catch (error) {
     const terminal = dependencies.isTerminal(error);
-    reportCoderouterFailure("provider_refresh", error, {
-      provider: currentProvider(before.credential),
-      terminal,
-    });
     await dependencies.fail(
       input.accountId,
       leaseId,
       terminal,
       dependencies.failureCode(error),
-    ).catch(() => undefined);
+    ).catch((leaseError: unknown) => {
+      reportCoderouterFailure("rds", leaseError, { operation: "fail_lease" });
+    });
🤖 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 `@web/services/coderouter/refresh.ts` around lines 107 - 118, Update the
refresh error handling around the lease block so provider-operation failures
remain reported as provider_refresh, while failures from dependencies.read,
dependencies.release, dependencies.encrypt, and dependencies.complete are caught
and reported under rds without a provider tag. In the dependencies.fail
rejection handler, stop silently discarding the error and report the rejection
under rds as well, preserving the existing lease-failure flow.
web/tests/coderouter-encryption.test.ts (1)

98-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the rejecting-provider branch of coalescingCredentialsProvider.

The suite covers the success path only. If provider() rejects, the finally block must clear pending so the next call retries. A regression there wedges every KMS request for the process lifetime. Add a case where the first exchange rejects, then assert the next call succeeds and performs a second exchange.

🧪 Proposed test
+  test("clears the pending exchange after a failure", async () => {
+    let calls = 0;
+    const now = 1_000;
+    const provider = coalescingCredentialsProvider(async () => {
+      calls++;
+      if (calls === 1) throw new Error("role exchange rejected");
+      return {
+        accessKeyId: "access-2",
+        secretAccessKey: "secret",
+        expiration: new Date(now + 10 * 60 * 1_000),
+      };
+    }, () => now);
+    await expect(provider()).rejects.toThrow("role exchange rejected");
+    expect((await provider()).accessKeyId).toBe("access-2");
+    expect(calls).toBe(2);
+  });
🤖 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 `@web/tests/coderouter-encryption.test.ts` around lines 98 - 120, Add a test
case in the “coderouter Vercel OIDC credentials” suite covering a rejected
exchange from coalescingCredentialsProvider: make the first provider invocation
reject, assert that rejection, then make the next invocation succeed and verify
it performs a second exchange, confirming pending is cleared for retries.
🤖 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.

Duplicate comments:
In `@web/services/coderouter/codexProxy.ts`:
- Around line 129-138: Unclassified credential errors are being silently skipped
instead of propagating infrastructure failures. Add one shared skip-predicate
helper in the CodeRouter service directory for CodeRouterRefreshBusy and
CodeRouterCredentialBroken, use it in the credential catch blocks in
web/services/coderouter/codexProxy.ts#L129-L138 and
web/services/coderouter/opencodeProxy.ts#L90-L92, and rethrow non-skippable
errors so proxyCodexRequest/reportCoderouterFailure can handle them; update
web/tests/coderouter-opencode-proxy.test.ts#L45-L46 to throw
CodeRouterRefreshBusy rather than a plain Error, with no other direct change
required there.
- Around line 142-150: Update the provider read around fetchProviderRead so the
fetch callback creates and passes a fresh AbortSignal.timeout(5_000) for each
retry attempt. Also catch a rejected providerRead call at the surrounding
account-processing flow and continue to the next account on transport failure,
preserving the existing rate-limit failover behavior.

In `@web/services/coderouter/opencodeProxy.ts`:
- Around line 198-200: Move the local isRecord definition from opencodeProxy.ts
into a shared CodeRouter helper module, using the consistent Record<string,
unknown> type guard. Remove the duplicate definitions from opencodeProxy.ts,
refresh.ts, accounts.ts, usage.ts, vault.ts, and encryption.ts, and import the
shared helper in each file; update vault.ts to use the shared typing.

In `@web/services/coderouter/providerFetch.ts`:
- Around line 7-12: Update the retry logic around the first response in the
provider fetch helper: when first.status is retryable, consume or cancel
first.body before issuing the second request, while preserving the existing
return behavior for non-retryable responses and the catch-path retry.

In `@web/services/coderouter/refresh.ts`:
- Around line 107-118: Update the refresh error handling around the lease block
so provider-operation failures remain reported as provider_refresh, while
failures from dependencies.read, dependencies.release, dependencies.encrypt, and
dependencies.complete are caught and reported under rds without a provider tag.
In the dependencies.fail rejection handler, stop silently discarding the error
and report the rejection under rds as well, preserving the existing
lease-failure flow.

In `@web/tests/coderouter-encryption.test.ts`:
- Around line 98-120: Add a test case in the “coderouter Vercel OIDC
credentials” suite covering a rejected exchange from
coalescingCredentialsProvider: make the first provider invocation reject, assert
that rejection, then make the next invocation succeed and verify it performs a
second exchange, confirming pending is cleared for retries.

In `@web/tests/coderouter-models-proxy.test.ts`:
- Around line 22-27: Update the fake select implementation in the proxy test to
accept and record its third argument, excludedAccountIds, and assert that
attempted account IDs are passed to createCodexModelsProxy as exclusions on
subsequent selections. Ensure the test would fail if the proxy stopped
forwarding attempted IDs, while preserving the existing account-selection
behavior.

In `@web/tests/coderouter-provider-fetch.test.ts`:
- Around line 26-36: Add tests alongside the existing fetchProviderRead retry
tests to verify retry termination: use an always-503 response callback and
assert it is called twice with a final 503 response, then use an always-throwing
callback and assert fetchProviderRead rejects after the bounded attempts.

In `@web/tests/coderouter-refresh.test.ts`:
- Around line 113-125: Add coverage in the createCredentialRefresher tests for
the post-claim re-read branch: configure read to return an expired credential
first and a valid credential second, then assert release is called, the provider
refresh is not called, and the existing valid token is returned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 566e9f19-9505-4b19-8751-a1bae663a583

📥 Commits

Reviewing files that changed from the base of the PR and between e012389 and db6cc10.

📒 Files selected for processing (19)
  • web/app/api/coderouter/accounts/[accountId]/route.ts
  • web/app/api/coderouter/accounts/route.ts
  • web/app/coderouter/page.tsx
  • web/db/schema.ts
  • web/services/coderouter/accounts.ts
  • web/services/coderouter/codexProxy.ts
  • web/services/coderouter/encryption.ts
  • web/services/coderouter/observability.ts
  • web/services/coderouter/opencodeProxy.ts
  • web/services/coderouter/providerFetch.ts
  • web/services/coderouter/refresh.ts
  • web/services/coderouter/repository.ts
  • web/services/coderouter/usage.ts
  • web/tests/coderouter-account-removal.test.ts
  • web/tests/coderouter-encryption.test.ts
  • web/tests/coderouter-models-proxy.test.ts
  • web/tests/coderouter-opencode-proxy.test.ts
  • web/tests/coderouter-provider-fetch.test.ts
  • web/tests/coderouter-refresh.test.ts

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant