Skip to content

feat(llms): add OrcaRouter as a named LLM provider - #2275

Open
XiaoHuo888-hue wants to merge 1 commit into
Open-Source-Legal:mainfrom
XiaoHuo888-hue:add-orcarouter-provider
Open

feat(llms): add OrcaRouter as a named LLM provider#2275
XiaoHuo888-hue wants to merge 1 commit into
Open-Source-Legal:mainfrom
XiaoHuo888-hue:add-orcarouter-provider

Conversation

@XiaoHuo888-hue

Copy link
Copy Markdown

Summary

Adds OrcaRouter as a first-class LLM provider. OrcaRouter is an OpenAI-compatible model routing gateway that fronts dozens of hosted models behind one endpoint — pick a router alias like orcarouter/auto or a specific hosted model. 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.

Changes

  • New provider component opencontractserver/pipeline/llm_providers/orcarouter_provider.py — mirrors the OpenAI provider. Declares ORCAROUTER_API_KEY (secret) and a base_url (optional, defaulting to https://api.orcarouter.ai/v1) so credentials are configurable live in System Settings → Pipeline Components, exactly like the other providers. The registry auto-discovers it and it surfaces in the System Settings LLM picker and Corpus.preferred_llm / AgentConfiguration.preferred_llm.
  • opencontractserver/llms/model_factory.py — pydantic-ai has no native orcarouter: provider prefix, so a bare "orcarouter:..." spec string would raise "Unknown model" at agent construction. The factory now always builds a concrete OpenAI-compatible model for this provider (DB credentials win; otherwise ORCAROUTER_API_KEY + the default endpoint), keeping the env-fallback contract safe.
  • Teststest_llm_model_factory.py: registry discovery + model construction (no-DB-creds still builds a concrete model; DB base_url/api_key override; invalid DB base_url falls back to default). test_llm_runtime_config.py: provider registered + schema.
  • Docs — model-spec table + API-keys section in docs/architecture/llms/README.md; ORCAROUTER_API_KEY added to the production sample env.
  • Changelog fragment changelog.d/orcarouter-provider.added.md.

Test plan

  • pre-commit run --all-files passes on the touched files (black, isort, flake8, mypy, pyupgrade, trailing-whitespace, end-of-file-fixer); the changelog fragment validates via python3 scripts/collate_changelog.py --check (the pre-commit wrapper needs bare python in PATH, which the CI runner provides).
  • Standalone verification exercises the same code paths: registry discovery, settings schema, and model construction for orcarouter: specs.
  • Live test: drove the factory-built model end-to-end against https://api.orcarouter.ai/v1/chat/completions with a real key — HTTP 200, returned ORCA-LIVE-OK.

Checklist

  • Tests pass locally for any code this PR touches
  • pre-commit run --all-files passes (black, isort, flake8, prettier)
  • A changelog fragment was added under changelog.d/
  • No new dependency added — reuses the existing pydantic-ai-slim[openai] / openai stack

Contributor License Agreement

By submitting this pull request, you agree to license your contribution under the project's Contributor License Agreement.

Disclosure: I'm an engineer on the OrcaRouter team.

Adds an orcarouter: provider to the pipeline LLM provider registry
(opencontractserver/pipeline/llm_providers/orcarouter_provider.py)
mirroring the OpenAI provider pattern. OrcaRouter is an OpenAI-compatible
model routing gateway; model specs like orcarouter:orcarouter/auto reuse
the existing pydantic-ai OpenAI client path.

pydantic-ai has no native orcarouter: prefix, so
opencontractserver/llms/model_factory.py now always constructs a concrete
OpenAI-compatible model for this provider instead of returning a bare spec
string (which would raise 'Unknown model'). DB-configured credentials win;
otherwise ORCAROUTER_API_KEY and the default endpoint
https://api.orcarouter.ai/v1 are used.

Docs: model-spec table + API-keys section in docs/architecture/llms/README.md,
ORCAROUTER_API_KEY in the production sample env. Changelog fragment added.

Signed-off-by: XiaoHuo888-hue <jinhao.song@myflashcloud.com>
@github-actions

Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


XiaoHuo888-hue seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

JSv4 commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Reviewed this as part of a batch pass over the open PRs. Thanks for the clean, well-commented provider module and for the up-front disclosure.

Two notes on state first: the branch merges cleanly with current main, so there's nothing to rebase. And no CI has run here beyond CLAAssistant — fork workflows need maintainer approval, and the CLA check is failing because the commit's email isn't linked to a GitHub account (you'll need to add that address to your account before you can sign). So the findings below are from reading and running the code locally, not from a CI report.

I found two things I'd call blockers. Both are reproduced, not inferred.


1. With no OrcaRouter key configured, the install's OPENAI_API_KEY is sent to api.orcarouter.ai

model_factory.py:222 resolves api_key = creds.get("api_key") or os.environ.get("ORCAROUTER_API_KEY") and passes the result — possibly None — straight into OpenAIProvider(api_key=api_key, ...) at line 239. OpenAIProvider only substitutes its 'api-key-not-set' placeholder when OPENAI_API_KEY is absent from the environment. When it's present — which it is in every sample env file in this repo, including docs/sample_env_files/backend/production/django.env — the underlying AsyncOpenAI falls back to it.

Reproduced against the pinned clients (pydantic-ai 1.107.5, openai 2.54.0), replaying lines 222–240 verbatim:

resolved api_key passed to factory : None
base_url the client will call      : https://api.orcarouter.ai/v1/
Authorization key the client sends : sk-THE-INSTALLS-REAL-OPENAI-SECRET

Trigger is ordinary misconfiguration: an operator adds the provider in System Settings but hasn't pasted a key yet, or pastes it into the wrong row. Every agent build on an orcarouter: spec then constructs cleanly and ships the install's OpenAI secret in an Authorization: Bearer header to a third-party host, on every chat turn and background task. Nothing logs a warning.

main already guards exactly this, for exactly this reason — model_factory.py:254-258:

# Ollama (and other OpenAI-compatible local servers) require *some*
# api_key for the underlying OpenAI client even when the server
# ignores it. Supply a harmless placeholder when none is configured.
if provider_key == "ollama" and not api_key:
    api_key = "ollama"

The OrcaRouter branch returns at line 237, above that guard. Suggested fix — mirror it, and warn, since requires_api_key = True means an unset key is a misconfiguration rather than a keyless local server:

api_key = creds.get("api_key") or os.environ.get("ORCAROUTER_API_KEY")
if not api_key:
    logger.warning(
        "No OrcaRouter api_key configured (DB or ORCAROUTER_API_KEY); "
        "requests to %s will be unauthenticated.", base_url,
    )
    api_key = "orcarouter-api-key-not-set"

Worth a regression test too. TestOrcaRouterProvider.setUp patches ORCAROUTER_API_KEY="sk-orca-test" unconditionally, so all four new cases run with a key present and the unset path — the default state of a fresh install — has no coverage.

2. The eight supported_models break an existing test

opencontractserver/tests/test_llm_runtime_retargeting.py:432test_every_supported_model_has_a_context_window — walks every registered provider's supported_models and asserts none falls back to DEFAULT_CONTEXT_WINDOW. I replayed its exact logic against this branch's provider list:

DEFAULT_CONTEXT_WINDOW = 128000
MISSING (would fail the assertion):
    orcarouter:orcarouter/auto          orcarouter:grok/grok-4.3
    orcarouter:openai/gpt-5.5           orcarouter:deepseek/deepseek-v4-pro
    orcarouter:google/gemini-3.5-flash  orcarouter:minimax/minimax-m2.7
    orcarouter:anthropic/claude-opus-4.8 orcarouter:qwen/qwen3.7-max
=> 8/8 models fall back to the default

So pytest goes red as soon as the workflows are approved. This is CLAUDE.md pitfall #20 and issue #2078.

The important wrinkle: adding bare entries won't fix it. get_context_window_for_model strips only the text before the first :, so "orcarouter:anthropic/claude-opus-4.8" looks up "anthropic/claude-opus-4.8", and the prefix loop can't match "claude-opus-4-8" because of the anthropic/ namespace. The entries need to be keyed by the full namespaced name.

This isn't just a red test — it's the real behavior. google/gemini-3.5-flash (~1M window) sized at 128K means compaction fires at ~96K on a model that could hold ten times that. The dangerous direction is the reverse: any routed model with a sub-128K window never compacts and dies on a hard overflow. And orcarouter/auto is unknowable by construction, since the gateway picks per request — it needs a deliberately conservative value.


Smaller items

  • base_url validation is duplicated. Lines 227–236 re-implement the urlparse(...).scheme not in ("http", "https") check and warning that already exist at lines 250–259, with urlparse imported function-locally twice in one function. The two copies differ in what happens next (default endpoint vs. None/env-fallback), so a future tightening applied to one and not the other leaves the gateway path — the one pointed at a third-party host — weaker. Worth extracting one helper.
  • The shared no-api_key warning can never fire for this provider. Lines 260–271 log "DB-configured base_url for provider %r has no api_key; the request will rely on the provider's env var, which a custom gateway may not honour" — written for precisely this situation. The early return skips it, so a keyless OrcaRouter misconfiguration surfaces only as a 401 in a worker log.
  • Responses-API models are silently unusable. requires_responses_api short-circuits on if provider_key != "openai", and this branch hard-codes OpenAIChatModel. Since validate_model_spec deliberately doesn't enforce supported_models, an operator can select orcarouter:openai/gpt-5.6-luna and get the exact 400 that CLAUDE.md pitfall Bump traefik from v2.9.1 to v2.9.4 in /compose/production/traefik #20 documents. Either reject that family here with a clear error, or note in a comment that OrcaRouter exposes only the chat-completions surface.
  • docs/test_scripts/llm_runtime_config.md:23,52 still says "the four providers"; this makes it five. The doc isn't touched by the PR.

Things I checked that are fine

  • The module-level ORCAROUTER_DEFAULT_BASE_URL import doesn't create an import cycle today — orcarouter_provider reaches only pipeline/base/* and types/protocols.py, none of which touch a Django model at import time or import opencontractserver.llms.
  • Adding a provider_key == "..." branch to _construct_model is the documented pattern, not a smell — model_factory.py:288-292 explicitly instructs it: "When adding a new provider under pipeline/llm_providers/, add a matching branch above."
  • The invalid-base_url-falls-back-to-default behavior and the DB-creds-win precedence both read correctly and are covered by the new tests.
  • http being accepted is consistent with every other provider, not a new hole.

Happy to look again once the two blockers are addressed.


Generated by Claude Code

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