Skip to content

feat(web): add OrcaRouter as a first-class provider with API key and PKCE auth - #346

Open
martinzudergaming-a11y wants to merge 1 commit into
vercel-labs:mainfrom
martinzudergaming-a11y:orcarouter/task-7368
Open

martinzudergaming-a11y wants to merge 1 commit into
vercel-labs:mainfrom
martinzudergaming-a11y:orcarouter/task-7368

Conversation

@martinzudergaming-a11y

Copy link
Copy Markdown

What

Adds OrcaRouter as a first-class provider in the json-render web app, with two explicit authentication choices and a model control built from the live catalog.

OrcaRouter is an OpenAI-compatible AI gateway that routes many providers behind one endpoint.

I'm an engineer on the OrcaRouter team. This PR is made on behalf of OrcaRouter, in coordination with the project. I am not affiliated with vercel-labs.

  • Provider seam in packages/core/src/orcarouter/ (exported from @json-render/core) — inference base URL https://api.orcarouter.ai/v1, Authorization: Bearer <key>.
  • API-key choice: provider id orcarouter, label OrcaRouter - API. The key is read from ORCAROUTER_API_KEY or pasted into the connect panel; it is stored in the Next.js env file this app already uses for provider secrets (apps/web/.env.local, already gitignored). No second credential store is introduced.
  • Connect flow: OAuth 2.0 + PKCE, Flow A (loopback redirect) with Flow B (out-of-band code) as the fallback the panel offers when loopback is unavailable. packages/core is a Node library and the primary consumer is a Node/Next server process that can bind 127.0.0.1:<random port>, so the code returns automatically and the user clicks once; the hosted deployment (json-render.dev) cannot receive a callback on the user's machine, so the same S256 verifier, exchange, and seam are also reachable through the out-of-band path. Flow C (device grant) is not implemented.
  • Provider id for the account path: orcarouter-oauth, label OrcaRouter - Auth — distinct labels everywhere both appear, as the spec asks.

The two choices are adapters over one OrcaCredentialSource interface and resolve to the same OrcaCredentialResult, so /api/generate, /api/docs-chat, and model discovery never learn which entry point produced the key.

How the credential works

The key belongs to the user, not to this project: it is billed to their OrcaRouter account, listed in their console, and revocable by them at any time from https://www.orcarouter.ai/console/authorized-apps. No client secret is involved — PKCE binds the auth code to this process, so an intercepted code cannot be redeemed by anyone else.

  • Origin separation (packages/core/src/orcarouter/origins.ts): authorization is https://www.orcarouter.ai with the fixed paths /auth and /api/v1/auth/keys; inference and the model catalog are https://api.orcarouter.ai/v1. https://api.orcarouter.ai/v1/auth/keys is a 404 and is never derived by swapping a hostname or appending /v1. Explicit per-role overrides (ORCA_AUTH_BASE_URL, ORCA_API_BASE_URL) win over the shared ORCA_BASE_URL; remote origins must be HTTPS and plain HTTP is accepted only for loopback.
  • PKCE: a fresh verifier and state from a CSPRNG on every attempt; code_challenge = base64url(sha256(verifier)) with no padding; the verifier stays in the process until exchange and never appears in a URL, log, error, or telemetry; the Flow A callback compares state in constant time. Denial, state mismatch, timeout, expired/reused code, 403, 429, and network failure all end the attempt with an actionable message instead of hanging or retrying in a loop.
  • Scope: the exchange response's actual scope is read and checked against what this client can use (api). The requested scope is never treated as the granted scope; a response that does not satisfy the requirement is rejected.
  • Durability, not refresh: the PKCE exchange returns a durable sk-orca-… key, not a refresh token. The stored key is reused across restarts until it is revoked; there is no proactive refresh and no invented refresh grant. Reconnecting is never done on startup, which keeps a user well under the 10-key/24h issuance cap.
  • 401 handling: a rejected request marks only the exact account and credential generation that sent it as needing reauthentication (CredentialStore), so a late async 401 cannot invalidate the credential a user just reconnected. The old secret is not silently deleted before a new login succeeds.
  • No key in the browser: /api/orcarouter/models, /api/orcarouter/connect/*, and /api/orcarouter/credential keep the key server-side; the client receives only masked state and minimal model metadata. secret_masked is asserted in the UI evidence below.

Model catalog and capability filtering

GET https://api.orcarouter.ai/v1/models is the only source of truth, requested with the user's key so the list reflects that workspace's actually callable models. Model IDs keep their vendor/model namespace verbatim. When OrcaRouter is selected the model control is a searchable dropdown built from that response — there is no free-text model field.

selectOrcaModels filters per entry point:

  • text chat/agent: ?capability=chat, and supported_endpoint_types must include one of openai / anthropic / gemini / openai-response; image-generation, openai-video, and jina-rerank models are excluded;
  • multimodal understanding: chat first, then architecture.input_modalities must explicitly contain the modality the entry point actually uploads. A record with no architecture block fails closed and never enters the multimodal list;
  • embedding: ?capability=embedding or a strict embeddings endpoint match;
  • image generation: ?capability=image or a strict image-generation match;
  • video: strict openai-video; rerank: strict jina-rerank.

Options are recomputed when the provider changes, when an attachment/modality/task changes, and when the catalog is refreshed. An already-selected model that is no longer compatible is cleared with a prompt to reselect rather than silently kept — the playground's image attachment is the concrete case, and the selector's options (not just a send-time guard) are what is filtered. Loading, empty, auth-error, network-error, refresh, and caching states are implemented; a live success is authoritative and the five-model seed (openai/gpt-5.5, anthropic/claude-opus-4.8, google/gemini-3.5-flash, deepseek/deepseek-v4-pro, orcarouter/auto) is only an outage fallback that is labelled degraded and never merged into a live result. A persisted model ID is re-validated against the compatible list before it is restored.

AI entry points covered

  • Playground generation — apps/web/app/api/generate/route.ts: OrcaRouter selectable; model from the live chat catalog; server-side credential seam; Bearer against api.orcarouter.ai/v1.
  • Docs assistant — apps/web/app/api/docs-chat/route.ts: same provider/model resolution.
  • Not rewired, deliberately: packages/core/src/experimental-evaluator.ts (the Jev path) speaks the Vercel AI Gateway v4 evaluation protocol, which OrcaRouter does not implement, so it keeps its existing behavior. examples/* are standalone demos outside the published packages and outside CI; the reusable provider lives in packages/core so they can adopt it.
  • Not applicable in this repo: image generation, video, embedding, rerank, audio. No such model call exists anywhere in the codebase — examples/image renders locally with satori/resvg and examples/remotion renders locally, neither calls a model. The capability filters above are implemented and tested for the day an entry point exists.
  • No i18n: the app is English-only (no locale catalog, no parity test), so there is nothing to route labels through. The provider mark is an inline asset following the house style for provider marks.

Testing

pnpm install --frozen-lockfile, then:

  • npx vitest run — 92 files, 1409 tests, all passing on this head (live tests skip without a key).
  • OrcaRouter-specific: npx vitest run packages/core/src/orcarouter apps/web/lib/orcarouter — 10 files, 194 tests, all passing.
  • npx vitest run apps/web/lib/orcarouter/live.test.ts — 4 tests, all passing with a real ORCAROUTER_API_KEY; it lists the live catalog through the project's own discovery path and completes a real chat completion through the project's own transport (not a standalone curl).
  • node scripts/orca-verify.mjs check-types|lint|version — clean, and covered by scripts/repo-gates.test.mjs so the same CI gates run inside the test suite.
  • The UI evidence run itself asserts the rendered control against the live catalog response, so a screenshot can never disagree with the catalog it claims to show.

Both adapters are tested independently and converge: API-key save/read/clear/redaction; verifier/challenge/state generation, authorize URL, the exact exchange path and body, successful persistence, denial, Flow A state mismatch, code reuse and expiry, scope downgrade, corrupt-key terminal classification, exact-account 401 reconnect, 429, and network failure. Fixtures use fake keys and codes only, and the tests assert that no verifier or key appears in logs or errors. GUI lifecycle is covered too: success, denial, exchange error, timeout, explicit cancel, switching auth method, closing the panel, unmount, reload, and pagehide all release the login lock, with a monotonic attempt/generation guard so a stale response cannot overwrite a newer login; a test proves a second login can start after pagehide without a remount.

git grep audit on this head: no client secret, no fixed verifier, no real sk-orca- key outside fake test fixtures, and no implementation call to /v1/auth/keys.

UI

Real screenshots from the running app (apps/web dev server, Chromium via Playwright), produced by scripts/orca-ui-evidence.py (run through node scripts/orca-verify.mjs ui-evidence). That script is committed; the orca-evidence/ directory it writes is generated output and gitignored. manifest.json records automation playwright, passed: true, catalog source https://api.orcarouter.ai/v1/models?capability=chat, 16 chat models, 2 image-input chat models.

Both auth choices, side by side, secret masked

OrcaRouter auth methods

Text model dropdown, populated from the live chat catalog (16 items)

OrcaRouter text model dropdown

Multimodal dropdown after attaching an image — only the 2 chat models that declare image input remain, and the previously selected text-only model was cleared

OrcaRouter multimodal model dropdown

Provider evidence

  • Inference (OpenAI wire format): https://api.orcarouter.ai/v1 — POST /v1/chat/completions with Authorization: Bearer sk-orca-…; documented at https://docs.orcarouter.ai/getting-started/get-api-key and https://docs.orcarouter.ai/compatibility/anthropic-sdk.
  • Model list: GET https://api.orcarouter.ai/v1/models (requires the Bearer key) — https://docs.orcarouter.ai/getting-started/models.
  • Authorization and exchange: https://www.orcarouter.ai/auth and POST https://www.orcarouter.ai/api/v1/auth/keys — https://docs.orcarouter.ai/getting-started/sign-in-with-orcarouter. The discovery document https://www.orcarouter.ai/.well-known/openid-configuration advertises code_challenge_methods_supported: ["S256","plain"] and token_endpoint_auth_methods_supported: ["none"] (no client secret), and its token_endpoint is on the www origin, which confirms the auth/inference origin split used here.
  • Revocation and account management: https://www.orcarouter.ai/console/authorized-apps (revoking the app deletes every key issued to it).
  • Terms and legal entity: https://www.orcarouter.ai/terms.html and https://www.orcarouter.ai/privacy.html — the operating entity is CONTINUUM AI PTE. LTD.
  • Routing/resale authorization: OrcaRouter is an aggregator; the routing and billing model is documented at https://docs.orcarouter.ai/routing/routing-dsl and https://docs.orcarouter.ai/routing/model-fallbacks, and https://docs.orcarouter.ai/integrations/overview states the drop-in OpenAI-compatible endpoint used here.
  • Maintenance owner: OrcaRouter team (https://docs.orcarouter.ai, https://www.orcarouter.ai); this integration is maintained alongside the upstream repository.
  • Verification date: 2026-09-20. All endpoints above were re-fetched on that date; the catalog facts (16 chat models, 2 with image input, 0 embedding/image models in this workspace) come from that live response.

Notes for reviewers

  • Authentication changes touch a credential destination, so a maintainer security review of the exact head commit is appropriate. This repository documents no MAINTAINERS/CODEOWNERS file and no sponsorship label, and no readiness checklist is present in the repo. The maintainer with the most commits on main (Chris Tate <chris@ctate.dev>, who also owns the release process described in AGENTS.md) is the right reviewer for the credential boundary.
  • Optional follow-ups left out on purpose: the device grant (Flow C), and adopting the reusable provider in examples/*.

Affiliation

Made on behalf of OrcaRouter by an engineer on the OrcaRouter team, in coordination with the json-render maintainers. No unrelated behavior was changed.

…PKCE auth

Signed-off-by: martinzudergaming-a11y <martinzudergaming-a11y@users.noreply.github.com>
@vercel

vercel Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

@martinzudergaming-a11y is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@socket-security

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
Addednpm/​just-bash@​2.14.5881009994100
Addednpm/​@​ai-sdk/​openai-compatible@​2.0.758810010098100

View full report

@vercel-security-reviewer

Copy link
Copy Markdown

Security review details

};
}
try {
const catalog = await fetchOrcaCatalog({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

discoverOrcaModels() hardcodes capability: "chat" when fetching the upstream catalog, so the /api/orcarouter/models?capability=… route requests a chat-filtered catalog for every capability, making non-chat requests (embedding/image/video/rerank) return empty results if upstream honors the server-side ?capability= filter.

Fix on Vercel

This branch has not been deployed

No deployments
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