ReqAnthropic

Hex.pmDocumentation

An Anthropic-focused API client for Elixir, built on Req.

req_anthropic is laser-focused on the Claude API. Because it isn't trying to be a generic LLM abstraction, it can expose Anthropic-specific features directly — beta headers, streaming, batches, files, the full managed-agents surface (agents, environments, sessions, vaults), and first-class tool builders for web search, web fetch, advisor, memory, bash, computer use, and the text editor.

You get two layers in one package:

Installation

Add req_anthropic to your dependencies in mix.exs:

def deps do
  [
    {:req_anthropic, "~> 0.1.0"}
  ]
end

Then run mix deps.get.

Authentication

The API key is resolved at request time, in this order:

  1. The :api_key option passed at the call site.
  2. Application.get_env(:req_anthropic, :api_key).
  3. The ANTHROPIC_API_KEY environment variable.

If none are set, ReqAnthropic.AuthError is raised before the request is sent.

# config/runtime.exs
import Config

config :req_anthropic,
  api_key: System.fetch_env!("ANTHROPIC_API_KEY")

Or override per-call:

ReqAnthropic.Messages.create(
  model: "claude-haiku-4-5",
  max_tokens: 256,
  messages: [%{role: "user", content: "ping"}],
  api_key: "sk-ant-..."
)

Quick start

Send a message

{:ok, message} =
  ReqAnthropic.Messages.create(
    model: "claude-haiku-4-5",
    max_tokens: 1024,
    messages: [%{role: "user", content: "Explain Elixir in one sentence."}]
  )

[%{"text" => text}] = message["content"]
IO.puts(text)

Stream a response

stream/1 returns a Stream of decoded SSE events. text_deltas/1 flattens those events to text chunks; collect/1 reassembles them into the same shape create/1 returns.

{:ok, stream} =
  ReqAnthropic.Messages.stream(
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    messages: [%{role: "user", content: "Write a haiku about BEAM."}]
  )

stream
|> ReqAnthropic.Messages.text_deltas()
|> Enum.each(&IO.write/1)

Or get the full assistant message back:

{:ok, stream} = ReqAnthropic.Messages.stream(model: "...", max_tokens: 1024, messages: [...])
{:ok, message} = ReqAnthropic.Messages.collect(stream)

Count tokens

{:ok, %{"input_tokens" => n}} =
  ReqAnthropic.Messages.count_tokens(
    model: "claude-haiku-4-5",
    messages: [%{role: "user", content: "How many tokens is this?"}]
  )

List and inspect models

Results are cached in ETS (default TTL: 1 hour). Pass cache: false to bypass.

{:ok, models} = ReqAnthropic.Models.list()
{:ok, model}  = ReqAnthropic.Models.get("claude-opus-4-6")

caps = ReqAnthropic.Models.capabilities("claude-opus-4-6")
caps.supports_vision         #=> true
caps.supports_computer_use   #=> true
caps.max_input_tokens        #=> 200_000

Tools

Tool builders return ready-to-send maps. Tools that require a beta header flag themselves so the resource module can wire it up.

alias ReqAnthropic.Tools

tools = [
  Tools.web_search(max_uses: 5),
  Tools.web_fetch(),
  Tools.custom(
    name: "get_weather",
    description: "Look up the weather for a city.",
    input_schema: %{
      type: "object",
      properties: %{city: %{type: "string"}},
      required: ["city"]
    },
    function: fn %{"city" => city} ->
      # Your real implementation here
      "72°F and sunny in #{city}"
    end
  )
]

{:ok, message} =
  ReqAnthropic.Messages.run(
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    tools: tools,
    messages: [%{role: "user", content: "What's the weather in Tokyo?"}]
  )

Messages.run/1 automatically executes custom tools that have a :function and loops until the model produces a final response. Use Messages.create/1 instead if you want to handle tool calls manually.

Available builders: web_search/1, web_fetch/1, bash/1, text_editor/1, computer/1, memory/1, advisor/1, custom/1.

Files

The Files API is a beta endpoint; the files-api-2025-04-14 header is added automatically.

{:ok, file}  = ReqAnthropic.Files.create(path: "report.pdf")
{:ok, list}  = ReqAnthropic.Files.list()
{:ok, info}  = ReqAnthropic.Files.get(file["id"])
{:ok, bytes} = ReqAnthropic.Files.content(file["id"])
{:ok, _}     = ReqAnthropic.Files.delete(file["id"])

Message Batches

{:ok, batch} =
  ReqAnthropic.Batches.create(
    requests: [
      %{
        custom_id: "req-1",
        params: %{
          model: "claude-haiku-4-5",
          max_tokens: 256,
          messages: [%{role: "user", content: "Hello!"}]
        }
      }
    ]
  )

{:ok, status} = ReqAnthropic.Batches.get(batch["id"])

# When complete, stream the JSONL results:
{:ok, stream} = ReqAnthropic.Batches.results(batch["id"])
Enum.each(stream, &IO.inspect/1)

Multi-turn conversations

ReqAnthropic.Conversation is a small client-side helper that keeps message history and default request options so you don't rebuild the payload yourself. The Anthropic API itself remains stateless.

alias ReqAnthropic.Conversation

convo =
  Conversation.new(
    model: "claude-haiku-4-5",
    max_tokens: 512,
    system: "You are concise."
  )
  |> Conversation.user("Tell me about Elixir.")

{:ok, convo} = Conversation.send(convo)
IO.puts(Conversation.last_text(convo))

convo = Conversation.user(convo, "What about OTP?")
{:ok, convo} = Conversation.send(convo)

Managed Agents

The full managed-agents API surface is supported. The managed-agents-2026-04-01 beta header is added automatically.

Agents and environments

{:ok, agent} =
  ReqAnthropic.Agents.create(
    model: "claude-sonnet-4-6",
    name: "Repo assistant",
    system: "You help users navigate code repositories."
  )

{:ok, env} =
  ReqAnthropic.Environments.create(
    name: "python-env",
    config: %{
      type: "cloud",
      packages: %{pip: ["pandas", "numpy"]},
      networking: %{type: "limited", allow_package_managers: true}
    }
  )

Sessions

alias ReqAnthropic.Sessions

{:ok, session} = Sessions.create(agent: agent["id"], environment_id: env["id"])

# Kick off work
{:ok, _} = Sessions.send_message(session["id"], "List the files in /workspace")

# Stream the agent's activity
{:ok, stream} = Sessions.stream(session["id"])

stream
|> Stream.each(&IO.inspect/1)
|> Enum.take(20)

# Steer or interrupt mid-execution
Sessions.interrupt(session["id"], message: "Actually, focus on README.md")

# Fetch historical events
{:ok, %{"data" => events}} = Sessions.events(session["id"], limit: 50)

# Tear down
Sessions.archive(session["id"])

Sessions.send_events/3 accepts the full event vocabulary if you need to emit user.custom_tool_result, user.tool_confirmation, or user.define_outcome events directly.

Vaults and credentials

Vaults store per-user MCP credentials so you don't have to run your own secret store.

alias ReqAnthropic.{Vaults, Vaults.Credentials}

{:ok, vault} = Vaults.create(display_name: "Alice")

{:ok, _cred} =
  Credentials.create(vault["id"],
    display_name: "Alice's Slack",
    auth: %{
      type: "static_bearer",
      mcp_server_url: "https://mcp.slack.com/mcp",
      token: "xoxp-..."
    }
  )

# Reference the vault when starting a session:
{:ok, session} =
  ReqAnthropic.Sessions.create(
    agent: agent["id"],
    environment_id: env["id"],
    vault_ids: [vault["id"]]
  )

Plugin layer

If you'd rather build the request yourself, attach the plugin to any Req struct. The same auth, error normalization, and base-URL handling apply.

req =
  Req.new()
  |> ReqAnthropic.attach(api_key: "sk-ant-...", beta: ["prompt-caching-2024-07-31"])

{:ok, response} =
  Req.post(req,
    url: "/v1/messages",
    json: %{
      model: "claude-haiku-4-5",
      max_tokens: 256,
      messages: [%{role: "user", content: "ping"}]
    }
  )

Error handling

Non-2xx responses are normalized into %ReqAnthropic.Error{}:

case ReqAnthropic.Messages.create(...) do
  {:ok, message} ->
    handle(message)

  {:error, %ReqAnthropic.Error{type: "rate_limit_error", request_id: id}} ->
    Logger.warning("Rate limited (request #{id}); backing off")

  {:error, %ReqAnthropic.Error{} = err} ->
    Logger.error("API error: #{Exception.message(err)}")
end

Bang variants (create!/1, get!/2, …) are available where it makes sense and raise ReqAnthropic.Error directly.

Configuration reference

config :req_anthropic,
  api_key: System.get_env("ANTHROPIC_API_KEY"),
  base_url: "https://api.anthropic.com",
  anthropic_version: "2023-06-01",
  beta: [],
  models_cache_ttl: :timer.hours(1)

Every key is also accepted as a per-call option. Anything else you want to pass through to Req goes under :req_options:

ReqAnthropic.Messages.create(
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [...],
  req_options: [receive_timeout: 60_000, retry: :transient]
)

Testing your code

Resource calls go through Req, so you can use Req.Test plug stubs in your own tests:

# config/test.exs
config :req_anthropic,
  api_key: "test-key",
  plug: {Req.Test, MyApp.AnthropicStub}

# test/some_test.exs
test "summarizes input" do
  Req.Test.stub(MyApp.AnthropicStub, fn conn ->
    Req.Test.json(conn, %{
      "id" => "msg_1",
      "role" => "assistant",
      "content" => [%{"type" => "text", "text" => "summary"}]
    })
  end)

  assert {:ok, _} = MyApp.summarize("...")
end

License

MIT