Migrating a production LLM integration rarely starts with model changes—it starts with the client config. To switch API key from OpenRouter to n4n, you point your OpenAI-compatible client at a new base URL, swap the bearer token, and reconcile a few header and model-name conventions. The good news: both gateways speak the OpenAI chat completions schema, so the diff is small but loaded with footguns.
Step 1: Provision and store the n4n credential
Before you switch API key from OpenRouter to n4n, provision the new credential and treat it like production secrets you already manage. Create the key in the n4n.ai dashboard, copy it once, and store it in your secret manager (Vault, AWS Secrets Manager, Doppler, or at minimum a .env file outside version control).
# .env - keep both during migration
OPENROUTER_API_KEY=sk-or-v1-xxxx
N4N_API_KEY=sk-n4n-xxxx
In Python, load it explicitly so failures surface at boot, not at request time:
import os
from dotenv import load_dotenv
load_dotenv()
N4N_API_KEY = os.environ["N4N_API_KEY"]
OPENROUTER_API_KEY = os.environ["OPENROUTER_API_KEY"]
Rotate the OpenRouter key after cutover if your threat model requires it. Key scopes differ: OpenRouter issues a single project key; n4n meters per token against the account tied to the key. Do not share keys across environments without logging which deployment used which.
Step 2: Repoint the base URL and auth header
When you switch API key from OpenRouter to n4n, the base URL change is the critical step. OpenRouter serves https://openrouter.ai/api/v1. n4n.ai exposes a single OpenAI-compatible endpoint that addresses 240+ models at https://api.n4n.ai/v1. The OpenAI SDK makes this a one-liner:
from openai import OpenAI
client = OpenAI(
api_key=os.environ["N4N_API_KEY"],
base_url="https://api.n4n.ai/v1",
)
Raw curl shows the header difference clearly:
curl https://api.n4n.ai/v1/chat/completions \
-H "Authorization: Bearer $N4N_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}'
OpenRouter mandated HTTP-Referer and X-Title for app attribution. n4n ignores those headers; sending them is harmless but noisy. Strip them from your middleware to reduce request size.
If you use a non-Python stack (Node, Go, Rust), the same rule applies: change the baseURL constructor arg or the request host. Do not leave the old host in a retry client or a secondary lambda.
Step 3: Map model identifiers
Model slugs are the most common 404 source. OpenRouter uses provider/model (e.g., anthropic/claude-3.5-sonnet). n4n normalizes many to native IDs or documented aliases. Fetch the live list before writing mappings:
curl https://api.n4n.ai/v1/models -H "Authorization: Bearer $N4N_API_KEY"
A pragmatic mapping layer:
MODEL_MAP = {
"openai/gpt-4o": "gpt-4o",
"openai/gpt-4o-mini": "gpt-4o-mini",
"anthropic/claude-3.5-sonnet": "claude-3.5-sonnet",
"google/gemini-pro-1.5": "gemini-1.5-pro",
}
def to_n4n_model(or_model: str) -> str:
return MODEL_MAP.get(or_model, or_model)
Do not assume tokenization or context window parity. A model that worked at 128k on OpenRouter may resolve to a 200k backend on n4n. Validate max_tokens against the returned model card. If your system prompts rely on provider-specific quirks (e.g., Anthropic’s leading newline rule), test them post-mapping.
Step 4: Port routing directives and cache-control
OpenRouter accepted X-OpenRouter-Provider or body extensions for provider pinning. n4n honors client routing directives via headers and forwards provider cache-control hints to the upstream. If you used prompt caching on Claude through OpenRouter, you set cache_control on a message block. That same structure passes through unchanged:
resp = client.chat.completions.create(
model="claude-3.5-sonnet",
messages=[
{"role":"system","content":"You are terse.","cache_control":{"type":"ephemeral"}},
{"role":"user","content":"Debug this."}
],
)
The gateway’s automatic fallback engages when a provider is rate-limited or degraded, so hard pinning is optional. If you need deterministic routing for eval suites, send the routing header n4n documents (commonly X-Route-To) and assert the response model field matches. Cache hints are advisory; a miss returns without error but inflates token cost. Log usage.prompt_tokens_details.cached_tokens to confirm savings.
Step 5: Adjust usage metering and logging
Both gateways return an OpenAI-style usage object, but n4n applies per-token usage metering with explicit cached token breakdowns when the provider supports it. OpenRouter’s native_tokens fields are absent. Parse defensively:
u = resp.usage
cached = 0
if hasattr(u, "prompt_tokens_details") and u.prompt_tokens_details:
cached = u.prompt_tokens_details.cached_tokens or 0
print({
"model": resp.model,
"prompt": u.prompt_tokens,
"completion": u.completion_tokens,
"cached": cached,
})
If you bill customers per token, rebuild the accounting pipeline on the standard fields only. Store the system_fingerprint if present—it identifies the exact backend build, useful when a model alias silently upgrades.
Step 6: Verify the switch end-to-end
A smoke test behind CI catches drift. Send a deterministic prompt, assert shape and content, and check tokens:
def smoke_test(client):
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role":"user","content":"Reply with the single word OK."}],
max_tokens=5,
temperature=0,
)
assert r.choices[0].message.content.strip() == "OK"
assert r.usage.total_tokens > 0
print("verified", r.model, r.usage.total_tokens)
smoke_test(client)
Run the script with USE_N4N=1 and again with the variable unset to confirm no hidden OpenRouter calls. Grep the repo for openrouter.ai and sk-or- to catch hardcoded strings in infra-as-code or scheduled jobs.
Common failures and debug tactics
401 Unauthorized – Key not loaded. Print client.api_key from config; check env injection in containers.
404 model_not_found – Mapping gap or alias typo. Query /v1/models and diff against your MODEL_MAP.
Cache savings zero – Provider rejected cache_control because the block was too short or misplaced. Anthropic requires ≥1024 tokens before a breakpoint. Inspect the forwarded request in debug mode.
Latency spikes – n4n’s fallback reroutes to a secondary provider under degradation. Expect this during provider incidents; it is not a config error.
Duplicate billing – You left a proxy that adds OpenRouter headers and also calls n4n. Kill the middleware layer.
Rollback plan
Keep both keys live for 48 hours. Flag-gate the client:
USE_N4N = os.environ.get("USE_N4N") == "1"
client = OpenAI(
api_key=N4N_API_KEY if USE_N4N else OPENROUTER_API_KEY,
base_url="https://api.n4n.ai/v1" if USE_N4N else "https://openrouter.ai/api/v1",
)
Deploy with USE_N4N=0, watch error rates, then flip per service. If p99 latency regresses beyond SLO, revert the env var. Because the request schema is identical, no code change is needed for rollback.
Final checklist
- n4n key in secret manager, OpenRouter key still present
- Base URL changed in all clients and retries
-
MODEL_MAPapplied and covered by unit test - Routing/cache headers ported, attribution headers removed
- Usage logging reads
prompt_tokens/completion_tokensonly - Smoke test passes in CI with
USE_N4N=1 - Repo grep clean of
openrouter.ai
The switch API key from OpenRouter to n4n is fundamentally a configuration migration, not a code rewrite. Do it behind a flag, verify the usage object, and you keep the same application logic while gaining a broader model pool and built-in fallback.