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/encode.gleam
import gleam/json.{type Json}
import gleam/list
import gleam/option.{type Option, None, Some}
import claude/types/content.{
type ContentBlockParam, type DocumentSource, type ImageSource,
type ToolResultContent, Base64Document, Base64Image, BlocksResult,
DocumentParam, ImageParam, RedactedThinkingParam, ServerToolUseParam,
StringResult, TextParam, ThinkingParam, ToolResultParam, ToolUseParam,
UrlDocument, UrlImage,
}
import claude/types/message.{
type MessageContent, type MessageParam, type Role, Assistant, BlockContent,
MessageParam, StringContent, User,
}
import claude/types/tool.{type Registry, type ToolChoice, Any, Auto, NoTools, SpecificTool}
/// Parse a JSON string and embed it as raw JSON (not as an escaped string).
/// If the string is not valid JSON, falls back to encoding the original string
/// as a JSON string value so the error is visible downstream.
@external(erlang, "claude_json_ffi", "json_string_to_raw")
fn raw_json(json_string: String) -> Json
/// Encode a Role to its API string representation.
fn role(r: Role) -> Json {
case r {
User -> json.string("user")
Assistant -> json.string("assistant")
}
}
/// Encode a MessageParam for the API request.
pub fn message_param(msg: MessageParam) -> Json {
let MessageParam(role: r, content: c) = msg
json.object([#("role", role(r)), #("content", message_content(c))])
}
/// Encode message content — either a plain string or a list of content blocks.
fn message_content(c: MessageContent) -> Json {
case c {
StringContent(s) -> json.string(s)
BlockContent(blocks) -> json.array(blocks, content_block_param)
}
}
/// Encode a ContentBlockParam for the API request.
pub fn content_block_param(block: ContentBlockParam) -> Json {
case block {
TextParam(text: t) ->
json.object([#("type", json.string("text")), #("text", json.string(t))])
ImageParam(source: src) ->
json.object([
#("type", json.string("image")),
#("source", image_source(src)),
])
DocumentParam(source: src) ->
json.object([
#("type", json.string("document")),
#("source", document_source(src)),
])
ToolUseParam(id: id, name: name, input: input) ->
json.object([
#("type", json.string("tool_use")),
#("id", json.string(id)),
#("name", json.string(name)),
#("input", raw_json(input)),
])
ToolResultParam(tool_use_id: id, content: c, is_error: err) -> {
let base = [
#("type", json.string("tool_result")),
#("tool_use_id", json.string(id)),
#("content", tool_result_content(c)),
]
let fields = case err {
True -> list.append(base, [#("is_error", json.bool(True))])
False -> base
}
json.object(fields)
}
ThinkingParam(thinking: t, signature: s) ->
json.object([
#("type", json.string("thinking")),
#("thinking", json.string(t)),
#("signature", json.string(s)),
])
RedactedThinkingParam(data: d) ->
json.object([
#("type", json.string("redacted_thinking")),
#("data", json.string(d)),
])
ServerToolUseParam(id: id, name: name, input: input) ->
json.object([
#("type", json.string("server_tool_use")),
#("id", json.string(id)),
#("name", json.string(name)),
#("input", raw_json(input)),
])
}
}
/// Encode tool result content.
fn tool_result_content(c: ToolResultContent) -> Json {
case c {
StringResult(s) -> json.string(s)
BlocksResult(blocks) -> json.array(blocks, content_block_param)
}
}
/// Encode an image source.
fn image_source(src: ImageSource) -> Json {
case src {
Base64Image(media_type: mt, data: d) ->
json.object([
#("type", json.string("base64")),
#("media_type", json.string(mt)),
#("data", json.string(d)),
])
UrlImage(url: u) ->
json.object([#("type", json.string("url")), #("url", json.string(u))])
}
}
/// Encode a document source.
fn document_source(src: DocumentSource) -> Json {
case src {
Base64Document(media_type: mt, data: d) ->
json.object([
#("type", json.string("base64")),
#("media_type", json.string(mt)),
#("data", json.string(d)),
])
UrlDocument(url: u) ->
json.object([#("type", json.string("url")), #("url", json.string(u))])
}
}
/// Encode a ToolChoice for the API request.
pub fn tool_choice(choice: ToolChoice) -> Json {
case choice {
Auto(disable_parallel: dp) ->
json.object([
#("type", json.string("auto")),
#("disable_parallel_tool_use", json.bool(dp)),
])
Any(disable_parallel: dp) ->
json.object([
#("type", json.string("any")),
#("disable_parallel_tool_use", json.bool(dp)),
])
SpecificTool(name: n, disable_parallel: dp) ->
json.object([
#("type", json.string("tool")),
#("name", json.string(n)),
#("disable_parallel_tool_use", json.bool(dp)),
])
NoTools ->
json.object([#("type", json.string("none"))])
}
}
/// Encode the full Messages API request body.
///
/// Only includes optional fields when they have values.
/// Does not include an empty tools array.
/// Includes stream: true only when stream is True.
pub fn create_params(
model model: String,
max_tokens max_tokens: Int,
messages messages: List(MessageParam),
tools tools: Registry,
system system: Option(String),
tool_choice tc: Option(ToolChoice),
thinking thinking: Option(Int),
stream stream: Bool,
) -> Json {
let fields = [
#("model", json.string(model)),
#("max_tokens", json.int(max_tokens)),
#("messages", json.array(messages, message_param)),
]
// Add tools if non-empty
let fields = case tool.is_empty(tools) {
True -> fields
False -> list.append(fields, [#("tools", tool.to_json(tools))])
}
// Add system if present
let fields = case system {
Some(s) -> list.append(fields, [#("system", json.string(s))])
None -> fields
}
// Add tool_choice if present
let fields = case tc {
Some(c) -> list.append(fields, [#("tool_choice", tool_choice(c))])
None -> fields
}
// Add thinking if present (budget_tokens with extended thinking)
let fields = case thinking {
Some(budget) ->
list.append(fields, [
#(
"thinking",
json.object([
#("type", json.string("enabled")),
#("budget_tokens", json.int(budget)),
]),
),
])
None -> fields
}
// Add stream if true
let fields = case stream {
True -> list.append(fields, [#("stream", json.bool(True))])
False -> fields
}
json.object(fields)
}