Behaviour for provider-specific request/response handling.
One module per provider/wire-protocol owns request building (URL, headers,
body) and response parsing for both complete and stream paths. Stock
adapters live under CouncilEx.Provider.Adapters.*.
Adding a new adapter
Implement all callbacks. Reference CouncilEx.Provider.Adapters.OpenAI
and CouncilEx.Provider.Adapters.Anthropic for examples covering
OpenAI-compatible (data-line SSE) and Anthropic (typed-event SSE)
protocols.
Summary
Callbacks
Build a complete-mode request body that includes the protocol-specific
tools array. Optional — adapters that do not implement this skip the
tool-call loop entirely.
Build a stream-mode request body that includes the protocol-specific
tools array. Optional.
Build the next-turn request after the dispatcher has executed the LLM's
tool calls. The adapter receives the cumulative conversation state
(the dispatcher threads state.messages and any adapter-specific
buffers) plus the executed %ToolCallResult{} list, and returns the
augmented {url, headers, body}. Optional.
Build the assistant-turn raw content for continue_with_tool_results/2 from
a complete-mode response body. Used by the non-streaming tool-loop to splice
the assistant turn into the next iteration's messages. Optional; if absent,
the dispatcher falls back to Map.get(body, "content", []) — which matches
Anthropic's top-level content array but does not match providers (Gemini,
OpenAI) that nest the assistant turn elsewhere in the response body.
Build the assistant-turn raw content for continue_with_tool_results/2 from
the per-iteration finalized stream state. Used by the streaming tool-loop
to splice the assistant turn into the next iteration's messages. Optional;
if absent, the dispatcher falls back to the joined text content from
state.chunks.
Extract %CouncilEx.ToolCall{} entries from a complete-mode response body.
Returns [] when the response carries no tool calls. Optional.
Types
@type complete_response() :: %{ content: String.t(), parsed: term() | nil, usage: %{input_tokens: non_neg_integer(), output_tokens: non_neg_integer()}, model: String.t() }
@type stream_event() :: {:chunk, CouncilEx.StreamChunk.t()} | {:usage, map()} | {:model, String.t()} | {:done, atom() | nil} | {:tool_call_request, CouncilEx.ToolCall.t()}
Callbacks
@callback build_complete_request_with_tools( CouncilEx.Request.t(), keyword() ) :: {url :: String.t(), headers :: [{String.t(), String.t()}], body :: map()}
Build a complete-mode request body that includes the protocol-specific
tools array. Optional — adapters that do not implement this skip the
tool-call loop entirely.
@callback build_stream_request_with_tools( CouncilEx.Request.t(), keyword() ) :: {url :: String.t(), headers :: [{String.t(), String.t()}], body :: map()}
Build a stream-mode request body that includes the protocol-specific
tools array. Optional.
@callback continue_with_tool_results( state :: map(), [CouncilEx.ToolCallResult.t()] ) :: {url :: String.t(), headers :: [{String.t(), String.t()}], body :: map()}
Build the next-turn request after the dispatcher has executed the LLM's
tool calls. The adapter receives the cumulative conversation state
(the dispatcher threads state.messages and any adapter-specific
buffers) plus the executed %ToolCallResult{} list, and returns the
augmented {url, headers, body}. Optional.
Build the assistant-turn raw content for continue_with_tool_results/2 from
a complete-mode response body. Used by the non-streaming tool-loop to splice
the assistant turn into the next iteration's messages. Optional; if absent,
the dispatcher falls back to Map.get(body, "content", []) — which matches
Anthropic's top-level content array but does not match providers (Gemini,
OpenAI) that nest the assistant turn elsewhere in the response body.
Build the assistant-turn raw content for continue_with_tool_results/2 from
the per-iteration finalized stream state. Used by the streaming tool-loop
to splice the assistant turn into the next iteration's messages. Optional;
if absent, the dispatcher falls back to the joined text content from
state.chunks.
@callback finalize_stream(map()) :: {:ok, complete_response()} | {:error, term()}
@callback init_stream_state() :: map()
@callback parse_complete_response(map()) :: {:ok, complete_response()} | {:error, term()}
@callback parse_stream_chunk(binary(), map()) :: {[stream_event()], map()}
@callback parse_tool_calls(map()) :: [CouncilEx.ToolCall.t()]
Extract %CouncilEx.ToolCall{} entries from a complete-mode response body.
Returns [] when the response carries no tool calls. Optional.