use adk_core::{Content, FinishReason, GenerateContentConfig, LlmResponse, Part, UsageMetadata};
use serde_json::Value;
use std::collections::HashMap;
pub(crate) fn build_request_body(
model: &str,
contents: &[Content],
tools: &HashMap<String, Value>,
config: Option<&GenerateContentConfig>,
stream: bool,
) -> Value {
let messages: Vec<Value> = contents.iter().map(content_to_message).collect();
let mut body = serde_json::json!({
"model": model,
"messages": messages,
"stream": stream,
});
if !tools.is_empty() {
let tool_array: Vec<Value> = tools
.iter()
.map(|(name, decl)| {
let description =
decl.get("description").and_then(|d| d.as_str()).unwrap_or_default();
let parameters = decl.get("parameters").cloned().unwrap_or(serde_json::json!({
"type": "object",
"properties": {}
}));
serde_json::json!({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": parameters,
}
})
})
.collect();
body["tools"] = Value::Array(tool_array);
}
if let Some(cfg) = config {
if let Some(temp) = cfg.temperature {
body["temperature"] = serde_json::json!(temp);
}
if let Some(top_p) = cfg.top_p {
body["top_p"] = serde_json::json!(top_p);
}
if let Some(max_tokens) = cfg.max_output_tokens {
body["max_tokens"] = serde_json::json!(max_tokens);
}
}
body
}
fn content_to_message(content: &Content) -> Value {
match content.role.as_str() {
"user" => {
let text = extract_text(&content.parts);
serde_json::json!({
"role": "user",
"content": text,
})
}
"model" | "assistant" => {
let mut msg = serde_json::json!({
"role": "assistant",
});
let text = get_text_content(&content.parts);
if let Some(t) = &text {
msg["content"] = Value::String(t.clone());
}
let tool_calls = extract_tool_calls(&content.parts);
if !tool_calls.is_empty() {
msg["tool_calls"] = Value::Array(tool_calls);
}
if text.is_none() && !msg.get("tool_calls").is_some_and(|tc| tc.is_array()) {
msg["content"] = Value::String(" ".to_string());
}
msg
}
"system" => {
let text = extract_text(&content.parts);
serde_json::json!({
"role": "system",
"content": text,
})
}
"function" | "tool" => {
if let Some(Part::FunctionResponse { function_response, id }) = content.parts.first() {
let tool_call_id = id.clone().unwrap_or_else(|| "unknown".to_string());
serde_json::json!({
"role": "tool",
"tool_call_id": tool_call_id,
"content": crate::tool_result::serialize_tool_result(&function_response.response),
})
} else {
serde_json::json!({
"role": "user",
"content": "",
})
}
}
_ => {
let text = extract_text(&content.parts);
serde_json::json!({
"role": "user",
"content": text,
})
}
}
}
fn extract_text(parts: &[Part]) -> String {
parts
.iter()
.filter_map(|p| match p {
Part::Text { text } => Some(text.clone()),
Part::Thinking { thinking, .. } => Some(thinking.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
fn get_text_content(parts: &[Part]) -> Option<String> {
let text = extract_text(parts);
if text.is_empty() { None } else { Some(text) }
}
fn extract_tool_calls(parts: &[Part]) -> Vec<Value> {
parts
.iter()
.filter_map(|part| {
if let Part::FunctionCall { name, args, id, .. } = part {
Some(serde_json::json!({
"id": id.clone().unwrap_or_else(|| format!("call_{name}")),
"type": "function",
"function": {
"name": name,
"arguments": serde_json::to_string(args).unwrap_or_default(),
}
}))
} else {
None
}
})
.collect()
}
pub(crate) fn parse_response(body: &Value) -> LlmResponse {
let content = body.get("choices").and_then(|c| c.get(0)).and_then(|choice| {
let message = choice.get("message")?;
let mut parts = Vec::new();
if let Some(reasoning) = message.get("reasoning_content").and_then(|r| r.as_str()) {
if !reasoning.is_empty() {
parts.push(Part::Thinking { thinking: reasoning.to_string(), signature: None });
}
}
if let Some(text) = message.get("content").and_then(|c| c.as_str()) {
if !text.is_empty() {
parts.push(Part::Text { text: text.to_string() });
}
}
if let Some(tool_calls) = message.get("tool_calls").and_then(|tc| tc.as_array()) {
for tc in tool_calls {
if let Some(func) = tc.get("function") {
let name =
func.get("name").and_then(|n| n.as_str()).unwrap_or_default().to_string();
let args: Value = func
.get("arguments")
.and_then(|a| a.as_str())
.and_then(|a| serde_json::from_str(a).ok())
.unwrap_or(serde_json::json!({}));
let id = tc.get("id").and_then(|i| i.as_str()).map(String::from);
parts.push(Part::FunctionCall { name, args, id, thought_signature: None });
}
}
}
if parts.is_empty() { None } else { Some(Content { role: "model".to_string(), parts }) }
});
let finish_reason = body
.get("choices")
.and_then(|c| c.get(0))
.and_then(|choice| choice.get("finish_reason"))
.and_then(|fr| fr.as_str())
.map(map_finish_reason);
let usage_metadata = body.get("usage").map(|u| {
let mut meta = UsageMetadata {
prompt_token_count: u.get("prompt_tokens").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
candidates_token_count: u.get("completion_tokens").and_then(|v| v.as_i64()).unwrap_or(0)
as i32,
total_token_count: u.get("total_tokens").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
..Default::default()
};
if let Some(details) = u.get("prompt_tokens_details") {
meta.cache_read_input_token_count =
details.get("cached_tokens").and_then(|v| v.as_i64()).map(|v| v as i32);
}
if let Some(details) = u.get("completion_tokens_details") {
meta.thinking_token_count =
details.get("reasoning_tokens").and_then(|v| v.as_i64()).map(|v| v as i32);
}
meta
});
LlmResponse {
content,
usage_metadata,
finish_reason,
citation_metadata: None,
partial: false,
turn_complete: true,
interrupted: false,
error_code: None,
error_message: None,
provider_metadata: None,
}
}
pub(crate) fn parse_sse_chunk(chunk: &Value) -> LlmResponse {
let content = chunk.get("choices").and_then(|c| c.get(0)).and_then(|choice| {
let delta = choice.get("delta")?;
let mut parts = Vec::new();
if let Some(reasoning) = delta.get("reasoning_content").and_then(|r| r.as_str()) {
if !reasoning.is_empty() {
parts.push(Part::Thinking { thinking: reasoning.to_string(), signature: None });
}
}
if let Some(text) = delta.get("content").and_then(|c| c.as_str()) {
if !text.is_empty() {
parts.push(Part::Text { text: text.to_string() });
}
}
if let Some(tool_calls) = delta.get("tool_calls").and_then(|tc| tc.as_array()) {
for tc in tool_calls {
if let Some(func) = tc.get("function") {
let name = func.get("name").and_then(|n| n.as_str());
if let Some(name) = name {
if !name.is_empty() {
let args: Value = func
.get("arguments")
.and_then(|a| a.as_str())
.and_then(|a| serde_json::from_str(a).ok())
.unwrap_or(serde_json::json!({}));
let id = tc.get("id").and_then(|i| i.as_str()).map(String::from);
parts.push(Part::FunctionCall {
name: name.to_string(),
args,
id,
thought_signature: None,
});
}
}
}
}
}
if parts.is_empty() { None } else { Some(Content { role: "model".to_string(), parts }) }
});
let finish_reason = chunk
.get("choices")
.and_then(|c| c.get(0))
.and_then(|choice| choice.get("finish_reason"))
.and_then(|fr| fr.as_str())
.map(map_finish_reason);
let is_final = finish_reason.is_some();
LlmResponse {
content,
usage_metadata: None,
finish_reason,
citation_metadata: None,
partial: !is_final,
turn_complete: is_final,
interrupted: false,
error_code: None,
error_message: None,
provider_metadata: None,
}
}
fn map_finish_reason(reason: &str) -> FinishReason {
match reason {
"stop" => FinishReason::Stop,
"length" => FinishReason::MaxTokens,
"tool_calls" => FinishReason::Stop,
"content_filter" => FinishReason::Safety,
_ => FinishReason::Other,
}
}
#[cfg(test)]
mod tests {
use super::*;
use adk_core::FunctionResponseData;
#[test]
fn test_build_request_body_basic() {
let contents = vec![Content {
role: "user".to_string(),
parts: vec![Part::Text { text: "Hello".to_string() }],
}];
let body = build_request_body("my-model", &contents, &HashMap::new(), None, false);
assert_eq!(body["model"], "my-model");
assert_eq!(body["stream"], false);
assert_eq!(body["messages"][0]["role"], "user");
assert_eq!(body["messages"][0]["content"], "Hello");
assert!(body.get("tools").is_none());
assert!(body.get("temperature").is_none());
}
#[test]
fn test_build_request_body_with_config() {
let config = GenerateContentConfig {
temperature: Some(0.5),
top_p: Some(0.5),
max_output_tokens: Some(1024),
..Default::default()
};
let body = build_request_body("m", &[], &HashMap::new(), Some(&config), true);
assert_eq!(body["stream"], true);
assert_eq!(body["temperature"], 0.5);
assert_eq!(body["top_p"], 0.5);
assert_eq!(body["max_tokens"], 1024);
}
#[test]
fn test_build_request_body_with_tools() {
let mut tools = HashMap::new();
tools.insert(
"get_weather".to_string(),
serde_json::json!({
"description": "Get weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string" }
}
}
}),
);
let body = build_request_body("m", &[], &tools, None, false);
let tool_array = body["tools"].as_array().unwrap();
assert_eq!(tool_array.len(), 1);
assert_eq!(tool_array[0]["type"], "function");
assert_eq!(tool_array[0]["function"]["name"], "get_weather");
assert_eq!(tool_array[0]["function"]["description"], "Get weather for a city");
}
#[test]
fn test_content_to_message_user() {
let content = Content {
role: "user".to_string(),
parts: vec![Part::Text { text: "Hi there".to_string() }],
};
let msg = content_to_message(&content);
assert_eq!(msg["role"], "user");
assert_eq!(msg["content"], "Hi there");
}
#[test]
fn test_content_to_message_system() {
let content = Content {
role: "system".to_string(),
parts: vec![Part::Text { text: "You are helpful.".to_string() }],
};
let msg = content_to_message(&content);
assert_eq!(msg["role"], "system");
assert_eq!(msg["content"], "You are helpful.");
}
#[test]
fn test_content_to_message_model_role() {
let content = Content {
role: "model".to_string(),
parts: vec![Part::Text { text: "Hello!".to_string() }],
};
let msg = content_to_message(&content);
assert_eq!(msg["role"], "assistant");
assert_eq!(msg["content"], "Hello!");
}
#[test]
fn test_content_to_message_assistant_role() {
let content = Content {
role: "assistant".to_string(),
parts: vec![Part::Text { text: "Sure".to_string() }],
};
let msg = content_to_message(&content);
assert_eq!(msg["role"], "assistant");
}
#[test]
fn test_content_to_message_with_tool_calls() {
let content = Content {
role: "model".to_string(),
parts: vec![Part::FunctionCall {
name: "get_weather".to_string(),
args: serde_json::json!({"city": "Seattle"}),
id: Some("call_123".to_string()),
thought_signature: None,
}],
};
let msg = content_to_message(&content);
assert_eq!(msg["role"], "assistant");
let tool_calls = msg["tool_calls"].as_array().unwrap();
assert_eq!(tool_calls.len(), 1);
assert_eq!(tool_calls[0]["id"], "call_123");
assert_eq!(tool_calls[0]["type"], "function");
assert_eq!(tool_calls[0]["function"]["name"], "get_weather");
}
#[test]
fn test_content_to_message_tool_response() {
let content = Content {
role: "tool".to_string(),
parts: vec![Part::FunctionResponse {
function_response: FunctionResponseData::new(
"get_weather",
serde_json::json!({"temp": 72}),
),
id: Some("call_123".to_string()),
}],
};
let msg = content_to_message(&content);
assert_eq!(msg["role"], "tool");
assert_eq!(msg["tool_call_id"], "call_123");
assert!(msg["content"].as_str().unwrap().contains("72"));
}
#[test]
fn test_content_to_message_empty_assistant_gets_placeholder() {
let content = Content { role: "model".to_string(), parts: vec![] };
let msg = content_to_message(&content);
assert_eq!(msg["role"], "assistant");
assert_eq!(msg["content"], " ");
}
#[test]
fn test_parse_response_text() {
let body = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"content": "Hello world"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15
}
});
let resp = parse_response(&body);
assert!(resp.turn_complete);
assert!(!resp.partial);
assert_eq!(resp.finish_reason, Some(FinishReason::Stop));
let content = resp.content.unwrap();
assert_eq!(content.role, "model");
assert_eq!(content.parts.len(), 1);
assert_eq!(content.parts[0].text().unwrap(), "Hello world");
let usage = resp.usage_metadata.unwrap();
assert_eq!(usage.prompt_token_count, 10);
assert_eq!(usage.candidates_token_count, 5);
assert_eq!(usage.total_token_count, 15);
}
#[test]
fn test_parse_response_with_tool_calls() {
let body = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Seattle\"}"
}
}]
},
"finish_reason": "tool_calls"
}],
"usage": {
"prompt_tokens": 20,
"completion_tokens": 10,
"total_tokens": 30
}
});
let resp = parse_response(&body);
assert_eq!(resp.finish_reason, Some(FinishReason::Stop));
let content = resp.content.unwrap();
assert_eq!(content.parts.len(), 1);
if let Part::FunctionCall { name, args, id, .. } = &content.parts[0] {
assert_eq!(name, "get_weather");
assert_eq!(args["city"], "Seattle");
assert_eq!(id.as_deref(), Some("call_abc"));
} else {
panic!("Expected FunctionCall part");
}
}
#[test]
fn test_parse_response_length_finish() {
let body = serde_json::json!({
"choices": [{
"message": { "role": "assistant", "content": "truncated" },
"finish_reason": "length"
}]
});
let resp = parse_response(&body);
assert_eq!(resp.finish_reason, Some(FinishReason::MaxTokens));
}
#[test]
fn test_parse_response_content_filter() {
let body = serde_json::json!({
"choices": [{
"message": { "role": "assistant", "content": "" },
"finish_reason": "content_filter"
}]
});
let resp = parse_response(&body);
assert_eq!(resp.finish_reason, Some(FinishReason::Safety));
}
#[test]
fn test_parse_sse_chunk_text() {
let chunk = serde_json::json!({
"choices": [{
"delta": { "content": "Hello" },
"finish_reason": null
}]
});
let resp = parse_sse_chunk(&chunk);
assert!(resp.partial);
assert!(!resp.turn_complete);
assert!(resp.finish_reason.is_none());
let content = resp.content.unwrap();
assert_eq!(content.parts[0].text().unwrap(), "Hello");
}
#[test]
fn test_parse_sse_chunk_final() {
let chunk = serde_json::json!({
"choices": [{
"delta": {},
"finish_reason": "stop"
}]
});
let resp = parse_sse_chunk(&chunk);
assert!(!resp.partial);
assert!(resp.turn_complete);
assert_eq!(resp.finish_reason, Some(FinishReason::Stop));
assert!(resp.content.is_none());
}
#[test]
fn test_parse_sse_chunk_tool_call() {
let chunk = serde_json::json!({
"choices": [{
"delta": {
"tool_calls": [{
"id": "call_xyz",
"function": {
"name": "search",
"arguments": "{\"q\":\"rust\"}"
}
}]
},
"finish_reason": null
}]
});
let resp = parse_sse_chunk(&chunk);
assert!(resp.partial);
let content = resp.content.unwrap();
assert_eq!(content.parts.len(), 1);
if let Part::FunctionCall { name, args, id, .. } = &content.parts[0] {
assert_eq!(name, "search");
assert_eq!(args["q"], "rust");
assert_eq!(id.as_deref(), Some("call_xyz"));
} else {
panic!("Expected FunctionCall part");
}
}
#[test]
fn test_parse_sse_chunk_empty_delta() {
let chunk = serde_json::json!({
"choices": [{
"delta": {},
"finish_reason": null
}]
});
let resp = parse_sse_chunk(&chunk);
assert!(resp.content.is_none());
assert!(resp.partial);
}
#[test]
fn test_map_finish_reason_variants() {
assert_eq!(map_finish_reason("stop"), FinishReason::Stop);
assert_eq!(map_finish_reason("length"), FinishReason::MaxTokens);
assert_eq!(map_finish_reason("tool_calls"), FinishReason::Stop);
assert_eq!(map_finish_reason("content_filter"), FinishReason::Safety);
assert_eq!(map_finish_reason("unknown"), FinishReason::Other);
}
#[test]
fn test_multiple_text_parts_joined() {
let parts = vec![
Part::Text { text: "Hello".to_string() },
Part::Text { text: "World".to_string() },
];
assert_eq!(extract_text(&parts), "Hello\nWorld");
}
#[test]
fn test_round_trip_text_content() {
let contents = vec![Content {
role: "user".to_string(),
parts: vec![Part::Text { text: "What is Rust?".to_string() }],
}];
let body = build_request_body("test-model", &contents, &HashMap::new(), None, false);
let response_json = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"content": "Rust is a systems programming language."
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 5,
"completion_tokens": 8,
"total_tokens": 13
}
});
let resp = parse_response(&response_json);
assert!(resp.content.is_some());
assert_eq!(resp.content.unwrap().role, "model");
assert_eq!(body["messages"][0]["content"], "What is Rust?");
}
#[test]
fn test_function_call_id_defaults() {
let content = Content {
role: "model".to_string(),
parts: vec![Part::FunctionCall {
name: "my_func".to_string(),
args: serde_json::json!({}),
id: None,
thought_signature: None,
}],
};
let msg = content_to_message(&content);
let tool_calls = msg["tool_calls"].as_array().unwrap();
assert_eq!(tool_calls[0]["id"], "call_my_func");
}
#[test]
fn test_tool_response_missing_id_defaults() {
let content = Content {
role: "tool".to_string(),
parts: vec![Part::FunctionResponse {
function_response: FunctionResponseData::new(
"test",
serde_json::json!({"ok": true}),
),
id: None,
}],
};
let msg = content_to_message(&content);
assert_eq!(msg["tool_call_id"], "unknown");
}
#[test]
fn test_content_to_message_thinking_as_text() {
let content = Content {
role: "user".to_string(),
parts: vec![Part::Thinking {
thinking: "Let me reason about this...".to_string(),
signature: None,
}],
};
let msg = content_to_message(&content);
assert_eq!(msg["role"], "user");
assert_eq!(msg["content"], "Let me reason about this...");
}
#[test]
fn test_content_to_message_assistant_thinking_as_text() {
let content = Content {
role: "model".to_string(),
parts: vec![
Part::Thinking {
thinking: "Step 1: analyze the question".to_string(),
signature: Some("sig123".to_string()),
},
Part::Text { text: "Here is my answer.".to_string() },
],
};
let msg = content_to_message(&content);
assert_eq!(msg["role"], "assistant");
assert_eq!(msg["content"], "Step 1: analyze the question\nHere is my answer.");
}
#[test]
fn test_parse_response_with_reasoning_content() {
let body = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"reasoning_content": "Let me think step by step...",
"content": "The answer is 42."
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30
}
});
let resp = parse_response(&body);
assert!(resp.turn_complete);
let content = resp.content.unwrap();
assert_eq!(content.parts.len(), 2);
assert!(content.parts[0].is_thinking());
assert_eq!(content.parts[0].thinking_text().unwrap(), "Let me think step by step...");
assert_eq!(content.parts[1].text().unwrap(), "The answer is 42.");
}
#[test]
fn test_parse_response_with_empty_reasoning_content() {
let body = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"reasoning_content": "",
"content": "Just the answer."
},
"finish_reason": "stop"
}]
});
let resp = parse_response(&body);
let content = resp.content.unwrap();
assert_eq!(content.parts.len(), 1);
assert_eq!(content.parts[0].text().unwrap(), "Just the answer.");
}
#[test]
fn test_parse_response_without_reasoning_content() {
let body = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"content": "No reasoning here."
},
"finish_reason": "stop"
}]
});
let resp = parse_response(&body);
let content = resp.content.unwrap();
assert_eq!(content.parts.len(), 1);
assert_eq!(content.parts[0].text().unwrap(), "No reasoning here.");
assert!(!content.parts[0].is_thinking());
}
#[test]
fn test_parse_sse_chunk_with_reasoning_content() {
let chunk = serde_json::json!({
"choices": [{
"delta": {
"reasoning_content": "Thinking about this..."
},
"finish_reason": null
}]
});
let resp = parse_sse_chunk(&chunk);
assert!(resp.partial);
let content = resp.content.unwrap();
assert_eq!(content.parts.len(), 1);
assert!(content.parts[0].is_thinking());
assert_eq!(content.parts[0].thinking_text().unwrap(), "Thinking about this...");
}
#[test]
fn test_parse_sse_chunk_reasoning_then_text() {
let chunk = serde_json::json!({
"choices": [{
"delta": {
"reasoning_content": "Step 1...",
"content": "Here's the result"
},
"finish_reason": null
}]
});
let resp = parse_sse_chunk(&chunk);
let content = resp.content.unwrap();
assert_eq!(content.parts.len(), 2);
assert!(content.parts[0].is_thinking());
assert_eq!(content.parts[0].thinking_text().unwrap(), "Step 1...");
assert_eq!(content.parts[1].text().unwrap(), "Here's the result");
}
#[test]
fn test_parse_response_with_cached_tokens() {
let body = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"content": "Hello"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 10,
"total_tokens": 110,
"prompt_tokens_details": {
"cached_tokens": 80
}
}
});
let resp = parse_response(&body);
let usage = resp.usage_metadata.unwrap();
assert_eq!(usage.prompt_token_count, 100);
assert_eq!(usage.candidates_token_count, 10);
assert_eq!(usage.total_token_count, 110);
assert_eq!(usage.cache_read_input_token_count, Some(80));
assert_eq!(usage.thinking_token_count, None);
}
#[test]
fn test_parse_response_with_reasoning_tokens() {
let body = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"content": "42"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 50,
"completion_tokens": 200,
"total_tokens": 250,
"completion_tokens_details": {
"reasoning_tokens": 150
}
}
});
let resp = parse_response(&body);
let usage = resp.usage_metadata.unwrap();
assert_eq!(usage.thinking_token_count, Some(150));
assert_eq!(usage.cache_read_input_token_count, None);
}
#[test]
fn test_parse_response_with_both_token_details() {
let body = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"content": "result"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 200,
"total_tokens": 300,
"prompt_tokens_details": {
"cached_tokens": 60
},
"completion_tokens_details": {
"reasoning_tokens": 120
}
}
});
let resp = parse_response(&body);
let usage = resp.usage_metadata.unwrap();
assert_eq!(usage.cache_read_input_token_count, Some(60));
assert_eq!(usage.thinking_token_count, Some(120));
}
#[test]
fn test_parse_response_without_token_details() {
let body = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"content": "Hello"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15
}
});
let resp = parse_response(&body);
let usage = resp.usage_metadata.unwrap();
assert_eq!(usage.cache_read_input_token_count, None);
assert_eq!(usage.thinking_token_count, None);
}
}