Packages

A well-typed, idiomatic Gleam client for Anthropic's Claude API with streaming support and tool use

Current section

Files

Jump to
anthropic_gleam src anthropic internal decoder.gleam
Raw

src/anthropic/internal/decoder.gleam

//// Shared JSON response decoders for Anthropic API
////
//// This module provides common JSON decoders used across the library for
//// parsing API responses. Both the high-level `api.gleam` and the sans-io
//// `http.gleam` modules use these shared decoders.
////
//// ## Internal Module
////
//// This module is primarily for internal use. Most users should use the
//// higher-level functions in `anthropic/api` or `anthropic/http` instead.
import anthropic/error.{type AnthropicError, type ApiErrorType}
import anthropic/message.{
type ContentBlock, type Role, Assistant, TextBlock, ToolUseBlock, User,
}
import anthropic/request.{
type CreateMessageResponse, type StopReason, type Usage, EndTurn, MaxTokens,
StopSequence, ToolUse,
}
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
// =============================================================================
// JSON Parsing
// =============================================================================
/// Parse a JSON string to Dynamic
@external(erlang, "gleam_json_ffi", "decode")
pub fn parse_json(json: String) -> Result(Dynamic, Nil)
/// Parse response body JSON and decode into CreateMessageResponse
pub fn parse_response_body(
body: String,
) -> Result(CreateMessageResponse, AnthropicError) {
case parse_json(body) {
Ok(dyn) -> decode_response(dyn)
Error(_) -> Error(error.json_error("Failed to parse response JSON"))
}
}
/// Decode a dynamic value into CreateMessageResponse
pub fn decode_response(
value: Dynamic,
) -> Result(CreateMessageResponse, AnthropicError) {
let decoder = response_decoder()
case decode.run(value, decoder) {
Ok(response) -> Ok(response)
Error(errors) ->
Error(error.json_error(
"Failed to decode response: " <> decode_errors_to_string(errors),
))
}
}
// =============================================================================
// Decoders
// =============================================================================
/// Decoder for CreateMessageResponse
pub fn response_decoder() -> decode.Decoder(CreateMessageResponse) {
use id <- decode.field("id", decode.string)
use response_type <- decode.field("type", decode.string)
use role_str <- decode.field("role", decode.string)
use content <- decode.field("content", decode.list(content_block_decoder()))
use model <- decode.field("model", decode.string)
use usage <- decode.field("usage", usage_decoder())
// Handle stop_reason - can be missing, null, or a string
// Use optional_field with optional(string) to handle all cases
use stop_reason_opt <- decode.optional_field(
"stop_reason",
None,
decode.optional(decode.string),
)
let stop_reason = case stop_reason_opt {
Some(s) -> parse_stop_reason(s)
None -> None
}
// Handle stop_sequence - can be missing, null, or a string
use stop_sequence <- decode.optional_field(
"stop_sequence",
None,
decode.optional(decode.string),
)
let role = parse_role(role_str)
decode.success(request.CreateMessageResponse(
id: id,
response_type: response_type,
role: role,
content: content,
model: model,
stop_reason: stop_reason,
stop_sequence: stop_sequence,
usage: usage,
))
}
/// Decoder for ContentBlock
pub fn content_block_decoder() -> decode.Decoder(ContentBlock) {
use block_type <- decode.field("type", decode.string)
case block_type {
"text" -> text_block_decoder()
"tool_use" -> tool_use_block_decoder()
_ ->
// Return a placeholder for unknown types
decode.success(TextBlock(
text: "[Unknown content type: " <> block_type <> "]",
))
}
}
/// Decoder for text blocks
pub fn text_block_decoder() -> decode.Decoder(ContentBlock) {
use text <- decode.field("text", decode.string)
decode.success(TextBlock(text: text))
}
/// Decoder for tool use blocks
pub fn tool_use_block_decoder() -> decode.Decoder(ContentBlock) {
use id <- decode.field("id", decode.string)
use name <- decode.field("name", decode.string)
use input <- decode.field("input", input_decoder())
decode.success(ToolUseBlock(id: id, name: name, input: input))
}
/// Decoder for tool input (converts dynamic to JSON string)
pub fn input_decoder() -> decode.Decoder(String) {
decode.new_primitive_decoder("Object", fn(data) {
let json_str = dynamic_to_json_string(data)
Ok(json_str)
})
}
/// Decoder for Usage
pub fn usage_decoder() -> decode.Decoder(Usage) {
use input_tokens <- decode.field("input_tokens", decode.int)
use output_tokens <- decode.field("output_tokens", decode.int)
decode.success(request.Usage(
input_tokens: input_tokens,
output_tokens: output_tokens,
))
}
// =============================================================================
// Parsing Helpers
// =============================================================================
/// Parse a role string to Role type
pub fn parse_role(str: String) -> Role {
case str {
"user" -> User
"assistant" -> Assistant
_ -> Assistant
}
}
/// Parse a stop reason string to Option(StopReason)
pub fn parse_stop_reason(str: String) -> Option(StopReason) {
case str {
"end_turn" -> Some(EndTurn)
"max_tokens" -> Some(MaxTokens)
"stop_sequence" -> Some(StopSequence)
"tool_use" -> Some(ToolUse)
_ -> None
}
}
/// Convert decode errors to a human-readable string
pub fn decode_errors_to_string(errors: List(decode.DecodeError)) -> String {
errors
|> list.map(fn(e) { "expected " <> e.expected <> ", got " <> e.found })
|> string.join("; ")
}
// =============================================================================
// Internal Helpers
// =============================================================================
/// Convert a dynamic value to a JSON string
fn dynamic_to_json_string(value: Dynamic) -> String {
let iodata = json_encode(value)
iolist_to_binary(iodata)
}
/// Encode dynamic value to JSON using Erlang's built-in json module (OTP 27+)
@external(erlang, "json", "encode")
fn json_encode(value: Dynamic) -> Dynamic
/// Convert iodata to binary string
@external(erlang, "erlang", "iolist_to_binary")
fn iolist_to_binary(data: Dynamic) -> String
// =============================================================================
// Error Response Parsing
// =============================================================================
/// Parse an API error response body into an AnthropicError
///
/// This function attempts to parse the Anthropic API error format:
/// ```json
/// {
/// "type": "error",
/// "error": {
/// "type": "invalid_request_error",
/// "message": "..."
/// }
/// }
/// ```
///
/// If parsing fails, it falls back to creating an error with the default type
/// and the raw body as the message.
pub fn parse_api_error(
status_code: Int,
body: String,
default_type: ApiErrorType,
) -> AnthropicError {
case parse_error_body(body) {
Ok(#(error_type, message, param, code)) ->
error.api_error(
status_code,
error.api_error_details_full(error_type, message, param, code),
)
Error(_) ->
error.api_error(status_code, error.api_error_details(default_type, body))
}
}
/// Parse error body JSON into error details tuple
///
/// Returns a tuple of (ApiErrorType, message, optional param, optional code)
pub fn parse_error_body(
body: String,
) -> Result(#(ApiErrorType, String, Option(String), Option(String)), Nil) {
case parse_json(body) {
Ok(dyn) ->
case decode.run(dyn, error_wrapper_decoder()) {
Ok(details) -> Ok(details)
Error(_) -> Error(Nil)
}
Error(_) -> Error(Nil)
}
}
/// Decoder for the error wrapper object
fn error_wrapper_decoder() -> decode.Decoder(
#(ApiErrorType, String, Option(String), Option(String)),
) {
use details <- decode.field("error", error_details_decoder())
decode.success(details)
}
/// Decoder for the inner error details
fn error_details_decoder() -> decode.Decoder(
#(ApiErrorType, String, Option(String), Option(String)),
) {
use error_type_str <- decode.field("type", decode.string)
use message <- decode.field("message", decode.string)
use param <- decode.optional_field(
"param",
None,
decode.string |> decode.map(Some),
)
use code <- decode.optional_field(
"code",
None,
decode.string |> decode.map(Some),
)
let error_type = error.api_error_type_from_string(error_type_str)
decode.success(#(error_type, message, param, code))
}