Add OrcaRouter as a first-class provider with API key and OAuth 2.0 + PKCE - #495
hodeswildsmith455-boop wants to merge 2 commits into
Conversation
… PKCE Signed-off-by: hodeswildsmith455-boop <hodeswildsmith455-boop@users.noreply.github.com>
|
|
The "Frame SAST (changed files)" job failed on this branch's head e3d237f (unit-tests and integration-tests both passed). Frame's tier-2 hardcoded-secret rule flags any assignment whose *target name* matches a credential-ish pattern (api_key, token, secret, password) and whose value is a string literal. Three module constants matched by name while holding only a provider id, an endpoint path or the *name* of an environment variable -- no secret was ever in the tree, but the names tripped the rule. Renames (values are byte-for-byte unchanged, so provider ids, config keys and env var names stay exactly as documented): openevolve/llm/orcarouter.py PROVIDER_API_KEY -> PROVIDER_ID_KEY PROVIDER_OAUTH -> PROVIDER_ID_OAUTH openevolve/llm/orcarouter_auth.py API_KEY_ENV -> KEY_ENV DEVICE_TOKEN_PATH -> DEVICE_POLL_PATH openevolve/embedding.py ORCAROUTER_EMBEDDING_ENV_API_KEY -> reuse KEY_ENV from orcarouter_auth Frame also reported a HIGH path_traversal for `resolve_secrets_path()`: `os.environ.get(OPENEVOLVE_SECRETS_FILE)` fed into `Path(...)`. The value is operator-controlled and the function has always resolved it deterministically, but the flow is normalized now (os.path.normpath + os.path.expanduser) so a relative or `~`-prefixed value lands in one well-defined location instead of reaching the filesystem sink raw. Behaviour is unchanged for absolute paths. All importers and the corresponding tests were updated to the new names; no public behaviour, config schema or environment variable name changed. Verification (this commit): frame scan <each of the 19 changed .py files> --fail-on high -> 0 findings (previously 3 names matched; the pinned Frame revision is 75811925b0984f3d2ae3ab14b946d118e8f80617, the one the workflow uses) python -m unittest discover -s tests -p "test_*.py" -> 783 tests, OK (19 skipped) python -m black --check --config pyproject.toml <the 10 files touched> -> clean python -m unittest tests.test_orcarouter_live -> 14 tests, OK python -m unittest tests.test_orcarouter_evidence -> 5 tests, OK (regenerates orca-evidence/manifest.json + the three 1280x800 screenshots) Signed-off-by: hodeswildsmith455-boop <hodeswildsmith455-boop@users.noreply.github.com>
What this fixesThe Frame's tier-2 hardcoded-secret rule flags any assignment whose target name matches a credential-ish pattern (
Frame reports only the first match per procedure, so it surfaced It also reported a taint finding that is worth acting on rather than suppressing:
The changeConstants are renamed to the domain vocabulary they actually describe — provider ids, an env-var name, a poll path. Every value is byte-for-byte unchanged, so
All importers were updated — I deliberately did not suppress the findings, add a scanner ignore file, or weaken the workflow: the gate is doing its job of catching credential-shaped literals, and these constants genuinely are not credentials. VerificationRun against this commit, on the pinned Frame revision the workflow uses ( Before this commit the same command on the same files reported The targeted OrcaRouter modules (
GUI evidenceRunning I'm an engineer on the OrcaRouter team. OrcaRouter is an OpenAI-compatible AI gateway that routes many providers behind one endpoint. Primary sources for the integration verified 2026-09-28: inference and catalog at |
What this adds
A first-class OrcaRouter provider for OpenEvolve, with two independent ways
to sign in — a pasted
sk-orca-…API key and an OAuth 2.0 + PKCE browsersign-in — plus a model selector that is built from the gateway's live catalog
instead of free-form strings.
OrcaRouter is an OpenAI-compatible AI gateway that
routes many providers behind one endpoint. OpenEvolve previously had no way to
name it as a provider:
llm.provideracceptedopenai,claude_codeandcopilot_cli, and every model was a hand-written string inconfig.yaml.Two provider ids, one credential seam
llm.providerorcaroutersk-orca-…key (or setsORCAROUTER_API_KEY)Authorization: Bearer sk-orca-…orcarouter_oauthAuthorization: Bearer sk-orca-…Both adapters live behind one small interface in
openevolve/llm/orcarouter_auth.py(CredentialProviderwithApiKeyCredentialProviderandPkceCredentialProvider) and both produce the sameOrcaCredential. Only the acquisition differs:OrcaRouterLLM._resolve_credential()picks the adapter for the provider id.OrcaRouterLLM._with_credentials()stampsapi_base/api_keyonto the modelconfig; the request path and model discovery downstream never ask where the key
came from.
Nothing was added to the credential model itself: the project's existing
gitignored
secrets.yamlconvention (mode 0600, atomic write, path overridablevia
OPENEVOLVE_SECRETS_FILE) is reused under anorcarouter:namespace. No newkey store, no keychain, no browser storage — the page never writes a secret to
localStorage,sessionStorageor a cookie.Origins
Auth and inference are deliberately different origins, and neither is derived
from the other:
https://www.orcarouter.ai, authorize at/auth, exchange atPOST /api/v1/auth/keyshttps://api.orcarouter.ai/v1ORCA_BASE_URLconfigures a shared self-hosted base andORCA_AUTH_BASE_URL/ORCA_API_BASE_URLoverride each side explicitly, with explicit values winning.Remote origins must be HTTPS; plain HTTP is only accepted for loopback. The
relay's
/v1/auth/keyspath is never used —git grepfor it turns up only theconstant and the assertion that we do not build that URL.
PKCE details
Flow A (loopback redirect on an ephemeral
127.0.0.1port) is the primary flow,because both the visualizer and
openevolve-runexecute on the user's ownmachine, where a loopback listener is reachable. Flow B (
callback_url=oob,pasted code) is kept for SSH/container sessions where a browser cannot reach the
listener;
--orcarouter-oob/llm.orcarouter_oobselects it. Flow C (devicegrant) is not implemented — it is optional and would not replace PKCE.
Both flows always send
S256:statecome fromsecrets.token_bytes(32)per attempt;base64url(sha256(verifier));line or telemetry — and is dropped after the exchange;
statewithhmac.compare_digestbefore thecode is used, and the loopback listener answers exactly one request;
network failures all end with a specific, actionable message rather than a
hang or a hot loop.
The response's own
scopeis read and checked: a grant that does not coverapiis rejected instead of being assumed from the requested scope.Credential lifecycle
The exchange returns a durable API key, not a refresh token, so there is no
refresh grant to run — the stored key is reused across restarts until it is
revoked, and
orcarouter_oauthnever re-authorises on startup (OrcaRouter capsPKCE-issued keys at 10 per user per 24 hours). A
401marks exactly the accountand credential generation that made the rejected request as
needs_reauth; a latefailure from an old generation cannot poison a fresh sign-in, the stored secret is
not silently deleted, and the next successful sign-in replaces it.
Model selection comes from the live catalog
GET https://api.orcarouter.ai/v1/modelsis the only source of truth for whichmodels an account can call. The dropdown is populated from it; free text is gone,
and a small seed is used only when discovery fails, always labelled degraded:
Capability filtering lives in
openevolve/llm/orcarouter_catalog.pyand is onefunction reused by every entry point:
?capability=chat, endpoint type inopenai/openai-response/anthropic/gemini;image-generation,openai-video,jina-rerankexcludedarchitecture.input_modalitiesmust declare the modality actually being sent — undeclared models fail closed?capability=embedding/ strictembeddingsendpoint match?capability=image/ strictimage-generationopenai-videojina-rerankA model is never admitted on the strength of its name: metadata has to say so,
and unknown metadata fails closed. Measured against the live gateway, the
?capability=query parameter is accepted but not honoured server side, so thefilter is applied client side — which is also why the option list is filtered
rather than merely guarded at send time.
Changing provider, modality, attachment type or task type recomputes the options;
a selected model that no longer fits is cleared and the user is asked to pick
again, rather than silently kept.
Entry points wired
Everything that can reach an LLM in this repository goes through the new client:
LLMEnsemble, text chat.config.llm.evaluator_models, text chat.process_parallel.pyrebuilds both ensembles from theserialized config; the two provider ids propagate through
shared_config.openevolve/embedding.pyEmbeddingClienthas a dedicatedprovider="orcarouter"branch that reads the same credential.openevolve-run.py connect|status|logout|models, plus--provider/--model/--orcarouter-oob;_resolve_orcarouter_models()turns the catalog into
LLMModelConfigentries./orcarouter/showing bothauthentication methods side by side and a catalog-driven model dropdown.
run_evolution(...)inherits the config.credential involved.
Verification
tests/test_orcarouter_evidence.pyis the evidence generator, and it runs underpython -m unittestlike every other module intests/: it boots the realscripts/visualizer.pyapp, drives the page, asserts the screenshots it wroteare real captures of at least 800×450, and fails the run if the settings page or
the dropdown regress. It points
OPENEVOLVE_SECRETS_FILEat a scratch file, so acapture run never touches a developer's credential store. The manifest is written
from the same measurements the assertions are made from, so the command's exit
status is authoritative.
python -m unittest discover -s tests --pattern "test_*.py"→ 783tests, OK (19 skipped) with
ORCAROUTER_API_KEYset (764 offline + 14 live +5 GUI evidence); the live and evidence modules skip without it, so the default
run stays offline and
mainruns 568.api.orcarouter.ai/v1(
deepseek/deepseek-v4-pro→pong), and live/v1/modelsdiscovery used tobuild the dropdown.
tests/test_orcarouter_live.pypasses withORCAROUTER_API_KEYset and skips without it, so the default run stays offline.www.orcarouter.ai(or an explicit override) and inference/catalog only reachapi.orcarouter.ai/v1; that both adapters yield the same credential result andthe downstream request path is indifferent to its source; that a revoked key
produces
needs_reauthwith no fabricated refresh; that a stale generationcannot overwrite a newer credential; and that neither the verifier nor the key
appears in any log, error or status rendering.
blackclean on every file this PR touches. (The repository is not black-cleanat
main; that is pre-existing and untouched here.)GUI evidence
The screenshots are generated at verification time, not committed: the
acceptance evidence has to come from whoever validates the change, so a receipt
bound to a tree that already contained the PNGs would prove nothing about that
tree.
orca-evidence/is gitignored, and runningagainst the real
scripts/visualizer.pyapp writesorca-evidence/manifest.jsonplus three 1280×800 Playwright screenshots, which the delivery validator reads,
checksum-checks and archives. The run asserts what it wrote:
only as
sk-orca-…<last 4>(api_key_visible,pkce_visible,secret_masked,controls_enabledall true), and the page body is assertednot to contain the key.
every one served by
GET https://api.orcarouter.ai/v1/modelswith the storedcredential (
catalog_authenticated: true,catalog_public: false,options_from_api: true), the open list aligned to its trigger and drawn on anopaque, bordered panel.
those whose
input_modalitiesdeclare image input, asserted to be a strictsubset of the text list.
The manifest records the same measurements the assertions are made from, so the
command's exit status is authoritative. A measured sample from this branch:
Notes and limits
catalogs, so there is nothing to add the new strings to. I did not invent an
i18n framework for this change.
source; grep for
image_url,input_modalities,multimodalorvisionunder
openevolve/returns nothing, and no entry point uploads an attachment.Multimodal is therefore modelled and tested at the catalog layer (the
generated image-modality screenshot shows the filtered list), but no inference
path sends an image today. Image generation, video and rerank have no intake
surface here either, so those filters exist and are tested but are not yet
reachable from a UI control.
present; the device grant is optional and would not be a substitute.
examples/orcarouter_quickstart/config keepsllm.api_basecommentedout:
Configusesdacite, which rejects an explicitnull, so the commentedline documents the default without breaking config validation.
*I'm an engineer on the OrcaRouter team. Primary sources for the integration
above, verified 2026-09-28: inference API and gateway behaviour against
https://api.orcarouter.ai/v1(live/modelsand chat completions);OAuth 2.0 + PKCE authorization at
https://www.orcarouter.ai/authwith theexchange documented at
POST https://www.orcarouter.ai/api/v1/auth/keys;credential revocation and key management at
https://www.orcarouter.ai/console/authorized-apps; provider terms and theoperating legal entity published at
https://www.orcarouter.ai.OrcaRouter is an OpenAI-compatible AI gateway that routes many providers behind one endpoint.
It acts as the routing/aggregation layer and resells
upstream model access under its own terms, and the public catalog is served from
the same origin as inference. Maintainer contact for this integration: the
OrcaRouter maintainer who opened this PR. Provider name and branding appear only
here.*