Decorator-based LLM model router with cost-aware, budget-tracking, and policy-enforcing composable layers.
Zero external dependencies (stdlib only). Pluggable into any platform: loom-ai, Claude Code, Crush, Codex, or your own orchestration.
pip install git+https://github.com/FlossWare/model-router-ai.gitimport asyncio
import os
from model_router_ai import (
ProviderRouter,
OpenAICompatProvider,
GeminiProvider,
CostAware,
BudgetGuard,
PolicyGuard,
LatencyOptimizer,
ThompsonSamplingSelector,
ChatMessage,
)
async def main():
# 1. Create the base router with providers
base = ProviderRouter()
base.add_provider(OpenAICompatProvider("groq"), api_key=os.environ["GROQ_API_KEY"])
base.add_provider(GeminiProvider(), api_key=os.environ["GEMINI_API_KEY"])
# 2. Stack decorators — each adds one concern
router = PolicyGuard(
BudgetGuard(
CostAware(
LatencyOptimizer(
ThompsonSamplingSelector(base)
),
prefer_free=True,
),
max_monthly=300.0,
),
allowed=["gemini-*", "llama-*"],
)
await router.initialize()
# 3. Use it
response = await router.chat([
ChatMessage(role="user", content="Explain the decorator pattern")
])
print(response.content)
asyncio.run(main())Each decorator wraps a ModelRouter and adds exactly one concern. Stack them to compose routing behavior:
PolicyGuard → filters models by allowlist/blocklist
└─ BudgetGuard → tracks spending against $300/month cap
└─ CostAware → sorts models by cost, prefers cheaper
└─ ThompsonSamplingSelector → Bayesian explore/exploit
└─ LatencyOptimizer → tracks per-model latency
└─ ProviderRouter → talks to LLM APIs
Order matters. PolicyGuard on the outside means "only consider allowed models, then optimize cost within those." Put CostAware outside ThompsonSampling to get "cheapest among proven models."
Sorts models by cost-per-token (cheapest first). Free models get priority by default.
router = CostAware(base, max_cost_per_call=0.01, prefer_free=True)Tracks cumulative spending and enforces a monthly cap. Alerts at configurable thresholds.
router = BudgetGuard(base, max_monthly=300.0, alert_thresholds=[50, 75, 90])
# Check status anytime
print(router.status) # BudgetStatus(spent_usd=42.50, remaining_usd=257.50, ...)
print(router.status.percent_used) # 14.2Raises BudgetExhaustedError when the cap is reached.
Filters models using glob patterns. Enforced on both explicit and auto-selected models.
# Only allow specific model families
router = PolicyGuard(base, allowed=["gemini-*", "claude-*"])
# Block expensive models
router = PolicyGuard(base, blocked=["gpt-4o", "o1-*"])
# Filter by provider
router = PolicyGuard(base, allowed_providers=["google", "groq"])Raises PolicyViolationError on blocked model access.
Tracks per-model response times with a sliding window.
router = LatencyOptimizer(base, window_size=20)
# After some calls:
print(router.fastest_models(top_n=5))
print(router.avg_latency("groq", "llama-3.3-70b"))Bayesian explore/exploit for model selection. Balances trying new models with using proven ones.
router = ThompsonSamplingSelector(base)
# After some calls:
print(await router.performance())
# {'gemini-2.5-flash': {'alpha': 15.0, 'beta': 2.0, 'mean': 0.88, 'trials': 15}, ...}| Provider | Class | Free Tier |
|---|---|---|
| Groq | OpenAICompatProvider("groq") |
Yes |
| OpenRouter | OpenAICompatProvider("openrouter") |
Yes (with free_only=True) |
| Cerebras | OpenAICompatProvider("cerebras") |
Yes |
| DeepInfra | OpenAICompatProvider("deepinfra") |
Yes |
| NVIDIA | OpenAICompatProvider("nvidia") |
Yes |
| Google Gemini | GeminiProvider() |
Yes |
| Cohere | CohereProvider() |
Yes |
| OpenAI | OpenAICompatProvider("openai") |
No |
| Vertex AI | VertexAIProvider(project_id="...") |
No |
Any OpenAI-compatible API works:
base.add_provider(
OpenAICompatProvider("my-provider", base_url="https://my-api.com/v1"),
api_key="sk-..."
)Built-in cost metadata for popular models (USD per 1M tokens):
| Model | Input | Output |
|---|---|---|
| Gemini 2.5 Flash | $0.15 | $0.60 |
| GPT-4o Mini | $0.15 | $0.60 |
| Claude Haiku 4.5 | $0.80 | $4.00 |
| O3-mini | $1.10 | $4.40 |
| Gemini 2.5 Pro | $1.25 | $10.00 |
| GPT-4o | $2.50 | $10.00 |
| Claude Sonnet 4.6 | $3.00 | $15.00 |
| Claude Opus 4.6 | $15.00 | $75.00 |
Custom costs:
from model_router_ai import ModelCost
base.add_provider(
OpenAICompatProvider("openai", cost_map={
"gpt-4o": ModelCost(input_per_1m=2.50, output_per_1m=10.0),
}),
api_key="sk-..."
)The base ProviderRouter accepts a pluggable selection strategy:
from model_router_ai import ProviderRouter, CascadeStrategy
router = ProviderRouter(strategy=CascadeStrategy(preferred=["gemini-2.5-flash"]))| Strategy | Behavior |
|---|---|
ThompsonSamplingStrategy |
Bayesian explore/exploit (default) |
RoundRobinStrategy |
Even spread across endpoints |
LatencyWeightedStrategy |
Prefer faster endpoints |
CascadeStrategy |
Try preferred models first |
BudgetGuard and ThompsonSamplingSelector can delegate to external implementations via injectable protocols. This lets packages like budget-ai and strategy-ai provide rich implementations without model-router-ai depending on them.
Inject a custom budget tracker into BudgetGuard:
from model_router_ai import BudgetGuard, UsageTracker
class MyTracker: # satisfies UsageTracker protocol via structural subtyping
async def record_usage(self, model, cost_usd, usage=None): ...
async def is_exceeded(self): ...
async def get_status(self): ...
def reset(self): ...
router = BudgetGuard(base, tracker=MyTracker())Without an injected tracker, BudgetGuard uses a built-in _SimpleBudgetTracker.
Inject a custom model selection strategy into ThompsonSamplingSelector:
from model_router_ai import ThompsonSamplingSelector, ModelSelector
class MySelector: # satisfies ModelSelector protocol
async def select(self, candidates: list[str]) -> str: ...
async def record(self, model_id: str, success: bool) -> None: ...
async def stats(self) -> dict: ...
router = ThompsonSamplingSelector(base, selector=MySelector())Pre-built adapters bridge the signature differences between model-router-ai's protocols and sibling FlossWare packages:
from model_router_ai.adapters import BudgetAIAdapter, StrategyAIAdapter
# budget-ai integration
from budget_ai import InMemoryBudgetTracker
router = BudgetGuard(base, tracker=BudgetAIAdapter(InMemoryBudgetTracker(max_cost=300.0)))
# strategy-ai integration
from strategy_ai import ThompsonSamplingSelector as TSSelector
router = ThompsonSamplingSelector(base, selector=StrategyAIAdapter(TSSelector()))Both adapters are optional — model-router-ai works without budget-ai or strategy-ai installed. Error handling is fail-open by default (fail_open=True on BudgetGuard).
Ready-to-use integration code lives in integrations/:
| Integration | Location | Description |
|---|---|---|
| MCP Server | integrations/mcp/ |
Exposes chat, multi_model_chat, list_models, budget_status, model_performance as MCP tools |
| CLI | integrations/cli/ |
mr-chat command with --free-only, --json, --list-models, --status |
| Skills | integrations/skills/ |
Claude Code /multi-model-query, /budget-check, /model-stats |
| CLAUDE.md | integrations/claude-code/ |
Paste-ready snippet for project integration |
See integrations/README.md and INTEGRATIONS.md for setup details.
pip install pytest
pytest tests/ -vMIT