use super::{
FinishReason, LLMRequest, LlmProvider, ProviderError, ProviderStreamChunk, TokenUsage,
};
use crate::shared::{ContentBlock, Message, Role, ToolDefinition};
use async_trait::async_trait;
use futures_util::stream::BoxStream;
use reqwest::{self, StatusCode, header::HeaderMap};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::{collections::BTreeMap, time::Duration};
const DEFAULT_BASE_URL: &str = "https://api.anthropic.com/v1";
const ANTHROPIC_VERSION: &str = "2023-06-01";
const DEFAULT_MAX_TOKENS: u32 = 4096;
pub struct AnthropicProvider {
pub api_key: String,
pub base_url: String,
client: reqwest::Client,
}
impl AnthropicProvider {
pub fn new(api_key: impl Into<String>) -> Self {
Self::with_base_url(api_key, DEFAULT_BASE_URL)
}
pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
Self::with_timeout(api_key, base_url, Duration::from_secs(120))
}
pub fn with_timeout(
api_key: impl Into<String>,
base_url: impl Into<String>,
timeout: Duration,
) -> Self {
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(timeout)
.build()
.expect("reqwest client configuration should be valid");
Self {
api_key: api_key.into(),
base_url: base_url.into(),
client,
}
}
}
#[derive(Debug, Serialize)]
struct AnthropicReq {
model: String,
max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
system: Option<String>,
messages: Vec<AnthropicMessage>,
#[serde(skip_serializing_if = "Vec::is_empty")]
tools: Vec<AnthropicTool>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
stream: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct AnthropicMessage {
role: String,
content: Vec<AnthropicContentBlock>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
enum AnthropicContentBlock {
Text {
text: String,
},
ToolUse {
id: String,
name: String,
input: Value,
},
ToolResult {
tool_use_id: String,
content: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
is_error: bool,
},
}
#[derive(Debug, Serialize)]
struct AnthropicTool {
name: String,
description: String,
input_schema: Value,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum StreamEvent {
MessageStart {
message: StreamMessageStart,
},
ContentBlockStart {
index: usize,
content_block: Value,
},
ContentBlockDelta {
index: usize,
delta: StreamDelta,
},
ContentBlockStop {
index: usize,
},
MessageDelta {
delta: StreamMessageDelta,
usage: Option<AnthropicUsage>,
},
MessageStop,
Ping,
Error {
error: AnthropicErrorBody,
},
}
#[derive(Debug, Deserialize)]
struct StreamMessageStart {
usage: Option<AnthropicUsage>,
}
#[derive(Debug, Deserialize)]
struct StreamMessageDelta {
stop_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum StreamDelta {
TextDelta { text: String },
InputJsonDelta { partial_json: String },
ThinkingDelta { thinking: String },
SignatureDelta { signature: String },
}
#[derive(Debug, Deserialize, Clone)]
struct AnthropicUsage {
input_tokens: Option<u32>,
output_tokens: Option<u32>,
}
impl From<AnthropicUsage> for TokenUsage {
fn from(value: AnthropicUsage) -> Self {
Self {
input_tokens: value.input_tokens,
output_tokens: value.output_tokens,
total_tokens: match (value.input_tokens, value.output_tokens) {
(Some(input), Some(output)) => Some(input + output),
_ => None,
},
}
}
}
#[derive(Debug, Deserialize)]
struct AnthropicErrorBody {
#[serde(rename = "type")]
kind: String,
message: String,
}
#[derive(Default)]
struct PendingToolBlock {
id: Option<String>,
name: Option<String>,
arguments: String,
}
#[async_trait]
impl LlmProvider for AnthropicProvider {
fn name(&self) -> &str {
"anthropic"
}
fn request_snapshot(&self, req: &LLMRequest) -> Value {
serde_json::to_value(build_anthropic_payload(req.clone())).unwrap_or(Value::Null)
}
async fn chat_completion(
&self,
req: LLMRequest,
) -> Result<BoxStream<'static, Result<ProviderStreamChunk, ProviderError>>, ProviderError> {
let payload = build_anthropic_payload(req);
let res = self
.client
.post(format!("{}/messages", self.base_url.trim_end_matches('/')))
.header("x-api-key", &self.api_key)
.header("anthropic-version", ANTHROPIC_VERSION)
.json(&payload)
.send()
.await
.map_err(map_transport_error)?;
let status = res.status();
if !status.is_success() {
let headers = res.headers().clone();
let err_text = res.text().await.unwrap_or_default();
return Err(map_status_error(status, &headers, err_text));
}
let mut res = res;
let output_stream = async_stream::stream! {
let mut byte_buffer = Vec::<u8>::new();
let mut sse_event = SseEvent::default();
let mut pending_tools = BTreeMap::<usize, PendingToolBlock>::new();
let mut stop_reason: Option<String> = None;
let mut usage: Option<TokenUsage> = None;
loop {
let chunk = match res.chunk().await {
Ok(Some(chunk)) => chunk,
Ok(None) => break,
Err(e) => {
yield Err(map_transport_error(e));
return;
}
};
let lines = match take_sse_lines(&mut byte_buffer, &chunk) {
Ok(lines) => lines,
Err(e) => {
yield Err(e);
return;
}
};
for line in lines {
if line.is_empty() {
if let Some(data) = sse_event.data.take() {
match serde_json::from_str::<StreamEvent>(&data) {
Ok(event) => {
match handle_stream_event(
event,
&mut pending_tools,
&mut stop_reason,
&mut usage,
) {
Ok(output) => {
for chunk in output.chunks {
yield Ok(chunk);
}
if output.done {
return;
}
}
Err(error) => {
yield Err(error);
return;
}
}
}
Err(e) => {
yield Err(ProviderError::transient(format!("failed to parse Anthropic SSE event: {e}; data={data}")));
return;
}
}
}
sse_event.event = None;
continue;
}
if let Some(event) = line.strip_prefix("event: ") {
sse_event.event = Some(event.to_string());
} else if let Some(data) = line.strip_prefix("data: ") {
match &mut sse_event.data {
Some(existing) => {
existing.push('\n');
existing.push_str(data);
}
None => sse_event.data = Some(data.to_string()),
}
}
}
}
yield Err(ProviderError::transient("Anthropic stream ended before message_stop"));
};
Ok(Box::pin(output_stream))
}
}
#[derive(Default)]
struct SseEvent {
event: Option<String>,
data: Option<String>,
}
#[derive(Default)]
struct StreamEventOutput {
chunks: Vec<ProviderStreamChunk>,
done: bool,
}
fn handle_stream_event(
event: StreamEvent,
pending_tools: &mut BTreeMap<usize, PendingToolBlock>,
stop_reason: &mut Option<String>,
usage: &mut Option<TokenUsage>,
) -> Result<StreamEventOutput, ProviderError> {
let mut output = StreamEventOutput::default();
match event {
StreamEvent::MessageStart { message } => {
if let Some(event_usage) = message.usage {
merge_usage(usage, event_usage);
}
}
StreamEvent::ContentBlockStart {
index,
content_block,
} => {
if content_block.get("type").and_then(Value::as_str) == Some("tool_use") {
let block = pending_tools.entry(index).or_default();
block.id = content_block
.get("id")
.and_then(Value::as_str)
.map(str::to_string);
block.name = content_block
.get("name")
.and_then(Value::as_str)
.map(str::to_string);
if let Some(input) = content_block.get("input") {
if input.as_object().is_some_and(|object| !object.is_empty()) {
block.arguments = input.to_string();
}
}
}
}
StreamEvent::ContentBlockDelta { index, delta } => match delta {
StreamDelta::TextDelta { text } => {
if !text.is_empty() {
output.chunks.push(ProviderStreamChunk::ContentDelta(text));
}
}
StreamDelta::ThinkingDelta { thinking } => {
if !thinking.is_empty() {
output
.chunks
.push(ProviderStreamChunk::ThinkingDelta(thinking));
}
}
StreamDelta::InputJsonDelta { partial_json } => {
pending_tools
.entry(index)
.or_default()
.arguments
.push_str(&partial_json);
}
StreamDelta::SignatureDelta { signature } => {
let _ = signature;
}
},
StreamEvent::ContentBlockStop { index } => {
if let Some(block) = pending_tools.remove(&index) {
output.chunks.push(ProviderStreamChunk::ToolCallDelta {
index,
id: block.id,
name: block.name,
arguments_delta: if block.arguments.is_empty() {
"{}".to_string()
} else {
block.arguments
},
});
}
}
StreamEvent::MessageDelta {
delta,
usage: event_usage,
} => {
*stop_reason = delta.stop_reason;
if let Some(event_usage) = event_usage {
merge_usage(usage, event_usage);
}
}
StreamEvent::MessageStop => {
output.chunks.push(ProviderStreamChunk::Done {
finish_reason: map_stop_reason(stop_reason.as_deref()),
usage: usage.take(),
});
output.done = true;
}
StreamEvent::Ping => {}
StreamEvent::Error { error } => {
return Err(map_stream_error(error));
}
}
Ok(output)
}
fn merge_usage(usage: &mut Option<TokenUsage>, incoming: AnthropicUsage) {
let incoming = TokenUsage::from(incoming);
match usage {
Some(existing) => {
if incoming.input_tokens.is_some() {
existing.input_tokens = incoming.input_tokens;
}
if incoming.output_tokens.is_some() {
existing.output_tokens = incoming.output_tokens;
}
existing.total_tokens = match (existing.input_tokens, existing.output_tokens) {
(Some(input), Some(output)) => Some(input + output),
_ => None,
};
}
None => *usage = Some(incoming),
}
}
fn build_anthropic_payload(req: LLMRequest) -> AnthropicReq {
let (messages, extra_system) = transform_messages(req.messages);
let system = join_system(req.system, extra_system);
let tools = req.tools.iter().map(build_tool_schema).collect();
AnthropicReq {
model: req.model,
max_tokens: req.max_tokens.unwrap_or(DEFAULT_MAX_TOKENS),
system,
messages,
tools,
temperature: req.temperature,
stream: true,
}
}
fn join_system(primary: Option<String>, extra: Option<String>) -> Option<String> {
match (primary, extra) {
(Some(primary), Some(extra)) if !extra.is_empty() => Some(format!("{primary}\n\n{extra}")),
(Some(primary), _) => Some(primary),
(None, Some(extra)) if !extra.is_empty() => Some(extra),
_ => None,
}
}
fn transform_messages(messages: Vec<Message>) -> (Vec<AnthropicMessage>, Option<String>) {
let mut out = Vec::<AnthropicMessage>::new();
let mut tool_results = Vec::<AnthropicContentBlock>::new();
let mut system_parts = Vec::<String>::new();
for message in messages {
match message.role {
Role::System => system_parts.push(flatten_blocks(message.blocks)),
Role::User => {
flush_tool_results(&mut out, &mut tool_results);
push_message(
&mut out,
"user",
message
.blocks
.into_iter()
.filter_map(user_content_block)
.collect(),
);
}
Role::Assistant => {
flush_tool_results(&mut out, &mut tool_results);
push_message(
&mut out,
"assistant",
message
.blocks
.into_iter()
.filter_map(assistant_content_block)
.collect(),
);
}
Role::Tool => {
tool_results.extend(message.blocks.into_iter().filter_map(tool_result_block));
}
}
}
flush_tool_results(&mut out, &mut tool_results);
let system = if system_parts.is_empty() {
None
} else {
Some(system_parts.join("\n\n"))
};
(out, system)
}
fn flush_tool_results(
out: &mut Vec<AnthropicMessage>,
tool_results: &mut Vec<AnthropicContentBlock>,
) {
if tool_results.is_empty() {
return;
}
let content = std::mem::take(tool_results);
push_message(out, "user", content);
}
fn push_message(out: &mut Vec<AnthropicMessage>, role: &str, content: Vec<AnthropicContentBlock>) {
if content.is_empty() {
return;
}
if let Some(last) = out.last_mut() {
if last.role == role {
last.content.extend(content);
return;
}
}
out.push(AnthropicMessage {
role: role.to_string(),
content,
});
}
fn user_content_block(block: ContentBlock) -> Option<AnthropicContentBlock> {
match block {
ContentBlock::Text { text } => Some(AnthropicContentBlock::Text { text }),
ContentBlock::ToolResult { .. } => tool_result_block(block),
ContentBlock::Thinking { .. } | ContentBlock::ToolUse { .. } => None,
}
}
fn assistant_content_block(block: ContentBlock) -> Option<AnthropicContentBlock> {
match block {
ContentBlock::Text { text } => Some(AnthropicContentBlock::Text { text }),
ContentBlock::ToolUse { id, name, input } => {
Some(AnthropicContentBlock::ToolUse { id, name, input })
}
ContentBlock::Thinking { .. } | ContentBlock::ToolResult { .. } => None,
}
}
fn tool_result_block(block: ContentBlock) -> Option<AnthropicContentBlock> {
let ContentBlock::ToolResult {
tool_use_id,
content,
is_error,
} = block
else {
return None;
};
Some(AnthropicContentBlock::ToolResult {
tool_use_id,
content: flatten_blocks(content),
is_error,
})
}
fn flatten_blocks(blocks: Vec<ContentBlock>) -> String {
let mut out = String::new();
for block in blocks {
match block {
ContentBlock::Text { text } => out.push_str(&text),
ContentBlock::Thinking { .. } => {}
ContentBlock::ToolUse { id, name, input } => {
out.push_str(&format!("[tool_use id={id} name={name} input={}]", input));
}
ContentBlock::ToolResult {
tool_use_id,
content,
is_error,
} => {
if is_error {
out.push_str("[tool_result error ");
} else {
out.push_str("[tool_result ");
}
out.push_str(&format!(
"tool_use_id={tool_use_id}]{}",
flatten_blocks(content)
));
}
}
}
out
}
fn build_tool_schema(tool: &ToolDefinition) -> AnthropicTool {
AnthropicTool {
name: tool.name.clone(),
description: tool.desc.clone(),
input_schema: normalize_anthropic_tool_schema(tool.arguments.clone()),
}
}
fn normalize_anthropic_tool_schema(mut schema: Value) -> Value {
let definitions = schema
.get("definitions")
.or_else(|| schema.get("$defs"))
.cloned()
.unwrap_or_else(|| Value::Object(Map::new()));
inline_local_refs(&mut schema, &definitions);
simplify_tool_schema(&mut schema);
schema
}
fn inline_local_refs(value: &mut Value, definitions: &Value) {
match value {
Value::Object(map) => {
if let Some(ref_value) = map.get("$ref").and_then(Value::as_str) {
if let Some(mut resolved) = resolve_local_ref(ref_value, definitions) {
inline_local_refs(&mut resolved, definitions);
if let Value::Object(resolved_map) = &mut resolved {
for (key, value) in map.iter() {
if key != "$ref" {
resolved_map
.entry(key.clone())
.or_insert_with(|| value.clone());
}
}
}
*value = resolved;
return;
}
}
for child in map.values_mut() {
inline_local_refs(child, definitions);
}
}
Value::Array(items) => {
for item in items {
inline_local_refs(item, definitions);
}
}
_ => {}
}
}
fn resolve_local_ref(ref_value: &str, definitions: &Value) -> Option<Value> {
let key = ref_value
.strip_prefix("#/definitions/")
.or_else(|| ref_value.strip_prefix("#/$defs/"))?;
definitions.get(key).cloned()
}
fn simplify_tool_schema(value: &mut Value) {
match value {
Value::Object(map) => {
for child in map.values_mut() {
simplify_tool_schema(child);
}
if let Some(compressed) = compress_enum_union(map) {
*value = compressed;
return;
}
map.remove("$schema");
map.remove("$defs");
map.remove("definitions");
map.remove("title");
map.remove("format");
}
Value::Array(items) => {
for item in items {
simplify_tool_schema(item);
}
}
_ => {}
}
}
fn compress_enum_union(map: &Map<String, Value>) -> Option<Value> {
let variants = map
.get("oneOf")
.or_else(|| map.get("anyOf"))
.and_then(Value::as_array)?;
let mut enum_values = Vec::<Value>::new();
let mut has_null = false;
for variant in variants {
let variant = variant.as_object()?;
if is_null_schema(variant) {
has_null = true;
continue;
}
let values = variant.get("enum")?.as_array()?;
if values.is_empty() || !values.iter().all(|value| value.is_string()) {
return None;
}
enum_values.extend(values.iter().cloned());
}
if enum_values.is_empty() {
return None;
}
if has_null {
enum_values.push(Value::Null);
}
let mut compressed = Map::new();
if let Some(description) = map.get("description") {
compressed.insert("description".to_string(), description.clone());
}
compressed.insert(
"type".to_string(),
if has_null {
Value::Array(vec![
Value::String("string".to_string()),
Value::String("null".to_string()),
])
} else {
Value::String("string".to_string())
},
);
compressed.insert("enum".to_string(), Value::Array(enum_values));
Some(Value::Object(compressed))
}
fn is_null_schema(map: &Map<String, Value>) -> bool {
matches!(map.get("type").and_then(Value::as_str), Some("null"))
|| matches!(
map.get("enum").and_then(Value::as_array),
Some(values) if values.len() == 1 && values[0].is_null()
)
}
fn map_stop_reason(reason: Option<&str>) -> FinishReason {
match reason {
Some("end_turn") | Some("stop_sequence") => FinishReason::Stop,
Some("tool_use") => FinishReason::ToolCalls,
Some("max_tokens") => FinishReason::Length,
Some("refusal") => FinishReason::ContentFilter,
Some(other) => FinishReason::Unknown(other.to_string()),
None => FinishReason::Stop,
}
}
fn map_transport_error(error: reqwest::Error) -> ProviderError {
if error.is_timeout() || error.is_connect() || error.is_request() {
ProviderError::transient(error.to_string())
} else {
ProviderError::permanent(error.to_string())
}
}
fn map_status_error(status: StatusCode, headers: &HeaderMap, text: String) -> ProviderError {
let message = parse_error_message(&text).unwrap_or(text);
match status.as_u16() {
408 | 409 | 500..=599 => ProviderError::transient(message),
429 => ProviderError::rate_limited(message, retry_after_secs(headers)),
_ => ProviderError::permanent(message),
}
}
fn parse_error_message(text: &str) -> Option<String> {
let value = serde_json::from_str::<Value>(text).ok()?;
value
.get("error")
.and_then(|error| error.get("message"))
.and_then(Value::as_str)
.map(str::to_string)
}
fn retry_after_secs(headers: &HeaderMap) -> Option<u64> {
headers
.get(reqwest::header::RETRY_AFTER)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<u64>().ok())
}
fn map_stream_error(error: AnthropicErrorBody) -> ProviderError {
match error.kind.as_str() {
"overloaded_error" | "rate_limit_error" => ProviderError::transient(error.message),
_ => ProviderError::permanent(error.message),
}
}
fn take_sse_lines(buffer: &mut Vec<u8>, chunk: &[u8]) -> Result<Vec<String>, ProviderError> {
buffer.extend_from_slice(chunk);
let mut lines = Vec::new();
while let Some(pos) = buffer.iter().position(|byte| *byte == b'\n') {
let mut line = buffer.drain(..=pos).collect::<Vec<_>>();
if line.last() == Some(&b'\n') {
line.pop();
}
if line.last() == Some(&b'\r') {
line.pop();
}
lines
.push(String::from_utf8(line).map_err(|e| {
ProviderError::transient(format!("invalid UTF-8 in SSE line: {e}"))
})?);
}
Ok(lines)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn transforms_tool_results_into_user_blocks() {
let req = LLMRequest {
model: "claude-test".to_string(),
system: Some("system".to_string()),
messages: vec![
Message {
role: Role::Assistant,
blocks: vec![ContentBlock::ToolUse {
id: "toolu_1".to_string(),
name: "read_file".to_string(),
input: json!({ "path": "Cargo.toml" }),
}],
},
Message {
role: Role::Tool,
blocks: vec![ContentBlock::ToolResult {
tool_use_id: "toolu_1".to_string(),
content: vec![ContentBlock::Text {
text: "ok".to_string(),
}],
is_error: false,
}],
},
],
tools: vec![],
temperature: None,
max_tokens: Some(128),
};
let payload = build_anthropic_payload(req);
assert_eq!(payload.system.as_deref(), Some("system"));
assert_eq!(payload.messages.len(), 2);
assert_eq!(payload.messages[0].role, "assistant");
assert!(matches!(
&payload.messages[0].content[0],
AnthropicContentBlock::ToolUse { id, .. } if id == "toolu_1"
));
assert_eq!(payload.messages[1].role, "user");
assert!(matches!(
&payload.messages[1].content[0],
AnthropicContentBlock::ToolResult { tool_use_id, content, is_error }
if tool_use_id == "toolu_1" && content == "ok" && !is_error
));
}
#[test]
fn anthropic_tool_schema_uses_input_schema_without_refs() {
let tool = ToolDefinition {
name: "write_file".to_string(),
desc: "edit files".to_string(),
arguments: json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "WriteFileArgs",
"type": "object",
"definitions": {
"Mode": {
"oneOf": [
{ "enum": ["write"], "type": "string" },
{ "enum": ["append"], "type": "string" }
]
}
},
"properties": {
"mode": {
"description": "edit mode",
"anyOf": [
{ "$ref": "#/definitions/Mode" },
{ "type": "null" }
]
}
}
}),
};
let schema = build_tool_schema(&tool);
let input_schema = schema.input_schema;
let blob = input_schema.to_string();
assert!(!blob.contains("$schema"));
assert!(!blob.contains("definitions"));
assert!(!blob.contains("$ref"));
assert!(!blob.contains("title"));
assert_eq!(
input_schema["properties"]["mode"],
json!({
"description": "edit mode",
"type": ["string", "null"],
"enum": ["write", "append", null]
})
);
}
#[test]
fn parses_streaming_tool_use_events() {
let events = [
r#"{"type":"message_start","message":{"usage":{"input_tokens":1,"output_tokens":1}}}"#,
r#"{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#,
r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'll check"}}"#,
r#"{"type":"content_block_stop","index":0}"#,
r#"{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_1","name":"read_file","input":{}}}"#,
r#"{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"path\":"}}"#,
r#"{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"Cargo.toml\"}"}}"#,
r#"{"type":"content_block_stop","index":1}"#,
r#"{"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":9}}"#,
r#"{"type":"message_stop"}"#,
];
let mut pending_tools = BTreeMap::<usize, PendingToolBlock>::new();
let mut stop_reason = None;
let mut usage = None;
let mut chunks = Vec::<ProviderStreamChunk>::new();
for event in events {
let event = serde_json::from_str::<StreamEvent>(event).expect("event should parse");
let output =
handle_stream_event(event, &mut pending_tools, &mut stop_reason, &mut usage)
.expect("event should map");
chunks.extend(output.chunks);
}
assert!(matches!(
&chunks[0],
ProviderStreamChunk::ContentDelta(text) if text == "I'll check"
));
assert!(matches!(
&chunks[1],
ProviderStreamChunk::ToolCallDelta { id: Some(id), name: Some(name), arguments_delta, .. }
if id == "toolu_1" && name == "read_file" && arguments_delta == "{\"path\":\"Cargo.toml\"}"
));
assert!(matches!(
&chunks[2],
ProviderStreamChunk::Done { finish_reason: FinishReason::ToolCalls, usage: Some(usage) }
if usage.input_tokens == Some(1)
&& usage.output_tokens == Some(9)
&& usage.total_tokens == Some(10)
));
}
}