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) }