Use LangChain with a custom OpenAI-compatible endpoint.

Updated 2026-07-16

ChatOpenAI takes a base_url parameter. Point it at https://api.apisrouter.com/v1, pass one key, and every chain, agent, and tool-calling loop you build routes through a single endpoint with any catalog model addressable by id, in Python and in JS.

Quick answer: base_url on the ChatOpenAI constructor.

The langchain-openai package's ChatOpenAI accepts base_url, api_key, and model directly. Set base_url to https://api.apisrouter.com/v1 and the client posts standard /v1/chat/completions requests to the gateway, with the model field passed through as a plain string. Everything downstream, LCEL pipelines, bind_tools, with_structured_output, streaming, works unchanged because it all rides the same protocol. Resolution order matters when things look wrong later: an explicit base_url parameter wins, then the OPENAI_API_BASE environment variable, then OPENAI_BASE_URL, which the underlying OpenAI SDK reads. Prefer the constructor parameter; it scopes the override to exactly the client you built.

import os
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="claude-sonnet-4-6",           # any catalog id
    base_url="https://api.apisrouter.com/v1",
    api_key=os.environ["APISROUTER_API_KEY"],
)

print(llm.invoke("Summarize RAG in one sentence.").content)

Why LangChain and a gateway compose well.

LangChain (langchain-ai on GitHub, roughly 141K stars) abstracts the model behind a runnable interface, but the abstraction has a practical seam: swapping vendors normally means swapping integration packages, ChatOpenAI for ChatAnthropic for ChatGoogleGenerativeAI, each with its own dependency, auth, and parameter quirks. A multi-vendor endpoint moves that seam out of your code. One ChatOpenAI client, constructed once, serves Claude, GPT, Gemini, and DeepSeek ids by changing a string. That collapses real complexity in multi-model applications. A router chain that sends hard questions to claude-opus-4-7 and bulk summarization to deepseek-v4-flash is two instances of the same class against the same endpoint, not two vendor SDKs in one dependency tree. Fallback chains built with with_fallbacks() stay inside one client type too, so retry behavior and error shapes stay uniform. One honest note on scope: the upstream docs position ChatOpenAI against the official OpenAI API surface, and vendor-specific response extensions are not preserved through it. A gateway that serves the standard chat-completions shape is exactly the case that works; if you need a vendor's proprietary response fields, use that vendor's native integration for those calls.

Full setup: tool calling in Python, the JS/TS variant.

Tool calling and structured output ride the same chat-completions request, so bind_tools and with_structured_output work against the gateway with any tool-capable id, the table below included. The pattern that pays for itself is instantiating clients per role and letting a fast model carry the volume. In JavaScript and TypeScript, @langchain/openai exposes the same override one level deeper: the ChatOpenAI constructor takes a configuration object that is handed to the OpenAI SDK client, and baseURL lives there. apiKey and model sit at the top level. Everything else about the integration, tools, streaming, LCEL composition, matches the Python behavior.

import os
from pydantic import BaseModel
from langchain_openai import ChatOpenAI

GATEWAY = dict(
    base_url="https://api.apisrouter.com/v1",
    api_key=os.environ["APISROUTER_API_KEY"],
)

class Verdict(BaseModel):
    decision: str
    confidence: float

strong = ChatOpenAI(model="claude-sonnet-4-6", **GATEWAY)
fast = ChatOpenAI(model="deepseek-v4-flash", **GATEWAY)

judge = strong.with_structured_output(Verdict)
result = judge.invoke("Should we cache this query? It repeats hourly.")

# volume work goes to the fast instance:
summaries = fast.batch([f"Summarize: {doc}" for doc in docs])

Choosing models per chain role.

Because every instance is the same class against the same endpoint, per-role model choice becomes an A/B you can actually run: pin the chain, swap one role's model string, and read the quality delta against the cost delta in the per-key usage view.

  • Agent loops and judges need the strongest reasoning you can justify: claude-sonnet-4-6 and gpt-5.5 hold up on multi-step tool use, and a misjudged tool call costs a whole loop iteration.
  • Bulk map steps, summarize-then-reduce pipelines, and enrichment passes are volume work; deepseek-v4-flash or glm-5.2 keep per-document cost flat where quality bars are lower.
  • Long-context ingestion, whole documents into one prompt rather than chunked retrieval, is where gemini-3.1-pro-preview earns a slot in the chain.
  • Embeddings are a separate client class (OpenAIEmbeddings) with its own base URL settings; moving chat traffic does not move your embedding provider, and your vector index stays valid.

Pay-as-you-go · transparent per-model pricing

Selected models are priced below official list prices. Exact input, output, cache, and per-request prices are shown for each model.

ModelOfficial PriceOur Price
Claude Sonnet 4.6$3.00 / $15.00 per M$2.40 / $12.00 per M
GPT-5.5$5.00 / $30.00 per M$4.00 / $24.00 per M
Gemini 3.1 Pro Preview$2.00 / $12.00 per M$1.60 / $9.60 per M
DeepSeek V4 Pro$0.43 / $0.87 per M$0.39 / $0.78 per M
GLM-5.2$1.14 / $4.00 per M$1.03 / $3.60 per M

Failure modes and version pinning.

The env-level override moves more than you meant. OPENAI_API_BASE is read by every langchain-openai client in the process, OpenAIEmbeddings included. If the gateway is serving your chat models but not your embedding model, an env-level override breaks retrieval while chat keeps working. Scope the override with the constructor parameter and the embeddings client never notices. Package split confusion. ChatOpenAI lives in langchain-openai (import from langchain_openai), not in the langchain core package; old tutorials importing from langchain.chat_models predate the split. Pin langchain-openai explicitly in your requirements, since it is the package whose version determines client behavior, and resolve it deliberately rather than inheriting whatever a meta-package drags in. Vendor-specific parameters do not translate. Fields the OpenAI protocol does not define do not exist here; requests carrying unsupported parameters get 400s. Keep request shapes to the standard surface, temperature, max_tokens, tools, response_format, and express vendor choice through the model id instead. Silent default endpoint. If base_url is None because a config layer failed to inject it, the client happily talks to api.openai.com and fails with a key error that looks like an auth bug. When a 401 surprises you, print the client's effective base URL before rotating any keys. JS: baseURL at the wrong level. In @langchain/openai, baseURL belongs inside configuration, not at the top level of the constructor object. Misplacing it produces default-endpoint behavior identical to the Python case above.

Who routes LangChain through a gateway.

  • Teams running multi-model chains, router patterns, judge-and-worker splits, map-reduce summarization, who want per-role vendor choice without per-vendor SDKs.
  • Production apps consolidating billing: one key, per-key usage, and spend attribution per environment or feature instead of several vendor dashboards.
  • Developers building fallback chains with uniform error handling, since with_fallbacks across ids on one endpoint keeps every failure shape identical.
  • Prototypers who want tomorrow's model today: new catalog ids are addressable the moment they appear, with zero dependency changes.
  • Developers without access to a given vendor's billing. Top-up based access with no card requirement removes the sign-up dependency per provider.

Verify the endpoint and debug the first chain.

List the gateway's models first; the model strings in your code must match served ids exactly, version suffixes included. First-run failures follow a pattern. A 401 with a key you believe in usually means the client resolved a different base URL than you think, often via a leftover OPENAI_API_BASE export; print the effective values before debugging further. A model-not-found error is an id typo. A 400 naming an unexpected parameter means something vendor-specific leaked into the request. And a hanging streaming call in notebooks is usually proxy or event-loop plumbing on your side of the wire, not the endpoint. Once chains run, the APIsRouter console shows per-request model, token counts, and spend. For map-reduce and agent workloads, per-key usage answers the question that matters in review: which role in the chain is spending the tokens, and is the strong model earning its share.

curl -s https://api.apisrouter.com/v1/models \
  -H "Authorization: Bearer $APISROUTER_API_KEY" | head -50

FAQ

Can ChatOpenAI talk to Claude or Gemini models through one endpoint?

Yes. ChatOpenAI speaks the chat-completions protocol to whatever base_url names, and the model field passes through as a plain string. Any id the gateway serves works, Claude, Gemini, DeepSeek, and GLM included, with tools and structured output intact on tool-capable ids.

Where does base_url go in Python versus JS?

Python: a top-level constructor parameter, ChatOpenAI(base_url=...). JS/TS: inside the configuration object, new ChatOpenAI({ configuration: { baseURL } }), because that object is handed to the underlying OpenAI SDK client.

What is the precedence between base_url and the environment variables?

Explicit base_url wins, then OPENAI_API_BASE, then OPENAI_BASE_URL from the underlying SDK. Prefer the constructor parameter: it scopes the override to one client and cannot be hijacked by a forgotten export elsewhere in the environment.

Does bind_tools work through a gateway?

Yes. Tool definitions and tool-call responses ride the standard chat-completions request and response, so bind_tools, agents, and with_structured_output work with any tool-capable id served by the endpoint. Choose ids that genuinely support tool use for agent work.

Do my embeddings and vector store need to change?

No. OpenAIEmbeddings is a separate client with its own endpoint settings. Scope the chat override to the ChatOpenAI constructor and your embedding provider and existing vectors stay untouched. Avoid env-level overrides precisely because they would move both.

Which package version should I pin?

Pin langchain-openai itself; it owns ChatOpenAI's behavior. Import from langchain_openai, not the legacy langchain.chat_models path, and upgrade it deliberately since client-level changes arrive through this package rather than langchain core.