feat(llm): add OpenRouter as a runtime-switchable provider (with PKCE login & key rotation)#27
Conversation
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>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
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>
|
Follow-up commit
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 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>
|
Follow-up commit 1. Previously the deploy flow silently reused the existing key if one was on the project. Now when Default is "Keep" so an accidental re-deploy doesn't churn credentials. 2. New For ad-hoc rotations without re-running the full deploy flow (no store provisioning, no Composio prompt, no migration pass): This also doubles as the "switch a Gateway project to OpenRouter after the fact" command — it always writes Implementation notes
Validation: cli |
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-keysubcommand so users can replace the key later without touching the dashboard.Motivation
AI_GATEWAY_API_KEYplus a Vercel account anyway.openai/text-embedding-3-largemodel 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.tsexposesgetLanguageModel(canonicalModelId)which returns either ananthropic/<id>string (Gateway) or aLanguageModelV3instance from@openrouter/ai-sdk-provider(OpenRouter).embedding.tsexposesgetEmbeddingModel()with the same dual-mode return.model-mapping.tsmaps stored canonical ids (claude-sonnet-4-5-20250929, etc.) to the slug each provider expects.src/env.ts: addsLLM_PROVIDER(enum, defaultvercel-ai-gateway),OPENROUTER_API_KEY,AI_GATEWAY_API_KEY, plus a cross-field check that refuses to boot withLLM_PROVIDER=openrouterand 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+stagedSummarizemerge)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:
env=...now includesLLM_PROVIDERandOPENROUTER_API_KEYwith explainer text; leaving them blank keeps the Gateway default.npx @composio/trustclaw deployCLI: select prompt for the provider, writes bothLLM_PROVIDERandOPENROUTER_API_KEYto the Vercel project env..env.exampledocuments 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):code_verifier+ S256code_challengeand a random CSRFstatehttps://openrouter.ai/auth?callback_url=http://localhost:<port>/callback&code_challenge=...&state=...in the user's browserstate, and POSTs{code, code_verifier, code_challenge_method: 'S256'}tohttps://openrouter.ai/api/v1/auth/keyssk-or-...to the Vercel project'sOPENROUTER_API_KEYManual 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 existingopenpackage — no new runtime deps.3. Override the key after the first deploy (commit
344de10)Two paths to rotate
OPENROUTER_API_KEYpost-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 writesLLM_PROVIDER=openrouteralongside the key.trustclaw deployre-run — when the project already hasOPENROUTER_API_KEY, the prompt now offerskeep / rotate via browser / rotate by paste(defaults to "keep" so accidental re-deploys don't churn credentials). Previously this skipped silently.upsertEnvVars()helper incli/src/env-vars.tsso bothsetEnvVars(deploy) and the new subcommand share the same Vercel API loop.resolveOpenRouterKeyis exported fromcli/src/inputs.tsfor reuse. The new subcommand is wired up incli/src/index.ts.Backward compatibility
anthropic/<id>string the previous code constructed inline.LLM_PROVIDERto Vercel when the user explicitly picks OpenRouter, so existing deploys redeploying through the new CLI keep the Gateway path.composioClawInstance.anthropicModelcolumn still stores the same canonical ids; only the resolution layer changes.Test plan
pnpm typecheck(web + cli) — passespnpm lint— no new warnings (one pre-existing warning inintegrations-step.tsx)pnpm exec prettier --checkon all touched files — cleanLLM_PROVIDER=openrouter+ ask-or-…key → confirm chat, memory save/search, compaction all route through OpenRouter (visible in https://openrouter.ai/activity). Existing memories returned bymemory_searchconfirm the shared embedding space.pnpm cli:deployagainst a throwaway Vercel project; pick OpenRouter; complete the browser login; confirmLLM_PROVIDER+OPENROUTER_API_KEYland in the project's env tab.npx @composio/trustclaw set-openrouter-keyagainst the same project; confirm the env var is replaced and a redeploy picks up the new key.Files touched