use crate::attachment;
use adk_core::{Content, FinishReason, LlmResponse, Part, UsageMetadata};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Deserialize)]
pub struct Message {
pub role: String,
pub content: Option<String>,
#[serde(skip)]
pub content_parts: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
impl Serialize for Message {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("role", &self.role)?;
if let Some(parts) = &self.content_parts {
map.serialize_entry("content", parts)?;
} else {
map.serialize_entry("content", &self.content)?;
}
if let Some(name) = &self.name {
map.serialize_entry("name", name)?;
}
if let Some(calls) = &self.tool_calls {
map.serialize_entry("tool_calls", calls)?;
}
if let Some(id) = &self.tool_call_id {
map.serialize_entry("tool_call_id", id)?;
}
if let Some(reasoning) = &self.reasoning_content {
map.serialize_entry("reasoning_content", reasoning)?;
}
map.end()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: FunctionCall,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
#[serde(rename = "type")]
pub tool_type: String,
pub function: FunctionDef,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionDef {
pub name: String,
pub description: String,
pub parameters: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChatCompletionRequest {
pub model: String,
pub messages: Vec<Message>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<ResponseFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<ThinkingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseFormat {
#[serde(rename = "type")]
pub format_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThinkingConfig {
#[serde(rename = "type")]
pub thinking_type: String,
}
impl ThinkingConfig {
pub fn enabled() -> Self {
Self { thinking_type: "enabled".to_string() }
}
pub fn disabled() -> Self {
Self { thinking_type: "disabled".to_string() }
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChatCompletionResponse {
#[serde(rename = "id")]
pub _id: String,
#[serde(rename = "object")]
pub _object: String,
#[serde(rename = "created")]
pub _created: u64,
#[serde(rename = "model")]
pub _model: String,
pub choices: Vec<Choice>,
#[serde(default)]
pub usage: Option<Usage>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Choice {
#[serde(rename = "index")]
pub _index: u32,
pub message: Option<Message>,
pub delta: Option<DeltaMessage>,
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct DeltaMessage {
#[serde(default, rename = "role")]
pub _role: Option<String>,
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub reasoning_content: Option<String>,
#[serde(default)]
pub tool_calls: Option<Vec<DeltaToolCall>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DeltaToolCall {
pub index: u32,
#[serde(default)]
pub id: Option<String>,
#[serde(rename = "type", default)]
pub _call_type: Option<String>,
#[serde(default)]
pub function: Option<DeltaFunction>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct DeltaFunction {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub arguments: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
#[serde(default)]
pub reasoning_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completion_tokens_details: Option<Value>,
#[serde(default)]
pub prompt_cache_hit_tokens: Option<u32>,
#[serde(default)]
pub prompt_cache_miss_tokens: Option<u32>,
}
pub fn usage(u: &Usage) -> UsageMetadata {
UsageMetadata {
prompt_token_count: u.prompt_tokens as i32,
candidates_token_count: u.completion_tokens as i32,
total_token_count: u.total_tokens as i32,
thinking_token_count: u
.reasoning_tokens
.or_else(|| {
u.completion_tokens_details
.as_ref()?
.get("reasoning_tokens")?
.as_u64()?
.try_into()
.ok()
})
.map(|tokens| tokens as i32),
cache_read_input_token_count: u.prompt_cache_hit_tokens.map(|tokens| tokens as i32),
provider_usage: serde_json::to_value(u).ok(),
..Default::default()
}
}
pub fn system_message(text: impl Into<String>) -> Message {
Message {
role: "system".to_string(),
content: Some(text.into()),
content_parts: None,
name: None,
tool_calls: None,
tool_call_id: None,
reasoning_content: None,
}
}
pub fn content_to_message(content: &Content) -> Message {
let role = match content.role.as_str() {
"model" | "assistant" => "assistant",
"user" => "user",
"system" => "system",
"tool" | "function" => "tool", other => other,
};
let mut text_parts = Vec::new();
let mut reasoning_parts = Vec::new();
let mut tool_calls = Vec::new();
let mut tool_call_id = None;
let mut content_parts = Vec::new();
let mut has_image = false;
for part in &content.parts {
let previous = text_parts.len();
match part {
Part::Text { text } => text_parts.push(text.clone()),
Part::FunctionCall { name, args, id, .. } => {
tool_calls.push(ToolCall {
id: id.clone().unwrap_or_else(|| format!("call_{}", tool_calls.len())),
call_type: "function".to_string(),
function: FunctionCall {
name: name.clone(),
arguments: serde_json::to_string(args).unwrap_or_default(),
},
});
}
Part::FunctionResponse { function_response, id, .. } => {
tool_call_id = id.clone();
text_parts
.push(crate::tool_result::serialize_tool_result(&function_response.response));
}
Part::InlineData { mime_type, data, .. }
if role == "user"
&& matches!(
mime_type.as_str(),
"image/png" | "image/jpeg" | "image/gif" | "image/webp"
) =>
{
has_image = true;
content_parts.push(serde_json::json!({
"type": "image_url",
"image_url": {"url": format!("data:{mime_type};base64,{}", attachment::encode_base64(data))}
}));
}
Part::InlineData { mime_type, data, .. } => {
text_parts.push(attachment::inline_attachment_to_text(mime_type, data));
}
Part::FileData { mime_type, file_uri, .. } => {
text_parts.push(attachment::file_attachment_to_text(mime_type, file_uri));
}
Part::Thinking { thinking, .. } => {
reasoning_parts.push(thinking.clone());
}
Part::ServerToolCall { .. } | Part::ServerToolResponse { .. } => {}
Part::EmbeddedResource { resource } => match resource {
adk_core::EmbeddedResource::Text(text) => text_parts.push(text.text.clone()),
adk_core::EmbeddedResource::Blob(blob) => {
let mime_type = blob.mime_type.as_deref().unwrap_or("application/octet-stream");
text_parts.push(attachment::inline_attachment_to_text(mime_type, &blob.data));
}
},
}
content_parts.extend(
text_parts[previous..]
.iter()
.map(|text| serde_json::json!({"type":"text", "text":text})),
);
}
let content_str = if text_parts.is_empty() {
(role == "assistant" && tool_calls.is_empty()).then(String::new)
} else {
Some(text_parts.join("\n"))
};
let reasoning_content =
if reasoning_parts.is_empty() { None } else { Some(reasoning_parts.join("\n")) };
Message {
role: role.to_string(),
content: content_str,
content_parts: has_image.then_some(content_parts),
name: None,
tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) },
tool_call_id,
reasoning_content,
}
}
pub fn pending_reasoning(buffer: &mut String, thinking_enabled: bool) -> Option<String> {
if thinking_enabled || buffer.is_empty() {
return None;
}
Some(std::mem::take(buffer))
}
pub fn final_chunk_parts(
delta: Option<&DeltaMessage>,
reasoning_buffer: &mut String,
thinking_enabled: bool,
) -> Vec<Part> {
let mut parts = Vec::new();
if let Some(thinking) = pending_reasoning(reasoning_buffer, thinking_enabled) {
parts.push(Part::Thinking { thinking, signature: None });
}
if let Some(text) = delta.and_then(|d| d.content.as_deref())
&& !text.is_empty()
{
parts.push(Part::Text { text: text.to_string() });
}
parts
}
pub fn convert_tools(tools: &std::collections::HashMap<String, Value>, strict: bool) -> Vec<Tool> {
tools
.values()
.filter_map(|tool| {
let name = tool.get("name")?.as_str()?;
let description = tool.get("description").and_then(|d| d.as_str()).unwrap_or("");
let parameters = tool.get("parameters").cloned().unwrap_or(serde_json::json!({
"type": "object",
"properties": {}
}));
Some(Tool {
tool_type: "function".to_string(),
function: FunctionDef {
name: name.to_string(),
description: description.to_string(),
parameters,
strict: if strict { Some(true) } else { None },
},
})
})
.collect()
}
pub fn from_response(response: &ChatCompletionResponse) -> LlmResponse {
let choice = response.choices.first();
let (content, finish_reason) = if let Some(choice) = choice {
let finish = choice.finish_reason.as_ref().map(|fr| match fr.as_str() {
"stop" => FinishReason::Stop,
"length" => FinishReason::MaxTokens,
"tool_calls" => FinishReason::Stop,
"content_filter" => FinishReason::Safety,
_ => FinishReason::Stop,
});
if let Some(msg) = &choice.message {
let mut parts = Vec::new();
if let Some(reasoning) = &msg.reasoning_content
&& !reasoning.is_empty()
{
parts.push(Part::Thinking { thinking: reasoning.clone(), signature: None });
}
if let Some(text) = &msg.content
&& !text.is_empty()
{
parts.push(Part::Text { text: text.clone() });
}
if let Some(tool_calls) = &msg.tool_calls {
for tc in tool_calls {
let args: Value = serde_json::from_str(&tc.function.arguments)
.unwrap_or(serde_json::json!({}));
parts.push(Part::FunctionCall {
name: tc.function.name.clone(),
args,
id: Some(tc.id.clone()),
thought_signature: None,
});
}
}
(
if parts.is_empty() {
None
} else {
Some(Content { role: "model".to_string(), parts })
},
finish,
)
} else {
(None, finish)
}
} else {
(None, None)
};
let usage = response.usage.as_ref().map(usage);
let turn_complete = content.as_ref().is_none_or(|c| !c.has_function_calls());
LlmResponse {
content,
usage_metadata: usage,
finish_reason,
citation_metadata: None,
partial: false,
turn_complete,
interrupted: false,
error_code: None,
error_message: None,
provider_metadata: None,
interaction_id: None,
}
}
pub fn create_tool_call_response(
tool_calls: Vec<(String, String, Value)>, finish_reason: Option<FinishReason>,
reasoning: Option<String>,
) -> LlmResponse {
let mut parts: Vec<Part> = Vec::new();
if let Some(ref text) = reasoning
&& !text.is_empty()
{
parts.push(Part::Thinking { thinking: text.clone(), signature: None });
}
parts.extend(tool_calls.into_iter().map(|(id, name, args)| Part::FunctionCall {
name,
args,
id: Some(id),
thought_signature: None,
}));
LlmResponse {
content: Some(Content { role: "model".to_string(), parts }),
usage_metadata: None,
finish_reason,
citation_metadata: None,
partial: false,
turn_complete: false,
interrupted: false,
error_code: None,
error_message: None,
provider_metadata: None,
interaction_id: None,
}
}
#[cfg(test)]
mod tests {
#[test]
fn pending_reasoning_is_owed_only_when_it_was_never_streamed() {
let mut buffer = String::from("because");
assert_eq!(pending_reasoning(&mut buffer, true), None);
assert_eq!(buffer, "because", "buffer must not be consumed");
assert_eq!(pending_reasoning(&mut buffer, false), Some("because".to_string()));
assert!(buffer.is_empty(), "buffer is drained once emitted");
assert_eq!(pending_reasoning(&mut buffer, false), None);
}
#[test]
fn final_chunk_carries_only_its_own_delta() {
let delta = DeltaMessage { content: Some("tail".into()), ..Default::default() };
let parts = final_chunk_parts(Some(&delta), &mut String::new(), false);
assert_eq!(parts.len(), 1);
assert!(matches!(&parts[0], Part::Text { text } if text == "tail"));
assert!(final_chunk_parts(None, &mut String::new(), false).is_empty());
let empty = DeltaMessage { content: Some(String::new()), ..Default::default() };
assert!(final_chunk_parts(Some(&empty), &mut String::new(), false).is_empty());
}
#[test]
fn final_chunk_orders_unstreamed_reasoning_before_text() {
let delta = DeltaMessage { content: Some("answer".into()), ..Default::default() };
let mut buffer = String::from("because");
let parts = final_chunk_parts(Some(&delta), &mut buffer, false);
assert!(matches!(&parts[0], Part::Thinking { thinking, .. } if thinking == "because"));
assert!(matches!(&parts[1], Part::Text { text } if text == "answer"));
}
use super::*;
#[test]
fn tool_call_response_is_not_turn_complete() {
let resp = create_tool_call_response(
vec![("call_1".to_string(), "get_weather".to_string(), serde_json::json!({}))],
Some(FinishReason::Stop),
None,
);
assert!(!resp.turn_complete);
assert!(resp.content.as_ref().unwrap().has_function_calls());
}
#[test]
fn content_to_message_keeps_inline_attachment_payload() {
let content = Content {
role: "user".to_string(),
parts: vec![Part::inline_data("application/pdf", b"%PDF".to_vec())],
};
let message = content_to_message(&content);
let payload = message.content.unwrap_or_default();
assert!(payload.contains("application/pdf"));
assert!(payload.contains("encoding=\"base64\""));
}
#[test]
fn content_to_message_keeps_file_attachment_payload() {
let content = Content {
role: "user".to_string(),
parts: vec![Part::file_data("text/csv", "https://example.com/data.csv")],
};
let message = content_to_message(&content);
let payload = message.content.unwrap_or_default();
assert!(payload.contains("text/csv"));
assert!(payload.contains("https://example.com/data.csv"));
}
#[test]
fn thinking_only_history_has_explicit_content() {
let content = Content {
role: "model".into(),
parts: vec![Part::Thinking {
thinking: "Retained reasoning from an interrupted turn".into(),
signature: None,
}],
};
let message = content_to_message(&content);
let json = serde_json::to_value(&message).unwrap();
assert_eq!(json["content"], "");
assert_eq!(json["reasoning_content"], "Retained reasoning from an interrupted turn");
assert!(json.get("tool_calls").is_none());
}
#[test]
fn content_to_message_maps_thinking_to_reasoning_content() {
let content = Content {
role: "model".to_string(),
parts: vec![
Part::Thinking {
thinking: "reason through the tool plan".to_string(),
signature: None,
},
Part::Text { text: "let me check the prices".to_string() },
],
};
let message = content_to_message(&content);
assert_eq!(message.role, "assistant");
assert_eq!(message.reasoning_content.as_deref(), Some("reason through the tool plan"));
assert_eq!(message.content.as_deref(), Some("let me check the prices"));
}
}