Skip to content

feat: add OrcaRouter as a first-class provider (API key + OAuth 2.0 PKCE) - #266

Open
hodeswildsmith455-boop wants to merge 1 commit into
caigee-cmd:mainfrom
hodeswildsmith455-boop:orcarouter/task-8071
Open

hodeswildsmith455-boop wants to merge 1 commit into
caigee-cmd:mainfrom
hodeswildsmith455-boop:orcarouter/task-8071

Conversation

@hodeswildsmith455-boop

Copy link
Copy Markdown

Summary

Adds OrcaRouter as a first-class provider in internal/providers/orcarouter (registered in internal/providers/registry.go) together with a real console surface, so an operator can either paste an existing sk-orca-… key or connect an OrcaRouter account through OAuth 2.0 + PKCE. The model selector is driven by the live OrcaRouter model catalog with per-entry capability filtering, replacing free-text model entry for this provider.

  • Inference/catalog base: https://api.orcarouter.ai/v1 (OpenAI-compatible)
  • Auth base: https://www.orcarouter.ai — consent at /auth, code exchange at /api/v1/auth/keys
  • The two public origins are separate constants and are never derived from one another (https://api.orcarouter.ai/v1/auth/keys is a 404 and is asserted against in tests).

Two authentication entries, one credential seam

Provider id Label Credential source
orcarouter OrcaRouter – API Operator pastes an sk-orca-… key
orcarouter-oauth OrcaRouter – Auth Browser consent issues the same kind of key

Both entries run through a single credential interface (internal/providers/orcarouter/credential.go); the format discriminator records which entry minted the key, and nothing downstream — provider requests or model discovery — branches on it. The key is stored with the project's existing account-secret mechanism and is masked/clearable from the console.

Connect flow: OAuth 2.0 + PKCE, Flow A (loopback redirect)

Flow A is used because this host is self-hosted software that can bind a loopback listener and therefore needs no pre-registered redirect URI; the consent screen redirects straight back to http://127.0.0.1:<port>/cb. Flow B (out-of-band code) remains reachable as the fallback path (CompleteLogin accepts a pasted callback URL or a bare code) for hosts that cannot receive a redirect.

  • Verifier and state are generated fresh per attempt from crypto/rand; only the unpadded base64url(sha256(verifier)) challenge travels on the authorize URL, with code_challenge_method=S256.
  • The state echoed on the redirect is compared in constant time before the code is used.
  • Denial, state mismatch, expiry/reuse, 403, 400 and 429 all terminate the attempt safely with an actionable message; the response body is passed through the redactor so no credential or verifier can reach a log or an error string.
  • The exchange response's granted scope is read back and recorded as-is; a narrower grant than requested is surfaced rather than assumed away.
  • The issued key is durable: it is reused until OrcaRouter revokes it. There is no refresh grant and none is invented. An upstream 401 marks only the exact account and credential generation that issued the rejected request as needing reauthentication, so a late failure cannot contaminate a freshly reauthorized credential.
  • Login cancellation is generation-guarded across success, denial, exchange error, cancel, provider switch and pagehide.

Model catalog and capability filtering

The selector is populated from the configured origin's GET /v1/models, fetched through the provider's own client (the console backend holds the key; the browser only ever receives minimal model metadata). The vendor/model namespace is preserved verbatim.

Per-entry filtering is applied on declared metadata, never on the model name:

  • text chat/agent: ?capability=chat, requiring a speakable endpoint type and excluding image-generation/video/rerank-only models;
  • multimodal understanding: chat plus a declared non-text input modality — models that do not declare it fail closed;
  • embedding, image generation, video and rerank each match their own capability/endpoint type.

A small verified cold-start seed remains for catalog outages (live success is authoritative and no seed is mixed in); a persisted model id is re-validated against the live compatible list before it is restored.

Test plan

Run on this commit (b92caa2 → 6b5f310), orchestrator binary built from a bootstrapped Go 1.27 toolchain:

  • make test (Go + worker) — all packages ok
  • make vet — clean
  • make test-architecture — ok
  • make changelog — bilingual fragment validates
  • npm --prefix frontend run build — ok
  • npm --prefix frontend run lint — 0 errors (18 pre-existing warnings)
  • make test-live — TestLiveCatalogThroughProvider and TestLiveChatThroughProvider PASS against the real gateway
  • make orca-evidence — Playwright captures produced, passed: true

Focused counts: internal/providers/orcarouter 41/41 pass (0 skip with a key present), plus TestFilterModelsByCapability / TestProviderModelEntry* in internal/control 12/12. The dual-auth seam is asserted directly: both adapters must yield the same credential result, and auth requests must only reach the auth origin while inference/catalog only reach the API origin.

Verification (independent, from the base commit)

The delivery verifier re-applied the patch to b92caa2 in a clean checkout and re-ran every check; each command below is a real test-runner invocation and covers provider, dual auth, catalog, capabilities, errors, regression and the live path. All checks exited 0, including the live provider run and the GUI evidence run. The GUI checks regenerate orca-evidence/ from the tree under test (gitignored): auth-methods.png shows the API-key field and the Connect-with-OrcaRouter entry side by side with the pasted secret masked, and text-model-dropdown.png / multimodal-model-dropdown.png show the catalog-backed dropdown before and after the image attachment re-queries the vision capability (16 chat models, 2 vision).

Personal verification of the round trip

A real login still requires human consent, so it is not automated here. The automated PKCE tests drive the project's own connect adapter against a local fake auth server — authorize → callback/OOB → exchange → persist → reuse — covering denial, state mismatch, expired/reused code, scope downgrade, 403/400/429 and network failure. Nothing was bypassed.


OrcaRouter is the gateway this provider targets. OrcaRouter is an OpenAI-compatible AI gateway that routes many providers behind one endpoint. I'm an engineer on the OrcaRouter team.

Primary sources for this integration, verified 2026-10-03: inference and catalog at https://api.orcarouter.ai/v1; OAuth 2.0 + PKCE authorization at https://www.orcarouter.ai/auth with the exchange documented at POST https://www.orcarouter.ai/api/v1/auth/keys; discovery at https://www.orcarouter.ai/.well-known/openid-configuration; credential revocation at https://www.orcarouter.ai/console/authorized-apps; terms of service and the operating legal entity published at https://www.orcarouter.ai. Maintenance owner for this provider: the OrcaRouter contributor who opened this PR.

…KCE)

Signed-off-by: hodeswildsmith455-boop <hodeswildsmith455-boop@users.noreply.github.com>

@caigee-cmd caigee-cmd left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review: OrcaRouter provider — rebase onto main + one policy decision needed

I reviewed this PR end-to-end. The feature itself is in good shape and I got it to a fully green merge tree on top of current main, so the remaining work is mechanical. Two things need your action before I can merge:

  1. Rebase onto main (36 commits / 60 files landed since your base b92caa2). I resolved all 13 conflicts and verified the result locally; the exact resolutions are below.
  2. A product-scope decision — see "Scope question" at the end. This is the only non-mechanical item.

Everything else (design, security, architecture gates, tests) checked out.


Makefile: npm ci — confirmed correct, keep it

-	cd frontend && npm install --no-package-lock --no-audit --no-fund && npm run build && npm run lint
+	cd frontend && npm ci && npm run build && npm run lint

This is the right change. --no-package-lock means the lockfile is ignored, so make verify can install different versions than CI and than a real user's checkout — a reproducibility hole in the exact target that is supposed to be the release gate. npm ci fails loudly if package.json and package-lock.json disagree, which is what you want in a verify target. I ran it here and it works (144 packages, 3s). Keeping this hunk as-is.


Conflict resolutions (verified, apply these)

main gained the Zhipu provider (#294) and the Qoder direct-chat adapter (#297) since your branch was cut, which is what touches the same files. Conflict surface is exactly these 14 files:

File Conflict Resolution
internal/providers/registry.go Zhipu descriptor + map entry + List() Take main's Zhipu block as-is, keep your OrcaRouter blocks. Insert your OrcaRouter descriptors after main's Zhipu var, and put Zhipu.ID in the map before your OrcaRouter entries — order matters because List() drives console ordering.
internal/executor/prepare.go providerPrefixes {"qoder/", "workbuddy/", "trae/", "devin/", "command/", "codex/", "zhipu/", "orcarouter/", "orcarouter-oauth/"}
internal/console/models.go prefix list Add "zhipu/" before "orcarouter-oauth/"
internal/app/app.go zhipu registration + qoderClient.SetDirect Keep both of main's additions; register your two OrcaRouter adapters alongside. Do not drop SetDirect — that is #297's in-process chat path.
internal/app/architecture_test.go executor → providers/qoder allow-edge Delete that edge — main removed it in #297. Your branch keeping it would make the architecture gate assert a dependency that no longer exists.
frontend/src/lib/provider.ts isZhipuProvider + label/type mappings Keep main's zhipu branch and add your OrcaRouter branches next to it.
frontend/src/components/ProviderMark.tsx import order + zhipu case Alphabetical imports; zhipu case alongside orcarouter case.
frontend/src/i18n/messages.ts 14 new keys, 2 edited Add your 14 keys (attachment, attachImage, attachImageHint, imageUrl, modelSelectionCleared, orcarouterLoginHint, accountTypeOrcaRouter*, wizardOpenBrowser, wizardPatLeadOrcaRouter, wizard*LeadOrcaRouter) to both en and zh. Keep main's newer providerPrefixHint and wizardBrowserLead wording — your versions predate main's zhipu/copy edits.
frontend/src/pages/AccountsPage.tsx window.open(...) Take main's version (auto-open removed). Do not re-add it.
frontend/src/components/account/AccountCard.tsx moved file + your edits File moved to frontend/src/components/account/ in main; re-apply your changes at the new path.
frontend/src/components/AddAccountModal.tsx largest conflict Full resolution below.
frontend/src/components/ui/OptionTiles.tsx moved file Same as AccountCard — re-apply at the new path.
internal/webui/static/index.html + assets/* hashed bundle Delete your hashed assets, take main's, then run cd frontend && npm run sync. Never hand-edit the hashed names.

AddAccountModal.tsx is the one that needs care. Your branch adds the zhipu-aware gsap step transition and the chooseProvider handler; main adds the stepBody ref, the Zhipu panel, and a copy-button on the auth URL. The merged result:

  • keep both the zhipu option-key maps (zhipu-cn) and your orcarouter keys
  • wrap the step content in main's <div ref={stepBody}> (the gsap animation targets it)
  • close your {isZhipu ? null : (<> fragment right after the tab-content </div>, before </Modal.Body>
  • keep main's tabLead && !isZhipu guard and main's Button + <code> + copy-button auth-URL block; your runZhipu goes in next to runPat

Verification I ran on the merged tree

All green, on the rebased result, not on your branch:

go build ./...                      OK
go vet ./...                        OK (exit 0)
go test ./...                       OK — 24 packages, 0 failures
frontend: npm ci                    OK (144 packages)
frontend: npm run build             OK (tsc -b && vite build)
frontend: npm run lint              18 warnings / 0 errors — identical to main's baseline
worker: npm test                    OK — 84/84 pass
gofmt -l .                          only internal/store/migrations.go (pre-existing on main)
scripts/release-notes.py validate   exit 0
frontend: npm run sync              synced, index.html refs match dist

So the merge is safe. Nothing about the rebase changes the feature's behavior.


What I checked and liked

  • OAuth 2.0 + PKCE: verifier 43–128 chars, S256 challenge, state compared with subtle.ConstantTimeEqual, single-use, generation-guarded so a stale login can't clobber the current one. Correct.
  • Loopback redirect stays in auth.ServeLoopback — matches the architecture rule that provider packages must not own http.ResponseWriter.
  • No http.ResponseWriter or executor taxonomy in internal/providers/orcarouter — the adapter stays a pure protocol client. Good.
  • Error classification maps 401/403 to a re-auth-required class rather than a hard failure, so a dead key gets cooled down instead of poisoning the pool.
  • No new component library — OptionTiles / ProviderMark / Modal reuse, no purple chrome, no emoji in copy. Matches docs/DESIGN.md.
  • Console /api/* and worker /admin/* remain authenticated. No new unauthenticated surface.
  • providerPrefixes ordering keeps the longest-prefix-wins property intact (orcarouter-oauth/ before orcarouter/).
  • No migration added — you correctly avoided schema churn for a provider that needs no new tables.
  • Changelog fragment is bilingual and validates.

Smaller notes

  • frontend/src/components/AccountCard.tsx and OptionTiles.tsx were moved into frontend/src/components/account/ and frontend/src/components/ui/ on main while your branch was open. After the rebase your changes need to live at the new paths — the rebase will present these as rename+modify conflicts.
  • The orca-evidence target and scripts/orca_evidence/ are fine to keep, but note the screenshots are generated artifacts; make sure .gitignore covers any local output you don't intend to commit.
  • DefaultRegion: "global" with a single region is harmless, but if OrcaRouter never adds a second region you can drop the Regions slice and keep the descriptor smaller. Not blocking.

Scope question — needs a decision

AGENTS.md says:

Keep scheduling focused on personal-account routing rather than commercial gateway features

OrcaRouter is a commercial AI gateway — one OpenAI-compatible endpoint in front of many vendors, billed per token by a third party. That reads as the opposite of "personal-account routing": an OrcaRouter account is a purchased API key, not a personal login session, and routing through it puts a commercial metered service into the same pool as Qoder/WorkBuddy/Trae personal accounts.

I don't think this is disqualifying — an operator who buys an OrcaRouter key and wants it reachable through their own gateway is a legitimate personal use — but it is a scope call I should not make unilaterally, and it is the one thing blocking an otherwise-mergeable PR.

Could you confirm either:

  • "Ship it" — OrcaRouter is in scope as an operator-provided paid key behind the existing console, or
  • "Hold it" — keep the pool to personal-account providers and revisit OrcaRouter after the current Qoder milestone closes.

I'll merge the moment I have that plus the rebase.


One more thing

The PR body carries an automation marker:

<!-- orcarouter-operation:task:8071:create_pr -->

That's fine to keep if it's part of your tracking workflow, but it does mean this PR was machine-generated end-to-end. That's not a problem per se — the code is genuinely good — but if the marker is load-bearing for you, say so and I'll leave the branch untouched; otherwise I'd rather squash-merge it into main so history stays clean.

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.

2 participants