CouncilEx.Provider.Adapter behaviour (CouncilEx v0.1.0)

Copy Markdown View Source

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

complete_response()

@type complete_response() :: %{
  content: String.t(),
  parsed: term() | nil,
  usage: %{input_tokens: non_neg_integer(), output_tokens: non_neg_integer()},
  model: String.t()
}

stream_event()

@type stream_event() ::
  {:chunk, CouncilEx.StreamChunk.t()}
  | {:usage, map()}
  | {:model, String.t()}
  | {:done, atom() | nil}
  | {:tool_call_request, CouncilEx.ToolCall.t()}

Callbacks

build_complete_request(t, keyword)

@callback build_complete_request(
  CouncilEx.Request.t(),
  keyword()
) :: {url :: String.t(), headers :: [{String.t(), String.t()}], body :: map()}

build_complete_request_with_tools(t, keyword)

(optional)
@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.

build_stream_request(t, keyword)

@callback build_stream_request(
  CouncilEx.Request.t(),
  keyword()
) :: {url :: String.t(), headers :: [{String.t(), String.t()}], body :: map()}

build_stream_request_with_tools(t, keyword)

(optional)
@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.

continue_with_tool_results(state, list)

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

extract_raw_content(body)

(optional)
@callback extract_raw_content(body :: map()) :: term()

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.

extract_raw_content_from_state(state)

(optional)
@callback extract_raw_content_from_state(state :: map()) :: term()

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.

finalize_stream(map)

@callback finalize_stream(map()) :: {:ok, complete_response()} | {:error, term()}

init_stream_state()

@callback init_stream_state() :: map()

parse_complete_response(map)

@callback parse_complete_response(map()) :: {:ok, complete_response()} | {:error, term()}

parse_stream_chunk(binary, map)

@callback parse_stream_chunk(binary(), map()) :: {[stream_event()], map()}

parse_tool_calls(map)

(optional)
@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.

validate_config(keyword)

@callback validate_config(keyword()) :: :ok | {:error, term()}