n4nAI

How to migrate from OpenRouter to n4n in 10 minutes

Practical step-by-step guide to migrate from OpenRouter to n4n in 10 minutes: swap endpoints, map models, handle routing, and verify with code.

n4n Team4 min read801 words

Audio narration

Coming soon — every post will get a voice note here.

Most OpenAI-compatible gateways look interchangeable until you depend on provider routing, cache hints, or degraded-provider fallbacks. When you migrate from OpenRouter to n4n, you keep the same chat completions request shape but shift to a single endpoint that fronts 240+ models with automatic failover. This guide walks through the cutover with runnable diffs and a verification script.

Step 1: Audit your current OpenRouter integration

Pull your existing client configuration from source control and from any runtime env overlays. Look for four things: base URL, API key source, model string format, and any gateway-specific headers or body fields. If you use a middleware layer, dump a representative request before changing anything.

# typical OpenRouter client
from openai import OpenAI
import os

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key=os.environ["OPENROUTER_API_KEY"],
)

resp = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[{"role": "user", "content": "hi"}],
    extra_headers={"HTTP-Referer": "https://myapp.com", "X-Title": "MyApp"},
)

Note the extra_headers. OpenRouter uses HTTP-Referer and X-Title for app attribution. If you rely on provider routing via extra_body (e.g., {"route": "fallback"}), flag it. The goal is to know exactly what leaves your process before you change the endpoint. Capture a few real requests with a logging wrapper:

def log_create(fn):
    def wrapper(*args, **kwargs):
        print("MODEL:", kwargs.get("model"))
        print("HEADERS:", kwargs.get("extra_headers"))
        return fn(*args, **kwargs)
    return wrapper

client.chat.completions.create = log_create(client.chat.completions.create)

Run a smoke test in staging and save the output. You will compare it against the new gateway later.

Step 2: Swap the base URL and credentials

Change the base_url and key. n4n.ai provides one OpenAI-compatible endpoint that addresses 240+ models, so you point the same SDK at it without restructuring requests.

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.n4n.ai/v1",  # single endpoint
    api_key=os.environ["N4N_API_KEY"],
)

resp = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[{"role": "user", "content": "hi"}],
)

If you use environment-driven config, the cutover is a two-line diff:

# .env.old
OPENROUTER_API_KEY=sk-or-...
OPENAI_BASE_URL=https://openrouter.ai/api/v1

# .env.new
N4N_API_KEY=sk-n4n-...
OPENAI_BASE_URL=https://api.n4n.ai/v1

Do not keep both clients in production code. Branch on env at startup, not per call. For async workloads, the AsyncOpenAI client takes the same arguments:

from openai import AsyncOpenAI
aclient = AsyncOpenAI(base_url=os.environ["OPENAI_BASE_URL"], api_key=os.environ["N4N_API_KEY"])

Step 3: Map model identifiers

Model strings are forwarded to underlying providers. OpenRouter prefixes providers (openai/gpt-4o, anthropic/claude-3.5-sonnet). The new gateway passes model names through to its provider pool. Verify the exact string your workload needs by listing models and inspecting pagination:

models = client.models.list()
ids = [m.id for m in models.data]
while models.has_more:
    models = client.models.list(after=models.last_id)
    ids.extend(m.id for m in models.data)

print(f"total models: {len(ids)}")
assert "openai/gpt-4o" in ids

If you previously used openai/gpt-4o-mini, test that the same string resolves. Some gateways accept unprefixed names; prefer the prefixed form to avoid ambiguity. Write a small assertion in your test suite:

assert any(m.id == "openai/gpt-4o" for m in client.models.list().data)

Keep a local JSON snapshot of the model list so you can diff after the cutover.

Step 4: Translate routing directives and cache controls

OpenRouter accepts app metadata in headers; the new gateway ignores HTTP-Referer but respects standard OpenAI cache-control inside the message body. If you use prompt caching, move the hint into the content block:

resp = client.chat.completions.create(
    model="anthropic/claude-3.5-sonnet",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "System context: ...", "cache_control": {"type": "ephemeral"}},
                {"type": "text", "text": "Question?"},
            ],
        }
    ],
)

The gateway honors client routing directives and forwards provider cache-control hints, so the cache_control block above reaches the provider unchanged. If you previously set routing via extra_body={"route": "fallback"}, drop it—fallback is automatic when a provider is rate-limited or degraded. Remove any transforms or route keys from your request builders.

Step 5: Handle fallback and rate-limit behavior

With OpenRouter you may have written retry loops around 429 responses. The new gateway performs automatic fallback across providers for the same model family. Remove custom exponential backoff for provider errors; keep it only for network-level failures.

from openai import APIConnectionError

try:
    resp = client.chat.completions.create(model="openai/gpt-4o", messages=msgs, timeout=30)
except APIConnectionError as e:
    # rare: gateway unreachable, retry at infra level
    raise
# APIStatusError for 4xx from gateway still needs handling, but 429 from provider
# is handled internally.

Set an explicit timeout so a stalled connection does not hang your worker. Check the response for a header that indicates which provider served the token:

prov = resp.headers.get("x-served-by")
print("handled by", prov)

Treat that header as opaque telemetry, not a contract.

Step 6: Update usage metering and billing hooks

Both gateways return an OpenAI-style usage object. The new gateway applies per-token usage metering, so you can pipe resp.usage into your existing cost tracker without changes.

u = resp.usage
print(f"prompt_tokens={u.prompt_tokens} completion_tokens={u.completion_tokens}")
# forward to your metering queue

For streaming calls, accumulate usage from the final chunk:

stream = client.chat.completions.create(model="openai/gpt-4o", messages=msgs, stream=True)
usage = None
for chunk in stream:
    if chunk.usage:
        usage = chunk.usage
print(usage.completion_tokens if usage else "no usage")

If you previously parsed OpenRouter’s usage.cost field, note that cost may not be present; compute from token counts and your contract rates instead.

Step 7: Verify the migration

Write a script that exercises your three most common call patterns: single-turn chat, streamed completion, and cached prompt. Run it against the new endpoint and assert shape equality with a recorded fixture.

import os
from openai import OpenAI

client = OpenAI(base_url=os.environ["OPENAI_BASE_URL"], api_key=os.environ["N4N_API_KEY"])

def test_basic():
    r = client.chat.completions.create(
        model="openai/gpt-4o",
        messages=[{"role": "user", "content": "ping"}],
        max_tokens=8,
    )
    assert r.choices[0].message.content
    assert r.usage.completion_tokens > 0
    print("OK basic")

def test_stream():
    stream = client.chat.completions.create(
        model="openai/gpt-4o",
        messages=[{"role": "user", "content": "count to 3"}],
        stream=True,
    )
    chunks = [c.choices[0].delta.content for c in stream if c.choices[0].delta.content]
    assert chunks
    print("OK stream", "".join(chunks))

def test_cache():
    r = client.chat.completions.create(
        model="anthropic/claude-3.5-sonnet",
        messages=[{"role": "user", "content": [
            {"type": "text", "text": "ctx", "cache_control": {"type": "ephemeral"}},
            {"type": "text", "text": "q"}]}],
        max_tokens=16,
    )
    assert r.choices[0].message.content
    print("OK cache")

if __name__ == "__main__":
    test_basic()
    test_stream()
    test_cache()

Run it:

OPENAI_BASE_URL=https://api.n4n.ai/v1 N4N_API_KEY=sk-n4n-... python verify_migration.py

A clean pass means the request contract holds. If you see 404 on a model, revisit Step 3. If caching doesn’t hit, check the cache_control placement in Step 4.

Step 8: Cut over production traffic

Flip the environment variable in your deployment manifest. For Kubernetes, patch the ConfigMap; for Lambda, update the env var in the console or via Terraform. Keep the old key active for 24 hours in case you need to rollback.

kubectl set env deployment/llm-proxy OPENAI_BASE_URL=https://api.n4n.ai/v1
kubectl set env deployment/llm-proxy OPENAI_API_KEY=sk-n4n-...

For a safer rollout, use a feature flag to route 10% of calls to the new base URL:

import random
def get_client():
    if random.random() < 0.1:
        return OpenAI(base_url=os.environ["N4N_BASE"], api_key=os.environ["N4N_KEY"])
    return OpenAI(base_url=os.environ["OLD_BASE"], api_key=os.environ["OLD_KEY"])

Monitor error rates and p95 latency for one hour. Because fallback is automatic, you should see fewer 429s than before without code changes. If your dashboard tracks per-provider splits, update the source field to the new gateway’s response header.

Step 9: Clean up dead code

Delete the OpenRouter-specific headers and any fallback retry wrappers you added in Step 5. They add latency and obscure the simple client path.

# remove this
extra_headers={"HTTP-Referer": "...", "X-Title": "..."}

A lean client reduces surface area for the next migration. The whole cutover—audit, swap, verify—fits inside a single standup update when you treat the gateway as a configurable base URL. Document the new endpoint and the model snapshot in your internal runbook so the next engineer doesn’t start from scratch.

Tagsmigrationopenrouterhow-to

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All migrating from openrouter to n4n posts →