Skip to content

feat(llm): add OpenRouter as a runtime-switchable provider (with PKCE login & key rotation)#27

Open
Chris Watts (seawatts) wants to merge 3 commits into
ComposioHQ:mainfrom
seawatts:openrouter-provider-switch
Open

feat(llm): add OpenRouter as a runtime-switchable provider (with PKCE login & key rotation)#27
Chris Watts (seawatts) wants to merge 3 commits into
ComposioHQ:mainfrom
seawatts:openrouter-provider-switch

Conversation

@seawatts

@seawatts Chris Watts (seawatts) commented May 13, 2026

Copy link
Copy Markdown

Summary

Adds OpenRouter alongside the existing Vercel AI Gateway as a runtime-switchable LLM + embedding provider. Vercel AI Gateway stays the default so existing deployments are unaffected; opting into OpenRouter is a single env var (LLM_PROVIDER=openrouter) plus an API key.

The CLI deploy flow mints the API key for users via OpenRouter's OAuth-PKCE flow so there's no copy-paste, and exposes both a re-run rotate prompt and a focused set-openrouter-key subcommand so users can replace the key later without touching the dashboard.

Motivation

  • Self-hosters outside Vercel don't get OIDC token auth for AI Gateway and have to manage AI_GATEWAY_API_KEY plus a Vercel account anyway.
  • OpenRouter gives users a single key that bills their own account and unifies usage across 300+ models.
  • Both providers proxy the same openai/text-embedding-3-large model at 1024 dims, so existing pgvector memories remain valid after switching — no re-embedding required.

What changed

1. Core provider abstraction (commit c33a9ab)

  • src/server/clients/ai/ (new):
    • provider.ts exposes getLanguageModel(canonicalModelId) which returns either an anthropic/<id> string (Gateway) or a LanguageModelV3 instance from @openrouter/ai-sdk-provider (OpenRouter).
    • embedding.ts exposes getEmbeddingModel() with the same dual-mode return.
    • model-mapping.ts maps stored canonical ids (claude-sonnet-4-5-20250929, etc.) to the slug each provider expects.
  • src/env.ts: adds LLM_PROVIDER (enum, default vercel-ai-gateway), OPENROUTER_API_KEY, AI_GATEWAY_API_KEY, plus a cross-field check that refuses to boot with LLM_PROVIDER=openrouter and no key.

Call-site refactors (all five inlined model-string constructions now go through the factory):

  • src/server/api/routers/trustclaw/agent/setup.ts (ToolLoopAgent)
  • src/server/api/routers/trustclaw/agent/compaction/memory-flush.ts (generateText)
  • src/server/api/routers/trustclaw/agent/compaction/run-compaction.ts (×2: summarize + stagedSummarize merge)
  • src/server/api/routers/trustclaw/agent/tools/memory-save.ts (embed)
  • src/server/api/routers/trustclaw/agent/tools/memory-search.ts (×2: tool + background context lookup)

Deploy-time UX so a non-engineer can pick OpenRouter without editing files:

  • README Deploy-to-Vercel button: env=... now includes LLM_PROVIDER and OPENROUTER_API_KEY with explainer text; leaving them blank keeps the Gateway default.
  • npx @composio/trustclaw deploy CLI: select prompt for the provider, writes both LLM_PROVIDER and OPENROUTER_API_KEY to the Vercel project env.
  • .env.example documents the new vars; README has a new "Choosing an LLM provider" section.

2. Browser-based OpenRouter login via PKCE (commit d10d21d)

Instead of asking the user to paste an sk-or-... key, the CLI now mints one for them automatically via OpenRouter's OAuth-PKCE flow (same pattern as the openrouter-web mission-control oauth-test reference):

  1. CLI generates a base64url code_verifier + S256 code_challenge and a random CSRF state
  2. Spins up a loopback HTTP server on a random ephemeral port
  3. Opens https://openrouter.ai/auth?callback_url=http://localhost:<port>/callback&code_challenge=...&state=... in the user's browser
  4. User logs in and approves in OpenRouter's UI
  5. CLI receives the redirect, validates state, and POSTs {code, code_verifier, code_challenge_method: 'S256'} to https://openrouter.ai/api/v1/auth/keys
  6. CLI writes the returned sk-or-... to the Vercel project's OPENROUTER_API_KEY

Manual paste is still offered as a fallback for headless / SSH sessions or when the PKCE flow errors (no browser, blocked network, timeout). New file cli/src/openrouter-auth.ts (~250 lines) using only Node built-ins (crypto, http) and the existing open package — no new runtime deps.

3. Override the key after the first deploy (commit 344de10)

Two paths to rotate OPENROUTER_API_KEY post-deploy:

  • trustclaw set-openrouter-key — focused subcommand that asks for a new key (browser PKCE or manual paste), writes it to the Vercel project, and skips everything else (no store provisioning, no Composio prompt). Best for quick rotations. Also doubles as the "switch from Gateway to OpenRouter after the fact" command since it always writes LLM_PROVIDER=openrouter alongside the key.
  • trustclaw deploy re-run — when the project already has OPENROUTER_API_KEY, the prompt now offers keep / rotate via browser / rotate by paste (defaults to "keep" so accidental re-deploys don't churn credentials). Previously this skipped silently.
  • Implementation: extracted a small upsertEnvVars() helper in cli/src/env-vars.ts so both setEnvVars (deploy) and the new subcommand share the same Vercel API loop. resolveOpenRouterKey is exported from cli/src/inputs.ts for reuse. The new subcommand is wired up in cli/src/index.ts.
  • README has a new "Rotating or overriding the OpenRouter API key later" section documenting all three paths (subcommand, redeploy, Vercel dashboard).

Backward compatibility

  • Default behaviour is unchanged. With no env set, every code path resolves to the same anthropic/<id> string the previous code constructed inline.
  • The CLI only writes LLM_PROVIDER to Vercel when the user explicitly picks OpenRouter, so existing deploys redeploying through the new CLI keep the Gateway path.
  • DB schema is untouched. The composioClawInstance.anthropicModel column still stores the same canonical ids; only the resolution layer changes.

Test plan

  • pnpm typecheck (web + cli) — passes
  • pnpm lint — no new warnings (one pre-existing warning in integrations-step.tsx)
  • pnpm exec prettier --check on all touched files — clean
  • PKCE primitive smoke: verifier and S256 challenge are both 43-char base64url; challenge has only unreserved chars; loopback server rejects state mismatches with 400.
  • Manual smoke (per reviewer): deploy with no LLM envs → confirm Gateway works; redeploy with LLM_PROVIDER=openrouter + a sk-or-… key → confirm chat, memory save/search, compaction all route through OpenRouter (visible in https://openrouter.ai/activity). Existing memories returned by memory_search confirm the shared embedding space.
  • Manual CLI smoke: pnpm cli:deploy against a throwaway Vercel project; pick OpenRouter; complete the browser login; confirm LLM_PROVIDER + OPENROUTER_API_KEY land in the project's env tab.
  • Manual rotate smoke: npx @composio/trustclaw set-openrouter-key against the same project; confirm the env var is replaced and a redeploy picks up the new key.

Files touched

Provider abstraction (commit c33a9ab):
.env.example
README.md
cli/src/deploy.ts
cli/src/env-vars.ts
cli/src/inputs.ts
package.json (+ pnpm-lock.yaml: @openrouter/ai-sdk-provider@^2.9.0)
src/env.ts
src/server/api/routers/trustclaw/agent/compaction/memory-flush.ts
src/server/api/routers/trustclaw/agent/compaction/run-compaction.ts
src/server/api/routers/trustclaw/agent/setup.ts
src/server/api/routers/trustclaw/agent/tools/memory-save.ts
src/server/api/routers/trustclaw/agent/tools/memory-search.ts
src/server/clients/ai/embedding.ts        (new)
src/server/clients/ai/index.ts            (new)
src/server/clients/ai/model-mapping.ts    (new)
src/server/clients/ai/provider.ts         (new)

PKCE login (commit d10d21d):
README.md
cli/src/inputs.ts
cli/src/openrouter-auth.ts                (new)

Key rotation (commit 344de10):
README.md
cli/src/env-vars.ts
cli/src/index.ts
cli/src/inputs.ts
cli/src/set-openrouter-key.ts             (new)

Adds OpenRouter alongside the existing Vercel AI Gateway as a provider
for LLM and embedding traffic, selected at runtime via LLM_PROVIDER.
Vercel AI Gateway stays the default so existing deployments are
unaffected.

What's new:
- src/server/clients/ai/: provider + embedding factories with a Claude
  id -> OpenRouter slug map. Both providers serve
  openai/text-embedding-3-large at 1024 dims, so pgvector data stays
  compatible after switching.
- src/env.ts: LLM_PROVIDER (enum, defaults to vercel-ai-gateway),
  OPENROUTER_API_KEY, AI_GATEWAY_API_KEY, plus a cross-field check that
  refuses to boot with LLM_PROVIDER=openrouter and no key.
- Agent runtime, compaction, memory-flush, memory-save, and
  memory-search all call the factories instead of inlining provider
  strings.

Deploy-time UX (so users can pick OpenRouter without editing files):
- README Deploy-to-Vercel button exposes LLM_PROVIDER and
  OPENROUTER_API_KEY as optional fields with explainer text.
- npx @composio/trustclaw deploy: new prompt picks the provider and
  collects the OpenRouter key if needed; the CLI writes both vars to
  the Vercel project env. Re-runs skip the prompt when the key is
  already on the project.
- .env.example and README ("Choosing an LLM provider" section) document
  the new vars and the zero-re-embedding switch guarantee.

Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

socket-security Bot commented May 13, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​openrouter/​ai-sdk-provider@​2.9.07210010098100

View full report

Replaces the manual "paste your OpenRouter key" prompt in `trustclaw
deploy` with a one-click OAuth-PKCE flow: the CLI opens the user's
browser to https://openrouter.ai/auth, mints a scoped key on approval,
and writes it to the Vercel project env automatically. No
copy-paste, and revoking that specific deployment doesn't affect the
user's other apps.

How it works:
- cli/src/openrouter-auth.ts (new): generates a base64url
  code_verifier + S256 code_challenge, spins up a loopback HTTP
  server on a random port, opens the browser, waits for the redirect
  with the auth code, then POSTs to
  https://openrouter.ai/api/v1/auth/keys to exchange the code for a
  user-controlled key. Mirrors the PKCE flow from the OpenRouter
  mission-control oauth-test reference. Validates state for CSRF and
  times out after 5 minutes.
- cli/src/inputs.ts: new "Log in via browser?" confirm prompt
  defaults to yes; manual paste stays available for headless / SSH
  sessions or as a fallback if the PKCE flow fails.

Backward compatibility: existing flows are unchanged. The browser
prompt only runs when the user picks LLM_PROVIDER=openrouter on a
project that doesn't already have the key.

Co-authored-by: Cursor <cursoragent@cursor.com>
@seawatts

Copy link
Copy Markdown
Author

Follow-up commit d10d21d replaces the manual API-key paste with a browser-based OAuth-PKCE flow. The CLI now mints a scoped OpenRouter key for the user automatically:

  1. trustclaw deploy → pick OpenRouter at the provider prompt
  2. New confirm prompt: "Log in via browser to mint a scoped OpenRouter key?" (default yes)
  3. CLI starts a loopback server on a random port, opens https://openrouter.ai/auth?...&code_challenge=...&state=... in the browser
  4. User approves in OpenRouter's UI
  5. CLI exchanges the returned code at POST https://openrouter.ai/api/v1/auth/keys, writes the resulting sk-or-... to OPENROUTER_API_KEY on the Vercel project

Manual paste is still offered as a fallback for headless / SSH sessions or when the PKCE flow errors (no browser, blocked network, timeout). Backward-compatible: PKCE only runs when the user opts into OpenRouter on a project without an existing key.

PKCE implementation mirrors the openrouter-web mission-control oauth-test reference (same code_verifier / S256 code_challenge derivation, same exchange endpoint, same response shape). Adds cli/src/openrouter-auth.ts; no new runtime deps (open and @clack/prompts were already in cli/package.json, crypto + http are Node built-ins).

Validation: cli typecheck/build pass, smoke-tested the verifier/challenge math + state-mismatch rejection on the loopback server locally.

Two paths to override the key after first deploy:

1. Re-running `trustclaw deploy` now offers a 3-way prompt when the
   project already has OPENROUTER_API_KEY set: keep / rotate via
   browser PKCE / rotate by paste. Previously the deploy silently
   reused the existing key, so users had to edit the Vercel dashboard
   to change it.

2. New `trustclaw set-openrouter-key` subcommand for ad-hoc rotations
   without re-running the full deploy flow. Skips store provisioning,
   Composio auth, and the migration pass - just authenticates with
   Vercel, finds the project, runs PKCE (or manual paste), and
   upserts LLM_PROVIDER + OPENROUTER_API_KEY. Also doubles as the
   "switch from Gateway to OpenRouter after the fact" command.

Implementation:
- cli/src/env-vars.ts: extracted a small `upsertEnvVars()` helper so
  both `setEnvVars` (deploy) and the new subcommand share the same
  POST + upsert=true loop. No behaviour change for setEnvVars.
- cli/src/inputs.ts: `resolveOpenRouterKey` exported, plus a new
  `resolveExistingOrRotate` flow for the deploy-re-run case. The
  default in that select is "Keep existing key" so an accidental
  re-deploy doesn't churn credentials.
- README: new "Rotating or overriding the OpenRouter API key later"
  section documents the three override paths (subcommand, redeploy,
  Vercel dashboard).

Co-authored-by: Cursor <cursoragent@cursor.com>
@seawatts

Copy link
Copy Markdown
Author

Follow-up commit 344de10 adds two override paths for OPENROUTER_API_KEY after the initial deploy:

1. trustclaw deploy re-run now offers a rotate prompt

Previously the deploy flow silently reused the existing key if one was on the project. Now when OPENROUTER_API_KEY is set, the user gets a 3-way select:

◆  OPENROUTER_API_KEY is already set on this project.
│  ● Keep the existing key
│  ◯ Rotate via browser login (PKCE)
│  ◯ Rotate by pasting a new key
└

Default is "Keep" so an accidental re-deploy doesn't churn credentials.

2. New trustclaw set-openrouter-key subcommand

For ad-hoc rotations without re-running the full deploy flow (no store provisioning, no Composio prompt, no migration pass):

$ npx @composio/trustclaw set-openrouter-key
◆ trustclaw set-openrouter-key
◇  Vercel project name › trustclaw
●  Project already has OPENROUTER_API_KEY - it will be replaced.
◆  Log in via browser to mint a scoped OpenRouter key? (No to paste manually)
│  ● Yes / ◯ No
└
●  Opening browser to log in to OpenRouter...
✓  Authorization complete
✓  API key issued
✓  Environment variables set
●  Key updated. Redeploy for the new key to take effect.

This also doubles as the "switch a Gateway project to OpenRouter after the fact" command — it always writes LLM_PROVIDER=openrouter alongside the key, so a project deployed with the gateway can be flipped to OpenRouter in one CLI call.

Implementation notes

  • cli/src/env-vars.ts: extracted a small upsertEnvVars() helper shared by both setEnvVars (deploy) and the new subcommand. No behaviour change for the existing setEnvVars.
  • cli/src/inputs.ts: resolveOpenRouterKey exported for reuse; new resolveExistingOrRotate flow handles the deploy-re-run case.
  • README has a new "Rotating or overriding the OpenRouter API key later" section that documents three paths (subcommand, deploy re-run, Vercel dashboard).

Validation: cli tsc --noEmit + pnpm build clean, main app pnpm typecheck clean.

@seawatts Chris Watts (seawatts) changed the title feat(llm): add OpenRouter as a runtime-switchable provider feat(llm): add OpenRouter as a runtime-switchable provider (with PKCE login & key rotation) May 13, 2026
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