Chat model for services that expose an OpenAI-compatible Chat Completions endpoint: Cloudflare Workers AI, Groq, OpenRouter, Together, DeepInfra, Fireworks, vLLM, SGLang, Ollama, LM Studio and others.
When to use this rather than ChatOpenAI
LangChain.ChatModels.ChatOpenAI is the client for OpenAI's own API and
follows OpenAI's changes, such as sending the token limit as
max_completion_tokens and sending system messages under the developer
role for reasoning models. Compatible services often do not adopt those
changes, and many of them ignore or silently drop what they do not
understand. A developer message sent to an SGLang or vLLM server, for
example, reaches a model whose chat template has no developer branch and is
rendered as nothing, so the model never sees its instructions.
This module targets the part of the API that compatible services share:
- It sends only what you set. Every optional field defaults to
niland is left out of the request whennil. A model configured with onlyendpointandmodelsends exactlymodel,streamandmessages. - System messages are always sent as
system. Every compatible server and chat template handles that role. - The token limit is sent as
max_tokens. reasoning_effortis sent whenever it is set, independent of any other option. Accepted values differ between services, so it is not validated here.- Credentials are explicit. There is no fallback to the global OpenAI API
key, and no OpenAI organization or project headers are sent. With
api_key: nil, noAuthorizationheader is sent at all, which suits local servers that need no key.
Usage
ChatOpenAICompatible.new!(%{
endpoint: "https://api.groq.com/openai/v1/chat/completions",
api_key: System.fetch_env!("GROQ_API_KEY"),
model: "llama-3.3-70b-versatile"
})endpoint is the full URL of the chat completions route, not a base URL.
Provider-specific parameters
Anything a service supports that has no field here goes in extra_body. It
is merged into the request body last. A nil value removes a key. See
LangChain.Utils.merge_extra_body/2 for the merge rules.
ChatOpenAICompatible.new!(%{
endpoint: "http://localhost:8000/v1/chat/completions",
model: "Qwen/Qwen3-8B",
extra_body: %{"top_k" => 20, "repetition_penalty" => 1.05}
})Extra headers go in req_config:
req_config: %{headers: [{"x-custom-header", "value"}]}Overriding stream, messages, tools or model through extra_body can
break response handling.
Thinking
Compatible services that serve reasoning models return the model's thinking
in a reasoning_content field beside content, on choices[].message for a
single response and on choices[].delta for each streamed chunk. That
thinking becomes a LangChain.Message.ContentPart of type :thinking, held
separately from the answer text and ordered ahead of it:
[
%ContentPart{type: :thinking, content: "Comparing the tenths place..."},
%ContentPart{type: :text, content: "9.9 is larger."}
]Thinking parts are never sent back to the service. A conversation presents the same prompt prefix turn after turn, which keeps prompt caching working.
Provider recipes
Cloudflare Workers AI
ChatOpenAICompatible.new!(%{
endpoint:
"https://api.cloudflare.com/client/v4/accounts/#{account_id}/ai/v1/chat/completions",
api_key: api_token,
model: "@cf/zai-org/glm-5.3-flash",
reasoning_effort: "low",
req_config: %{
headers: [
{"cf-aig-gateway-id", gateway_id},
{"x-session-affinity", session_id}
]
}
})cf-aig-gateway-idroutes the call through a named AI Gateway. On the Workers Free plan some models, GLM among them, are reachable only this way.x-session-affinitysends requests that share a session id to the same replica, so a repeated prompt prefix is served from the prompt cache. Use one id for requests that share a system prompt, such as one per conversation. Without it, repeated prefixes are not cached.reasoning_effortcontrols how long the model thinks, which affects latency and output tokens.- AI Gateway can answer a byte-identical request from its response cache.
Ollama and LM Studio
Local servers need no key:
ChatOpenAICompatible.new!(%{
endpoint: "http://localhost:11434/v1/chat/completions",
model: "llama3.2"
})LM Studio listens on http://localhost:1234/v1/chat/completions by default.
vLLM and SGLang
ChatOpenAICompatible.new!(%{
endpoint: "http://localhost:8000/v1/chat/completions",
model: "Qwen/Qwen3-8B"
})Add api_key when the server was started with one.
OpenRouter and Groq
ChatOpenAICompatible.new!(%{
endpoint: "https://openrouter.ai/api/v1/chat/completions",
api_key: System.fetch_env!("OPENROUTER_API_KEY"),
model: "meta-llama/llama-3.3-70b-instruct"
})Groq's endpoint is https://api.groq.com/openai/v1/chat/completions.
Token usage
Usage is reported as a LangChain.TokenUsage in the :usage key of the
returned message's metadata. When streaming, most services report usage
only when stream_options: %{include_usage: true} is set.
Connection Retry Behavior
retry_count controls how many times a request is retried when a pooled
HTTP connection turns out to be stale (the server closed it between
requests). Only closed-connection errors are retried. Timeouts, rate limits,
authentication errors and invalid requests are returned immediately.
Summary
Functions
Calls the service passing the ChatOpenAICompatible struct with configuration, plus either a simple message or the list of messages to act as the prompt.
Return the params formatted for an API request.
Setup a ChatOpenAICompatible client configuration.
Setup a ChatOpenAICompatible client configuration and return it or raise an error if invalid.
Restores the model from the config.
Determine if an error should be retried. If true, a fallback LLM may be
used. If false, the error is understood to be more fundamental with the
request rather than a service issue and it should not be retried or fallback
to another service.
Generate a config map that can later restore the model's configuration.
Types
@type t() :: %LangChain.ChatModels.ChatOpenAICompatible{ api_key: term(), callbacks: term(), endpoint: term(), extra_body: term(), frequency_penalty: term(), json_response: term(), json_schema: term(), max_tokens: term(), model: term(), parallel_tool_calls: term(), presence_penalty: term(), reasoning_effort: term(), receive_timeout: term(), req_config: term(), retry_count: term(), seed: term(), stop: term(), stream: term(), stream_options: term(), temperature: term(), tool_choice: term(), top_p: term(), verbose_api: term() }
Functions
Calls the service passing the ChatOpenAICompatible struct with configuration, plus either a simple message or the list of messages to act as the prompt.
Optionally pass in a list of tools available to the LLM for requesting execution in response.
NOTE: This function can be used directly, but the primary interface
should be through LangChain.Chains.LLMChain.
@spec for_api(t(), [LangChain.Message.t()], LangChain.ChatModels.ChatModel.tools()) :: %{ required(atom()) => any() }
Return the params formatted for an API request.
@spec new(attrs :: map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
Setup a ChatOpenAICompatible client configuration.
Setup a ChatOpenAICompatible client configuration and return it or raise an error if invalid.
Restores the model from the config.
@spec retry_on_fallback?(LangChain.LangChainError.t()) :: boolean()
Determine if an error should be retried. If true, a fallback LLM may be
used. If false, the error is understood to be more fundamental with the
request rather than a service issue and it should not be retried or fallback
to another service.
Generate a config map that can later restore the model's configuration.
api_key, callbacks and req_config are left out. req_config can carry
credentials in its headers.