Skip to content

feat: add OrcaRouter provider with API-key and PKCE login - #161

Open
lovejones2914-spec wants to merge 1 commit into
MGdaasLab:masterfrom
lovejones2914-spec:orcarouter/task-6034
Open

feat: add OrcaRouter provider with API-key and PKCE login#161
lovejones2914-spec wants to merge 1 commit into
MGdaasLab:masterfrom
lovejones2914-spec:orcarouter/task-6034

Conversation

@lovejones2914-spec

Copy link
Copy Markdown

What

Adds OrcaRouter as a first-class model provider to
WHartTest, with two explicit authentication choices and a capability-filtered
model catalog.

OrcaRouter is an OpenAI-compatible AI gateway built for both models and agents, with adaptive routing, automatic failover, zero-markup inference, observability, guardrails, and agent-tool governance.

It also runs gateway-level, zero-trust security for AI agents on the same endpoint — screening every prompt/response and governing every tool call on a default-deny basis, with no application code changes.

Discord: discord.gg/YEubt8enRA · X: https://x.com/OrcaRouter

  • Provider entries in WHartTest_Django/langgraph_integration/models.py
    (LLMConfig.PROVIDER_CHOICES):
    • orcarouterOrcaRouter - API (user pastes an existing sk-orca-… key)
    • orcarouter_oauthOrcaRouter - Auth (browser login, OAuth 2.0 + PKCE)
  • Inference base URL: https://api.orcarouter.ai/v1 (OpenAI-compatible wire
    format, so the existing ChatOpenAI adapter is reused unchanged)
  • Connect flow: OAuth 2.0 + PKCE, Flow B (out-of-band code) — chosen because
    WHartTest is self-hosted software whose install address differs on every
    deployment, and the browser that approves the request is frequently not on the
    machine running Django, so a loopback listener on the Django host would often
    be unreachable. Flow B needs no predictable address and no callback
    registration.
  • OrcaRouter also joins the knowledge-base embedding service enum
    (knowledge/models.py), reusing the project's existing CustomAPIEmbeddings
    adapter against https://api.orcarouter.ai/v1/embeddings.

Affiliation disclosure: I'm an engineer on the OrcaRouter team. This change
is made on behalf of OrcaRouter.

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.
No client secret is involved — PKCE binds the authorization code to this
process, so an intercepted code cannot be redeemed by anyone else.

Both entries are adapters on one small credential seam
(CredentialProviderCredentialResult in
langgraph_integration/orcarouter.py). The pasted-key adapter and the PKCE
adapter produce the same object, so the LLM adapter, the model catalog and every
AI entry point are indifferent to where a credential came from.

  • Persistence: the credential is written to the project's existing
    LLMConfig.api_key column — the same column the other providers use, which
    LLMConfigSerializer already marks write_only and never returns over the
    API. No new secret store was introduced, and no plaintext side file exists.
  • Durable, not refreshable: a PKCE-issued key is a long-lived API key, not a
    refresh token. Nothing schedules a proactive refresh and no refresh grant is
    invented. classify_auth_failure() treats 401/403 as terminal
    reauthentication and orcarouter.should_transition_to_needs_reauth() is
    generation-guarded, so a late failure from an old request can never mark a
    newly reauthorized credential as broken.
  • Secrets never leak: the verifier and state come from secrets.token_bytes
    per attempt, never enter a URL, log, telemetry or error string;
    mask_secret()/scrub() back the redaction path.

PKCE specifics

  • S256 only (base64url(sha256(verifier)), unpadded) on every flow, including
    Flow A-shaped usage, because the consent screen also lets a user choose
    "show me a code".
  • state is compared in constant time (hmac.compare_digest) before a code is
    redeemed, and the attempt is single-use.
  • Auth and inference use different public origins: authorization at
    https://www.orcarouter.ai/auth and exchange at
    https://www.orcarouter.ai/api/v1/auth/keys; inference and discovery at
    https://api.orcarouter.ai/v1. The two origins are never derived from one
    another; ORCA_BASE_URL is the shared self-hosted fallback with explicit
    ORCA_AUTH_BASE_URL / ORCA_API_BASE_URL overrides winning. Non-loopback
    origins must be HTTPS.
  • The exchange response's granted scope is read back and enforced: a grant
    narrower than api is refused with an actionable message instead of being
    assumed.

Model discovery and capability filtering

The model picker is generated from the live catalog at
GET https://api.orcarouter.ai/v1/models, fetched server-side — the browser
never holds an API key. The backend returns only minimal model metadata
(id, name, context_length, input_modalities, endpoint_types,
reasoning_efforts), and the UI renders a searchable role="listbox" picker.
Model IDs keep their vendor/model namespace verbatim.

The list endpoint advertises supported_endpoint_types; input modalities live on
the per-model detail record, so modalities are enriched from
GET /v1/models/{id} under a bounded worker pool and wall-clock budget, then
cached with a TTL.

Filtering is capability-specific and fails closed:

Entry point Filter
Chat / agent must advertise openai/anthropic/gemini/openai-response; image-generation, openai-video, jina-rerank excluded
Multimodal chat chat, and architecture.input_modalities must declare the attached non-text modality
Embedding ?capability=embedding / strict embeddings endpoint
Image generation strict image-generation endpoint
Video generation strict openai-video endpoint
Rerank strict jina-rerank endpoint

A model that declares no modality metadata never appears in a multimodal picker.
Changing the provider, or toggling the multimodal switch, recomputes the options;
a selection that is no longer compatible is cleared with a visible prompt rather
than silently retained. Capability filtering happens on the options handed to the
selector, so a send-time guard would be a second layer, not a substitute.

On live-discovery failure the project keeps a small verified seed (the five cited
models, with openai/gpt-5.5 retaining its low/medium/high/xhigh reasoning
ladder) and marks the result degraded so the UI says so. A successful live
result is authoritative and never mixes the seed in. There is no fallback to free
text.

AI input entry points covered

Entry point Coverage
LangGraph chat / agent conversation covered (chat)
Chat with image attachments (multimodal) covered (chat + declared image modality)
Requirement review, test-case generation/optimization, orchestrator & MCP flows covered — these all resolve the same active LLMConfig, so they route through the new provider automatically
Knowledge-base query rewrite covered (chat)
Knowledge-base embeddings covered (orcarouter embedding service)
Reranker not wired — the repository has no per-vendor reranker registry (only xinference and a generic custom URL), so there is no seam to add a named entry to. CAPABILITY_RERANK is implemented and tested in the catalog layer so it can be wired if such an entry point is added.
Image / video / audio generation no such entry point exists in this repository

Testing

Backend (SQLite; the repo's own PG-only migrations cannot run on SQLite and no
PostgreSQL server is available in this environment, so the schema is built from
the models via a throwaway settings module outside the repo):

python3 manage.py test langgraph_integration.tests_orcarouter
Ran 61 tests — OK (0 failures, 0 errors)

python3 manage.py test langgraph_integration knowledge
Ran 85 tests — OK (skipped=16)

The 61 tests cover: PKCE verifier/challenge/state generation and freshness,
authorize-URL shape (auth origin, callback_url=oob, S256, verifier absent from
the URL), the full authorize → code → exchange → persist flow against a local
fake auth server
asserting the exact /api/v1/auth/keys path and request body,
denial/reused-code/expired (403), wrong challenge method (400), 429, scope
downgrade, timeout and network failure, constant-time state comparison,
single-use attempts, cancel, origin policy (separate defaults, explicit overrides
winning, HTTP only for loopback), redaction, capability filters for text-only /
image-input chat / embedding / image / video / rerank with modality fail-closed,
bounded/oversized catalogs, degraded fallback with reasoning metadata intact,
live results never mixing in the seed, generation-safe needs_reauth, that no
fake refresh grant is modelled, and that the browser never receives the key.
Fixtures use only fake keys and fake codes.

Frontend: npx vue-tsc --noEmit passes clean. npm run build fails with 9
pre-existing TypeScript errors in files this change does not touch
— verified
by running vue-tsc -b against a pristine HEAD checkout, which reports the
identical 9 errors (TestCaseMindmap.vue, DatabaseConfigPanel.vue,
InterfacesPanel.vue, ChatMessages.vue, and a duplicate key in
src/i18n/index.ts). This change introduces zero new build errors.

Live verification

  • Real inference through the implemented provider path:
    create_llm_instance(config) with the OrcaRouter provider returned
    ChatOpenAI with base_url=https://api.orcarouter.ai/v1, and a real
    invoke() returned ORCA_OK.
  • Real catalog: GET https://api.orcarouter.ai/v1/models?capability=chat
    returned 167 chat models; ?capability=embedding returned 5;
    ?capability=image returned 8.
  • With an image attachment the picker narrows from 167 → 122 models, all of
    which declare image input; a text-only model present in the chat list
    (deepseek/deepseek-v4-pro) is absent, and openai/gpt-5.5 is retained.
  • Both providers appear in GET /api/lg/providers/ (orcarouter,
    orcarouter_oauth) and orcarouter appears in the knowledge-base embedding
    services list.

UI evidence (real automation, not mockups)

Playwright drove the real UI (Vite dev server + Django backend, a real login
through the actual login form, and the real provider config modal). 29/29
assertions passed; the run is reproducible with python3 e2e_orcarouter.py.

  • auth-methods.png — the OrcaRouter config panel shows both entries at
    once: the API-key entry (password-type, masked) and the "Connect with
    OrcaRouter" PKCE entry, both enabled. Asserts the key control is
    type="password".
  • text-model-dropdown.png — the real text picker expanded with 167 items
    from the live catalog.
  • multimodal-model-dropdown.png — after attaching an image, the picker
    reopens with 122 image-capable models.

The dropdown panel is asserted from the DOM: role="listbox" present,
aria-expanded="true" on the trigger, right-edge delta vs. the trigger
0.00px, opaque background (rgb(255,255,255)), and a visible
1px solid rgb(229,230,235) border. Screenshots are 1600×1000 and contain only
the dedicated test account, with no API key, token or personal data visible.

Not implemented

Flow C (device grant) is not implemented; Flow A (loopback) is not used, for the
reason given above. Neither is required for this client.

Provider evidence

Verified on 2026-09-11 by direct request:

  • Inference (OpenAI-compatible, Authorization: Bearer sk-orca-…):
    https://api.orcarouter.ai/v1/chat/completions — exercised live, see above.
  • Model list: GET https://api.orcarouter.ai/v1/models (returns 200; accepts an
    optional ?capability= filter) — the catalog source used by this PR.
  • Authorization / exchange:
    https://www.orcarouter.ai/auth and
    POST https://www.orcarouter.ai/api/v1/auth/keys; notarized live by the
    discovery document https://www.orcarouter.ai/.well-known/openid-configuration
    (200, code_challenge_methods_supported: ["S256","plain"]).
    Note https://api.orcarouter.ai/v1/auth/keys is a 404 — the relay is at /v1
    and the auth endpoints are not.
  • Revocation / account management:
    https://www.orcarouter.ai/console/authorized-apps (200) — revoking the app
    deletes every key it was issued, which is why a 401 is treated as terminal
    reauthentication rather than a retry.
  • Service documentation: https://docs.orcarouter.ai (308 redirect, exists).

Maintenance owner: the OrcaRouter team (this contribution is made on behalf
of OrcaRouter); verification date 2026-09-11.

Not verified, therefore not claimed: I could not confirm a terms-of-service
URL or a named operating legal entity (/terms, /privacy and /legal all
return 404 from this environment), nor a separate aggregator routing/resale
authorization document. I am flagging that gap rather than asserting something I
did not check; happy to supply these from the maintainers' preferred source.

Notes

  • No dependency was added: PKCE uses hashlib, hmac, base64 and secrets
    from the standard library, and the HTTP calls reuse the requests dependency
    the project already ships.
  • The lockfile was not modified.
  • Only the applicable files listed in the integration guide were touched; no CI
    configuration, licence, release process or unrelated formatting was changed.

Signed-off-by: lovejones2914-spec <lovejones2914-spec@users.noreply.github.com>
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