Repository navigation
feat: add OrcaRouter as a first-class provider (API key + OAuth 2.0 PKCE) - #266
hodeswildsmith455-boop wants to merge 1 commit into
Conversation
…KCE) Signed-off-by: hodeswildsmith455-boop <hodeswildsmith455-boop@users.noreply.github.com>
caigee-cmd
left a comment
There was a problem hiding this comment.
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:
- Rebase onto
main(36 commits / 60 files landed since your baseb92caa2). I resolved all 13 conflicts and verified the result locally; the exact resolutions are below. - 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 lintThis 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&& !isZhipuguard and main'sButton+<code>+ copy-button auth-URL block; yourrunZhipugoes in next torunPat
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,
statecompared withsubtle.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 ownhttp.ResponseWriter. - No
http.ResponseWriteror executor taxonomy ininternal/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/Modalreuse, no purple chrome, no emoji in copy. Matchesdocs/DESIGN.md. - Console
/api/*and worker/admin/*remain authenticated. No new unauthenticated surface. providerPrefixesordering keeps the longest-prefix-wins property intact (orcarouter-oauth/beforeorcarouter/).- 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.tsxandOptionTiles.tsxwere moved intofrontend/src/components/account/andfrontend/src/components/ui/onmainwhile 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-evidencetarget andscripts/orca_evidence/are fine to keep, but note the screenshots are generated artifacts; make sure.gitignorecovers 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 theRegionsslice 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.
Summary
Adds OrcaRouter as a first-class provider in
internal/providers/orcarouter(registered ininternal/providers/registry.go) together with a real console surface, so an operator can either paste an existingsk-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.https://api.orcarouter.ai/v1(OpenAI-compatible)https://www.orcarouter.ai— consent at/auth, code exchange at/api/v1/auth/keyshttps://api.orcarouter.ai/v1/auth/keysis a 404 and is asserted against in tests).Two authentication entries, one credential seam
orcaroutersk-orca-…keyorcarouter-oauthBoth 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 (CompleteLoginaccepts a pasted callback URL or a bare code) for hosts that cannot receive a redirect.stateare generated fresh per attempt fromcrypto/rand; only the unpaddedbase64url(sha256(verifier))challenge travels on the authorize URL, withcode_challenge_method=S256.stateechoed on the redirect is compared in constant time before the code is used.statemismatch, expiry/reuse,403,400and429all 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.scopeis read back and recorded as-is; a narrower grant than requested is surfaced rather than assumed away.401marks 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.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:
?capability=chat, requiring a speakable endpoint type and excluding image-generation/video/rerank-only models;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 okmake vet— cleanmake test-architecture— okmake changelog— bilingual fragment validatesnpm --prefix frontend run build— oknpm --prefix frontend run lint— 0 errors (18 pre-existing warnings)make test-live—TestLiveCatalogThroughProviderandTestLiveChatThroughProviderPASS against the real gatewaymake orca-evidence— Playwright captures produced,passed: trueFocused counts:
internal/providers/orcarouter41/41 pass (0 skip with a key present), plusTestFilterModelsByCapability/TestProviderModelEntry*ininternal/control12/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
b92caa2in 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 regenerateorca-evidence/from the tree under test (gitignored):auth-methods.pngshows the API-key field and the Connect-with-OrcaRouter entry side by side with the pasted secret masked, andtext-model-dropdown.png/multimodal-model-dropdown.pngshow 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,
statemismatch, expired/reused code, scope downgrade,403/400/429and 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 athttps://www.orcarouter.ai/authwith the exchange documented atPOST https://www.orcarouter.ai/api/v1/auth/keys; discovery athttps://www.orcarouter.ai/.well-known/openid-configuration; credential revocation athttps://www.orcarouter.ai/console/authorized-apps; terms of service and the operating legal entity published athttps://www.orcarouter.ai. Maintenance owner for this provider: the OrcaRouter contributor who opened this PR.