LangChain.ChatModels.ChatOpenAICompatible (LangChain v0.15.3)

Copy Markdown View Source

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 nil and is left out of the request when nil. A model configured with only endpoint and model sends exactly model, stream and messages.
  • 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_effort is 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, no Authorization header 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-id routes the call through a named AI Gateway. On the Workers Free plan some models, GLM among them, are reachable only this way.
  • x-session-affinity sends 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_effort controls 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

t()

@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

call(model, prompt, tools \\ [])

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.

for_api(model, messages, tools)

@spec for_api(t(), [LangChain.Message.t()], LangChain.ChatModels.ChatModel.tools()) ::
  %{
    required(atom()) => any()
  }

Return the params formatted for an API request.

new(attrs \\ %{})

@spec new(attrs :: map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}

Setup a ChatOpenAICompatible client configuration.

new!(attrs \\ %{})

@spec new!(attrs :: map()) :: t() | no_return()

Setup a ChatOpenAICompatible client configuration and return it or raise an error if invalid.

restore_from_map(data)

Restores the model from the config.

retry_on_fallback?(arg1)

@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.

serialize_config(model)

@spec serialize_config(t()) :: %{required(String.t()) => any()}

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.