Packages
A well-typed, idiomatic Gleam client for Anthropic's Claude API with streaming support and tool use
Current section
Files
Jump to
Current section
Files
src/anthropic/api.gleam
//// API functions for Anthropic Messages API
////
//// This module provides the core functions for interacting with Claude's
//// Messages API, including message creation and response parsing.
////
//// ## Quick Start
////
//// For most use cases, use the unified chat functions:
////
//// ```gleam
//// import anthropic/api
//// import anthropic/client
//// import anthropic/types/request
//// import anthropic/types/message.{user_message}
////
//// // Initialize client
//// let assert Ok(client) = client.init()
////
//// // Create a request
//// let req = request.new(
//// "claude-sonnet-4-20250514",
//// [user_message("Hello, Claude!")],
//// 1024,
//// )
////
//// // Non-streaming chat
//// case api.chat(client, req) {
//// Ok(response) -> io.println(request.response_text(response))
//// Error(err) -> io.println(error.error_to_string(err))
//// }
////
//// // Streaming chat (batch mode - collects all events)
//// case api.chat_stream(client, req) {
//// Ok(result) -> io.println(api.stream_text(result))
//// Error(err) -> io.println(error.error_to_string(err))
//// }
//// ```
import anthropic/client.{type Client, messages_endpoint}
import anthropic/config.{api_key_to_string}
import anthropic/error.{type AnthropicError}
import anthropic/internal/decoder
import anthropic/internal/sse
import anthropic/internal/validation
import anthropic/request.{type CreateMessageRequest, type CreateMessageResponse}
import anthropic/streaming.{type StreamEvent}
import anthropic/streaming/decoder as stream_decoder
import gleam/http as gleam_http
import gleam/http/request as http_request
import gleam/http/response.{type Response}
import gleam/httpc
import gleam/list
import gleam/result
import gleam/string
// =============================================================================
// Message Creation
// =============================================================================
/// Create a message using the Anthropic Messages API
///
/// @deprecated Use `api.chat` instead for a more intuitive API
///
/// ## Example
///
/// ```gleam
/// // Prefer using api.chat instead:
/// case api.chat(client, request) {
/// Ok(response) -> io.println(response_text(response))
/// Error(err) -> io.println(error_to_string(err))
/// }
/// ```
@deprecated("Use api.chat instead")
pub fn create_message(
client: Client,
message_request: CreateMessageRequest,
) -> Result(CreateMessageResponse, AnthropicError) {
chat(client, message_request)
}
// =============================================================================
// Response Parsing
// =============================================================================
/// Parse a response body into CreateMessageResponse
fn parse_response(body: String) -> Result(CreateMessageResponse, AnthropicError) {
decoder.parse_response_body(body)
}
// =============================================================================
// Unified Chat API
// =============================================================================
/// Send a chat message and receive a response (non-streaming)
///
/// This is the primary function for interacting with Claude. It sends a message
/// and returns the complete response.
///
/// ## Example
///
/// ```gleam
/// import anthropic/api
/// import anthropic/client
/// import anthropic/types/request
/// import anthropic/types/message.{user_message}
///
/// let assert Ok(client) = client.init()
/// let req = request.new(
/// "claude-sonnet-4-20250514",
/// [user_message("What is the capital of France?")],
/// 1024,
/// )
///
/// case api.chat(client, req) {
/// Ok(response) -> io.println(request.response_text(response))
/// Error(err) -> handle_error(err)
/// }
/// ```
pub fn chat(
client: Client,
message_request: CreateMessageRequest,
) -> Result(CreateMessageResponse, AnthropicError) {
// Validate the request using shared validation module
use _ <- result.try(validation.validate_request_or_error(message_request))
// Encode request to JSON
let body = request.request_to_json_string(message_request)
// Make the API call
use response_body <- result.try(client.post_and_handle(
client,
messages_endpoint,
body,
))
// Parse the response
parse_response(response_body)
}
// =============================================================================
// Streaming Chat API
// =============================================================================
/// Result of a streaming chat request
pub type StreamResult {
StreamResult(
/// List of parsed streaming events
events: List(StreamEvent),
)
}
/// Error during streaming
pub type StreamError {
/// HTTP-level error during request
HttpError(error: AnthropicError)
/// Error parsing SSE data
SseParseError(message: String)
/// Error decoding event JSON
EventDecodeError(message: String)
/// API returned an error response
ApiError(status: Int, body: String)
}
/// Send a chat message with streaming response (batch mode)
///
/// This function sends a message and returns all streaming events after
/// the response completes. For true real-time streaming, use the sans-io
/// functions in `anthropic/streaming/handler`.
///
/// ## Example
///
/// ```gleam
/// import anthropic/api
/// import anthropic/client
/// import anthropic/types/request
/// import anthropic/types/message.{user_message}
///
/// let assert Ok(client) = client.init()
/// let req = request.new(
/// "claude-sonnet-4-20250514",
/// [user_message("Tell me a story")],
/// 2048,
/// )
///
/// case api.chat_stream(client, req) {
/// Ok(result) -> io.println(api.stream_text(result))
/// Error(err) -> handle_stream_error(err)
/// }
/// ```
pub fn chat_stream(
client: Client,
message_request: CreateMessageRequest,
) -> Result(StreamResult, StreamError) {
// Ensure streaming is enabled
let streaming_request = request.with_stream(message_request, True)
// Validate the request
case validation.validate_request_or_error(streaming_request) {
Error(err) -> Error(HttpError(error: err))
Ok(_) -> {
// Encode request to JSON
let body = request.request_to_json_string(streaming_request)
// Make the HTTP request
case make_streaming_request(client, body) {
Error(err) -> Error(HttpError(error: err))
Ok(http_response) -> {
case http_response.status {
200 -> parse_sse_body(http_response.body)
status -> Error(ApiError(status: status, body: http_response.body))
}
}
}
}
}
}
/// Callback function type for processing streaming events
pub type EventCallback =
fn(StreamEvent) -> Nil
/// Send a streaming chat message with a callback for each event
///
/// **Note**: Despite the callback, this function collects ALL events before
/// calling the callbacks. It does NOT provide true real-time streaming.
/// For real-time streaming, use the sans-io functions in `anthropic/streaming/handler`.
///
/// ## Example
///
/// ```gleam
/// api.chat_stream_with_callback(client, request, fn(event) {
/// case api.event_text(event) {
/// Ok(text) -> io.print(text)
/// Error(_) -> Nil
/// }
/// })
/// ```
pub fn chat_stream_with_callback(
client: Client,
message_request: CreateMessageRequest,
callback: EventCallback,
) -> Result(StreamResult, StreamError) {
use stream_result <- result.try(chat_stream(client, message_request))
// Call callback for each event (after all collected)
list.each(stream_result.events, callback)
Ok(stream_result)
}
// =============================================================================
// Stream Result Utilities
// =============================================================================
/// Get the full text from a streaming result
///
/// ## Example
///
/// ```gleam
/// case api.chat_stream(client, request) {
/// Ok(result) -> io.println(api.stream_text(result))
/// Error(_) -> io.println("Error")
/// }
/// ```
pub fn stream_text(result: StreamResult) -> String {
result.events
|> list.filter_map(event_text)
|> string.join("")
}
/// Extract text from a single streaming event
///
/// ## Example
///
/// ```gleam
/// list.each(result.events, fn(event) {
/// case api.event_text(event) {
/// Ok(text) -> io.print(text)
/// Error(_) -> Nil
/// }
/// })
/// ```
pub fn event_text(event: StreamEvent) -> Result(String, Nil) {
case event {
streaming.ContentBlockDeltaEventVariant(delta_event) -> {
case delta_event.delta {
streaming.TextContentDelta(text_delta) -> Ok(text_delta.text)
_ -> Error(Nil)
}
}
_ -> Error(Nil)
}
}
/// Check if a streaming result completed successfully
pub fn stream_complete(result: StreamResult) -> Bool {
list.any(result.events, fn(event) {
case event {
streaming.MessageStopEvent -> True
_ -> False
}
})
}
/// Check if a streaming result contains an error
pub fn stream_has_error(result: StreamResult) -> Bool {
list.any(result.events, fn(event) {
case event {
streaming.ErrorEvent(_) -> True
_ -> False
}
})
}
/// Get the message ID from a streaming result
pub fn stream_message_id(result: StreamResult) -> Result(String, Nil) {
result.events
|> list.find_map(fn(event) {
case event {
streaming.MessageStartEvent(msg) -> Ok(msg.id)
_ -> Error(Nil)
}
})
}
/// Get the model from a streaming result
pub fn stream_model(result: StreamResult) -> Result(String, Nil) {
result.events
|> list.find_map(fn(event) {
case event {
streaming.MessageStartEvent(msg) -> Ok(msg.model)
_ -> Error(Nil)
}
})
}
// =============================================================================
// Internal Streaming Functions
// =============================================================================
/// Parse SSE body text into streaming events
fn parse_sse_body(body: String) -> Result(StreamResult, StreamError) {
let state = sse.new_parser_state()
let parse_result = sse.parse_chunk(state, body)
let events =
parse_result.events
|> list.filter_map(fn(sse_event) {
case stream_decoder.decode_event(sse_event) {
Ok(event) -> Ok(event)
Error(_) -> Error(Nil)
}
})
// Handle any remaining buffered data
let remaining_events = case sse.flush(parse_result.state) {
Ok(sse_event) -> {
case stream_decoder.decode_event(sse_event) {
Ok(event) -> [event]
Error(_) -> []
}
}
Error(_) -> []
}
Ok(StreamResult(events: list.append(events, remaining_events)))
}
/// Make a streaming HTTP request using gleam_httpc
fn make_streaming_request(
api_client: Client,
body: String,
) -> Result(Response(String), AnthropicError) {
let base_url = api_client.config.base_url
let full_url = base_url <> messages_endpoint
// Parse the URL and create request
use req <- result.try(
http_request.to(full_url)
|> result.map_error(fn(_) {
error.config_error("Invalid URL: " <> full_url)
}),
)
// Set headers and body for streaming
let req =
req
|> http_request.set_method(gleam_http.Post)
|> http_request.set_header("content-type", "application/json")
|> http_request.set_header(
"x-api-key",
api_key_to_string(api_client.config.api_key),
)
|> http_request.set_header("anthropic-version", client.api_version)
|> http_request.set_header("accept", "text/event-stream")
|> http_request.set_body(body)
// Make the request with configured timeout
httpc.configure()
|> httpc.timeout(api_client.config.timeout_ms)
|> httpc.dispatch(req)
|> result.map_error(fn(err) { http_error_to_anthropic_error(err) })
}
/// Convert httpc error to AnthropicError
fn http_error_to_anthropic_error(err: httpc.HttpError) -> AnthropicError {
case err {
httpc.InvalidUtf8Response -> error.http_error("Invalid UTF-8 in response")
httpc.FailedToConnect(ip4, ip6) ->
error.network_error(
"Failed to connect to server (IPv4: "
<> connect_error_to_string(ip4)
<> ", IPv6: "
<> connect_error_to_string(ip6)
<> ")",
)
httpc.ResponseTimeout -> error.timeout_error(0)
}
}
/// Convert ConnectError to string
fn connect_error_to_string(err: httpc.ConnectError) -> String {
case err {
httpc.Posix(code) -> "POSIX error: " <> code
httpc.TlsAlert(code, detail) -> "TLS alert " <> code <> ": " <> detail
}
}