Packages
claude_gleam
1.0.0
Gleam SDK for the Anthropic Claude API with agentic tool-use loop and BEAM concurrency
Current section
Files
Jump to
Current section
Files
src/claude/json/decode.gleam
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode.{type DecodeError, type Decoder}
import gleam/json
import gleam/option.{None}
import claude/types/content.{
type Citation, type ContentBlock, CharLocationCitation,
ContentBlockLocationCitation, PageLocationCitation, RedactedThinking,
ServerToolUse, Text, Thinking, ToolUse, WebSearchResult,
WebSearchResultLocationCitation,
}
import claude/types/message.{
type Message, type Role, type StopReason, type Usage, Assistant, EndTurn,
MaxTokens, Message, PauseTurn, Refusal, StopSequence, ToolUseStop, Usage, User,
}
/// Re-serialize a dynamic value (arbitrary JSON) back to a JSON string.
@external(erlang, "claude_json_ffi", "dynamic_to_json_string")
fn dynamic_to_json_string(term: Dynamic) -> String
/// Decode a full API response message from a JSON string.
pub fn message(
json_string: String,
) -> Result(Message, List(DecodeError)) {
json.parse(json_string, message_decoder())
|> map_json_error
}
/// Map gleam_json's DecodeError to a list of dynamic DecodeErrors.
fn map_json_error(
result: Result(a, json.DecodeError),
) -> Result(a, List(DecodeError)) {
case result {
Ok(v) -> Ok(v)
Error(json.UnexpectedEndOfInput) ->
Error([
decode.DecodeError(
expected: "valid JSON",
found: "unexpected end of input",
path: [],
),
])
Error(json.UnexpectedByte(b)) ->
Error([
decode.DecodeError(
expected: "valid JSON",
found: "unexpected byte: " <> b,
path: [],
),
])
Error(json.UnexpectedSequence(s)) ->
Error([
decode.DecodeError(
expected: "valid JSON",
found: "unexpected sequence: " <> s,
path: [],
),
])
Error(json.UnableToDecode(errors)) -> Error(errors)
}
}
/// Decoder for the top-level Message type.
pub fn message_decoder() -> Decoder(Message) {
use id <- decode.field("id", decode.string)
use role <- decode.field("role", role_decoder())
use content <- decode.field("content", decode.list(content_block_decoder()))
use model <- decode.field("model", decode.string)
use stop_reason <- decode.field(
"stop_reason",
decode.optional(stop_reason_decoder()),
)
use stop_sequence <- decode.field(
"stop_sequence",
decode.optional(decode.string),
)
use usage <- decode.field("usage", usage_decoder())
decode.success(Message(
id: id,
role: role,
content: content,
model: model,
stop_reason: stop_reason,
stop_sequence: stop_sequence,
usage: usage,
))
}
/// Decoder for Role.
pub fn role_decoder() -> Decoder(Role) {
use s <- decode.then(decode.string)
case s {
"user" -> decode.success(User)
"assistant" -> decode.success(Assistant)
_ -> decode.failure(User, "Role")
}
}
/// Decoder for StopReason.
pub fn stop_reason_decoder() -> Decoder(StopReason) {
use s <- decode.then(decode.string)
case s {
"end_turn" -> decode.success(EndTurn)
"tool_use" -> decode.success(ToolUseStop)
"max_tokens" -> decode.success(MaxTokens)
"stop_sequence" -> decode.success(StopSequence)
"pause_turn" -> decode.success(PauseTurn)
"refusal" -> decode.success(Refusal)
_ -> decode.failure(EndTurn, "StopReason")
}
}
/// Decoder for Usage.
pub fn usage_decoder() -> Decoder(Usage) {
use input_tokens <- decode.field("input_tokens", decode.int)
use output_tokens <- decode.field("output_tokens", decode.int)
use cache_creation <- decode.optional_field(
"cache_creation_input_tokens",
None,
decode.optional(decode.int),
)
use cache_read <- decode.optional_field(
"cache_read_input_tokens",
None,
decode.optional(decode.int),
)
decode.success(Usage(
input_tokens: input_tokens,
output_tokens: output_tokens,
cache_creation_input_tokens: cache_creation,
cache_read_input_tokens: cache_read,
))
}
/// Decoder for ContentBlock — discriminates on the "type" field.
fn content_block_decoder() -> Decoder(ContentBlock) {
use block_type <- decode.field("type", decode.string)
case block_type {
"text" -> text_block_decoder()
"thinking" -> thinking_block_decoder()
"redacted_thinking" -> redacted_thinking_block_decoder()
"tool_use" -> tool_use_block_decoder()
"server_tool_use" -> server_tool_use_block_decoder()
"web_search_result" -> web_search_result_block_decoder()
_ -> decode.failure(Text(text: "", citations: []), "ContentBlock")
}
}
/// Decoder for Text content block.
fn text_block_decoder() -> Decoder(ContentBlock) {
use text <- decode.field("text", decode.string)
use citations <- decode.optional_field(
"citations",
[],
decode.list(citation_decoder()),
)
decode.success(Text(text: text, citations: citations))
}
/// Decoder for Citation — discriminates on the "type" field.
pub fn citation_decoder() -> Decoder(Citation) {
use citation_type <- decode.field("type", decode.string)
case citation_type {
"char_location" -> {
use cited_text <- decode.field("cited_text", decode.string)
use document_index <- decode.field("document_index", decode.int)
use start_char_index <- decode.field("start_char_index", decode.int)
use end_char_index <- decode.field("end_char_index", decode.int)
decode.success(CharLocationCitation(
cited_text: cited_text,
document_index: document_index,
start_char_index: start_char_index,
end_char_index: end_char_index,
))
}
"page_location" -> {
use cited_text <- decode.field("cited_text", decode.string)
use document_index <- decode.field("document_index", decode.int)
use start_page <- decode.field("start_page", decode.int)
use end_page <- decode.field("end_page", decode.int)
decode.success(PageLocationCitation(
cited_text: cited_text,
document_index: document_index,
start_page: start_page,
end_page: end_page,
))
}
"content_block_location" -> {
use cited_text <- decode.field("cited_text", decode.string)
use document_index <- decode.field("document_index", decode.int)
use start_block_index <- decode.field("start_block_index", decode.int)
use end_block_index <- decode.field("end_block_index", decode.int)
decode.success(ContentBlockLocationCitation(
cited_text: cited_text,
document_index: document_index,
start_block_index: start_block_index,
end_block_index: end_block_index,
))
}
"web_search_result_location" -> {
use cited_text <- decode.field("cited_text", decode.string)
use url <- decode.field("url", decode.string)
use title <- decode.field("title", decode.string)
decode.success(WebSearchResultLocationCitation(
cited_text: cited_text,
url: url,
title: title,
))
}
_ ->
decode.failure(
CharLocationCitation(
cited_text: "",
document_index: 0,
start_char_index: 0,
end_char_index: 0,
),
"Citation",
)
}
}
/// Decoder for Thinking content block.
fn thinking_block_decoder() -> Decoder(ContentBlock) {
use thinking <- decode.field("thinking", decode.string)
use signature <- decode.field("signature", decode.string)
decode.success(Thinking(thinking: thinking, signature: signature))
}
/// Decoder for RedactedThinking content block.
fn redacted_thinking_block_decoder() -> Decoder(ContentBlock) {
use data <- decode.field("data", decode.string)
decode.success(RedactedThinking(data: data))
}
/// Decoder for ToolUse content block.
/// The "input" field is an arbitrary JSON object which we re-serialize to a string.
fn tool_use_block_decoder() -> Decoder(ContentBlock) {
use id <- decode.field("id", decode.string)
use name <- decode.field("name", decode.string)
use input_dynamic <- decode.field("input", dynamic_decoder())
// Re-serializing the already-parsed Dynamic value back to a JSON string for
// storage in the ContentBlock. This string will be parsed again when dispatched
// to a tool handler via tool.dispatch. This round-trip is a known trade-off of
// the String-based ToolUse interface — it simplifies conversation history
// encoding at the cost of an extra serialize-parse cycle.
let input_string = dynamic_to_json_string(input_dynamic)
decode.success(ToolUse(id: id, name: name, input: input_string))
}
/// Decoder for ServerToolUse content block.
fn server_tool_use_block_decoder() -> Decoder(ContentBlock) {
use id <- decode.field("id", decode.string)
use name <- decode.field("name", decode.string)
use input_dynamic <- decode.field("input", dynamic_decoder())
let input_string = dynamic_to_json_string(input_dynamic)
decode.success(ServerToolUse(id: id, name: name, input: input_string))
}
/// Decoder for WebSearchResult content block.
fn web_search_result_block_decoder() -> Decoder(ContentBlock) {
use id <- decode.field("id", decode.string)
use title <- decode.field("title", decode.string)
use url <- decode.field("url", decode.string)
use encrypted_content <- decode.field("encrypted_content", decode.string)
decode.success(WebSearchResult(
id: id,
title: title,
url: url,
encrypted_content: encrypted_content,
))
}
/// A decoder that returns the raw Dynamic value without transforming it.
/// This is used to capture the raw JSON value for ToolUse input.
fn dynamic_decoder() -> Decoder(Dynamic) {
decode.new_primitive_decoder("Dynamic", fn(dyn) { Ok(dyn) })
}