Claudio
A modern, feature-complete Elixir client for the Anthropic API
Claudio provides a comprehensive, idiomatic Elixir interface for Claude AI models with support for streaming, tool calling, prompt caching, vision, and batch processing.
Why Claudio?
- ๐ Production Ready: Configurable timeouts, automatic retries, and comprehensive error handling
- โก High Performance: Built on Req for fast HTTP operations with excellent streaming support
- ๐ Idiomatic Elixir: Fluent API, pattern matching on errors, and proper supervision tree integration
- ๐ฆ Feature Complete: Messages, Batches, Tools, Caching, Vision - everything you need
- ๐งช Well Tested: 76 tests covering unit and integration scenarios
- ๐ Fully Documented: Complete API documentation with examples on HexDocs
Features
- โ Messages API - Send messages with streaming support
- โ Request Builder - Type-safe, fluent API for building requests
- โ Tool/Function Calling - Integrate external tools with structured schemas
- โ Message Batches - Process up to 100,000 requests asynchronously
- โ Prompt Caching - Cache large contexts for up to 90% cost reduction
- โ Vision Support - Analyze images (base64, URL, Files API)
- โ PDF/Document Support - Process documents directly
- โ Streaming Responses - Real-time Server-Sent Events (SSE) streaming
- โ Token Counting - Estimate costs before making requests
- โ Configurable Timeouts - Fine-tune connection and receive timeouts
- โ Automatic Retries - Handle transient failures gracefully
- โ Structured Errors - Pattern match on error types
- โ Cache Metrics - Track cache hits and creation
Installation
Add claudio to your list of dependencies in mix.exs:
def deps do
[
{:claudio, "~> 0.1.2"}
]
endThen fetch dependencies:
mix deps.getQuick Start
1. Get an API Key
Sign up for an Anthropic API key at console.anthropic.com
2. Set Your API Key
export ANTHROPIC_API_KEY="your-api-key-here"3. Send Your First Message
# Create a client
client = Claudio.Client.new(%{
token: System.get_env("ANTHROPIC_API_KEY")
})
# Use the Request builder (recommended)
alias Claudio.Messages.{Request, Response}
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.add_message(:user, "Explain quantum computing in simple terms")
|> Request.set_max_tokens(1024)
{:ok, response} = Claudio.Messages.create(client, request)
# Extract the text
text = Response.get_text(response)
IO.puts(text)Examples
Multi-Turn Conversation
alias Claudio.Messages.{Request, Response}
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.set_system("You are a helpful Python tutor")
|> Request.add_message(:user, "How do I read a file in Python?")
|> Request.add_message(:assistant, "You can use the open() function...")
|> Request.add_message(:user, "What about writing to a file?")
|> Request.set_max_tokens(500)
{:ok, response} = Claudio.Messages.create(client, request)
IO.puts(Response.get_text(response))Streaming Responses
Perfect for chat interfaces or real-time applications:
alias Claudio.Messages.{Request, Stream}
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.add_message(:user, "Write a haiku about Elixir")
|> Request.set_max_tokens(100)
|> Request.enable_streaming()
{:ok, stream_response} = Claudio.Messages.create(client, request)
# Stream text in real-time
stream_response.body
|> Stream.parse_events()
|> Stream.filter_events(:content_block_delta)
|> Enum.each(fn event ->
IO.write(event.delta.text)
end)Tool/Function Calling
Let Claude use your functions:
alias Claudio.{Tools, Messages.Request}
# Define a weather tool
weather_tool = Tools.define_tool(
"get_weather",
"Get current weather for a location",
%{
type: "object",
properties: %{
location: %{type: "string", description: "City name"},
unit: %{type: "string", enum: ["celsius", "fahrenheit"]}
},
required: ["location"]
}
)
# Create request with tool
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.add_message(:user, "What's the weather in Tokyo?")
|> Request.add_tool(weather_tool)
|> Request.set_max_tokens(500)
{:ok, response} = Claudio.Messages.create(client, request)
# Check if Claude wants to use the tool
if Tools.has_tool_uses?(response) do
tool_uses = Tools.extract_tool_uses(response)
Enum.each(tool_uses, fn tool_use ->
# Execute your function
result = get_weather(tool_use.input["location"])
# Send result back to Claude
tool_result = Tools.create_tool_result(tool_use.id, Jason.encode!(result))
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.add_messages(response.content)
|> Request.add_message(:user, [tool_result])
|> Request.set_max_tokens(500)
{:ok, final_response} = Claudio.Messages.create(client, request)
IO.puts(Response.get_text(final_response))
end)
end
defp get_weather(location) do
# Your weather API implementation
%{temp: 72, condition: "sunny", location: location}
endVision - Analyze Images
# From a file
image_data = File.read!("screenshot.png") |> Base.encode64()
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.add_message_with_image(
:user,
"What's in this image?",
image_data,
"image/png"
)
|> Request.set_max_tokens(500)
{:ok, response} = Claudio.Messages.create(client, request)
IO.puts(Response.get_text(response))
# Or from a URL
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.add_message_with_image_url(
:user,
"Describe this diagram",
"https://example.com/diagram.jpg"
)
|> Request.set_max_tokens(500)Prompt Caching - Save 90% on Costs
Cache large contexts like documentation or code:
large_codebase = File.read!("lib/my_app.ex")
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.set_system_with_cache("""
You are a code reviewer. Here is the codebase:
#{large_codebase}
Review code changes carefully for bugs and style.
""", ttl: "5m")
|> Request.add_message(:user, "Review this function: def foo(x), do: x + 1")
|> Request.set_max_tokens(1000)
{:ok, response} = Claudio.Messages.create(client, request)
# Check cache savings
IO.inspect(response.usage.cache_read_input_tokens, label: "Tokens from cache")
IO.inspect(response.usage.cache_creation_input_tokens, label: "Tokens cached")Batch Processing
Process thousands of requests asynchronously:
alias Claudio.Batches
# Create a batch of analysis tasks
requests =
Enum.map(1..1000, fn i ->
%{
custom_id: "review-#{i}",
params: %{
model: "claude-3-5-sonnet-20241022",
max_tokens: 500,
messages: [
%{role: "user", content: "Analyze pull request ##{i}"}
]
}
}
end)
# Submit batch (processes asynchronously)
{:ok, batch} = Batches.create(client, requests)
IO.puts("Batch created: #{batch["id"]}")
# Wait for completion with progress updates
{:ok, completed} = Batches.wait_for_completion(
client,
batch["id"],
fn status ->
counts = status["request_counts"]
progress = counts["succeeded"] + counts["errored"]
total = counts["processing"]
IO.puts("Progress: #{progress}/#{total}")
end,
poll_interval: 10_000 # Check every 10 seconds
)
# Download results as JSONL
{:ok, results_jsonl} = Batches.get_results(client, batch["id"])
# Parse results
results =
results_jsonl
|> String.split("\n", trim: true)
|> Enum.map(&Jason.decode!/1)
Enum.each(results, fn result ->
case result["result"]["type"] do
"succeeded" ->
message = result["result"]["message"]
IO.puts("#{result["custom_id"]}: Success")
"errored" ->
error = result["result"]["error"]
IO.puts("#{result["custom_id"]}: Error - #{error["message"]}")
end
end)Configuration
Basic Setup
# config/config.exs
config :claudio,
default_api_version: "2023-06-01",
default_beta_features: []Timeout Configuration
Configure timeouts for different use cases:
# config/config.exs
config :claudio, Claudio.Client,
timeout: 60_000, # Connection timeout: 60s
recv_timeout: 120_000 # Receive timeout: 120s (important for streaming)
# For long-running operations
config :claudio, Claudio.Client,
timeout: 60_000,
recv_timeout: 600_000 # 10 minutes
# Production with retries
config :claudio, Claudio.Client,
timeout: 30_000,
recv_timeout: 180_000,
retry: true # Automatic retry on transient failuresCustom Retry Logic
config :claudio, Claudio.Client,
retry: [
delay: 1000, # Initial delay: 1s
max_retries: 3, # Retry up to 3 times
max_delay: 10_000 # Max delay: 10s
]Error Handling
Claudio provides structured error types for pattern matching:
alias Claudio.APIError
case Claudio.Messages.create(client, request) do
{:ok, response} ->
# Success
handle_response(response)
{:error, %APIError{type: :rate_limit_error} = error} ->
# Rate limited - wait and retry
Logger.warning("Rate limited: #{error.message}")
Process.sleep(60_000)
retry_request()
{:error, %APIError{type: :authentication_error}} ->
# Invalid API key
Logger.error("Authentication failed - check your API key")
{:error, %APIError{type: :invalid_request_error} = error} ->
# Bad request - fix and retry
Logger.error("Invalid request: #{error.message}")
fix_and_retry()
{:error, %APIError{type: :overloaded_error}} ->
# Service overloaded - retry with backoff
exponential_backoff_retry()
{:error, %APIError{} = error} ->
# Other API error
Logger.error("API error [#{error.status_code}]: #{error.message}")
{:error, reason} ->
# Network or timeout error
Logger.error("Request failed: #{inspect(reason)}")
endError Types
:authentication_error- Invalid API key:invalid_request_error- Malformed request:rate_limit_error- Too many requests:overloaded_error- Service overloaded:permission_error- Insufficient permissions:not_found_error- Resource not found:api_error- General API error
Best Practices
1. Use the Request Builder
The fluent Request API is more maintainable than raw maps:
# Good โ
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.add_message(:user, "Hello")
|> Request.set_max_tokens(100)
# Works, but less maintainable
request = %{
"model" => "claude-3-5-sonnet-20241022",
"messages" => [%{"role" => "user", "content" => "Hello"}],
"max_tokens" => 100
}2. Handle Errors Properly
Always pattern match on error types:
# Good โ
case Claudio.Messages.create(client, request) do
{:ok, response} -> handle_success(response)
{:error, %APIError{type: :rate_limit_error}} -> retry_with_backoff()
{:error, error} -> handle_error(error)
end
# Bad โ
{:ok, response} = Claudio.Messages.create(client, request) # Crashes on error3. Use System Prompts
Guide the model's behavior with system prompts:
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.set_system("You are a helpful coding assistant. Always explain your code.")
|> Request.add_message(:user, "Write a function to reverse a string")4. Set Appropriate Timeouts
Long operations need longer timeouts:
# For batch processing or large responses
config :claudio, Claudio.Client,
recv_timeout: 600_000 # 10 minutes5. Enable Retries in Production
Handle transient failures automatically:
config :claudio, Claudio.Client,
retry: true6. Cache Large Contexts
Use prompt caching for repeated contexts:
# Cache documentation or code for multiple queries
request =
Request.new("claude-3-5-sonnet-20241022")
|> Request.set_system_with_cache(large_documentation, ttl: "5m")7. Count Tokens for Cost Control
{:ok, count} = Claudio.Messages.count_tokens(client, request)
estimated_cost = count["input_tokens"] * 0.003 / 1000
IO.puts("Estimated cost: $#{estimated_cost}")Testing
# Run unit tests
mix test
# Run with integration tests (requires ANTHROPIC_API_KEY)
export ANTHROPIC_API_KEY="your-key"
mix test --include integration
# Run specific test file
mix test test/messages_test.exs
# Check code formatting
mix format --check-formattedDocumentation
Full API documentation is available on HexDocs:
- Main Documentation - Complete API reference
- Getting Started Guide - Detailed tutorial
- GitHub Repository - Source code
Generate documentation locally:
mix docs
open doc/index.htmlContributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
-
Create your feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
-
Ensure all tests pass (
mix test) -
Commit your changes (
git commit -am 'Add amazing feature') -
Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Links
- Hex Package - Latest releases
- Documentation - Full API reference
- GitHub - Source code
- Anthropic API Docs - Official API documentation
- Anthropic Console - Get your API key
Acknowledgments
Built with โค๏ธ using Req for HTTP client operations.
Made with Elixir ๐