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.
Search across all documentation pages
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.
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.
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:
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:
policy, not catalog stringstype 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.
| 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.
Start with three:
fast - classification, cheap drafts, high QPSbalanced - default agent brainstrong - hard reasoning, final answers, escalationsAdd named routes (planner, coder, embedder) when steps diverge.
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.
| 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 |
Agents mid-run can keep the route snapshot from start-of-run for consistency.
models. Double failover storms. Fix: prefer one layer for model failover.| 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 |
Yes for early teams.
Add remote overrides later when on-call needs faster changes than deploys allow.
Deploy a new map with rolling restart, or hot-reload a validated remote config and pin in-flight runs to their snapshot.
Either beside the route as params or in a separate prompt/profile config.
Keep sampling defaults explicit per policy.
Yes for exploratory policies.
Avoid for compliance-sensitive or tightly evaluated paths unless you constrain allowed_models.
Flag chooses policy name (balanced vs strong), not raw slug strings in flag payloads when possible.
Use separate policies (vision, embed) with their own slugs and endpoints.
CI validation + canary traffic + automatic rollback on error rate / eval score.
For a single model, DEFAULT_MODEL env works.
Multiple policies need a structured map.
Hardcoding the policy name is fine.
Hardcoding the slug is not.
Presets are server-side config on OpenRouter.
App-level policies remain useful for multi-service consistency and offline validation.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026