ADR-0004: API-client agnostic design
View SourceStatus
Accepted
Context
Conjure needs to interact with the Claude API to:
- Send system prompts with skill information
- Receive responses containing tool calls
- Send tool results back to Claude
The Elixir ecosystem has multiple HTTP clients and API wrapper patterns:
- HTTPoison, Finch, Req, Mint (HTTP clients)
- Tesla (middleware-based client)
- Custom wrappers with retry logic, telemetry, authentication
- Official Anthropic SDKs (when available for Elixir)
Different organizations have different:
- HTTP client preferences
- Authentication patterns (API keys, OAuth, service accounts)
- Retry and backoff strategies
- Rate limiting requirements
- Observability integrations
Building a Claude API client into Conjure would:
- Force HTTP client choice on users
- Duplicate effort with existing clients
- Create maintenance burden for API changes
- Limit flexibility for enterprise deployments
Decision
We will not implement a Claude API client. Conjure will be API-client agnostic.
Instead, we will:
- Accept callbacks for API interactions:
Conjure.Conversation.run_loop(
messages,
skills,
fn messages -> MyApp.Claude.call(messages) end, # User provides this
opts
)- Provide helper functions for request/response formatting:
# Build the tools parameter for API requests
tools = Conjure.API.build_tools_param(skills)
# Build system prompt with skills fragment
system = Conjure.API.build_system_prompt(base_prompt, skills)
# Parse tool_use blocks from response
{:ok, parsed} = Conjure.API.parse_response(api_response)
# Format tool results for next request
message = Conjure.API.format_tool_results_message(results)- Document integration patterns for common clients in the README and examples.
Consequences
Positive
- Users keep full control over HTTP layer
- Works with any Claude API client or wrapper
- No HTTP client dependency in Conjure
- Users can apply their own retry, rate limiting, caching strategies
- Supports custom authentication (API keys, OAuth, etc.)
- Future-proof against API changes (users update their client)
Negative
- More setup required for new users
- Users must understand Claude API message format
- No "batteries included" experience
- Example code needed for each HTTP client
Neutral
- Helper functions handle the Conjure-specific formatting
- Response parsing is provided but optional
- Users can bypass helpers for custom integrations
Alternatives Considered
Built-in HTTP client with optional override
Provide a default client that users can replace. Rejected because:
- Still forces an HTTP dependency
- "Optional override" patterns are confusing
- Maintenance burden for a non-core feature
Behaviour-based client abstraction
Define a Conjure.APIClient behaviour. Rejected because:
- Over-engineered for simple HTTP calls
- Callbacks are simpler and more flexible
- Behaviours imply Conjure manages the client lifecycle
Require a specific client library
Depend on a popular client like Req. Rejected because:
- Forces dependency choice on all users
- May conflict with existing project setup
- Limits enterprise customization
Integration Examples
With Req
defp call_claude(messages, system, tools) do
Req.post!("https://api.anthropic.com/v1/messages",
json: %{model: "claude-sonnet-4-5-20250929", system: system, messages: messages, tools: tools},
headers: [{"x-api-key", api_key()}, {"anthropic-version", "2023-06-01"}]
).body
endWith HTTPoison
defp call_claude(messages, system, tools) do
body = Jason.encode!(%{model: "claude-sonnet-4-5-20250929", ...})
{:ok, resp} = HTTPoison.post(url, body, headers)
Jason.decode!(resp.body)
end