Migrating a production LLM integration off OpenRouter requires a disciplined OpenRouter to n4n model name mapping pass before you touch any traffic. The two gateways speak OpenAI-compatible APIs, but model identifiers and routing directives are not byte-for-byte identical, and silent mismatches will surface as 404s or surprise billing.
Step 1: Inventory every model string your code sends
Start by extracting all literal model references from source, config, and persisted prompts. A regex scan catches most cases, but structured config (YAML, JSON) often hides model names inside environment overrides.
grep -rEn "models?['\"]?\s*[:=]\s*['\"][a-z0-9/-]+['\"]" ./src ./config | sort -u
For Python services that build requests dynamically, static grep misses runtime concatenation. Trace the call sites of client.chat.completions.create and dump the model argument from a staging replay:
import ast, json
class ModelVisitor(ast.NodeVisitor):
def __init__(self):
self.models = set()
def visit_Call(self, node):
for kw in node.keywords:
if kw.arg == "model" and isinstance(kw.value, ast.Constant):
self.models.add(kw.value.value)
self.generic_visit(node)
tree = ast.parse(open("app/llm.py").read())
v = ModelVisitor()
v.visit(tree)
print(json.dumps(sorted(v.models), indent=2))
The output is your cutover checklist. Do not skip models referenced in feature flags or A/B buckets; those light up under rare code paths. If you use a central LLM wrapper, the inventory may be small. If you let product teams call the gateway directly, expect a long tail of forgotten slugs.
Step 2: Translate OpenRouter model IDs to n4n’s namespace
OpenRouter uses a provider/model slug (e.g., anthropic/claude-3.5-sonnet, openai/gpt-4o-mini). n4n.ai exposes a single OpenAI-compatible endpoint that addresses 240+ models, and its model field uses the same provider-prefixed convention for the majority of public models. The OpenRouter to n4n model name mapping work is confirming each slug exists in the target catalog and resolving aliases.
Build an explicit mapping table rather than assuming parity:
{
"openai/gpt-4o": "openai/gpt-4o",
"openai/gpt-4o-mini": "openai/gpt-4o-mini",
"anthropic/claude-3.5-sonnet": "anthropic/claude-3.5-sonnet",
"google/gemini-pro-1.5": "google/gemini-1.5-pro",
"mistralai/mixtral-8x7b-instruct": "mistralai/mixtral-8x7b-instruct-v0.1"
}
Differences appear in version suffixes and provider renames. Pull the live catalog from the gateway’s /v1/models endpoint and diff programmatically:
import openai, json
# Point the client at the target gateway
client = openai.OpenAI(base_url="https://api.n4n.ai/v1", api_key="YOUR_KEY")
remote = {m.id for m in client.models.list().data}
mapping = json.load(open("mapping.json"))
missing = [src for src in mapping if mapping[src] not in remote]
assert not missing, f"Unmapped or renamed: {missing}"
If a source slug has no target equivalent, decide on a functional substitute (same context window class, similar capability) and record the rationale in the mapping file. This prevents last-minute swaps that change output quality. Store the table in version control; treat it as a schema migration, not a throwaway script.
Step 3: Replicate routing rules with client directives
OpenRouter requests often embed routing preferences in the body: route: "fallback" or a models array for load balancing. n4n honors client routing directives, so the same JSON shape forwards without a shim. The key is to map the primary and secondary models using the table from Step 2.
Example fallback request against the new gateway:
{
"model": "anthropic/claude-3.5-sonnet",
"route": "fallback",
"models": [
"anthropic/claude-3.5-sonnet",
"openai/gpt-4o"
],
"messages": [{"role": "user", "content": "Summarize this ticket."}]
}
If your OpenRouter integration used a provider pin via header or body field, convert it to the equivalent directive in the new call. Test the degraded path explicitly: block the primary at the network layer (or use a bad key for that model only) and confirm the gateway returns a valid completion from the secondary.
For weighted routing, keep the structure identical to what your original client sent. The gateway forwards it. Do not invent new fields—stick to the documented request shape. A common mistake is leaving the old model field as the OpenRouter-only alias after you meant to switch; the mapping table should be applied to every occurrence, including inside models arrays.
This OpenRouter to n4n model name mapping step is where most silent regressions hide: a fallback list that still references a retired slug will fail closed only when the primary dies.
Step 4: Forward provider cache-control hints
Both gateways pass through provider-native cache hints. Anthropic’s cache_control on a system message is the common case. When you map model names, the hint stays in the same position:
resp = client.chat.completions.create(
model="anthropic/claude-3.5-sonnet",
messages=[
{"role": "system", "content": "You are a terse API copilot.",
"cache_control": {"type": "ephemeral"}},
{"role": "user", "content": "Explain idempotency keys."}
],
)
If you previously relied on automatic prompt caching for certain model families, verify the target gateway forwards the hint unchanged. A quick check: send the same request twice and inspect the usage blob for cache_read_tokens or equivalent. If the counter does not move, the hint was stripped—usually a client-side serialization bug, not a gateway deficit.
For OpenAI models, cache hints are sometimes expressed via the system message cache_control beta field or via specific prompt prefixes. Keep the exact payload. The mapping exercise should not alter message structure, only the model string and routing envelopes.
Step 5: Swap the endpoint and run a shadow test
Change the base_url in your OpenAI client constructor. Keep the old OpenRouter client intact behind a flag so you can run parallel requests during cutover.
from openai import OpenAI
def make_client(use_n4n: bool) -> OpenAI:
if use_n4n:
return OpenAI(base_url="https://api.n4n.ai/v1", api_key=N4N_KEY)
return OpenAI(base_url="https://openrouter.ai/api/v1", api_key=OR_KEY)
# Shadow mode: call both, compare status
def chat_shadow(model, messages):
src = make_client(False); dst = make_client(True)
mapped = MODEL_MAP[model]
a = src.chat.completions.create(model=model, messages=messages)
b = dst.chat.completions.create(model=mapped, messages=messages)
assert a.choices[0].message.content[:50] == b.choices[0].message.content[:50]
return b
Run this against a captured traffic sample. Any mismatch in model behavior should be traced to a mapping error or a routing directive that was not forwarded. Capture latency percentiles from both paths; the new gateway should be within your error budget, not necessarily faster.
A clean OpenRouter to n4n model name mapping leaves no model string unaccounted for and no routing rule implicit. Do the inventory, encode the table, forward the directives, and shadow before you flip the flag.
Verify success
Success is not “it returns 200.” Confirm these three things:
- Model coverage – The
/v1/modelsdiff in Step 2 showed zero missing targets, and production logs show nomodel_not_founderrors after the switch. - Routing behavior – Inject a failure of the primary model and watch the request succeed via fallback within your latency SLO.
- Metering accuracy – Pull per-token usage from the gateway’s metering endpoint (or the
usagefield in responses) and reconcile against your expected call volume. Per-token usage metering lets you tie out to the minute.
If all three hold, retire the OpenRouter client. Keep the mapping file as the record of what changed during the migration.