Switching Models by Config, Not Code: A Routing Pattern
Hardcoding anthropic/claude-... inside every agent node turns a one-line model swap into a multi-file PR.
Push model choice into config (env, YAML, feature flags, or a remote map) so production can rebalance cost and capability without rewriting business logic.
Summary
Agent code should request a logical policy (fast, planner, critic) rather than a raw OpenRouter slug at every call site.
A small resolver loads the current map, optional fallbacks, and provider preferences, then issues the OpenRouter request.
Recipe
Resolve policy name → OpenRouter slug (+ extras) in one module owned by platform/config.
from dataclasses import dataclass, field
import json
import os
@dataclass(frozen=True)
class Route:
model: str
fallbacks: tuple[str, ...] = ()
provider: dict = field(default_factory=dict)
def load_routes(path: str | None = None) -> dict[str, Route]:
path = path or os.environ.get("MODEL_ROUTES_PATH", "model_routes.json")
with open(path, encoding="utf-8") as f:
raw = json.load(f)
return {
name: Route(
model=item["model"],
fallbacks=tuple(item.get("fallbacks", [])),
provider=item.get("provider", {}),
)
for name, item in raw.items()
}
def resolve(policy: str, routes: dict[str, Route] | None = None) -> Route:
routes = routes or load_routes()
try:
return routes[policy]
except KeyError as exc:
raise KeyError(f"unknown model policy: {policy}") from excWhen to reach for this:
- More than one agent or service shares models
- You change models weekly for cost or quality
- You need environment-specific maps (dev uses cheap, prod uses pinned)
- You want kill-switch overrides without redeploying containers (with a remote config source)
Working Example
model_routes.json (illustrative slugs - verify at build):
{
"fast": {
"model": "openai/gpt-4o-mini",
"fallbacks": ["google/gemini-2.5-flash"],
"provider": { "sort": "throughput" }
},
"balanced": {
"model": "anthropic/claude-sonnet-4.5",
"fallbacks": ["openai/gpt-4o-mini"],
"provider": {}
},
"strong": {
"model": "openai/gpt-4o",
"fallbacks": ["anthropic/claude-sonnet-4.5"],
"provider": { "data_collection": "deny" }
}
}import os
from openai import OpenAI
from routes import resolve # the resolver above
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.environ["OPENROUTER_API_KEY"],
)
def complete(policy: str, messages: list[dict]):
route = resolve(policy)
extra = {}
if route.fallbacks:
extra["models"] = list(route.fallbacks)
if route.provider:
extra["provider"] = route.provider
resp = client.chat.completions.create(
model=route.model,
messages=messages,
extra_body=extra or None,
)
return {
"policy": policy,
"requested": route.model,
"resolved": resp.model,
"content": resp.choices[0].message.content,
"usage": resp.usage,
}
if __name__ == "__main__":
print(complete("balanced", [{"role": "user", "content": "Name one agent stop condition."}]))What this demonstrates:
- Call sites depend on
policy, not catalog strings - Fallbacks and provider prefs travel with the route definition
- Logs capture policy, requested slug, and resolved slug
TypeScript sketch
type Route = {
model: string;
fallbacks?: string[];
provider?: Record<string, unknown>;
};
const routes = JSON.parse(process.env.MODEL_ROUTES_JSON!) as Record<string, Route>;
function resolve(policy: string): Route {
const route = routes[policy];
if (!route) throw new Error(`unknown policy: ${policy}`);
return route;
}Ship MODEL_ROUTES_JSON or a mounted file from your deploy system.
Deep Dive
Why config beats constants
| Hardcoded slugs | Config routes |
|---|---|
| PR + redeploy to change model | Edit map / flag / remote config |
| Different files drift | One source of truth |
| Hard to A/B | Swap policy weights externally |
| Secrets of "what runs prod" live in git archaeology | Explicit map versioned or audited |
Git-versioned JSON is already a large improvement.
Remote config (LaunchDarkly-style flags, SSM parameters, etc.) unlocks no-redeploy changes - add validation so bad maps fail closed.
Recommended policies
Start with three:
fast- classification, cheap drafts, high QPSbalanced- default agent brainstrong- hard reasoning, final answers, escalations
Add named routes (planner, coder, embedder) when steps diverge.
Validation rules
REQUIRED = {"model"}
def validate_routes(routes: dict) -> None:
for name, item in routes.items():
missing = REQUIRED - set(item)
if missing:
raise ValueError(f"{name} missing {missing}")
if not isinstance(item["model"], str) or "/" not in item["model"]:
raise ValueError(f"{name} model does not look like an OpenRouter slug")Reject deploy if validation fails.
Never silently fall back to a mystery default in production.
OpenRouter features that belong in config
| Field | Put in route config? |
|---|---|
model | Yes |
models fallbacks | Yes |
provider preferences | Yes |
plugins / auto-router knobs | Yes when used |
| System prompts | Usually separate prompt config |
| Tool schemas | Code or versioned schema registry |
Hot reload vs process restart
- Restart-based: simplest; ship new file with deploy
- Periodic reload: reread file every N minutes with checksum
- Push-based: listen to config service; still validate before swap
Agents mid-run can keep the route snapshot from start-of-run for consistency.
Gotchas
- Config without evals. Cheaper models fail different tools. Fix: gate map changes with a golden suite.
- Overrides forever. Emergency slug overrides become permanent. Fix: expiry and tickets.
- Divergent maps per service. "fast" means three different models. Fix: shared package or config repo.
- Logging only the policy name. Finance cannot price the run. Fix: log resolved slug and usage.
- Putting secrets in the routes file. Only model ids belong there. Fix: keys stay in secret stores.
- Auto-router as every policy. Evals become non-deterministic. Fix: pin models for scored paths; use auto for sandbox traffic.
- Circular fallbacks in app code plus OpenRouter
models. Double failover storms. Fix: prefer one layer for model failover.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Config route map (this page) | Multi-agent systems | One-off notebook |
| Hardcoded constants | Spike lasting hours | Anything shared |
openrouter/auto only | Highly mixed prompts | Strict SLA + evals |
| Full multi-provider abstraction | Multi-gateway future | Day-one OpenRouter-only app |
| Per-request user-selected model | Productized model picker | Untrusted free-form spend |
FAQs
Should routes live in the monorepo?
Yes for early teams.
Add remote overrides later when on-call needs faster changes than deploys allow.
How do I change models without downtime?
Deploy a new map with rolling restart, or hot-reload a validated remote config and pin in-flight runs to their snapshot.
Where do temperature and max tokens go?
Either beside the route as params or in a separate prompt/profile config.
Keep sampling defaults explicit per policy.
Can policies point at openrouter/auto?
Yes for exploratory policies.
Avoid for compliance-sensitive or tightly evaluated paths unless you constrain allowed_models.
How do feature flags fit?
Flag chooses policy name (balanced vs strong), not raw slug strings in flag payloads when possible.
What about multimodal or embedding models?
Use separate policies (vision, embed) with their own slugs and endpoints.
How do I prevent a bad map from shipping?
CI validation + canary traffic + automatic rollback on error rate / eval score.
Is env-only configuration enough?
For a single model, DEFAULT_MODEL env works.
Multiple policies need a structured map.
Should each LangGraph node hardcode a policy string?
Hardcoding the policy name is fine.
Hardcoding the slug is not.
How does this interact with OpenRouter presets?
Presets are server-side config on OpenRouter.
App-level policies remain useful for multi-service consistency and offline validation.
Related
- Model-Swapping Rules for Cost and Capability Flexibility - fundamentals rules behind the pattern
- Model Slugs and Naming Conventions on OpenRouter - slug grammar
- Calling OpenRouter from Python and TypeScript SDKs - request wiring
- Auto-Router: Letting OpenRouter Pick the Best Model for a Prompt - when config should hand off selection
- OpenRouter Setup & Routing Best Practices - checklist form
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.