Building lcel n4n.ai model routing gpt-4o llama lets you send simple classification prompts to Llama 3.3 and escalate to GPT-4o only when the task needs stronger reasoning. This walkthrough shows how to assemble that logic with LangChain Expression Language (LCEL) and an OpenAI-compatible gateway. You’ll end up with a single chain that picks the right model per request without changing caller code.
Step 1: Install dependencies and configure the endpoint
Install the minimal LangChain packages. You do not need the full langchain metapackage.
pip install langchain-openai langchain-core python-dotenv
Set your API key for the gateway. The endpoint is OpenAI-compatible, so ChatOpenAI works unchanged aside from base_url:
import os
from dotenv import load_dotenv
load_dotenv()
os.environ["N4N_API_KEY"] = os.getenv("N4N_API_KEY", "")
BASE_URL = "https://api.n4n.ai/v1"
Keep the key out of source control. The gateway fronts 240+ models, but we will pin two.
Step 2: Instantiate the two chat models
Use explicit model strings. On OpenRouter-class gateways the namespace typically prefixes the provider. Adjust if your deployment uses different slugs.
from langchain_openai import ChatOpenAI
llama = ChatOpenAI(
model="meta-llama/llama-3.3-70b-instruct",
temperature=0,
base_url=BASE_URL,
api_key=os.environ["N4N_API_KEY"],
max_tokens=1024,
)
gpt4o = ChatOpenAI(
model="openai/gpt-4o",
temperature=0,
base_url=BASE_URL,
api_key=os.environ["N4N_API_KEY"],
max_tokens=1024,
)
Set temperature=0 for deterministic routing tests. In production you may relax that per model.
Step 3: Define routing predicates
Routing logic should be cheap and side-effect free. A common pattern: use Llama for short, factual prompts and GPT-4o for anything requiring multi-step analysis.
def needs_powerful_model(x: dict) -> bool:
prompt = x.get("prompt", "")
if not isinstance(prompt, str):
prompt = str(prompt)
# Escalate on explicit ask or length.
if "analyze" in prompt.lower() or "reason" in prompt.lower():
return True
if len(prompt.split()) > 40:
return True
return False
Why heuristic routing instead of an LLM judge
A second model call to decide routing adds latency and cost. For most workloads a regex or token-count check captures 90% of the value. Reserve learned routers for when you have clear failure data.
Step 4: Compose the branch with LCEL
RunnableBranch is the native LCEL primitive for conditional execution. The first tuple whose predicate returns True wins; the final positional argument is the default.
from langchain_core.runnables import RunnableBranch, RunnablePassthrough
router = RunnableBranch(
(needs_powerful_model, gpt4o),
llama,
)
chain = RunnablePassthrough.assign(model=RunnableLambda(lambda x: "gpt-4o" if needs_powerful_model(x) else "llama-3.3")) | router
The RunnablePassthrough.assign step annotates which model will handle the request. This is useful for logging without parsing the response.
Handling structured input
If your caller passes a dict with more fields, keep the branch signature consistent:
def route_fn(x: dict) -> ChatOpenAI:
return gpt4o if needs_powerful_model(x) else llama
router = RunnableBranch(
(lambda x: needs_powerful_model(x), gpt4o),
llama,
)
RunnableBranch accepts callables directly; wrapping in lambdas is optional.
Step 5: Add streaming and metadata passthrough
LCEL chains stream if every link supports it. Both ChatOpenAI instances do. Wrap the chain in .with_config({"callbacks": [...]}) or just .stream():
for chunk in chain.stream({"prompt": "Summarize the RFC briefly."}):
print(chunk.content, end="", flush=True)
The gateway returns provider response metadata. Access it after invocation:
resp = chain.invoke({"prompt": "What is the capital of France?"})
print(resp.response_metadata.get("model"))
print(resp.response_metadata.get("usage"))
Per-token usage appears in usage. The gateway meters tokens accurately even when it forwards provider cache-control hints, so your billing matches what you see in the response.
Step 6: Execute and verify success
Run two contrasting prompts and confirm the branch selects different models.
simple = chain.invoke({"prompt": "What is 2+2?"})
complex_p = chain.invoke({"prompt": "Analyze the geopolitical implications of the recent oil supply shift over the last decade in detail, including secondary economic effects."})
print("Simple model:", simple.response_metadata.get("model"))
print("Complex model:", complex_p.response_metadata.get("model"))
Verification checklist:
- The simple prompt resolves to
meta-llama/llama-3.3-70b-instruct(or your slug). - The complex prompt resolves to
openai/gpt-4o. - Both responses contain
usage.prompt_tokensandusage.completion_tokensgreater than zero. - Streaming yields incremental
AIMessageChunkobjects without errors.
If both return the same model, your predicate is too loose or too strict—adjust the word threshold.
Step 7: Production hardening
The core of lcel n4n.ai model routing gpt-4o llama is now a single Runnable you can drop into a FastAPI app or a LangServe endpoint. A few notes from shipping this:
Timeouts: Set request_timeout on ChatOpenAI (e.g., 30s). LCEL propagates exceptions; wrap with RunnableRetry if you want automatic retries on transient 5xx.
Fallback: The gateway automatically falls back when a provider is rate-limited or degraded, but that is separate from your branch. Your branch picks the primary model; the gateway handles provider-level resilience. Do not build your own provider ping logic.
Cache control: Forward cache hints by passing extra_body={"cache": {"type": "ephemeral"}} to ChatOpenAI if your workload repeats system prompts. The gateway honors client routing directives and forwards those hints.
Observability: Log the model annotation from Step 4 alongside the response latency. Over a week you will see exactly how often GPT-4o is invoked—typically far less than 20% on support-style traffic.
Closing implementation note
You now have a composable, testable routing layer. Because everything is LCEL, you can swap RunnableBranch for a RunnableParallel that calls both models and picks the better answer later, or insert a RunnableLambda that rewrites prompts per model. The routing decision stays declarative and the caller sends one dict.
To extend, add more branches: a small embedding model for semantic routing, or a fine-tuned classifier. The pattern does not change.