Behaviour for wire format translation between normalized messages and provider APIs.
Each adapter handles:
- Encoding
LLM.Contextinto provider-specific JSON request bodies - Providing the streaming endpoint path and extra headers
- Decoding full responses back into
LLM.Response.t() - Decoding SSE chunks back into normalized stream structs
Built-in adapters:
| Module | Provider API |
|---|---|
LLM.Adapter.OpenAI | OpenAI Chat Completions |
LLM.Adapter.OpenAIResponse | OpenAI Responses API |
LLM.Adapter.Anthropic | Anthropic Messages API |
LLM.Adapter.Gemini | Google Gemini API |
Implementing a custom adapter
Implement all required callbacks plus any optional ones your provider needs:
defmodule MyApp.Adapter do
@behaviour LLM.Adapter
@impl true
def build_request(context, opts) do
%{
"model" => Keyword.fetch!(opts, :model),
"messages" => encode_messages(context)
}
end
@impl true
def decode_response(%{"choices" => [choice | _]} = raw) do
{:ok,
%LLM.Response{
message: %LLM.Message{role: :assistant, content: choice["text"]},
raw: raw
}}
end
@impl true
def stream_path, do: "/v1/chat/completions"
@impl true
def stream_headers(_opts), do: [{"content-type", "application/json"}]
@impl true
def decode_chunk("data: [DONE]", _state), do: {:done, %{}}
def decode_chunk("data: " <> json, state) do
# Parse SSE data and return normalized chunks
{[%LLM.Stream.Chunk{text: text}], state}
end
end
Summary
Callbacks
Return authentication headers for the provider.
Build the request body from a context and options.
Decode a single SSE event (stateless variant).
Decode a single SSE event into stream chunks.
Decode a full (non-streaming) response from the provider.
Initialize the adapter state for a new streaming request.
List available models from the provider.
Return the non-streaming endpoint path.
Normalize a thinking/reasoning option to the provider's expected format.
Return extra HTTP headers needed for the streaming request.
Return the streaming endpoint path (e.g., "/v1/chat/completions").
Functions
Extract the name and schema from a structured output spec.
Types
@type chunk_type() :: LLM.Stream.Chunk.t() | LLM.Stream.ToolCall.t() | LLM.Stream.Thinking.t() | LLM.Stream.Stop.t() | LLM.Stream.Error.t()
@type model_info() :: %{ id: String.t(), name: String.t() | nil, description: String.t() | nil, context_window: non_neg_integer() | nil, max_output: non_neg_integer() | nil, capabilities: [atom()] | nil }
Callbacks
Return authentication headers for the provider.
Receives the provider config map and options. Returns a list of header tuples.
@callback build_request( LLM.Context.t(), keyword() ) :: map()
Build the request body from a context and options.
Receives an LLM.Context.t() and a keyword list of options (including :model).
Returns a map suitable for Jason.encode!/1.
@callback decode_chunk(String.t()) :: [chunk_type()] | :done
Decode a single SSE event (stateless variant).
Used by adapters that don't need to track state across chunks.
@callback decode_chunk(String.t(), map()) :: {[chunk_type()] | :done, map()}
Decode a single SSE event into stream chunks.
Receives the raw SSE data string and adapter state. Returns a tuple of
{chunks, new_state} where chunks is a list of stream structs or :done,
or {:done, new_state} to signal end of stream.
@callback decode_response(map()) :: {:ok, LLM.Response.t()} | {:error, term()}
Decode a full (non-streaming) response from the provider.
Receives the decoded JSON response body. Returns {:ok, LLM.Response.t()} or
{:error, reason}.
@callback init_stream_state() :: map()
Initialize the adapter state for a new streaming request.
Called once when a stream starts. The state is passed to decode_chunk/2.
@callback list_models(map()) :: {:ok, [model_info()]} | {:error, term()}
List available models from the provider.
Returns {:ok, [model_info()]} or {:error, reason}.
Not all providers support this — implement as a no-op if unsupported.
@callback non_stream_path() :: String.t()
Return the non-streaming endpoint path.
If unimplemented, defaults to the streaming path.
May contain {model} which is replaced with the actual model name.
Normalize a thinking/reasoning option to the provider's expected format.
Maps atoms like :low, :medium, :high to provider-specific values.
Return extra HTTP headers needed for the streaming request.
Content-Type and auth headers are added separately by LLM.Stream.
@callback stream_path() :: String.t()
Return the streaming endpoint path (e.g., "/v1/chat/completions").
May contain {model} which is replaced with the actual model name.