///|
/// Parsing for Anthropic's streaming Messages events and `tool_use` blocks.
///
/// Anthropic streams a sequence of typed SSE events rather than OpenAI's
/// uniform `chat.completion.chunk`s:
///
/// - `message_start` — carries the initial message shell and input usage.
/// - `content_block_start` — a new content block (text or tool_use) begins.
/// - `content_block_delta` — an incremental `text_delta` or
/// `input_json_delta` for the current block.
/// - `content_block_stop` — the current block ends.
/// - `message_delta` — top-level updates (stop_reason, output usage).
/// - `message_stop` — the message is complete.
///
/// This module converts a single Anthropic event payload into the common
/// `StreamChunk`, so the same accumulation logic can be reused.
///|
/// Parse one Anthropic streaming event, given its `event:` type and `data:`
/// JSON payload, into a common `StreamChunk`.
///
/// Returns `None` for events that carry no usable delta (message_start,
/// content_block_start/stop, ping, etc.). Raises `LLMError::Decode` on
/// malformed JSON.
pub fn parse_anthropic_event(
event_type : String,
payload : String,
) -> StreamChunk? raise LLMError {
let json = @json.parse(payload) catch {
err => raise Decode("anthropic event: " + err.to_string())
}
guard json is Object(obj) else { return None }
match event_type {
"content_block_delta" => parse_content_block_delta(obj)
"content_block_start" => parse_content_block_start(obj)
"message_delta" => parse_message_delta(obj)
_ => None
}
}
///|
/// Handle `content_block_delta`: either a `text_delta` (assistant text) or an
/// `input_json_delta` (streamed tool-call arguments).
fn parse_content_block_delta(obj : Map[String, Json]) -> StreamChunk? {
let index = match obj.get("index") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
guard obj.get("delta") is Some(Object(delta)) else { return None }
match delta.get("type") {
Some(String("text_delta")) =>
match delta.get("text") {
Some(String(t)) =>
Some({ content: Some(t), tool_calls: [], finish_reason: None })
_ => None
}
Some(String("input_json_delta")) =>
match delta.get("partial_json") {
Some(String(j)) =>
Some({
content: None,
tool_calls: [{ index, id: None, name: None, arguments: Some(j) }],
finish_reason: None,
})
_ => None
}
_ => None
}
}
///|
/// Handle `content_block_start`: if it opens a `tool_use` block, emit a
/// tool-call fragment carrying the id and name so the accumulator can begin
/// assembling arguments.
fn parse_content_block_start(obj : Map[String, Json]) -> StreamChunk? {
let index = match obj.get("index") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
guard obj.get("content_block") is Some(Object(block)) else { return None }
guard block.get("type") is Some(String("tool_use")) else { return None }
let id = match block.get("id") {
Some(String(s)) => Some(s)
_ => None
}
let name = match block.get("name") {
Some(String(s)) => Some(s)
_ => None
}
Some({
content: None,
tool_calls: [{ index, id, name, arguments: None }],
finish_reason: None,
})
}
///|
/// Handle `message_delta`: extract the `stop_reason` if present.
fn parse_message_delta(obj : Map[String, Json]) -> StreamChunk? {
guard obj.get("delta") is Some(Object(delta)) else { return None }
match delta.get("stop_reason") {
Some(String(s)) =>
Some({
content: None,
tool_calls: [],
finish_reason: Some(anthropic_stop_reason(s)),
})
_ => None
}
}
///|
/// Parse the `tool_use` content blocks from a complete (non-streaming)
/// Anthropic response into common `ToolCall`s.
///
/// Anthropic represents tool calls as `{"type":"tool_use","id":...,
/// "name":...,"input":{...}}` blocks in the response `content` array; the
/// `input` object is re-serialized into the `arguments` JSON string to match
/// the OpenAI shape.
pub fn parse_anthropic_tool_calls(json : Json) -> Array[ToolCall] {
let out = []
guard json is Object(obj) else { return out }
guard obj.get("content") is Some(Array(blocks)) else { return out }
for block in blocks {
guard block is Object(b) else { continue }
guard b.get("type") is Some(String("tool_use")) else { continue }
let id = match b.get("id") {
Some(String(s)) => s
_ => ""
}
let name = match b.get("name") {
Some(String(s)) => s
_ => ""
}
let arguments = match b.get("input") {
Some(input) => input.stringify()
None => "{}"
}
out.push({ id, function: { name, arguments } })
}
out
}