Anthropic Elixir API Client

An unofficial Elixir client for the Anthropic API, built around the same shape as Anthropic's official SDKs: an explicit Client struct, resource modules (Messages, Models, Batches), typed content blocks, native tool use, streaming, and automatic retries.

Features

Installation

def deps do
[
{:anthropic, "~> 0.5", hex: :anthropic_community}
]
end

Configuration

Build a Client explicitly and pass it to every call:

client = Anthropic.Client.new(api_key: System.fetch_env!("ANTHROPIC_API_KEY"))

api_key and base_url also fall back to Application.get_env(:anthropic, ...) and then to ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL environment variables if not passed explicitly:

# config/config.exs
import Config
config :anthropic, api_key: System.get_env("ANTHROPIC_API_KEY")

Other Client options: base_url, api_version, max_retries (default 2), timeout (default 600_000 ms), default_model, default_headers.

Usage

Basic conversation

{:ok, message} =
Anthropic.Messages.create(client,
model: "claude-opus-4-8",
max_tokens: 1024,
messages: [%{role: "user", content: "Explain monads in computer science. Be concise."}]
)
message.content
#=> [%Anthropic.Messages.Content.Text{text: "Monads are..."}]
message.stop_reason
#=> "end_turn"

Use Anthropic.Messages.create!/2 for a bang variant that returns the message directly and raises Anthropic.Error on failure.

Streaming

client
|> Anthropic.Messages.stream(model: "claude-opus-4-8", max_tokens: 1024,
messages: [%{role: "user", content: "Write a haiku about Elixir"}])
|> Stream.each(fn
%Anthropic.Messages.StreamEvent.ContentBlockDelta{delta: %{"type" => "text_delta", "text" => text}} ->
IO.write(text)
_other ->
:ok
end)
|> Stream.run()

Or fold the stream into a final Message, equivalent to create/2:

{:ok, message} =
client
|> Anthropic.Messages.stream(model: "claude-opus-4-8", max_tokens: 1024,
messages: [%{role: "user", content: "Write a haiku about Elixir"}])
|> Anthropic.Messages.stream_to_message()

Tool use

Define a tool with a JSON Schema input_schema:

defmodule MyApp.WeatherTool do
use Anthropic.Tools
@impl true
def name, do: "get_weather"
@impl true
def description, do: "Get the current weather for a given city."
@impl true
def input_schema do
%{
"type" => "object",
"properties" => %{
"location" => %{"type" => "string", "description" => "City and state, e.g. San Francisco, CA"}
},
"required" => ["location"]
}
end
@impl true
def execute(%{"location" => location}) do
{:ok, "72F and sunny in #{location}"}
end
end

Then drive the full agentic loop with Anthropic.ToolRunner:

{:ok, message, _history} =
Anthropic.ToolRunner.run(
client,
[model: "claude-opus-4-8", max_tokens: 1024,
messages: [%{role: "user", content: "What's the weather in Paris?"}]],
[MyApp.WeatherTool]
)

ToolRunner executes every requested tool, feeds results back to the API, and repeats until the assistant stops requesting tools.

Server tools

Web search, web fetch, code execution, bash, text editor, and memory are executed by Anthropic server-side — no execute/1 to implement, just add the tool definition and read the typed result block back:

{:ok, message} =
Anthropic.Messages.create(client,
model: "claude-opus-4-8",
max_tokens: 1024,
tools: [Anthropic.Tools.WebSearch.new(max_uses: 3)],
messages: [%{role: "user", content: "What's the latest Elixir release?"}]
)
for %Anthropic.Messages.Content.WebSearchToolResult{content: results} <- message.content do
IO.inspect(results)
end

Available: Anthropic.Tools.WebSearch, WebFetch, CodeExecution, Bash, TextEditor, Memory — each a thin, validated builder around that tool's versioned wire shape (version: defaults to the latest known version, override to pin an older one). Results decode into Anthropic.Messages.Content.WebSearchToolResult, WebFetchToolResult, CodeExecutionToolResult, BashCodeExecutionToolResult, TextEditorCodeExecutionToolResultcontent on each is the raw decoded JSON payload (not deeply typed; see each tool's docs for its shape). The invocation itself decodes as Anthropic.Messages.Content.ServerToolUse, distinct from ToolUse so Anthropic.ToolRunner never tries to dispatch it to a client-side tool.

Computer use and the MCP connector are beta-only and not yet supported.

Prompt caching

Attach Anthropic.CacheControl.ephemeral/1 to a content block's :cache_control field to mark it as a cache breakpoint:

{:ok, message} =
Anthropic.Messages.create(client,
model: "claude-opus-4-8",
max_tokens: 1024,
messages: [
%{
role: "user",
content: [
%Anthropic.Messages.Content.Text{text: large_document, cache_control: Anthropic.CacheControl.ephemeral()},
%{type: "text", text: "Summarize the above."}
]
}
]
)

Extended thinking

{:ok, message} =
Anthropic.Messages.create(client,
model: "claude-opus-4-8",
max_tokens: 4096,
thinking: Anthropic.Thinking.enabled(budget_tokens: 10_000),
messages: [%{role: "user", content: "..."}]
)

Structured outputs

Constrain Claude's response to a given JSON Schema:

{:ok, message} =
Anthropic.Messages.create(client,
model: "claude-opus-4-8",
max_tokens: 1024,
output_config:
Anthropic.OutputConfig.json_schema(%{
"type" => "object",
"properties" => %{"answer" => %{"type" => "string"}},
"required" => ["answer"]
}),
messages: [%{role: "user", content: "..."}]
)

Images

{:ok, image} = Anthropic.Messages.Content.Image.process_image("/path/to/image.png", :path)
{:ok, message} =
Anthropic.Messages.create(client,
model: "claude-opus-4-8",
max_tokens: 1024,
messages: [
%{role: "user", content: [image, %{type: "text", text: "What's in this image?"}]}
]
)

Documents (PDF/text)

{:ok, doc} = Anthropic.Messages.Content.Document.process_document("/path/to/report.pdf", :path)
{:ok, message} =
Anthropic.Messages.create(client,
model: "claude-opus-4-8",
max_tokens: 1024,
messages: [
%{role: "user", content: [doc, %{type: "text", text: "Summarize this document."}]}
]
)

Anthropic.Messages.Content.Document also supports from_url/2 (a hosted PDF), from_text/2 (inline plain text), and from_content/2 (pre-formatted content, for citing structured content rather than a raw document).

Citations

Pass citations: %{enabled: true} to a document (or search-result) block to have Claude cite the specific passages it draws on. Citations come back attached to the response's Text blocks, decoded into typed structs:

{:ok, doc} =
Anthropic.Messages.Content.Document.process_document("/path/to/report.pdf", :path,
citations: %{enabled: true}
)
{:ok, message} =
Anthropic.Messages.create(client,
model: "claude-opus-4-8",
max_tokens: 1024,
messages: [%{role: "user", content: [doc, %{type: "text", text: "What was Q3 revenue?"}]}]
)
for %Anthropic.Messages.Content.Text{citations: citations} <- message.content, citations do
for %Anthropic.Messages.Content.Citation.CharLocation{cited_text: cited_text} <- citations do
IO.puts("Cited: #{cited_text}")
end
end

Five citation types are supported, discriminated by struct: Citation.CharLocation, Citation.PageLocation, Citation.ContentBlockLocation, Citation.SearchResultLocation, Citation.WebSearchResultLocation.

Models

{:ok, %{data: models}} = Anthropic.Models.list(client)
{:ok, model} = Anthropic.Models.retrieve(client, "claude-opus-4-8")
# Or auto-paginate through every page as a lazy Stream:
client |> Anthropic.Models.list_all() |> Enum.each(&IO.puts(&1["id"]))

Batches

{:ok, batch} =
Anthropic.Batches.create(client, [
%{custom_id: "request-1", params: [model: "claude-opus-4-8", max_tokens: 100, messages: [%{role: "user", content: "Hi"}]]},
%{custom_id: "request-2", params: [model: "claude-opus-4-8", max_tokens: 100, messages: [%{role: "user", content: "Hello"}]]}
])
{:ok, batch} = Anthropic.Batches.retrieve(client, batch.id)
if batch.processing_status == "ended" do
{:ok, results} = Anthropic.Batches.results(client, batch)
end
# List (a page at a time, or auto-paginated):
{:ok, %{data: batches}} = Anthropic.Batches.list(client)
client |> Anthropic.Batches.list_all() |> Enum.to_list()
{:ok, _deleted} = Anthropic.Batches.delete(client, batch.id)

Files

Upload a file once and reference it by file_id instead of inlining base64 data. This is a beta API — Anthropic.Files automatically sends the anthropic-beta header it currently requires:

{:ok, file} = Anthropic.Files.create(client, "/path/to/report.pdf")
{:ok, %{data: files}} = Anthropic.Files.list(client)
client |> Anthropic.Files.list_all() |> Enum.to_list()
{:ok, file} = Anthropic.Files.retrieve(client, file.id)
{:ok, binary} = Anthropic.Files.download(client, file.id)
{:ok, _deleted} = Anthropic.Files.delete(client, file.id)

Referencing the uploaded file back in a message is also beta and not yet modeled by Content.Image/Document — pass a raw map and add the same beta header to that call (see Anthropic.Files moduledoc for a full example).

Counting tokens

{:ok, %{input_tokens: n}} =
Anthropic.Messages.count_tokens(client,
model: "claude-opus-4-8",
messages: [%{role: "user", content: "Hello, Claude"}]
)

Error handling

Every resource function returns {:ok, result} | {:error, %Anthropic.Error{}}. Anthropic.Error mirrors the API's error.type taxonomy (invalid_request_error, rate_limit_error, overloaded_error, api_error, etc.) plus client-local types (:connection_error, :timeout, :decode_error, :validation_error):

case Anthropic.Messages.create(client, model: "claude-opus-4-8", max_tokens: 1024, messages: []) do
{:ok, message} -> message
{:error, %Anthropic.Error{type: :validation_error, message: msg}} -> {:error, msg}
{:error, error} -> raise error
end