Skip to content

coderouter: OpenAI and OpenRouter API keys as Responses providers - #12111

Merged
lawrencecchen merged 17 commits into
mainfrom
feat-coderouter-api-key-providers
Sep 8, 2026
Merged

lawrencecchen merged 17 commits into
mainfrom
feat-coderouter-api-key-providers

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #12103 (retarget to main after it merges).

Teams can add an OpenAI API key or an OpenRouter API key as a coderouter account next to their Codex sign-ins.

  • /v1/responses and /v1/models select from a provider pool (codex, openai-apikey, openrouter-apikey) with the same session stickiness, placement spread, 429 cooldown and failover. Codex sign-ins still go to the ChatGPT backend; an OpenAI key goes to api.openai.com; an OpenRouter key goes to openrouter.ai, with bare OpenAI model ids rewritten to openai/<model> and OpenRouter attribution headers set.
  • A key the provider rejects (401) is marked broken with last failure api_key_rejected and the request moves to the next account. The dashboard shows the state; the fix is to remove and re-add the key.
  • Storage reuses the KMS envelope. The provider account id is a SHA-256 fingerprint of the key, so re-adding the same key updates the row and the row never carries the secret. API keys have no expiry and skip the refresher.
  • Dashboard add panel: two new tabs posting {provider, apiKey, label} to /api/coderouter/accounts. The cr CLI is a separate repo and does not gain cr add openai here.

Operator step before merge: migration web/db/migrations/20260907150000_coderouter_api_key_providers widens the provider CHECK on coderouter_accounts, coderouter_credentials, coderouter_session_accounts. Run bun run cloud-vm:migrate -- staging then -- production first, per services/coderouter/README.md. Verified on a scratch Postgres.

Tests: tests/coderouter-api-key-providers.test.ts (parsing, fingerprint, masking, refresher, upstream routing for both keys, model rewrite, 401 failover, models catalog), existing proxy, refresh, selector and accounts tests updated for the pooled provider and the new fixture field. Typecheck and complexity gate pass.

Not verified live: no OpenAI or OpenRouter key was available in this session to send a real request through.

https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS


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


Note

High Risk
Adds handling and encrypted storage of third-party API keys and changes core Codex proxy routing, session pooling, and credential lifecycle; requires the provider CHECK migration before deploy.

Overview
Teams can add OpenAI and OpenRouter API keys as coderouter accounts alongside Codex sign-ins. The dashboard Add account panel gets two new tabs with forms that POST { provider, apiKey, label? } to /api/coderouter/accounts.

Routing: /v1/responses and /v1/models now select from a shared pool (codex, openai-apikey, openrouter-apikey) with the same session stickiness, placement spread, 429 cooldown, and failover. Traffic goes to the ChatGPT Codex backend, api.openai.com, or OpenRouter as appropriate; OpenRouter rewrites bare model ids to openai/<model> and sets attribution headers.

Credentials: Keys are validated and stored in the existing KMS envelope; providerAccountId is a SHA-256 fingerprint so re-adding the same key updates one row. API keys do not expire and skip OAuth refresh—a 401 triggers a forced “refresh” that marks the account broken (api_key_rejected) and fails over to the next account.

Data: Schema and migration 20260907150000_coderouter_api_key_providers widen provider CHECKs on coderouter_accounts, coderouter_credentials, and coderouter_session_accounts. Session bindings for the Responses surface are keyed under the pool’s first provider (codex) so failover between pool members does not leave duplicate bindings.

Reviewed by Cursor Bugbot for commit 8b4b167. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Adds OpenAI and OpenRouter API keys as coderouter providers, so teams can route /v1/responses and /v1/models through these keys alongside Codex sign-ins. Codex sign-ins still go to the ChatGPT backend; OpenAI keys go to api.openai.com; OpenRouter keys go to openrouter.ai with bare OpenAI model IDs rewritten to openai/<model>.

Behavior

  • Session stickiness, placement spread, 429 cooldown, and failover apply across the provider pool.
  • A key rejected with 401 marks the account broken (api_key_rejected) and the request moves to the next account.
  • Keys are stored with the KMS envelope; the provider account ID is a SHA-256 fingerprint, so re-adding the same key updates the row and never stores the secret.
  • Session bindings are stored under the surface's first provider, so a session that moves between a Codex sign-in and a key replaces its binding instead of adding a second one.
  • API keys have no expiry and skip the refresher; the dashboard add panel gains two tabs posting {provider, apiKey, label} to /api/coderouter/accounts.

Migration

  • Run migration 20260907150000_coderouter_api_key_providers on staging and production before merging; it widens the provider CHECK on three coderouter tables.

Written for commit 8b4b167. Summary will update on new commits.

Review in cubic

…m picker

The coderouter page showed Claude upstream accounts and hosted subrouter
accounts (Codex) as two unrelated sections, each with its own add flow,
plus a per-page team switcher and provider logos.

Now one Accounts section lists both sources in one table with provider,
label, status, and actions, and one add panel offers Anthropic API key,
Claude Code OAuth token, Amazon Bedrock, Codex, and OpenCode. The Codex
and OpenCode entries are text-only CLI commands. The team scope moves to
the bottom-left sidebar as a dashboard-wide switcher that persists the
same cookie the server already reads, so the coderouter page no longer
renders its own team links. Usage first, accounts second, machines last.

Removes the unused aiAccounts and claudeUpstream catalogs and adds
coderouterAccounts and teamSwitcher (en, ja).

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS
The team scope was a separate row above the account row. It is now a
Team entry inside the bottom-left account menu, next to Settings and
Billing, that opens a submenu listing every permitted team with the
current one checked. The trigger shows the current team under the user
name. The scope hook moves to dashboard-team-scope.ts and keeps the
cookie-based switch. The sidebar shell is back to its main layout.

The "not configured" notice now says Codex accounts are hidden because
the hosted account service is not configured on this deployment.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS
The sidebar footer had a standalone sun/moon button next to the account
menu. The switch is now a menu item after Team, named after the theme it
switches to, and the footer holds only the account control. The toggle
logic is a shared useThemeToggle hook so the site header keeps its button.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS
The accounts table now includes the coderouter-native accounts that
`cr add` stores (Codex and OpenCode Go sign-ins), with their session
count, state, and a remove action against /api/coderouter/accounts.
Before, the page only listed hosted-subrouter accounts and Claude
upstream accounts, so the accounts coderouter actually routes with were
invisible.

"Pricing coverage" is gone as a metric card. The API-equivalent footnote
now says what the value is, and only when some tokens had no list price
does it add the share that was left out.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS
Teams can add an OpenAI API key or an OpenRouter API key as a coderouter
account next to their Codex sign-ins. The Responses surface
(/v1/responses, /v1/models) now selects from a provider pool: Codex
sign-ins go to the ChatGPT backend as before, an OpenAI key goes to
api.openai.com, and an OpenRouter key goes to openrouter.ai with bare
OpenAI model ids rewritten to openai/<model>. Session stickiness,
placement spread, rate-limit cooldown and failover are unchanged; a key
the provider rejects is marked broken with last failure api_key_rejected
and the request moves on.

Keys are stored like every other credential (KMS envelope, AAD bound to
team/account/provider). The provider account id is a SHA-256 fingerprint
of the key, so re-adding a key updates the same row and the row never
carries the secret. API keys have no expiry and skip the refresher.

Migration 20260907150000 widens the provider CHECK on the three
coderouter tables. Run it on staging and production before merging.

The dashboard add panel gains OpenAI API key and OpenRouter API key
tabs posting to /api/coderouter/accounts.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS
@vercel

vercel Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cmux166 Ready Ready Preview Sep 8, 2026 10:24am UTC
cmux41 Canceled Canceled Sep 8, 2026 10:24am UTC

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 3 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 63ce4940-fcd7-436b-a63a-eea0152d702e

📥 Commits

Reviewing files that changed from the base of the PR and between f0a9407 and 8b4b167.

📒 Files selected for processing (19)
  • web/app/[locale]/dashboard/components/coderouter-accounts.tsx
  • web/db/migrations/20260907150000_coderouter_api_key_providers/migration.sql
  • web/db/schema.ts
  • web/messages/en.json
  • web/messages/ja.json
  • web/services/coderouter/accounts.ts
  • web/services/coderouter/analytics.ts
  • web/services/coderouter/codexProxy.ts
  • web/services/coderouter/encryption.ts
  • web/services/coderouter/refresh.ts
  • web/services/coderouter/repository.ts
  • web/services/coderouter/types.ts
  • web/tests/coderouter-accounts.test.tsx
  • web/tests/coderouter-api-key-providers.test.ts
  • web/tests/coderouter-models-proxy.test.ts
  • web/tests/coderouter-opencode-proxy.test.ts
  • web/tests/coderouter-refresh.test.ts
  • web/tests/coderouter-responses-proxy.test.ts
  • web/tests/coderouter-session-selector.test.ts

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.

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

Stale Bugbot comment from a previous run.

Comment thread web/services/coderouter/encryption.ts
Comment thread web/services/coderouter/repository.ts Outdated
Base UI Tabs replace the hand-rolled tab buttons in the add panel, so
arrow keys and roving focus work. Team items in the account menu are a
Menu.RadioGroup, which owns the checked state. The accounts section is
keyed by team so a team switch never keeps a half-filled form. A team
switch drops only the team query parameter. A 503 from a Claude account
route shows the Claude save or remove error, not the shared-account
copy. The hosted-subrouter notice is shown to account managers only and
now says which accounts it is about. Test fixtures reset per test.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4ba728b. Configure here.

Comment thread web/services/coderouter/codexProxy.ts
@lawrencecchen
lawrencecchen changed the base branch from feat-coderouter-accounts-team-switcher to main September 8, 2026 07:44
…ey-providers

# Conflicts:
#	web/app/[locale]/dashboard/components/coderouter-accounts.tsx
#	web/messages/en.json
#	web/messages/ja.json
#	web/tests/coderouter-accounts.test.tsx
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

…sion binding per surface, models 401 failover

The envelope identity check still listed only codex and opencode-go, so an
OpenAI or OpenRouter key could not be encrypted or decrypted. It now reads
the shared provider list. Session bindings are stored under the surface's
first provider, so a session that moves between a Codex sign-in and a key
replaces its row instead of adding a second one. Model discovery on a 401
forces a refresh, which marks a rejected key broken, and tries the next
account.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Addressed the Bugbot findings: the envelope identity check now uses the shared provider list (the high-severity one, a real miss), session bindings are stored under the surface's first provider so a move between a sign-in and a key replaces the row, and /v1/models fails over on 401 by forcing a refresh that marks a rejected key broken. Tests added for all three.

@lawrencecchen
lawrencecchen merged commit 5fda406 into main Sep 8, 2026
18 of 22 checks passed
rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 8, 2026
92f8da5 Fix sidebar Sign In button and account avatar not rendering on Intel Macs (manaflow-ai#12126)
5a16fbb cloud: guest `cmux self` and `cmux vm ls` via GET /api/vm/self (manaflow-ai#12116)
9cbdb7c web: remove the public /dashboard/admin page (manaflow-ai#12110)
5fda406 coderouter: OpenAI and OpenRouter API keys as Responses providers (manaflow-ai#12111)
aerickson pushed a commit to aerickson/cmux that referenced this pull request Sep 13, 2026
…naflow-ai#12111)

* web(dashboard): one accounts list, sidebar team switcher, no page team picker

The coderouter page showed Claude upstream accounts and hosted subrouter
accounts (Codex) as two unrelated sections, each with its own add flow,
plus a per-page team switcher and provider logos.

Now one Accounts section lists both sources in one table with provider,
label, status, and actions, and one add panel offers Anthropic API key,
Claude Code OAuth token, Amazon Bedrock, Codex, and OpenCode. The Codex
and OpenCode entries are text-only CLI commands. The team scope moves to
the bottom-left sidebar as a dashboard-wide switcher that persists the
same cookie the server already reads, so the coderouter page no longer
renders its own team links. Usage first, accounts second, machines last.

Removes the unused aiAccounts and claudeUpstream catalogs and adds
coderouterAccounts and teamSwitcher (en, ja).

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* web(dashboard): put the team picker inside the account menu

The team scope was a separate row above the account row. It is now a
Team entry inside the bottom-left account menu, next to Settings and
Billing, that opens a submenu listing every permitted team with the
current one checked. The trigger shows the current team under the user
name. The scope hook moves to dashboard-team-scope.ts and keeps the
cookie-based switch. The sidebar shell is back to its main layout.

The "not configured" notice now says Codex accounts are hidden because
the hosted account service is not configured on this deployment.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* web(dashboard): move the theme switch into the account menu

The sidebar footer had a standalone sun/moon button next to the account
menu. The switch is now a menu item after Team, named after the theme it
switches to, and the footer holds only the account control. The toggle
logic is a shared useThemeToggle hook so the site header keeps its button.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* web(dashboard): list cr-added accounts, drop the pricing coverage card

The accounts table now includes the coderouter-native accounts that
`cr add` stores (Codex and OpenCode Go sign-ins), with their session
count, state, and a remove action against /api/coderouter/accounts.
Before, the page only listed hosted-subrouter accounts and Claude
upstream accounts, so the accounts coderouter actually routes with were
invisible.

"Pricing coverage" is gone as a metric card. The API-equivalent footnote
now says what the value is, and only when some tokens had no list price
does it add the share that was left out.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* coderouter: route Responses calls through OpenAI and OpenRouter API keys

Teams can add an OpenAI API key or an OpenRouter API key as a coderouter
account next to their Codex sign-ins. The Responses surface
(/v1/responses, /v1/models) now selects from a provider pool: Codex
sign-ins go to the ChatGPT backend as before, an OpenAI key goes to
api.openai.com, and an OpenRouter key goes to openrouter.ai with bare
OpenAI model ids rewritten to openai/<model>. Session stickiness,
placement spread, rate-limit cooldown and failover are unchanged; a key
the provider rejects is marked broken with last failure api_key_rejected
and the request moves on.

Keys are stored like every other credential (KMS envelope, AAD bound to
team/account/provider). The provider account id is a SHA-256 fingerprint
of the key, so re-adding a key updates the same row and the row never
carries the secret. API keys have no expiry and skip the refresher.

Migration 20260907150000 widens the provider CHECK on the three
coderouter tables. Run it on staging and production before merging.

The dashboard add panel gains OpenAI API key and OpenRouter API key
tabs posting to /api/coderouter/accounts.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* web(dashboard): account menu order is settings, theme, billing, team

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* web(dashboard): pin the theme in the menu order test

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* web(dashboard): address review findings on the accounts page and menu

Base UI Tabs replace the hand-rolled tab buttons in the add panel, so
arrow keys and roving focus work. Team items in the account menu are a
Menu.RadioGroup, which owns the checked state. The accounts section is
keyed by team so a team switch never keeps a half-filled form. A team
switch drops only the team query parameter. A 503 from a Claude account
route shows the Claude save or remove error, not the shared-account
copy. The hosted-subrouter notice is shown to account managers only and
now says which accounts it is about. Test fixtures reset per test.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* web(dashboard): style the active add tab with Base UI's data-active

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* web(dashboard): assert the active tab without assuming attribute order

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* web(dashboard): show account identifiers to managers only, label refreshing state

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

* coderouter: admit API-key providers in the envelope identity, one session binding per surface, models 401 failover

The envelope identity check still listed only codex and opencode-go, so an
OpenAI or OpenRouter key could not be encrypted or decrypted. It now reads
the shared provider list. Session bindings are stored under the surface's
first provider, so a session that moves between a Codex sign-in and a key
replaces its row instead of adding a second one. Model discovery on a 401
forces a refresh, which marks a rejected key broken, and tries the next
account.

Claude-Session: https://claude.ai/code/session_01LMP2oJjwAkLbLxLaWmLaLS

This branch was successfully deployed

2 active deployments
Preview – cmux41 — 8b4b167f Deployed Sep 8, 2026 by vercel[bot]
Preview – cmux166 — 8b4b167f Deployed Sep 8, 2026 by vercel[bot]
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