Connect LangChain's ChatOpenAI and ChatAnthropic to Router One
LangChain reaches Router One through the same chat model classes it uses for OpenAI and Anthropic. ChatOpenAI with the base URL https://api.router.one/v1 sends Chat Completions, which serves every chat model in the catalog; ChatAnthropic with the host root https://api.router.one sends Anthropic Messages, which serves the Claude-family models and DeepSeek IDs. LangChain runs the chain, the agent loop and your tools in your process, and each model request it sends gets its own cost and latency trace in Dashboard → Logs. This guide covers installation and the base-URL variables, a Python ChatOpenAI setup that keeps streaming usage, how Python and JS decide between Chat Completions and the Responses API (they decide differently), ChatAnthropic for Claude's thinking and JSON output, init_chat_model and create_agent, retries, and the error you see when a long call fails. Checked against langchain-openai 1.7.0, langchain-anthropic 1.7.5 and langchain 1.4.4 (Python) and @langchain/openai 1.6.2 and @langchain/anthropic 1.5.12 (JS) on 2026-10-09, per each package's docs and source.
Install the packages and pass the base URL in code
Python: pip install -U langchain-openai gives you ChatOpenAI; add langchain-anthropic for ChatAnthropic, and langchain for init_chat_model and create_agent. All three need Python 3.10 or newer. JS: npm install @langchain/openai @langchain/core, plus @langchain/anthropic for ChatAnthropic; @langchain/openai 1.6.2 requires Node.js 22 or later and is built on the openai 7.x SDK. Pass the base URL in code rather than through the environment. ChatOpenAI resolves it in the same order in both languages: the base_url argument (configuration.baseURL in JS), then OPENAI_API_BASE, then OPENAI_BASE_URL. A stale OPENAI_API_BASE from an old tutorial therefore wins over the OPENAI_BASE_URL you just set. The key comes from api_key (apiKey in JS) and falls back to OPENAI_API_KEY; the examples read it from ROUTER_ONE_API_KEY, a name this guide chooses, so nothing left in the environment can redirect them. In Python, a custom base URL has a second effect, covered below: streaming usage stays off unless you ask for it.
python -m pip install -U langchain-openai langchain-anthropic langchain npm install @langchain/openai @langchain/anthropic @langchain/core export ROUTER_ONE_API_KEY="sk-your-router-one-key"
Configure LangChain to use the Router One base URL
Save this as langchain_example.py and run it. base_url is ChatOpenAI's alias for openai_api_base and must end in /v1, because the openai SDK underneath appends /chat/completions. use_responses_api=False is an explicit choice, and in Python an explicit value always wins: every request goes to POST /v1/chat/completions, whatever the model name or the options you pass, so any chat model ID from /models works, vendor prefix included. stream_usage=True matters because ChatOpenAI turns streaming usage on by default only when neither a base URL nor OPENAI_BASE_URL is set. With Router One's base URL it stays off, the stream request goes out without include_usage, and usage_metadata on the streamed message is empty. With it on, invoke() and stream() both return usage_metadata with input and output token counts that you can match against Dashboard → Logs. If you drop api_key, the key falls back to OPENAI_API_KEY.
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="anthropic/claude-sonnet-5",
base_url="https://api.router.one/v1",
api_key=os.environ["ROUTER_ONE_API_KEY"],
use_responses_api=False, # every request goes to /v1/chat/completions
stream_usage=True, # off by default once base_url is set
)
reply = llm.invoke("Hello!")
print(reply.content, reply.usage_metadata)
for chunk in llm.stream("Count to five."):
print(chunk.content, end="", flush=True)Python and JS choose the endpoint differently
ChatOpenAI can send either Chat Completions or the Responses API, and the two language packages decide in different ways. Router One serves Chat Completions for every chat model but Responses only for the GPT-family models, DeepSeek IDs and Grok chat models, so the choice decides which IDs work. In Python, an explicit use_responses_api value is final. Left unset, ChatOpenAI switches to Responses by itself for Responses-only settings (reasoning, include, truncation, text, previous_response_id, context management, built-in tools, or output_version set to responses/v1) and for model names it associates with Responses: a list of name prefixes and, when tools are bound, names that start with gpt-6 — neither of which Router One's vendor-prefixed IDs such as openai/gpt-5.6-sol or openai/gpt-6-sol match — and any name containing codex, which codex-auto-review does. In JS, useResponsesApi: false does not turn that automatic behavior off. @langchain/openai 1.6.2 still sends a request to Responses when the model name contains gpt-5.6 or codex (or one of a few -pro names), when built-in or custom tools are bound, or when a Responses-only call option such as previous_response_id, text, truncation, include or reasoning.summary is set. openai/gpt-5.6-sol, openai/gpt-5.6-terra, the azure/gpt-5.6-* IDs and codex-auto-review therefore go to POST /v1/responses from LangChain JS even with the flag set. Router One serves those IDs natively on that endpoint, so the calls still work, just not over Chat Completions. To force Chat Completions for every ID in JS, construct ChatOpenAICompletions, which @langchain/openai exports, with the same fields.
| Setting | Endpoint it sends to | What to check |
|---|---|---|
| Python ChatOpenAI(..., use_responses_api=False) | POST /v1/chat/completions for every model name and option | Any chat model ID in the catalog |
| Python ChatOpenAI with use_responses_api unset | Chat Completions, or Responses for Responses-only settings, built-in tools and codex model names | codex-auto-review moves to POST /v1/responses, which serves it; a Claude ID pushed there by a Responses-only setting gets HTTP 400 |
| JS new ChatOpenAI({ useResponsesApi: false }) | Chat Completions, except for model names containing gpt-5.6 or codex, bound built-in or custom tools, and Responses-only call options | Those requests go to POST /v1/responses, which serves GPT-family, DeepSeek and Grok chat IDs |
| JS new ChatOpenAICompletions({ ... }) | POST /v1/chat/completions, always | Any chat model ID; Responses-only features are not available on this class |
| ChatAnthropic (Python or JS) with the host root | POST /v1/messages | Claude-family and DeepSeek IDs only; other IDs are rejected with HTTP 400 before any model is called |
| OpenAIEmbeddings | No route (HTTP 404) | Keep embeddings on a provider that serves them |
ChatAnthropic for Claude and DeepSeek on /v1/messages
ChatAnthropic speaks Anthropic Messages, which Router One serves for the currently listed Claude-family models (the anthropic/*, aws/claude-* and vertex/claude-* IDs) and the DeepSeek IDs deepseek-v4.1-flash and deepseek-v4-flash. Give it the host root https://api.router.one without /v1: the Anthropic SDK underneath appends /v1/messages itself, so a base URL ending in /v1 produces /v1/v1/messages and an HTTP 404 whose message tells you to remove the trailing /v1. Without base_url, ChatAnthropic reads ANTHROPIC_API_URL and then ANTHROPIC_BASE_URL; the key falls back to ANTHROPIC_API_KEY and is sent as an x-api-key header, which the gateway accepts as an equivalent of the Bearer header. Always set max_tokens. ChatAnthropic fills it from a profile table keyed by Anthropic's own model names; Router One's catalog IDs are not in that table, so it falls back to 4096, and a longer answer stops early with stop_reason max_tokens. ChatAnthropic also keeps what Chat Completions loses in LangChain Python: thinking arrives as thinking content blocks, and with_structured_output(schema, method="json_schema") sends the schema as output_config.format, the path the gateway documents for reliable JSON from Claude IDs. The default method, function_calling, forces a tool call instead, and on Claude Opus 5.5 the gateway downgrades a forced tool_choice to auto, so the model may answer in plain text. Anthropic server tools pass through as well: web search and code execution are accepted and metered, while web fetch and the MCP connector are rejected with HTTP 400.
import os
from pydantic import BaseModel
from langchain_anthropic import ChatAnthropic
claude = ChatAnthropic(
model="anthropic/claude-sonnet-5",
base_url="https://api.router.one", # host root: the SDK appends /v1/messages
api_key=os.environ["ROUTER_ONE_API_KEY"],
max_tokens=2048, # otherwise 4096 for catalog IDs
)
class Verdict(BaseModel):
prime: bool
reason: str
checker = claude.with_structured_output(Verdict, method="json_schema")
print(checker.invoke("Is 221 prime?"))LangChain JS: the same classes with different defaults
In JS the base URL goes inside configuration, the options object handed to the underlying openai client. streamUsage defaults to true in JS, so streamed messages carry usage_metadata without an extra setting. JS also keeps Claude's thinking text from Chat Completions: it lands in additional_kwargs.reasoning_content on the message, whereas Python's ChatOpenAI drops reasoning_content, as its own documentation warns for fields outside the OpenAI specification. For ChatAnthropic in JS, pass the host root as anthropicApiUrl (or clientOptions.baseURL); otherwise it reads ANTHROPIC_API_URL or ANTHROPIC_BASE_URL. It also defaults maxTokens to 4096 for catalog IDs, so set maxTokens there as well. Save the example below as langchain-example.mjs and run it with node on Node.js 22 or later.
import { ChatOpenAI, ChatOpenAICompletions } from "@langchain/openai";
const fields = {
model: "anthropic/claude-sonnet-5",
apiKey: process.env.ROUTER_ONE_API_KEY,
configuration: { baseURL: "https://api.router.one/v1" },
};
// Chat Completions for this ID; names containing gpt-5.6 or codex still go to Responses
const llm = new ChatOpenAI({ ...fields, useResponsesApi: false });
console.log((await llm.invoke("Hello!")).content);
// Chat Completions for every ID, whatever the name
const completionsOnly = new ChatOpenAICompletions({
...fields,
model: "openai/gpt-5.6-sol",
});
console.log((await completionsOnly.invoke("Hello!")).content);Agents: init_chat_model and create_agent
init_chat_model builds the same ChatOpenAI when you name the integration explicitly, as below. The text before the first colon picks LangChain's integration and everything after it is the model name, so the full catalog ID, slash included, reaches the gateway. Keep the openai: prefix, or pass model_provider="openai". Without it, init_chat_model guesses the integration from the name: deepseek-v4.1-flash selects the DeepSeek integration and grok-4.7 the xAI one, each with its own package, endpoint and key, and anthropic/claude-sonnet-5 fails with Unable to infer model provider. Pass the result to create_agent from langchain.agents; an agent built from a model instance sends exactly the requests that instance sends. Each agent step is one POST /v1/chat/completions with its own row in Dashboard → Logs: the model's tool call comes back, LangChain runs the tool in your process, and the next request carries the result. LangGraph is the runtime under create_agent, and checkpoints, state and human-in-the-loop pauses stay in your process and your storage; the gateway sees only the messages of each model request. Give each agent its own key with maxSpend, so a run that keeps calling tools stops with HTTP 402 when it reaches the cap, and your other keys keep working.
import os
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
model = init_chat_model(
"openai:anthropic/claude-sonnet-5", # openai: picks ChatOpenAI; the rest is the model ID
base_url="https://api.router.one/v1",
api_key=os.environ["ROUTER_ONE_API_KEY"],
use_responses_api=False,
stream_usage=True,
)
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
agent = create_agent(model=model, tools=[add])
result = agent.invoke({"messages": [{"role": "user", "content": "What is 2 + 3?"}]})
print(result["messages"][-1].content)Long calls, retries and the error you actually see
A non-streaming call that runs past the gateway's keepalive point, about 25 seconds, and then fails returns HTTP 200 with a top-level error object instead of choices. LangChain Python raises ValueError carrying the gateway's error; LangChain JS fails with an unrelated TypeError, Cannot read properties of undefined (reading 'message'), which hides the cause. Streaming avoids both, so use llm.stream() for long generations and reasoning models. Retries stack on the gateway's own failover: requests are served by the model you name, and a route that returns 429, 5xx or times out is retried on another route for the same model before the gateway answers. Python ChatOpenAI leaves retries to the openai SDK, two by default, set with max_retries. LangChain JS wraps each call in its own retry loop, up to 6 retries by default, and sets the inner openai client to 0; lower maxRetries for batch jobs so one failing input does not become seven requests. Every client attempt the gateway forwards to a model gets its own request_id in Dashboard → Logs, which is what to quote when you report a failure.
Which model ID should LangChain send?
Copy the exact model ID from /models, preserving case, hyphens, and version suffixes; do not substitute a display name. Open its detail page and match the supported API endpoints, context window, and capabilities such as tool calling to the provider and features selected in LangChain. A catalog listing does not mean the client can use every feature of that model. Give each client or application a dedicated API key with a maxSpend cap.
Which API protocol is LangChain using?
OpenAI-compatible describes an interface format; it does not make Chat Completions (/v1/chat/completions), Responses (/v1/responses), and Anthropic Messages (/v1/messages) interchangeable. Check the installed client version, provider configuration, and actual request path against the model detail page and API compatibility fact sheet. A successful plain-text chat does not establish support for hosted tools, conversation state, or file-editing features.
Verify the LangChain call in your request trace
Send a simple text request from LangChain, then match its trace in Dashboard → Logs by time, model, and request_id: tokens, cost, latency, and status. Next, test streaming, tool calls, and multi-turn history separately. For failures, retain the actual request path, full error message, and request_id. If there is no matching log, either the request never reached the gateway (client configuration or connectivity) or the gateway refused it before calling a model — a 401 for the key, a 402 for funds, a 429 from your key's or account's limits, or a 400 or 404 for the model ID or endpoint; a streamed request that failed before any output may have no log either. The status and error message the client received tell which, so check them before attributing the error to the gateway or upstream.
FAQ
Why does LangChain JS still call /v1/responses with useResponsesApi: false?
Because in @langchain/openai 1.6.2 the flag only turns Responses on; it cannot turn the automatic switch off. The package also sends a request to Responses when the model name contains gpt-5.6 or codex, when built-in or custom tools are bound, or when a Responses-only call option is set. On Router One this affects openai/gpt-5.6-sol, openai/gpt-5.6-terra, the azure/gpt-5.6-* IDs and codex-auto-review, which the gateway serves natively on /v1/responses, so those calls still succeed. If you need Chat Completions for every ID, use ChatOpenAICompletions from @langchain/openai with the same fields. Python behaves differently: there, use_responses_api=False always wins.
Why is usage_metadata empty when I stream in Python?
ChatOpenAI enables stream_usage by default only when no base URL is configured and OPENAI_BASE_URL is not set. Pointing it at Router One, by argument or by environment variable, leaves stream_usage off, so the stream request goes out without include_usage and no usage chunk comes back. Set stream_usage=True on the model, or pass stream_usage=True to a single stream() call. Dashboard → Logs records the tokens and cost either way; the setting only decides whether your code sees them.
Where did Claude's thinking go?
LangChain Python's ChatOpenAI does not keep response fields outside the OpenAI specification, and reasoning_content is one of them, so thinking from IDs such as anthropic/claude-sonnet-4.6-thinking is dropped from the message even though it was generated and billed as output. Use ChatAnthropic, which returns thinking as content blocks over /v1/messages. LangChain JS's ChatOpenAI keeps it in additional_kwargs.reasoning_content.
How do I get reliable JSON from a Claude model?
Use ChatAnthropic with with_structured_output(schema, method="json_schema"), which sends the schema as output_config.format on /v1/messages. ChatOpenAI's with_structured_output defaults to json_schema through response_format, and response_format on /v1/chat/completions is not a reliable way to get JSON from Claude-family IDs. ChatAnthropic's own default, function_calling, forces a tool call, which the gateway downgrades to tool_choice auto on Claude Opus 5.5. For other IDs, check structured output on the model page; the gateway rejects a json_schema response_format without a name or a schema with HTTP 400 and forwards a valid one unchanged.
Should I set OPENAI_API_BASE or OPENAI_BASE_URL?
Neither, if you can pass base_url in code. ChatOpenAI checks the argument first, then OPENAI_API_BASE, then OPENAI_BASE_URL, in Python and JS alike, so an old OPENAI_API_BASE wins over a newer OPENAI_BASE_URL, while the plain openai SDK ignores OPENAI_API_BASE altogether. In Python, setting OPENAI_BASE_URL also switches off the default stream usage. For ChatAnthropic the variables are ANTHROPIC_API_URL, then ANTHROPIC_BASE_URL, and either one must hold the host root.
Can I use OpenAIEmbeddings through Router One?
No. Router One serves no /v1/embeddings, so OpenAIEmbeddings pointed at the gateway gets HTTP 404. Keep the embedder of a retriever or vector store on a provider that offers embeddings, with that provider's key, and send only the chat model's calls through Router One.
Which models can LangChain use through the gateway?
Choose a current catalog model that supports both the endpoint and the features LangChain uses. Check /models and the model detail page for the exact ID, current rates, and capabilities; a family name such as GPT or Claude is not a compatibility guarantee. A client that lists a model has only read the ID, from its own configuration or from GET /v1/models; verify an actual request too.
Models are listed, but requests fail with 400 or 404. What should I check?
Record the actual request path and error message, then check the exact model ID. A 400 can indicate invalid parameters, unsupported tools, or a model/endpoint mismatch; a 404 can indicate an incorrect path or missing resource, so it does not by itself establish that a model was retired. If the error says must be called via, use the named endpoint or select a model supported on the current endpoint. Do not add or remove /v1 or /chat/completions across all clients indiscriminately.
Does this work from Mainland China?
Yes. The gateway is reachable from Mainland China without a VPN, and the configuration is identical to the global setup.
How do I debug a 401/402/403/429?
Start from what your client received: the status, the error body and the X-Request-ID header. A 401 (missing, unknown, revoked or expired key), a 402 (wallet balance, or the key's maxSpend cap) and a 429 from your key's or account's rate limits are refused before any model is called, so they never appear in Dashboard → Logs — nor does a 400 or 404 for a wrong model ID or endpoint. For 401, check that the full key is sent and still active in Dashboard → API Keys; for 402, top up or raise the key's maxSpend; for a 403, keep the full error body. For 429, back off and retry later; a 429 that does show up in Logs came back from upstream after the gateway's retries. Keep the request_id and follow the error-codes reference.