use serde::Deserialize;
use serde_json::Value;
use crate::inference::error::InferenceError;
use crate::inference::types::{
AssistantMessage, ChatChoice, ChatResponse, FunctionCall, ToolCall, UsageBlock,
};
pub fn parse(text: &str) -> Result<ChatResponse, InferenceError> {
let raw: AnthropicResponse =
serde_json::from_str(text).map_err(|e| InferenceError::Deserialise {
message: e.to_string(),
body: text.to_string(),
})?;
Ok(raw.into_chat_response())
}
#[derive(Debug, Deserialize)]
struct AnthropicResponse {
id: String,
#[serde(default)]
model: String,
#[serde(default)]
content: Vec<ContentBlock>,
#[serde(default)]
stop_reason: Option<String>,
#[serde(default)]
usage: AnthropicUsage,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum ContentBlock {
Text {
#[serde(default)]
text: String,
},
ToolUse {
id: String,
name: String,
#[serde(default)]
input: Value,
},
#[serde(other)]
Other,
}
#[derive(Debug, Default, Deserialize)]
struct AnthropicUsage {
#[serde(default)]
input_tokens: u32,
#[serde(default)]
output_tokens: u32,
#[serde(default)]
cache_read_input_tokens: u32,
#[serde(default)]
cache_creation_input_tokens: u32,
}
impl AnthropicResponse {
fn into_chat_response(self) -> ChatResponse {
let mut text = String::new();
let mut tool_calls: Vec<ToolCall> = Vec::new();
for block in self.content {
match block {
ContentBlock::Text { text: t } => text.push_str(&t),
ContentBlock::ToolUse { id, name, input } => tool_calls.push(ToolCall {
id,
kind: "function".into(),
function: FunctionCall {
name,
arguments: serde_json::to_string(&input)
.unwrap_or_else(|_| "{}".to_string()),
},
}),
ContentBlock::Other => {}
}
}
let content = if text.is_empty() { None } else { Some(text) };
let finish_reason = self.stop_reason.as_deref().map(map_stop_reason);
let usage = UsageBlock {
prompt_tokens: self.usage.input_tokens,
completion_tokens: self.usage.output_tokens,
total_tokens: self
.usage
.input_tokens
.saturating_add(self.usage.output_tokens),
cache_read_input_tokens: self.usage.cache_read_input_tokens,
cache_creation_input_tokens: self.usage.cache_creation_input_tokens,
prompt_tokens_details: None,
cost: None,
};
ChatResponse {
id: self.id,
model: self.model,
choices: vec![ChatChoice {
message: AssistantMessage {
content,
tool_calls,
},
finish_reason,
}],
usage,
}
}
}
fn map_stop_reason(reason: &str) -> String {
match reason {
"end_turn" | "stop_sequence" => "stop".to_string(),
"max_tokens" => "length".to_string(),
"tool_use" => "tool_calls".to_string(),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inference::types::StopReason;
#[test]
fn parses_text_and_native_usage() {
let body = r#"{
"id": "msg_1",
"model": "claude-sonnet-4-5",
"content": [{"type": "text", "text": "pong"}],
"stop_reason": "end_turn",
"usage": {
"input_tokens": 12,
"output_tokens": 3,
"cache_read_input_tokens": 5,
"cache_creation_input_tokens": 2
}
}"#;
let resp = parse(body).expect("parse");
assert_eq!(resp.id, "msg_1");
assert_eq!(resp.first_text().as_deref(), Some("pong"));
assert_eq!(resp.stop_reason(), Some(StopReason::Stop));
let usage = resp.usage();
assert_eq!(
usage.prompt_tokens, 12,
"input_tokens must not deserialise to 0"
);
assert_eq!(usage.completion_tokens, 3);
assert_eq!(usage.cache_read_tokens, 5);
assert_eq!(usage.cache_creation_tokens, 2);
assert_eq!(usage.total_tokens(), 15);
}
#[test]
fn parses_tool_use_blocks() {
let body = r#"{
"id": "msg_2",
"model": "claude-sonnet-4-5",
"content": [
{"type": "text", "text": "let me check"},
{"type": "tool_use", "id": "toolu_9", "name": "get_weather",
"input": {"loc": "SEA"}}
],
"stop_reason": "tool_use",
"usage": {"input_tokens": 20, "output_tokens": 8}
}"#;
let resp = parse(body).expect("parse");
assert_eq!(resp.first_text().as_deref(), Some("let me check"));
assert_eq!(resp.stop_reason(), Some(StopReason::ToolCalls));
let calls = resp.first_tool_calls();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].id, "toolu_9");
assert_eq!(calls[0].kind, "function");
assert_eq!(calls[0].function.name, "get_weather");
let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args["loc"], "SEA");
}
#[test]
fn maps_stop_reasons() {
assert_eq!(map_stop_reason("end_turn"), "stop");
assert_eq!(map_stop_reason("stop_sequence"), "stop");
assert_eq!(map_stop_reason("max_tokens"), "length");
assert_eq!(map_stop_reason("tool_use"), "tool_calls");
assert_eq!(map_stop_reason("refusal"), "refusal");
}
#[test]
fn ignores_unknown_block_types() {
let body = r#"{
"id": "msg_3",
"content": [
{"type": "thinking", "thinking": "hmm"},
{"type": "text", "text": "answer"}
],
"stop_reason": "end_turn",
"usage": {"input_tokens": 1, "output_tokens": 1}
}"#;
let resp = parse(body).expect("parse");
assert_eq!(resp.first_text().as_deref(), Some("answer"));
}
#[test]
fn parse_error_is_deserialise() {
let Err(err) = parse("{not json") else {
panic!("expected a Deserialise error");
};
assert!(matches!(err, InferenceError::Deserialise { .. }));
}
}