use serde_json::{json, Value};
use crate::utils::parse_json_value_strict_str;
use std::collections::HashMap;
use crate::providers::StreamChunk;
#[derive(Debug, Default)]
pub struct ToolCallAccumulator {
current_tool_blocks: HashMap<usize, PartialToolCall>,
}
#[derive(Debug, Clone)]
struct PartialToolCall {
id: String,
name: String,
input: String, }
impl ToolCallAccumulator {
pub fn new() -> Self {
Self {
current_tool_blocks: HashMap::new(),
}
}
pub fn start_tool(&mut self, index: usize, id: String, name: String) {
self.current_tool_blocks.insert(
index,
PartialToolCall {
id,
name,
input: String::new(),
},
);
}
pub fn accumulate_input(&mut self, index: usize, partial_json: &str) -> Option<Value> {
if let Some(tool) = self.current_tool_blocks.get_mut(&index) {
tool.input.push_str(partial_json);
Some(json!({
"id": tool.id,
"function": {
"name": tool.name,
"arguments": tool.input
}
}))
} else {
None
}
}
pub fn complete_tool(&mut self, index: usize) -> Option<Value> {
if let Some(tool) = self.current_tool_blocks.remove(&index) {
let arguments = if tool.input.is_empty() {
json!({})
} else {
parse_json_value_strict_str(&tool.input).unwrap_or_else(|_| json!({}))
};
Some(json!({
"id": tool.id,
"function": {
"name": tool.name,
"arguments": arguments
}
}))
} else {
None
}
}
}
pub fn parse_anthropic_chunk(
chunk_json: &Value,
accumulator: &mut ToolCallAccumulator,
has_tools: bool,
) -> Vec<StreamChunk> {
let mut results = Vec::new();
let chunk_type = match chunk_json["type"].as_str() {
Some(t) => t,
None => return results,
};
match chunk_type {
"content_block_delta" => {
let delta = &chunk_json["delta"];
let content_type = delta["type"].as_str().unwrap_or("");
match content_type {
"text_delta" => {
if let Some(text) = delta["text"].as_str() {
results.push(StreamChunk::Content(text.to_string()));
}
}
"input_json_delta" if has_tools => {
if let Some(partial_json) = delta["partial_json"].as_str() {
let index = chunk_json["index"].as_u64().unwrap_or(0) as usize;
if let Some(partial_tool) =
accumulator.accumulate_input(index, partial_json)
{
results.push(StreamChunk::ToolCallPartial(partial_tool));
}
}
}
"thinking_delta" => {
if let Some(thinking) = delta["thinking"].as_str() {
results.push(StreamChunk::Reasoning(thinking.to_string()));
}
}
"signature_delta" => {
if let Some(signature) = delta["signature"].as_str() {
results.push(StreamChunk::Signature(signature.to_string()));
}
}
_ => {}
}
}
"content_block_start" => {
let content_block = &chunk_json["content_block"];
if content_block["type"] == "tool_use" {
let index = chunk_json["index"].as_u64().unwrap_or(0) as usize;
let id = content_block["id"].as_str().unwrap_or("").to_string();
let name = content_block["name"].as_str().unwrap_or("").to_string();
accumulator.start_tool(index, id, name);
}
}
"content_block_stop" => {
if has_tools {
let index = chunk_json["index"].as_u64().unwrap_or(0) as usize;
if let Some(complete_tool) = accumulator.complete_tool(index) {
results.push(StreamChunk::ToolCallComplete(complete_tool));
}
}
}
"message_delta" => {
let usage = &chunk_json["usage"];
if usage.is_object() {
let input_tokens = usage["input_tokens"].as_u64().unwrap_or(0);
let output_tokens = usage["output_tokens"].as_u64().unwrap_or(0);
if output_tokens > 0 || input_tokens > 0 {
results.push(StreamChunk::Usage {
input_tokens,
output_tokens,
cached_tokens: 0,
});
}
}
}
"message_start" => {
if let Some(usage) = chunk_json["message"]["usage"].as_object() {
let input_tokens = usage
.get("input_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let cached_tokens = usage
.get("cache_read_input_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0);
if input_tokens > 0 {
results.push(StreamChunk::Usage {
input_tokens,
output_tokens: 0,
cached_tokens,
});
}
}
}
_ => {}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_accumulator_start() {
let mut acc = ToolCallAccumulator::new();
acc.start_tool(0, "toolu_123".to_string(), "get_weather".to_string());
assert_eq!(acc.current_tool_blocks.len(), 1);
assert_eq!(acc.current_tool_blocks[&0].id, "toolu_123");
assert_eq!(acc.current_tool_blocks[&0].name, "get_weather");
}
#[test]
fn test_tool_accumulator_accumulate() {
let mut acc = ToolCallAccumulator::new();
acc.start_tool(0, "toolu_123".to_string(), "get_weather".to_string());
let partial1 = acc.accumulate_input(0, "{\"city\":").unwrap();
assert_eq!(partial1["function"]["arguments"], "{\"city\":");
let partial2 = acc.accumulate_input(0, "\"SF\"}").unwrap();
assert_eq!(partial2["function"]["arguments"], "{\"city\":\"SF\"}");
}
#[test]
fn test_tool_accumulator_complete() {
let mut acc = ToolCallAccumulator::new();
acc.start_tool(0, "toolu_123".to_string(), "get_weather".to_string());
acc.accumulate_input(0, "{\"city\":\"SF\"}");
let complete = acc.complete_tool(0).unwrap();
assert_eq!(complete["id"], "toolu_123");
assert_eq!(complete["function"]["name"], "get_weather");
assert_eq!(complete["function"]["arguments"]["city"], "SF");
assert_eq!(acc.current_tool_blocks.len(), 0); }
#[test]
fn test_parse_text_delta() {
let mut acc = ToolCallAccumulator::new();
let chunk = json!({
"type": "content_block_delta",
"delta": {
"type": "text_delta",
"text": "Hello"
}
});
let chunks = parse_anthropic_chunk(&chunk, &mut acc, false);
assert_eq!(chunks.len(), 1);
match &chunks[0] {
StreamChunk::Content(text) => assert_eq!(text, "Hello"),
_ => panic!("Expected Content chunk"),
}
}
#[test]
fn test_parse_tool_use_flow() {
let mut acc = ToolCallAccumulator::new();
let start_chunk = json!({
"type": "content_block_start",
"index": 0,
"content_block": {
"type": "tool_use",
"id": "toolu_123",
"name": "get_weather"
}
});
let chunks = parse_anthropic_chunk(&start_chunk, &mut acc, true);
assert_eq!(chunks.len(), 0);
let delta_chunk = json!({
"type": "content_block_delta",
"index": 0,
"delta": {
"type": "input_json_delta",
"partial_json": "{\"city\":\"SF\"}"
}
});
let chunks = parse_anthropic_chunk(&delta_chunk, &mut acc, true);
assert_eq!(chunks.len(), 1);
match &chunks[0] {
StreamChunk::ToolCallPartial(tool) => {
assert_eq!(tool["id"], "toolu_123");
assert_eq!(tool["function"]["arguments"], "{\"city\":\"SF\"}");
}
_ => panic!("Expected ToolCallPartial"),
}
let stop_chunk = json!({
"type": "content_block_stop",
"index": 0
});
let chunks = parse_anthropic_chunk(&stop_chunk, &mut acc, true);
assert_eq!(chunks.len(), 1);
match &chunks[0] {
StreamChunk::ToolCallComplete(tool) => {
assert_eq!(tool["id"], "toolu_123");
assert_eq!(tool["function"]["name"], "get_weather");
}
_ => panic!("Expected ToolCallComplete"),
}
}
#[test]
fn test_parse_thinking_delta() {
let mut acc = ToolCallAccumulator::new();
let chunk = json!({
"type": "content_block_delta",
"delta": {
"type": "thinking_delta",
"thinking": "Let me think about this..."
}
});
let chunks = parse_anthropic_chunk(&chunk, &mut acc, false);
assert_eq!(chunks.len(), 1);
match &chunks[0] {
StreamChunk::Reasoning(text) => assert_eq!(text, "Let me think about this..."),
_ => panic!("Expected Reasoning chunk"),
}
}
#[test]
fn test_parse_signature_delta() {
let mut acc = ToolCallAccumulator::new();
let chunk = json!({
"type": "content_block_delta",
"delta": {
"type": "signature_delta",
"signature": "sig_abc123"
}
});
let chunks = parse_anthropic_chunk(&chunk, &mut acc, false);
assert_eq!(chunks.len(), 1);
match &chunks[0] {
StreamChunk::Signature(sig) => assert_eq!(sig, "sig_abc123"),
_ => panic!("Expected Signature chunk"),
}
}
}