Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions aider/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from aider.dump import dump # noqa: F401
from aider.llm import litellm
from aider.openrouter import OpenRouterModelManager
from aider.orcarouter import OrcaRouterModelManager
from aider.sendchat import ensure_alternating_roles, sanity_check_messages
from aider.utils import check_pip_install_extra

Expand Down Expand Up @@ -113,6 +114,8 @@
"gemini-exp": "gemini/gemini-2.5-pro-exp-03-25",
"grok3": "xai/grok-3-beta",
"optimus": "openrouter/openrouter/optimus-alpha",
# OrcaRouter (https://www.orcarouter.ai)
"orcarouter-auto": "orcarouter/orcarouter/auto",
}
# Model metadata loaded from resources and user's files.

Expand Down Expand Up @@ -168,11 +171,15 @@ def __init__(self):

# Manager for the cached OpenRouter model database
self.openrouter_manager = OpenRouterModelManager()
# Manager for the cached OrcaRouter model database
self.orcarouter_manager = OrcaRouterModelManager()

def set_verify_ssl(self, verify_ssl):
self.verify_ssl = verify_ssl
if hasattr(self, "openrouter_manager"):
self.openrouter_manager.set_verify_ssl(verify_ssl)
if hasattr(self, "orcarouter_manager"):
self.orcarouter_manager.set_verify_ssl(verify_ssl)

def _load_cache(self):
if self._cache_loaded:
Expand Down Expand Up @@ -253,6 +260,11 @@ def get_model_info(self, model):
if litellm_info:
return litellm_info

if not cached_info and model.startswith("orcarouter/"):
orcarouter_info = self.orcarouter_manager.get_model_info(model)
if orcarouter_info:
return orcarouter_info

if not cached_info and model.startswith("openrouter/"):
# First try using the locally cached OpenRouter model database
openrouter_info = self.openrouter_manager.get_model_info(model)
Expand Down Expand Up @@ -715,6 +727,7 @@ def fast_validate_environment(self):

keymap = dict(
openrouter="OPENROUTER_API_KEY",
orcarouter="ORCAROUTER_API_KEY",
openai="OPENAI_API_KEY",
deepseek="DEEPSEEK_API_KEY",
gemini="GEMINI_API_KEY",
Expand Down Expand Up @@ -1026,6 +1039,26 @@ def send_completion(self, messages, functions, stream, temperature=None):

self.github_copilot_token_to_open_ai_key(kwargs["extra_headers"])

# OrcaRouter (https://www.orcarouter.ai): route via the OpenAI-compatible
# endpoint at api.orcarouter.ai/v1 using ORCAROUTER_API_KEY. The user-facing
# model name keeps the orcarouter/ prefix; we rewrite it to openai/<rest>
# only for the underlying litellm call.
if isinstance(kwargs.get("model"), str) and kwargs["model"].startswith("orcarouter/"):
rest = kwargs["model"][len("orcarouter/"):]
kwargs["model"] = "openai/" + rest
kwargs.setdefault("api_base", "https://api.orcarouter.ai/v1")
orca_key = os.environ.get("ORCAROUTER_API_KEY")
if orca_key and "api_key" not in kwargs:
kwargs["api_key"] = orca_key
attribution = {
"HTTP-Referer": "https://aider.chat/",
"X-Title": "aider",
}
existing_headers = kwargs.get("extra_headers") or {}
for hk, hv in attribution.items():
existing_headers.setdefault(hk, hv)
kwargs["extra_headers"] = existing_headers

res = litellm.completion(**kwargs)
return hash_object, res

Expand Down
160 changes: 160 additions & 0 deletions aider/orcarouter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""
OrcaRouter model metadata caching and lookup.

OrcaRouter (https://www.orcarouter.ai) is an OpenAI-compatible LLM meta-router
that exposes 150+ upstream models under the ``orcarouter/<vendor>/<model>``
naming convention plus a virtual ``orcarouter/auto`` adaptive router.

This module keeps a local cached copy of the OrcaRouter pricing/catalog feed
(downloaded from ``https://www.orcarouter.ai/api/pricing`` -- a public endpoint
that needs no auth) and exposes a helper class that returns metadata for a
given model in a format compatible with litellm's ``get_model_info``.

Pricing formula (per OrcaRouter quota constant ``QuotaPerUnit = 500_000``,
i.e. $2 per 1M tokens for ``model_ratio = 1``):

input USD/token = model_ratio * 2 / 1_000_000
output USD/token = model_ratio * completion_ratio * 2 / 1_000_000
"""
from __future__ import annotations

import json
import time
from pathlib import Path
from typing import Dict

import requests


# $2 / 1M tokens per unit ratio. Source: OrcaRouter common/constants.go QuotaPerUnit.
_USD_PER_M_TOKENS_AT_RATIO_1 = 2.0


def _per_token(usd_per_million):
"""Convert USD-per-1M-tokens to USD-per-token, or None on bad input."""
if usd_per_million is None:
return None
try:
return float(usd_per_million) / 1_000_000.0
except (TypeError, ValueError):
return None


class OrcaRouterModelManager:
MODELS_URL = "https://www.orcarouter.ai/api/pricing"
CACHE_TTL = 60 * 60 * 24 # 24 h

def __init__(self):
self.cache_dir = Path.home() / ".aider" / "caches"
self.cache_file = self.cache_dir / "orcarouter_models.json"
self.content = None
self.verify_ssl = True
self._cache_loaded = False

# ------------------------------------------------------------------ #
# Public API #
# ------------------------------------------------------------------ #
def set_verify_ssl(self, verify_ssl):
"""Enable/disable SSL verification for API requests."""
self.verify_ssl = verify_ssl

def get_model_info(self, model):
"""
Return metadata for *model* or an empty ``dict`` when unknown.

``model`` should use the aider naming convention, e.g.
``orcarouter/openai/gpt-4o`` or ``orcarouter/anthropic/claude-opus-4.7``.
"""
self._ensure_content()
records = self._records()
if not records:
return {}

route = self._strip_prefix(model)
record = next(
(item for item in records if item.get("model_name") == route),
None,
)
if not record:
return {}

try:
model_ratio = float(record.get("model_ratio") or 0)
except (TypeError, ValueError):
model_ratio = 0.0
try:
completion_ratio = float(record.get("completion_ratio") or 1)
except (TypeError, ValueError):
completion_ratio = 1.0

input_per_m = model_ratio * _USD_PER_M_TOKENS_AT_RATIO_1
output_per_m = model_ratio * completion_ratio * _USD_PER_M_TOKENS_AT_RATIO_1

context_length = record.get("context_length") or None
max_output = record.get("max_completion_tokens") or context_length

return {
"max_input_tokens": context_length,
"max_tokens": context_length,
"max_output_tokens": max_output,
"input_cost_per_token": _per_token(input_per_m),
"output_cost_per_token": _per_token(output_per_m),
"litellm_provider": "orcarouter",
}

# ------------------------------------------------------------------ #
# Internal helpers #
# ------------------------------------------------------------------ #
def _strip_prefix(self, model):
return model[len("orcarouter/"):] if model.startswith("orcarouter/") else model

def _records(self):
"""Return list of model records regardless of whether the API wraps them."""
if isinstance(self.content, list):
return self.content
if isinstance(self.content, dict):
for key in ("data", "models", "pricing"):
val = self.content.get(key)
if isinstance(val, list):
return val
return []

def _ensure_content(self):
self._load_cache()
if not self.content:
self._update_cache()

def _load_cache(self):
if self._cache_loaded:
return
try:
self.cache_dir.mkdir(parents=True, exist_ok=True)
if self.cache_file.exists():
cache_age = time.time() - self.cache_file.stat().st_mtime
if cache_age < self.CACHE_TTL:
try:
self.content = json.loads(self.cache_file.read_text())
except json.JSONDecodeError:
self.content = None
except OSError:
pass

self._cache_loaded = True

def _update_cache(self):
try:
response = requests.get(
self.MODELS_URL, timeout=10, verify=self.verify_ssl
)
if response.status_code == 200:
self.content = response.json()
try:
self.cache_file.write_text(json.dumps(self.content, indent=2))
except OSError:
pass
except Exception as ex: # noqa: BLE001
print(f"Failed to fetch OrcaRouter model list: {ex}")
try:
self.cache_file.write_text("{}")
except OSError:
pass
77 changes: 77 additions & 0 deletions aider/website/docs/llms/orcarouter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
parent: Connecting to LLMs
nav_order: 510
---

# OrcaRouter

Aider can connect to [models provided by OrcaRouter](https://www.orcarouter.ai/models),
an OpenAI-compatible LLM meta-router that exposes 150+ upstream models
(OpenAI, Anthropic, Google, DeepSeek, xAI, Qwen, MiniMax, Kimi, Z.ai, ...) under
a single API key.

You'll need an [OrcaRouter API key](https://www.orcarouter.ai).

First, install aider:

{% include install.md %}

Then configure your API key:

```
export ORCAROUTER_API_KEY=<key> # Mac/Linux
setx ORCAROUTER_API_KEY <key> # Windows, restart shell after setx
```

Start working with aider and OrcaRouter on your codebase:

```bash
# Change directory into your codebase
cd /to/your/project

# Use any model from OrcaRouter's catalog
aider --model orcarouter/openai/gpt-4o
aider --model orcarouter/anthropic/claude-opus-4.7
aider --model orcarouter/deepseek/deepseek-v3.1

# List models known to aider with the orcarouter/ prefix
aider --list-models orcarouter/
```

## Adaptive routing with `orcarouter/auto`

OrcaRouter ships a virtual router named `auto` that automatically picks
an upstream for each request based on configurable strategies (`cheapest`,
`balanced`, `quality`, `adaptive`, `gated_adaptive`). You can target it with
the built-in alias:

```bash
aider --model orcarouter-auto
# equivalent to: aider --model orcarouter/orcarouter/auto
```

The routing policy and the upstream pool are configured in the
[OrcaRouter console](https://www.orcarouter.ai/console/routing).

{: .tip }
When using `orcarouter/orcarouter/auto` with aider's tool-calling features,
make sure the router pool only contains tool-capable upstream models, or pin
a specific model such as `orcarouter/openai/gpt-4o` to avoid 4xx errors from
upstreams that do not support function calling.

## Provider routing preferences

OrcaRouter accepts an `extra_body` block on chat completion requests for
routing preferences (e.g. fallback chains). You can configure it via a
`.aider.model.settings.yml` file:

```yaml
- name: orcarouter/openai/gpt-4o
extra_params:
extra_body:
models: ["openai/gpt-4o-mini", "openai/gpt-4o"]
route: "fallback"
```

See [Advanced model settings](https://aider.chat/docs/config/adv-model-settings.html#model-settings)
for more details about model settings files.
Loading