For more than two years, the canonical way to reach OpenRouter from LangChain was a one-line hack: point ChatOpenAI at OpenRouter's base URL and move on. It worked just well enough to spread — and just badly enough to quietly corrupt your traces.
Why the base_url Override Misled Your Telemetry
The base_url override sent your requests to OpenRouter but lied about who answered them. The pattern — ChatOpenAI(base_url='https://openrouter.ai/api/v1') — reused the OpenAI client against a different gateway, so LangChain recorded the provider as OpenAI regardless of which backend actually served the call. LangChain's own announcement (GitHub Discussion #35672, March 2026) names the two core failure modes as "response-schema mismatches and misattributed provider identity" in traces .
The trick survived because it handled the easy path. Plain text completions round-tripped fine, which is why so many tutorials shipped it. It broke silently on everything OpenRouter-specific: reasoning content, session IDs, and routing metadata were dropped or mangled, because the OpenAI response schema has no place to put them . You got an answer; you did not get the fields needed to audit cost, latency, or which model your request was routed to.
"[The dedicated package] solves this with a dedicated integration built on the official OpenRouter Python SDK," — LangChain team, integration announcement (source: GitHub Discussion #35672).
The deeper reason the hack metastasized is that no blessed alternative existed. The only prior attempt, langchain-openrouter 0.0.1, shipped in February 2024 and was yanked as outdated . A real first-party package did not arrive until 0.1.0 on March 4, 2026 . Once it landed, maintenance moved fast: 0.1.0 → 0.2.0 → 0.2.1 → 0.2.2 → 0.2.3 → 0.2.4, six releases in under four months, with 0.2.4 published June 23, 2026 . That cadence is the real signal — multi-provider routing is now maintained framework surface, and the base_url trick is officially obsolete.
0.2.4 Under the Microscope

The June 23, 2026 release is incremental, not a redesign — but three of its changes have direct consequences for agentic code. The headline diff bumps the underlying openrouter Python SDK floor to >=0.9.2 and deletes a file-based workaround that earlier builds carried . If your project pinned or vendored that workaround explicitly, remove it: leaving it in place can conflict with the new install.
The most functionally meaningful addition is that parallel_tool_calls now surfaces on bind_tools() . It was previously absent, which meant agentic workloads that fan out several tool invocations in a single turn fell back to sequential execution even when the underlying model supported parallelism. If you run multi-tool agents, this flag is the difference between one round-trip and several.
The release also adds test coverage for cache_control passthrough on tool definitions . This is not a new code path — prompt caching on tool schemas already existed — but it was untested, meaning the behavior was brittle and could silently break on an SDK upgrade. Coverage is the point here; for agents that cache large tool schemas to cut latency and token spend, an untested passthrough is a production liability.
0.2.4 builds on a stable streaming baseline rather than re-architecting it. The immediately prior 0.2.3, on May 1, 2026, fixed streaming by merging fragmented reasoning_details chunks and added session_id and trace fields . With that in place, 0.2.4 layers on tool-calling fidelity instead of touching the stream handling.
One housekeeping item rounds out the diff: a model-profile refresh. Before deploying, confirm that any pinned model IDs in your config still resolve. OpenRouter warns that unversioned model aliases can silently shift to newer checkpoint versions and that it will keep routing at a changed rate unless you constrain or pin your choice — so explicit, versioned model IDs are the safer default .
How OpenRouter Picks Between Backends: Uptime × Cheapness
OpenRouter's default routing is uptime-aware but cost-biased — not random, and not strictly cheapest. For any model served by multiple backends, it first deprioritizes providers that have had significant outages in the last 30 seconds, then selects among the remaining lowest-cost candidates with each weighted by the inverse square of its price . The result is load balancing that leans toward cheap endpoints while still spreading traffic, so a single provider's latency spike or rate-limit wall does not stall every request.
The weighting is concrete. OpenRouter's own example: a $1-per-million-token provider is roughly 9× more likely to be routed first than a $3 provider, because inverse-square weighting amplifies small price gaps. Meanwhile a $2 provider that was failing seconds ago may be skipped entirely, even though its price alone would qualify it . Probability, not a fixed ranking, decides each call — which is exactly why your traces need to record the provider that actually served a response rather than assume the cheapest one did.
You can trade that resilience for determinism. Setting either sort or order disables the load-balancing weighting: order pins an explicit provider sequence, and sort forces a single axis — price, throughput, or latency. OpenRouter exposes shorthands for the two most common cases: the :floor slug suffix is equivalent to sorting by price, and :nitro to sorting by throughput . Determinism is useful for reproducibility, but it removes the automatic failover that the default algorithm gives you for free.
| Control | Effect | Hard or soft |
|---|---|---|
| Default (no sort/order) | Outage-deprioritize, then inverse-square price weighting | Probabilistic |
sort / :floor / :nitro | Disables balancing; deterministic single-axis ordering | Deterministic |
preferred_max_latency / preferred_min_throughput | Steers selection by rolling percentiles | Soft (prefer) |
max_price | Rejects providers above the ceiling | Hard (reject) |
Latency and throughput controls are soft. preferred_max_latency and preferred_min_throughput steer selection using rolling p50/p75/p90/p99 percentiles measured over a 5-minute window, but they only prefer compliant providers — they will not hard-block a request if none qualify . Among the routing knobs, only max_price causes an outright rejection rather than a best-effort preference, which is the distinction that matters once you start encoding real budget guardrails.
Budget Guardrails: max_price, ZDR, and Strict Rejection

max_price is a hard ceiling, not a preference: you pass prompt, completion, request, and image rates in USD per million tokens — for example {prompt: 1, completion: 2} caps prompts at $1/M and completions at $2/M — and OpenRouter rejects every backend priced above those limits. If no provider qualifies, the request errors out instead of silently routing to a pricier endpoint . That failure mode is the entire point. The prefer-style fields covered earlier (preferred_max_latency, preferred_min_throughput) degrade gracefully; max_price does not, which is exactly what a finance-enforced spend ceiling needs.
| Field | Behavior when no backend qualifies | Use case |
|---|---|---|
max_price | Request fails (strict rejection) | Hard spend ceiling |
preferred_max_latency | Falls back to best available | Soft performance bias |
zdr / data_collection | Routes away from non-compliant providers | Data-residency policy |
only / ignore | Allowlist / blocklist per request | Provider governance |
The compliance knobs work the same composable way. data_collection and zdr (Zero Data Retention) steer requests away from backends that log or retain inference data, letting you encode GDPR, HIPAA, or contractual data-residency constraints in the routing policy rather than in application code . The only and ignore lists add per-request provider allowlists and blocklists. Stack them together — max_price plus zdr plus an only allowlist — and each call carries its own policy envelope: a cost ceiling, a data-retention rule, and a sanctioned-provider set, all evaluated before a single token is generated.
One caveat makes max_price more than a convenience. OpenRouter warns that if a model's price changes, it keeps routing and bills the new rate unless you constrain the choice — so a request with neither a max_price ceiling nor an explicit, versioned model ID can produce unexpected charges silently when a provider reprices or a model version rolls forward . Pinning a concrete model ID avoids surprise version changes; pairing it with max_price caps the bill even if list prices move underneath you . Treat the model catalog as volatile infrastructure, and these two fields become the difference between a budget you declare and a budget the gateway actually enforces.
LiteLLM vs OpenRouter: Both Now Officially-Blessed Adapters
As of March 2026, LangChain ships two maintained routing adapters where six months earlier there were none. Alongside langchain-openrouter, the team adopted langchain-litellm — originally a community project by Akshay Dongare — into the langchain-ai org, giving developers two officially-supported paths off the old glue code . The split is clean: OpenRouter is a hosted gateway you call; LiteLLM is a proxy/library you run.
Quick Answer: Pick OpenRouter for managed multi-backend access with zero ops — 400+ models across 70+ providers behind one key at a 5.5% pay-as-you-go fee. Pick LiteLLM when you need on-prem deployment, per-team or per-tag budget attribution, or BYOK compliance you control yourself.
OpenRouter is the no-ops option. It fronts 400+ active models across 70+ providers and passes inference through at provider list prices with no per-token markup, adding a 5.5% pay-as-you-go platform fee instead . Its free tier lists 25+ free models and 50 requests per day, while BYOK pay-as-you-go grants 1M free requests per month before a 5% fee . You write none of the routing infrastructure; you accept that someone else operates it.
LiteLLM moves that machinery into your own deployment. Its router documents load balancing across providers with cooldowns, fallbacks, retries, and Redis-backed TPM/RPM tracking . The budgeting is where it pulls ahead for finance-conscious teams:
- Provider budgets — cap each backend independently, e.g. $100/day for OpenAI and $100/day for Azure, tracked in Redis over windows like 1d or 30d .
- Tag budgets — attribute spend to a workload, e.g. $10/day for
product:chat-bot, skipping providers over budget and erroring when all exceed it . - Virtual keys — per-key, per-user, and per-team spend in USD via
completion_cost(), withmax_budgetdefaults and upper bounds .
The decision rule follows the constraint that actually binds you. "Multi-provider routing is no longer a bring-your-own-glue concern — the framework now ships maintained adapters for it," notes the LangChain team in the adoption announcement (source: LangChain Discussion #35672). Choose OpenRouter when managed access with zero infrastructure overhead is the priority. Choose LiteLLM when on-prem deployment, fine-grained budget attribution per team or tag, or BYOK compliance requirements apply.
The Swap: Replacing base_url With ChatOpenRouter

Migrating off the base_url hack is a small diff with outsized telemetry payoff. Install the package with pip install 'langchain-openrouter>=0.2.4'; it requires Python >=3.10 and <4.0 and ships under an MIT license . The constructor surface is intentionally close to ChatOpenAI, so most call sites change in one line:
# Before — the base_url override
llm = ChatOpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=OR_KEY,
model="openai/gpt-4o",
)
# After — the first-party integration
llm = ChatOpenRouter(
model="openai/gpt-4o",
openrouter_api_key=OR_KEY,
)Provider routing rides along as a kwarg rather than buried in model_kwargs: pass openrouter_provider={...} on the constructor to set order, max_price, or sort preferences as first-class fields . For agentic workloads, keep using bind_tools() — as of 0.2.4 it correctly forwards parallel_tool_calls, and tool definitions now carry cache_control passthrough .
Streaming works without intervention after the 0.2.3 fix that merged fragmented reasoning_details on May 1, 2026, so delete any custom chunk-reassembly logic you wrote to paper over that bug .
The acceptance test is attribution, not output. After upgrading, open a LangSmith trace and confirm the run records the actual OpenRouter sub-provider — Anthropic, Google, or whichever endpoint served the call — instead of a flat openai identity. If traces still read openai, a stray ChatOpenAI+base_url instance is still wired in somewhere upstream in your chain, and the dedicated integration's correct provider identification never takes effect .
The Telemetry Blind Spot When a Proxy Selects Your Endpoint
Correct provider attribution in langchain-openrouter 0.2.4 records which OpenRouter route you requested — not always which sub-provider physically executed the call. When allow_fallbacks is true, OpenRouter can try models in priority order on errors such as context-length validation, moderation flags, rate limiting, and downtime, billing the model ultimately used . Your trace captures intent; the served endpoint can differ silently. That is the blind spot: routing reduces waste from overusing premium models but can hide which provider answered unless you capture the telemetry yourself.
Close the gap with the correlation fields the integration already exposes. The prior 0.2.3 release (May 1, 2026) added session_id and trace alongside its streaming fix for fragmented reasoning_details . These correlate requests inside OpenRouter's own dashboard, but cross-system attribution only works if you forward them explicitly into your observability stack — LangSmith on one side, OpenRouter's records on the other, joined on a shared key.
Parallel tool calls are now semantically correct in 0.2.4, surfaced via parallel_tool_calls on bind_tools . Verify your agentic loop is not assuming one backend's tool-call schema. With OpenRouter advertising 400+ models across 70+ providers , schema variance is real, and it tends to appear precisely when allow_fallbacks routes you to a secondary endpoint mid-run.
The durable takeaway: treat the backend and model catalog as volatile infrastructure, not a static dependency. OpenRouter warns that if model pricing changes it keeps routing and charges the new rate unless you constrain or pin choices . Versioned model IDs pin behavior; unversioned aliases do not. Adopt 0.2.4 for the correct tracing and tool semantics, then configure hard limits, audit the actually-routed provider, and pin versions — otherwise the proxy's convenience becomes your audit gap.
Frequently asked questions
Do I need to migrate from ChatOpenAI(base_url=…) to ChatOpenRouter?
Migration is not strictly required, but it is recommended. Pointing ChatOpenAI at OpenRouter via a base_url override produces bugs from response-schema mismatches and misattributes provider identity in traces, which LangChain documented when it announced the dedicated package on March 8, 2026 GitHub Discussion #35672. ChatOpenRouter fixes both, handles reasoning content natively, and exposes OpenRouter metadata as first-class fields — plus it surfaces parallel_tool_calls, which the workaround path never did.
What does parallel_tool_calls change for agentic workloads?
parallel_tool_calls lets a model invoke multiple tools in a single turn instead of one at a time. Before version 0.2.4, released June 23, 2026, the flag was not surfaced on bind_tools() for ChatOpenRouter, so agentic loops fell back to sequential calls even when the backend model supported parallelism (release notes). For multi-tool agents, that reduces round trips and end-to-end latency.
How does max_price differ from just picking a cheap model?
max_price is a hard ceiling enforced at request time: it rejects any provider above the specified prompt, completion, request, or image rates — for example prompt ≤ $1/M and completion ≤ $2/M tokens — and fails the request rather than routing to a pricier backend (provider routing). Manually selecting a cheap model ID gives no such protection: OpenRouter warns that if that model's price changes, it keeps routing and charges the new rate unless you constrain or pin choices (pricing).
Does langchain-openrouter 0.2.4 support streaming?
Yes. Streaming was fixed in version 0.2.3 on May 1, 2026 by merging fragmented reasoning_details chunks, and 0.2.4 builds on that fix (changelog). If you wrote custom chunk-reassembly logic to work around the earlier fragmentation, remove it before upgrading to avoid double-handling the stream.
What is the difference between OpenRouter and LiteLLM for multi-backend routing?
OpenRouter is a hosted gateway: zero ops, 400+ models across 70+ providers and a 5.5% pay-as-you-go fee (pricing). LiteLLM is self-hostable, with Redis-backed budget tracking per key, team, and tag for on-prem spend control (LangChain releases). Both became official LangChain adapters in the same March 2026 announcement, so choose based on whether managed simplicity or self-hosted spend governance is the priority.