Current section

Files

Jump to
alloy lib alloy provider.ex
Raw

lib/alloy/provider.ex

defmodule Alloy.Provider do
@moduledoc """
Behaviour for LLM providers.
Each provider translates between its native wire format and Alloy's
normalized `Alloy.Message` structs. The agent loop only sees normalized
messages - adding a new provider means implementing this behaviour.
## Completion Response
Providers return a map with:
- `:stop_reason` - `:tool_use` (continue looping) or `:end_turn` (done)
- `:messages` - list of `Alloy.Message` structs from the response
- `:usage` - map with `:input_tokens` and `:output_tokens`
"""
@type stop_reason :: :tool_use | :end_turn
@type tool_def :: %{name: String.t(), description: String.t(), input_schema: map()}
@type completion_response :: %{
stop_reason: stop_reason(),
messages: [Alloy.Message.t()],
usage: map()
}
@doc """
Send messages to the provider and get a completion response.
## Parameters
- `messages` - Conversation history as normalized `Alloy.Message` structs
- `tool_defs` - Tool definitions (JSON Schema format)
- `config` - Provider-specific configuration (API keys, model, etc.)
## Returns
- `{:ok, completion_response()}` on success
- `{:error, term()}` on failure
"""
@callback complete(
messages :: [Alloy.Message.t()],
tool_defs :: [tool_def()],
config :: map()
) :: {:ok, completion_response()} | {:error, term()}
@doc """
Stream a completion, calling `on_chunk` for each text delta.
Returns the same `{:ok, completion_response()}` as `complete/3` once
the stream finishes -- the full accumulated response.
"""
@callback stream(
messages :: [Alloy.Message.t()],
tool_defs :: [tool_def()],
config :: map(),
on_chunk :: (String.t() -> :ok)
) :: {:ok, completion_response()} | {:error, term()}
@optional_callbacks [stream: 4]
# ── Shared Helpers (used by provider implementations) ──────────────
@doc """
Recursively convert atom keys to strings in maps.
Used by providers to prepare JSON-compatible request bodies.
"""
@spec stringify_keys(term()) :: term()
def stringify_keys(map) when is_map(map) do
Map.new(map, fn
{k, v} when is_atom(k) -> {Atom.to_string(k), stringify_keys(v)}
{k, v} -> {k, stringify_keys(v)}
end)
end
def stringify_keys(list) when is_list(list), do: Enum.map(list, &stringify_keys/1)
def stringify_keys(value), do: value
@doc """
Decode a JSON binary response body, passing through maps unchanged.
Returns `{:ok, decoded_map}` or `{:error, reason}`.
"""
@spec decode_body(binary() | map()) :: {:ok, map()} | {:error, String.t()}
def decode_body(body) when is_map(body), do: {:ok, body}
def decode_body(body) when is_binary(body) do
case Jason.decode(body) do
{:ok, decoded} -> {:ok, decoded}
{:error, _} -> {:error, "Failed to decode response JSON"}
end
end
end