ExAthena ships four built-in providers plus a :mock for tests. Consumers
can also pass any module that implements ExAthena.Provider directly.
Ollama (:ollama)
Local Ollama via /api/chat. Native OpenAI-style tool_calls on modern
models (llama3.1, qwen2.5-coder, mistral-nemo, llama3.2, phi-3.5).
Streaming via newline-delimited JSON chunks.
config :ex_athena, :ollama,
base_url: "http://localhost:11434",
model: "llama3.1"Per-call override:
ExAthena.query("…", provider: :ollama, model: "qwen2.5-coder")Capabilities
| Feature | Status |
|---|---|
| Native tool calls | ✅ (model-dependent) |
| Streaming | ✅ |
| JSON mode | ✅ via format: "json" |
| Resume | ❌ (use ExAthena.Session in Phase 2) |
OpenAI-compatible (:openai_compatible / :openai / :llamacpp)
/v1/chat/completions. Covers every endpoint that speaks OpenAI chat
completions: OpenAI proper, OpenRouter, LM Studio, vLLM, Groq, Together AI,
DeepInfra, Fireworks, llama.cpp's server mode. Streaming via SSE.
config :ex_athena, :openai_compatible,
base_url: "https://api.openai.com/v1",
api_key: System.get_env("OPENAI_API_KEY"),
model: "gpt-4o-mini"Swap endpoint per-call
ExAthena.query("…",
provider: :openai_compatible,
base_url: "https://openrouter.ai/api/v1",
api_key: System.get_env("OPENROUTER_API_KEY"),
model: "anthropic/claude-opus-4.1")Capabilities
| Feature | Status |
|---|---|
| Native tool calls | ✅ |
| Streaming | ✅ SSE |
| JSON mode | ✅ via response_format: %{type: "json_object"} |
| Resume | ❌ |
Claude (:claude)
Wraps the claude_code SDK. Preserves every feature the SDK provides
natively — hooks, can_use_tool callbacks, MCP servers, session resume,
prompt cache reuse — by passing them through via :provider_opts.
config :ex_athena, :claude,
api_key: System.get_env("ANTHROPIC_API_KEY"),
model: "claude-opus-4-5"The claude_code dep is declared optional: true on ex_athena; if you
use this provider, add it to your own deps:
{:claude_code, "~> 0.36"}Capabilities
| Feature | Status |
|---|---|
| Native tool calls | ✅ tool_use blocks |
| Streaming | Phase 2 (via ExAthena.Session) |
| JSON mode | ❌ (use structured output in Phase 2) |
| Resume | ✅ via the SDK's session resume |
Mock (:mock)
Unit-test double. Scripted responses either via canned text or a responder function, plus optional per-call event lists for streaming tests.
ExAthena.query("ping", provider: :mock, mock: [text: "pong"])
# Dynamic:
responder = fn request -> %ExAthena.Response{text: "echo: " <> hd(request.messages).content} end
ExAthena.query("hi", provider: :mock, mock: [responder: responder])
# Streaming:
events = [
%ExAthena.Streaming.Event{type: :text_delta, data: "Hello"},
%ExAthena.Streaming.Event{type: :text_delta, data: " world"},
%ExAthena.Streaming.Event{type: :stop, data: :stop}
]
ExAthena.stream("hi", fn _ -> :ok end,
provider: :mock,
mock: [text: "Hello world"],
mock_events: events)Custom providers
Implement the ExAthena.Provider behaviour:
defmodule MyApp.MyProvider do
@behaviour ExAthena.Provider
@impl true
def capabilities, do: %{native_tool_calls: false, streaming: false}
@impl true
def query(%ExAthena.Request{} = req, _opts) do
# … make your call, return {:ok, %ExAthena.Response{}}
end
end
ExAthena.query("hi", provider: MyApp.MyProvider)Capabilities are used by the agent loop (Phase 2) to pick the right tool-call protocol. Declare what you actually support — if you lie, the loop will fall back automatically.