Self-hosted LLM router with a managed safety net.
OpenAI-compatible. BYOK. Single-workspace. Streaming. model="auto".
OrcaRouter Lite is the open-source single-workspace edition of OrcaRouter. Run it on your laptop, ship it in your product, or use hosted api.orcarouter.ai directly for the long tail of models you don't want to manage keys for.
Why us? LiteLLM is a library; OpenRouter is closed-source hosted; Ollama is local-only. We're the self-hosted server with a managed fallback — a sentence none of those can say.
Two ways to use OrcaRouter:
Run Lite on your own machine; bring your own provider keys.
git clone https://github.com/Continuum-AI-Corp/OrcaRouter-Lite.git
cd OrcaRouter-Lite
cp .env.example .env
# add at least one: OPENAI_API_KEY=sk-... (or ORCAROUTER_API_KEY=...)
docker compose up
# logs: ✓ orcarouter-lite ready. API key: sk-orca-abc123...Base URL: http://localhost:8000/v1. Use the sk-orca-* key printed at startup.
No clone, no docker. Register, get a key, point any OpenAI SDK at hosted.
# 1. Register at https://www.orcarouter.ai and copy your sk-orca-* key
# 2. Use https://api.orcarouter.ai/v1 as the base URLAccount required. Hosted handles routing, billing, and the long tail of providers — billed per-token on your OrcaRouter account. See docs.orcarouter.ai/introduction.
Examples below use Path A's localhost base URL — swap for https://api.orcarouter.ai/v1 if you're on Path B.
Python
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="sk-orca-abc123...",
)
r = client.chat.completions.create(
model="auto", # or "gpt-4o-mini", "claude-3-5-sonnet-latest", ...
messages=[{"role": "user", "content": "Hello!"}],
)
print(r.choices[0].message.content)Node.js
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "http://localhost:8000/v1",
apiKey: "sk-orca-abc123...",
});
const r = await client.chat.completions.create({
model: "auto",
messages: [{ role: "user", content: "Hello!" }],
});
console.log(r.choices[0].message.content);curl
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-orca-abc123..." \
-H "Content-Type: application/json" \
-d '{"model":"auto","messages":[{"role":"user","content":"Hello!"}]}'Open http://localhost:8000/ for the dashboard — providers, routing, analytics, keys (Path A only).
| OrcaRouter Lite | LiteLLM library | OpenRouter | Ollama | |
|---|---|---|---|---|
| Self-hosted server | ✓ | as a library | ✗ | ✓ |
| OpenAI-compatible | ✓ | ✓ | ✓ | ✓ |
| Multi-provider (OpenAI/Anthropic/Google/…) | ✓ | ✓ | ✓ | ✗ |
| Built-in dashboard | ✓ | ✗ | ✓ | ✗ |
model="auto" (cheapest capable) |
✓ | ✗ | ✗ | n/a |
| Streaming | ✓ | ✓ | ✓ | ✓ |
| BYOK | ✓ | ✓ | ✗ | n/a |
| Hosted-as-fallback | ✓ | ✗ | n/a | ✗ |
| No Postgres / no Redis required | ✓ | n/a | n/a | ✓ |
Send model="auto" and OrcaRouter picks the cheapest model in your configured providers that meets the request's capability requirements (tools, vision, JSON mode). No manual routing rules; no rate-limit gymnastics; no if x: ... cost optimization in your code.
client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": [
{"type": "text", "text": "What's in this image?"},
{"type": "image_url", "image_url": {"url": "data:..."}},
]}],
)
# → routes to the cheapest VISION-capable model your keys coverThe resolved model is exposed back to callers via the x-orca-resolved-model response header so you can log/display what was actually used.
Already running Lite? Set ORCAROUTER_API_KEY to your sk-orca-* from www.orcarouter.ai, and hosted becomes one more provider in the routing chain — covering models your local keys don't:
# .env
ORCAROUTER_API_KEY=sk-orca-hosted-abc...Use cases:
- Try-before-you-buy — no local provider keys needed
- Local logging — hosted handles routing, Lite stores RequestLog rows for the dashboard
- Failover — local providers fail, hosted is the safety net
OpenAI-compatible SSE format with the standard data: ... \n\n framing and a terminal [DONE] sentinel — drop-in for any SDK that already streams from OpenAI.
for chunk in client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "Tell me a story"}],
stream=True,
):
print(chunk.choices[0].delta.content or "", end="", flush=True)Lite speaks three inbound protocols against one routing pipeline. Clients that only talk Anthropic or Gemini wire formats connect directly — no OpenAI SDK required:
# Claude Code, pointed at Lite (no /v1 suffix in the base URL)
export ANTHROPIC_BASE_URL=http://localhost:8000
export ANTHROPIC_API_KEY=sk-orca-...
claude# google-genai SDK, pointed at Lite
from google import genai
from google.genai.types import HttpOptions
client = genai.Client(api_key="sk-orca-...",
http_options=HttpOptions(base_url="http://localhost:8000"))
client.models.generate_content(model="auto", contents="Hello!")Requests are translated at the edge into the same internal pipeline, so
model="auto", the cross-provider prompt cache (shared across protocols),
routing strategies, and the analytics dashboard all work identically. Guides:
integrations/claude-code.md,
integrations/gemini-sdk.md.
100+ chat models are loaded at startup from LiteLLM's community-maintained pricing database — no model list to maintain manually. Each entry exposes:
id(e.g.gpt-4o,claude-3-5-sonnet-latest)provider(mapped to your configured keys)- Capability flags:
supports_tools,supports_vision,supports_json_mode - Per-token input/output cost (drives the savings widget +
model="auto")
GET /v1/models returns the OpenAI-format catalogue.
Railway: mount a volume at
/data, then setRAILWAY_RUN_UID=0andDATABASE_URL=sqlite+aiosqlite:////data/orca.db. Miss the first and the deploy fails its healthcheck (the image runs as non-root, the volume mounts root-owned); miss the second and every redeploy wipes your keys and analytics. Full variable list:railway.toml.
POST /v1/chat/completions— proxy + streaming +model="auto"+ cross-provider prompt cachePOST /v1/messages— Anthropic Messages API ingress (Claude Code / Anthropic SDKs connect directly;+ /count_tokens)POST /v1beta/models/{model}:generateContent— Gemini API ingress (google-genai SDK connects directly;+ :streamGenerateContent,GET /v1beta/models)GET /v1/models— discoverable model catalog (100+ models fromlitellm.model_cost)GET/PUT/DELETE /v1/providers/{provider}— set / list / revoke encrypted provider keysGET/PUT /v1/routing— change strategy (balanced/cheapest/fastest/quality)GET /v1/analytics/{recent,spend,latency,savings,unreachable}— local analytics, no telemetry leaves the boxGET /v1/hosted— hosted-fallback status (drives the dashboard's "Get $5 free credit" card)GET/POST/DELETE /v1/keys/...— list / rotate / revoke API keys- Single-page dashboard at
/ - SQLite by default; Postgres opt-in via
DATABASE_URL; Redis optional - Multi-worker safe: set
REDIS_URLand config changes (provider keys, routing, quality overrides) are invalidated across all workers/replicas via pub/sub. Single worker (the default) needs no Redis.
Deterministic requests (temperature=0 or pinned seed) are served from cache on repeat — works across every provider, not just Anthropic. Backend is Redis when REDIS_URL is set, in-process LRU otherwise. Cache hits return instantly with x-orca-cache: HIT and cost $0.
$ curl ... -d '{"model":"auto","messages":[...], "temperature": 0}' -i
HTTP/1.1 200 OK
x-orca-cache: MISS
x-orca-resolved-model: gpt-4o-mini
$ curl ... # same payload again
HTTP/1.1 200 OK
x-orca-cache: HIT ← served from cache, no upstream callGET /v1/analytics/savings?baseline=gpt-4o&days=7 reports what your traffic would have cost on always-GPT-4 vs what it actually cost. The dashboard shows it as a tile.
Drop-in configs for Claude Code, Gemini SDK, Continue.dev, Aider, Cursor, LangChain, LlamaIndex, Vercel AI SDK, and any tool that speaks the OpenAI Chat Completions protocol — plus native Anthropic and Gemini wire formats. See integrations/.
This is the single-workspace edition. By design, no:
- multi-tenancy, RBAC, SSO
- billing, wallets, points, partner program
- admin console, audit logs, trust & safety
- multi-pod deployment / Kubernetes
- email / Slack / webhooks for alerts
For those, see the hosted product or the (forthcoming) Teams edition.
Built test-first. Every behaviour shipped here had a failing test first.
pip install -e ".[dev]"
PYTHONPATH=. pytest -v
# 487 passed| Slice | Tests | What |
|---|---|---|
| 1. Config | 5 | env loading, defaults, env_provider_keys() |
| 2. Seed | 3 | bootstrap workspace + API key + RoutingConfig, idempotent |
| 3. Auth middleware | 4 | bearer-token validation, 401 on missing/invalid |
| 4. App factory | 3 | /health, error envelope, /v1/* gating |
| 5. Provider keys CRUD | 5 | encrypted at rest, plaintext never round-trips |
| 6. Router cache | 13 | env+DB+hosted deployment assembly with precedence |
| 7. Chat completion | 5 | OpenAI format, RequestLog, validation |
| 8. Analytics | 4 | recent / spend / latency p50/p99 |
| 9. /v1/{models,keys,routing} | 8 | list/create/revoke + strategy update |
| 10. Streaming | 4 | SSE format, [DONE] sentinel, log writeback |
| 11. Catalog | 7 | 100+ models, capability flags, pricing |
12. model="auto" |
21 | capability detection, cheapest-meeting-needs (unit + integration) |
| 13. Cost savings | 9 | savings vs always-GPT-4 baseline + hosted-auto comparison |
| 14. Prompt cache | 15 | cross-provider exact-match cache + chat integration |
| 15. Benchmark | 4 | summarize() + render_markdown() aggregation |
| 16. Hosted status | 7 | /v1/hosted config-source + signup-URL surface |
| 17. Hosted-auto savings | 3 | _hosted_auto_savings edge cases on synthetic catalogs |
| 18. Unreachable models | 7 | "models you can't reach" tile clears when hosted is on |
| 19. Multi-protocol auth | 6 | x-api-key / x-goog-api-key / ?key= scoping, /v1beta guard, per-protocol 401 envelopes |
20. Anthropic /v1/messages |
53 | request/response/stream translation + ingress integration |
21. Gemini /v1beta |
40 | translation incl. schema-enum normalization + generateContent/stream ingress |
| 22. Packaging | 6 | console script, wheel-bundled dashboard, design-dir resolution |
| Total | 487 |
Slice rows show the tests added when each slice shipped; the total is the current full suite.
app/
├── main.py FastAPI factory + lifespan + SPA mount
├── config.py Settings (~15 fields)
├── deps.py DI helpers
├── seed.py First-run bootstrap
├── auto_routing.py model="auto" capability + cost scoring
├── router_cache.py Single-workspace router
├── prompt_cache.py Cross-provider exact-match cache (Redis or in-memory LRU)
├── schemas.py OpenAI-compatible request schema
├── middleware/auth.py sk-orca-* validation
└── routes/
├── chat.py /v1/chat/completions (blocking + streaming)
├── models.py /v1/models
├── providers.py BYOK CRUD
├── routing.py strategy config
├── analytics.py recent / spend / latency / savings / unreachable
├── keys.py list / rotate / revoke API keys
├── hosted.py /v1/hosted — hosted-fallback status for the dashboard
└── health.py
packages/
├── litellm_adapter/ Router wrapper + 100+ model catalog
├── auth/ hashing + AES-256-GCM
└── db/ models + engine + session
- OpenAI-compatible chat completions
- Streaming (SSE)
-
model="auto"cheapest-capable routing - Hosted-as-upstream
- Encrypted BYOK at rest
- Local analytics dashboard
- CI (GitHub Actions)
- Cross-provider prompt caching
- Continue.dev / Aider / LangChain / Cursor / Vercel AI SDK integrations
- Public benchmark + savings claim
- Embeddings + image-gen proxy
See DEMO.md for the failover demo.
MIT. See LICENSE.
