Skip to content

feat: add OrcaRouter as a first-class provider with API-key and PKCE login - #1866

Open
beilsteinernest215-cmd wants to merge 1 commit into
anthropics:mainfrom
beilsteinernest215-cmd:orcarouter/task-7704
Open

beilsteinernest215-cmd wants to merge 1 commit into
anthropics:mainfrom
beilsteinernest215-cmd:orcarouter/task-7704

Conversation

@beilsteinernest215-cmd

Copy link
Copy Markdown

What this adds

This adds OrcaRouter as a first-class, named model provider for the action, with two independent ways to sign in. OrcaRouter is an OpenAI-compatible AI gateway that routes many providers behind one endpoint. I'm an engineer on the OrcaRouter team.

Neither entry point is a fallback for the other — a user who already holds a key pastes it, and a user who does not runs the connect command. Both land on the same credential the rest of the code already understands.

Entry point Input How it is obtained
OrcaRouter — API orcarouter_api_key (sk-orca-…) Pasted by the user, or read from ORCAROUTER_API_KEY
OrcaRouter — Auth orcarouter_auth: true bun base-action/src/orcarouter/setup-cli.ts --connect, OAuth 2.0 + PKCE (S256)

OrcaRouter is only used when it is explicitly asked for (orcarouter_provider, orcarouter_auth or orcarouter_api_key). A bare ORCAROUTER_API_KEY in the ambient environment is deliberately not enough — this action is often run with credentials that belong to a different tool, and silently re-routing an unrelated run to a new gateway would be a bad trade for convenience. base-action/test/orcarouter/provider.test.ts pins that behaviour.

The credential is a seam, not a fork

base-action/src/orcarouter/credentials.ts exposes one OrcaRouterCredentialSource interface. apiKeyCredentialSource() and pkceCredentialSource() are the two adapters; everything downstream — provider wiring in provider.ts, model discovery in catalog.ts, and terminal 401 recovery in connect.ts — consumes a resolved credential and never learns which adapter produced it. provider.test.ts asserts both adapters return the same result shape and reach the same inference endpoint with the same wire.

PKCE follows Flow B (out-of-band code) rather than Flow A. This is a GitHub Action: its natural runtime is a headless runner with no browser and no loopback listener, so an oob callback is the only shape that works everywhere the action runs. createPkceAttempt() mints a fresh 32-byte verifier and a 16-byte state from crypto on every attempt, sends base64url(sha256(verifier)) as an unpadded challenge with a mandatory S256 method, and keeps the verifier in-process until the exchange. When a callback echoes state it is compared in constant time; a mismatch aborts before the code is redeemed. Denial, state mismatch, an expired or reused code, a 400 method downgrade, a 403 scope shortfall, 429 and network errors each end the attempt with an actionable message rather than a hang or a retry loop.

Two origins, never derived from each other

Authentication and inference are separate public origins and are configured separately:

  • consent: https://www.orcarouter.ai/auth
  • exchange: https://www.orcarouter.ai/api/v1/auth/keys
  • inference and catalog: https://api.orcarouter.ai/v1

ORCA_AUTH_BASE_URL and ORCA_API_BASE_URL override a shared ORCA_BASE_URL; explicit values win. Non-loopback origins are forced to HTTPS. No code path derives one origin from the other by swapping a hostname or appending /v1 — endpoints.test.ts has a test named for exactly that mistake. Verified on 2026-09-28: POST https://www.orcarouter.ai/api/v1/auth/keys answers 403 {"error":"Invalid code or code_verifier"} for a fake code, while POST https://api.orcarouter.ai/v1/auth/keys answers 301 rather than serving the endpoint.

Model selection comes from the catalog, not from a text box

When the provider is OrcaRouter the model control is populated from GET /v1/models on the configured origin, using the user's own key as the Bearer token. The live response is authoritative; the small hand-verified seed in catalog.ts is used only when discovery fails, is marked degraded, and is never merged into a live result. Model ids keep their vendor/model namespace verbatim.

Each entry point filters the same catalog by its own requirement, fail-closed:

  • text chat/agent — ?capability=chat, must advertise one of openai / anthropic / gemini / openai-response, and is excluded if it is a dedicated image-generation / openai-video / jina-rerank model;
  • multimodal — chat first, then architecture.input_modalities must name the modality the entry point actually accepts; an undeclared modality fails closed;
  • embedding, image generation, video, rerank — strict endpoint-type match.

Capability is never guessed from a model's name. When the provider changes, or an attachment or task type changes, the options handed to the selector are recomputed and a selection that is no longer compatible is cleared with a prompt instead of being silently kept. The verified seed keeps its metadata, including the reasoning ladder on openai/gpt-5.5 (low/medium/high/xhigh, default medium).

Coverage

This action has one AI entry point: the agent run itself, configured through action.yml inputs and written into settings.env by setup-claude-code-settings.ts. That is the surface wired here, for both credential adapters. validate-env.ts learns about the provider and reports the missing-credential case clearly, sanitizer.ts redacts sk-orca-… tokens from logs, and setup-cli.ts adds --connect / --logout / --status so both entry points are reachable from a terminal.

Testing

Run against a clean checkout of this branch with a fresh install:

  • full suite bun test: 1078 pass, 4 skip, 0 fail across 64 files;
  • base-action/test/orcarouter/live.test.ts, with a key in the environment: 4 pass, 0 fail — the 4 skips above are these tests skipping when no key is present in the ambient environment;
  • bun run format:check — clean; bun run typecheck — clean.

The live run is the one worth quoting: it discovers 16 text chat models from GET https://api.orcarouter.ai/v1/models, narrows them to 2 once an image input is required, and completes a real inference call through the provider's own environment against https://api.orcarouter.ai/v1/messages on deepseek/deepseek-v4-pro. The catalog and inference assertions go through the code added here — they are not a standalone curl. No test, log line or error message contains a credential; connect.test.ts and provider.test.ts assert that for the PKCE and API-key paths respectively.

Why there are no screenshots

This repository has no rendered interface to screenshot: no react-dom / vue / svelte / next / electron dependency in any package.json, no tracked .vue or .svelte file, and the only HTML in the tree is documentation under docs/. The integration is therefore demonstrated through the CLI surface and the test suite above.

Provider evidence

All URLs below were fetched and confirmed reachable on 2026-09-28.

  • Inference and model catalog — https://api.orcarouter.ai/v1/models (openai-compatible, Bearer auth; returns the workspace's callable models).
  • Authorization — https://www.orcarouter.ai/auth (consent screen); exchange — https://www.orcarouter.ai/api/v1/auth/keys.
  • Discovery metadata — https://www.orcarouter.ai/.well-known/openid-configuration advertises authorization_endpoint, token_endpoint, code_challenge_methods_supported: [S256, plain] and token_endpoint_auth_methods_supported: [none], i.e. PKCE without a client secret. This implementation pins S256; it never sends plain.
  • Key revocation — https://www.orcarouter.ai/console/authorized-apps.
  • Pricing and routing model (many upstream providers behind one endpoint) — https://www.orcarouter.ai/pricing and https://www.orcarouter.ai/providers/anthropic.
  • Responsible party for verification — the OrcaRouter team; the author of this pull request is an engineer on that team.

Not implemented

Flow A (loopback redirect) and Flow C (device grant) are both valid flows that this change does not add. Flow B covers the action's headless runtime; device grant would be a useful addition for interactive terminals and is left as a follow-up rather than folded in untested here.

…login

Signed-off-by: beilsteinernest215-cmd <beilsteinernest215-cmd@users.noreply.github.com>

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