use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::{Error, Result};
fn format_json_python_style(value: &serde_json::Value) -> String {
match value {
serde_json::Value::Object(map) => {
let pairs: Vec<String> = map
.iter()
.map(|(k, v)| format!("\"{}\": {}", k, format_json_python_style(v)))
.collect();
format!("{{{}}}", pairs.join(", "))
}
serde_json::Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(format_json_python_style).collect();
format!("[{}]", items.join(", "))
}
serde_json::Value::String(_) => {
serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
}
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Null => "null".to_string(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Provider {
Anthropic,
OpenAI,
OpenAIResponses,
Bedrock,
GoogleGenAI,
GoogleAnthropic,
OpenRouter,
OpenRouterResponses,
}
impl Provider {
pub fn from_model_string(model: &str) -> Result<(Self, String)> {
let parts: Vec<&str> = model.split(':').collect();
if parts.len() < 2 {
return Err(Error::InvalidProvider(format!(
"Invalid model string format: {}. Expected 'provider:model'",
model
)));
}
let (provider_str, model_name) = if parts.len() >= 3 && parts[1] == "resp" {
let provider = match parts[0] {
"openai" => Provider::OpenAIResponses,
"openrouter" => Provider::OpenRouterResponses,
_ => return Err(Error::InvalidProvider(parts[0].to_string())),
};
return Ok((provider, parts[2..].join(":")));
} else {
(parts[0], parts[1..].join(":"))
};
let provider = match provider_str {
"anthropic" => Provider::Anthropic,
"openai" => Provider::OpenAI,
"bedrock" => Provider::Bedrock,
"google-genai" | "gemini" | "google-gemini" => Provider::GoogleGenAI,
"google-anthropic" | "vertexai" => Provider::GoogleAnthropic,
"openrouter" => Provider::OpenRouter,
_ => return Err(Error::InvalidProvider(provider_str.to_string())),
};
Ok((provider, model_name))
}
pub fn supports_native_tools(&self) -> bool {
matches!(
self,
Provider::Anthropic
| Provider::OpenAI
| Provider::OpenAIResponses
| Provider::Bedrock
| Provider::GoogleGenAI
| Provider::GoogleAnthropic
| Provider::OpenRouter
| Provider::OpenRouterResponses
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChatRole {
System,
User,
Assistant,
#[serde(rename = "tool")]
Tool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CacheMarker {
Ephemeral,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: ChatRole,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_marker: Option<CacheMarker>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_families: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
impl ChatMessage {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: ChatRole::System,
content: content.into(),
cache_marker: None,
model_families: None,
signature: None,
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: ChatRole::User,
content: content.into(),
cache_marker: None,
model_families: None,
signature: None,
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: ChatRole::Assistant,
content: content.into(),
cache_marker: None,
model_families: None,
signature: None,
}
}
pub fn with_cache_marker(mut self, marker: CacheMarker) -> Self {
self.cache_marker = Some(marker);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TokenUsage {
pub input_tokens: u64,
pub output_tokens: u64,
pub cached_tokens: u64,
}
impl TokenUsage {
pub fn new(input_tokens: u64, output_tokens: u64, cached_tokens: u64) -> Self {
Self {
input_tokens,
output_tokens,
cached_tokens,
}
}
pub fn total_tokens(&self) -> u64 {
self.input_tokens + self.output_tokens
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CostInfo {
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_read_input_tokens: u64,
pub cache_write_input_tokens: u64,
pub microdollar_cost: u64,
pub provider_cost_microdollars: Option<u64>,
pub microdollar_adjust: u64,
}
impl CostInfo {
pub fn from_usage(usage: &TokenUsage, model: &str) -> Self {
let mut info = Self {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
cache_read_input_tokens: usage.cached_tokens,
cache_write_input_tokens: 0, microdollar_cost: 0,
provider_cost_microdollars: None,
microdollar_adjust: 0,
};
info.microdollar_cost = info.calculate_microdollar_cost(model);
info
}
pub fn from_usage_with_provider_cost(
usage: &TokenUsage,
model: &str,
provider_cost_dollars: f64,
) -> Self {
let mut info = Self::from_usage(usage, model);
info.provider_cost_microdollars = Some((provider_cost_dollars * 1_000_000.0) as u64);
info
}
pub fn with_provider_cost(mut self, cost_dollars: f64) -> Self {
self.provider_cost_microdollars = Some((cost_dollars * 1_000_000.0) as u64);
self
}
fn calculate_microdollar_cost(&self, model: &str) -> u64 {
let model_lower = model.to_lowercase();
if model_lower.contains("claude") || model_lower.contains("anthropic") {
if model_lower.contains("haiku") {
let input_cost = (self.input_tokens as f64) * 0.80;
let output_cost = (self.output_tokens as f64) * 4.0;
let cache_cost = (self.cache_read_input_tokens as f64) * 0.08;
(input_cost + output_cost + cache_cost).ceil() as u64
} else if model_lower.contains("opus") {
let input_cost = (self.input_tokens as f64) * 15.0;
let output_cost = (self.output_tokens as f64) * 75.0;
let cache_cost = (self.cache_read_input_tokens as f64) * 1.50;
(input_cost + output_cost + cache_cost).ceil() as u64
} else {
let input_cost = (self.input_tokens as f64) * 3.0;
let output_cost = (self.output_tokens as f64) * 15.0;
let cache_cost = (self.cache_read_input_tokens as f64) * 0.30;
(input_cost + output_cost + cache_cost).ceil() as u64
}
}
else if model_lower.contains("gpt-4o") || model_lower.contains("o4-mini") {
if model_lower.contains("mini") {
let input_cost = (self.input_tokens as f64) * 1.10;
let output_cost = (self.output_tokens as f64) * 4.40;
let cache_cost = (self.cache_read_input_tokens as f64) * 0.275;
(input_cost + output_cost + cache_cost).ceil() as u64
} else {
let input_cost = (self.input_tokens as f64) * 5.0;
let output_cost = (self.output_tokens as f64) * 15.0;
let cache_cost = (self.cache_read_input_tokens as f64) * 1.25;
(input_cost + output_cost + cache_cost).ceil() as u64
}
} else if model_lower.contains("o3") {
let input_cost = (self.input_tokens as f64) * 10.0;
let output_cost = (self.output_tokens as f64) * 40.0;
let cache_cost = (self.cache_read_input_tokens as f64) * 2.5;
(input_cost + output_cost + cache_cost).ceil() as u64
} else if model_lower.contains("o1") {
let input_cost = (self.input_tokens as f64) * 15.0;
let output_cost = (self.output_tokens as f64) * 60.0;
(input_cost + output_cost).ceil() as u64
}
else if model_lower.contains("gemini") {
if model_lower.contains("flash") {
let input_cost = (self.input_tokens as f64) * 0.075;
let output_cost = (self.output_tokens as f64) * 0.30;
(input_cost + output_cost).ceil() as u64
} else if model_lower.contains("pro") {
let input_cost = (self.input_tokens as f64) * 1.25;
let output_cost = (self.output_tokens as f64) * 5.0;
(input_cost + output_cost).ceil() as u64
} else {
0
}
} else {
0
}
}
pub fn total_microdollars(&self) -> u64 {
let base_cost = self
.provider_cost_microdollars
.unwrap_or(self.microdollar_cost);
base_cost.wrapping_add(self.microdollar_adjust)
}
pub fn calculated_microdollars(&self) -> u64 {
self.microdollar_cost.wrapping_add(self.microdollar_adjust)
}
pub fn has_provider_cost(&self) -> bool {
self.provider_cost_microdollars.is_some()
}
pub fn total_dollars(&self) -> f64 {
(self.total_microdollars() as f64) / 1_000_000.0
}
pub fn total_cents(&self) -> u64 {
self.total_microdollars() / 10_000
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MediaSource {
Base64 { data: String, mime_type: String },
Url { url: String },
FileId { file_id: String },
FileUri {
uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
mime_type: Option<String>,
},
}
impl MediaSource {
pub fn base64(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self::Base64 {
data: data.into(),
mime_type: mime_type.into(),
}
}
pub fn url(url: impl Into<String>) -> Self {
Self::Url { url: url.into() }
}
pub fn file_id(file_id: impl Into<String>) -> Self {
Self::FileId {
file_id: file_id.into(),
}
}
pub fn file_uri(uri: impl Into<String>) -> Self {
Self::FileUri {
uri: uri.into(),
mime_type: None,
}
}
pub fn youtube(url: impl Into<String>) -> Self {
Self::FileUri {
uri: url.into(),
mime_type: Some("video/*".to_string()),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VideoMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub start_offset: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_offset: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fps: Option<f32>,
}
impl VideoMetadata {
pub fn new() -> Self {
Self::default()
}
pub fn with_start(mut self, offset: impl Into<String>) -> Self {
self.start_offset = Some(offset.into());
self
}
pub fn with_end(mut self, offset: impl Into<String>) -> Self {
self.end_offset = Some(offset.into());
self
}
pub fn with_fps(mut self, fps: f32) -> Self {
self.fps = Some(fps);
self
}
pub fn with_clip(mut self, start: impl Into<String>, end: impl Into<String>) -> Self {
self.start_offset = Some(start.into());
self.end_offset = Some(end.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MediaPart {
Text { text: String },
Image {
source: MediaSource,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
},
Audio {
source: MediaSource,
#[serde(skip_serializing_if = "Option::is_none")]
format: Option<String>,
},
Video {
source: MediaSource,
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<VideoMetadata>,
},
Document { source: MediaSource },
}
impl MediaPart {
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
pub fn image(source: MediaSource) -> Self {
Self::Image {
source,
detail: None,
}
}
pub fn image_with_detail(source: MediaSource, detail: impl Into<String>) -> Self {
Self::Image {
source,
detail: Some(detail.into()),
}
}
pub fn audio(source: MediaSource) -> Self {
Self::Audio {
source,
format: None,
}
}
pub fn audio_with_format(source: MediaSource, format: impl Into<String>) -> Self {
Self::Audio {
source,
format: Some(format.into()),
}
}
pub fn video(source: MediaSource) -> Self {
Self::Video {
source,
metadata: None,
}
}
pub fn video_with_metadata(source: MediaSource, metadata: VideoMetadata) -> Self {
Self::Video {
source,
metadata: Some(metadata),
}
}
pub fn youtube(url: impl Into<String>) -> Self {
Self::Video {
source: MediaSource::youtube(url),
metadata: None,
}
}
pub fn youtube_clip(
url: impl Into<String>,
start: impl Into<String>,
end: impl Into<String>,
) -> Self {
Self::Video {
source: MediaSource::youtube(url),
metadata: Some(VideoMetadata::new().with_clip(start, end)),
}
}
pub fn document(source: MediaSource) -> Self {
Self::Document { source }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultimodalMessage {
pub role: ChatRole,
pub parts: Vec<MediaPart>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_marker: Option<CacheMarker>,
}
impl MultimodalMessage {
pub fn user(parts: Vec<MediaPart>) -> Self {
Self {
role: ChatRole::User,
parts,
cache_marker: None,
}
}
pub fn user_with_media(text: impl Into<String>, media: MediaPart) -> Self {
Self {
role: ChatRole::User,
parts: vec![media, MediaPart::text(text)],
cache_marker: None,
}
}
pub fn user_with_video(text: impl Into<String>, source: MediaSource) -> Self {
Self::user_with_media(text, MediaPart::video(source))
}
pub fn user_with_image(text: impl Into<String>, source: MediaSource) -> Self {
Self::user_with_media(text, MediaPart::image(source))
}
pub fn user_with_youtube(text: impl Into<String>, url: impl Into<String>) -> Self {
Self::user_with_media(text, MediaPart::youtube(url))
}
pub fn with_cache_marker(mut self, marker: CacheMarker) -> Self {
self.cache_marker = Some(marker);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CreateResult {
Single(ToolCall),
Multiple(Vec<ToolCall>),
Empty,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub tool_use_id: String,
pub tool_name: String,
pub args: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_arguments: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_obj: Option<serde_json::Value>,
}
impl ToolCall {
pub fn new(tool_name: impl Into<String>, args: serde_json::Value) -> Self {
Self {
tool_use_id: Uuid::new_v4().to_string(),
tool_name: tool_name.into(),
args,
raw_arguments: None,
signature: None,
tool_obj: None,
}
}
pub fn create(data: serde_json::Value) -> Result<CreateResult> {
if let Some(arr) = data.as_array() {
if arr.is_empty() {
return Ok(CreateResult::Empty);
}
let mut results = Vec::new();
for item in arr {
match Self::create(item.clone())? {
CreateResult::Single(tc) => results.push(tc),
CreateResult::Multiple(tcs) => results.extend(tcs),
CreateResult::Empty => {}
}
}
return Ok(CreateResult::Multiple(results));
}
if data.get("function").is_some() {
Self::from_provider_format(data, Provider::OpenAI).map(CreateResult::Single)
} else if data
.get("type")
.and_then(|v| v.as_str())
.map(|t| t == "function_call")
.unwrap_or(false)
&& data.get("name").is_some()
{
Self::from_provider_format(data, Provider::OpenAIResponses).map(CreateResult::Single)
} else if data.get("functionCall").is_some() {
Self::from_provider_format(data, Provider::GoogleGenAI).map(CreateResult::Single)
} else if data.get("name").is_some() && data.get("input").is_some() {
Self::from_provider_format(data, Provider::Anthropic).map(CreateResult::Single)
} else if data.get("_tool_name").is_some() {
let tool_name = data["_tool_name"]
.as_str()
.ok_or_else(|| Error::Parse("_tool_name must be a string".to_string()))?
.to_string();
let tool_use_id = data
.get("_tool_use_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| Uuid::new_v4().to_string());
let signature = data
.get("signature")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| {
data.get("thought_signature")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
});
let mut args = serde_json::Map::new();
if let Some(obj) = data.as_object() {
for (k, v) in obj {
if !k.starts_with('_') && k != "signature" && k != "thought_signature" {
args.insert(k.clone(), v.clone());
}
}
}
Ok(CreateResult::Single(Self {
tool_use_id,
tool_name,
args: serde_json::Value::Object(args),
raw_arguments: None,
signature,
tool_obj: Some(data),
}))
} else {
Err(Error::Parse("Unknown tool call format".to_string()))
}
}
pub fn from_provider_format(
provider_format: serde_json::Value,
provider: Provider,
) -> Result<Self> {
match provider {
Provider::Anthropic | Provider::Bedrock => Ok(Self {
tool_use_id: provider_format["id"]
.as_str()
.ok_or_else(|| Error::Parse("Missing 'id' in tool call".to_string()))?
.to_string(),
tool_name: provider_format["name"]
.as_str()
.ok_or_else(|| Error::Parse("Missing 'name' in tool call".to_string()))?
.to_string(),
args: provider_format["input"].clone(),
raw_arguments: None,
signature: None,
tool_obj: Some(provider_format),
}),
Provider::OpenAI | Provider::OpenRouter => {
let function = &provider_format["function"];
let arguments = &function["arguments"];
let (args, raw_arguments) = if let Some(args_str) = arguments.as_str() {
let parsed: serde_json::Value = serde_json::from_str(args_str)?;
(parsed, Some(args_str.to_string()))
} else if arguments.is_object() {
let raw = serde_json::to_string(arguments)?;
(arguments.clone(), Some(raw))
} else {
return Err(Error::Parse("Missing 'arguments' in tool call".to_string()));
};
Ok(Self {
tool_use_id: provider_format["id"]
.as_str()
.ok_or_else(|| Error::Parse("Missing 'id' in tool call".to_string()))?
.to_string(),
tool_name: function["name"]
.as_str()
.ok_or_else(|| Error::Parse("Missing 'name' in function".to_string()))?
.to_string(),
args,
raw_arguments,
signature: None,
tool_obj: Some(provider_format),
})
}
Provider::OpenAIResponses | Provider::OpenRouterResponses => {
let args_str = provider_format["arguments"].as_str().unwrap_or("");
let call_id = provider_format
.get("call_id")
.and_then(|v| v.as_str())
.or_else(|| provider_format.get("id").and_then(|v| v.as_str()))
.ok_or_else(|| Error::Parse("Missing 'call_id' in tool call".to_string()))?;
Ok(Self {
tool_use_id: call_id.to_string(),
tool_name: provider_format["name"]
.as_str()
.ok_or_else(|| Error::Parse("Missing 'name' in tool call".to_string()))?
.to_string(),
args: serde_json::from_str(args_str).unwrap_or_else(|_| serde_json::json!({})),
raw_arguments: Some(args_str.to_string()),
signature: None,
tool_obj: Some(provider_format),
})
}
Provider::GoogleGenAI | Provider::GoogleAnthropic => {
let func_call = &provider_format["functionCall"];
let name = func_call["name"]
.as_str()
.ok_or_else(|| Error::Parse("Missing 'name' in functionCall".to_string()))?;
let args = func_call
.get("args")
.cloned()
.unwrap_or(serde_json::json!({}));
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
args.to_string().hash(&mut hasher);
let hash = hasher.finish();
let tool_use_id = format!("google_{}_{}", name, hash);
Ok(Self {
tool_use_id,
tool_name: name.to_string(),
args,
raw_arguments: None,
signature: None,
tool_obj: Some(provider_format),
})
}
}
}
pub fn to_provider_assistant_message(&self, provider: Provider) -> serde_json::Value {
match provider {
Provider::Anthropic | Provider::Bedrock => {
serde_json::json!({
"role": "assistant",
"content": [{
"type": "tool_use",
"id": self.tool_use_id,
"name": self.tool_name,
"input": self.args
}]
})
}
Provider::OpenAI | Provider::OpenRouter => {
let args_str = self.raw_arguments.clone().unwrap_or_else(|| {
format_json_python_style(&self.args)
});
serde_json::json!({
"role": "assistant",
"tool_calls": [{
"id": self.tool_use_id,
"type": "function",
"function": {
"name": self.tool_name,
"arguments": args_str
}
}]
})
}
Provider::OpenAIResponses | Provider::OpenRouterResponses => {
let args_str = self.raw_arguments.clone().unwrap_or_else(|| {
serde_json::to_string(&self.args).unwrap_or_else(|_| "{}".to_string())
});
serde_json::json!({
"type": "function_call",
"id": format!("fc_{}", self.tool_use_id),
"call_id": self.tool_use_id,
"name": self.tool_name,
"arguments": args_str
})
}
Provider::GoogleGenAI | Provider::GoogleAnthropic => {
serde_json::json!({
"role": "model",
"parts": [{
"functionCall": {
"name": self.tool_name,
"args": self.args
}
}]
})
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub tool_use_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
pub content: String,
pub is_error: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_obj: Option<serde_json::Value>,
}
impl ToolResult {
pub fn success(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
tool_use_id: tool_use_id.into(),
tool_name: None,
content: content.into(),
is_error: false,
signature: None,
tool_obj: None,
}
}
pub fn success_with_name(
tool_use_id: impl Into<String>,
tool_name: impl Into<String>,
content: impl Into<String>,
) -> Self {
Self {
tool_use_id: tool_use_id.into(),
tool_name: Some(tool_name.into()),
content: content.into(),
is_error: false,
signature: None,
tool_obj: None,
}
}
pub fn error(tool_use_id: impl Into<String>, error: impl Into<String>) -> Self {
Self {
tool_use_id: tool_use_id.into(),
tool_name: None,
content: error.into(),
is_error: true,
signature: None,
tool_obj: None,
}
}
pub fn with_tool_name(mut self, name: impl Into<String>) -> Self {
self.tool_name = Some(name.into());
self
}
pub fn with_tool_obj(mut self, obj: serde_json::Value) -> Self {
self.tool_obj = Some(obj);
self
}
pub fn to_provider_format(&self, provider: Provider) -> serde_json::Value {
match provider {
Provider::Anthropic | Provider::Bedrock => {
serde_json::json!({
"type": "tool_result",
"tool_use_id": self.tool_use_id,
"content": self.content,
"is_error": self.is_error
})
}
Provider::OpenAI | Provider::OpenRouter => {
serde_json::json!({
"role": "tool",
"tool_call_id": self.tool_use_id,
"content": self.content
})
}
Provider::OpenAIResponses | Provider::OpenRouterResponses => {
serde_json::json!({
"type": "function_call_output",
"call_id": self.tool_use_id,
"output": self.content
})
}
Provider::GoogleGenAI | Provider::GoogleAnthropic => {
let name = self.tool_name.clone().unwrap_or_else(|| {
self.tool_obj
.as_ref()
.and_then(|obj| obj.get("_tool_name"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown".to_string())
});
serde_json::json!({
"functionResponse": {
"name": name,
"response": {
"result": self.content
}
}
})
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningSegment {
pub content: String,
pub segment_index: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider_metadata: Option<serde_json::Value>,
}
impl ReasoningSegment {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
segment_index: 0,
signature: None,
provider_metadata: None,
}
}
pub fn with_index(content: impl Into<String>, segment_index: usize) -> Self {
Self {
content: content.into(),
segment_index,
signature: None,
provider_metadata: None,
}
}
pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
self.signature = Some(signature.into());
self
}
pub fn with_provider_metadata(mut self, metadata: serde_json::Value) -> Self {
self.provider_metadata = Some(metadata);
self
}
pub fn with_segment_index(mut self, index: usize) -> Self {
self.segment_index = index;
self
}
pub fn to_provider_format(&self, provider: Provider) -> serde_json::Value {
match provider {
Provider::Anthropic | Provider::Bedrock | Provider::GoogleAnthropic => {
let mut obj = serde_json::json!({
"type": "thinking",
"thinking": self.content
});
if let Some(ref sig) = self.signature {
obj["signature"] = serde_json::Value::String(sig.clone());
}
obj
}
Provider::OpenAI | Provider::OpenRouter => {
let mut obj = serde_json::json!({
"type": "reasoning",
"content": self.content
});
if let Some(ref sig) = self.signature {
obj["signature"] = serde_json::Value::String(sig.clone());
}
obj
}
Provider::OpenAIResponses | Provider::OpenRouterResponses => {
let mut obj = serde_json::json!({
"type": "reasoning",
"content": [{
"type": "output_text",
"text": self.content
}]
});
if let Some(ref sig) = self.signature {
obj["signature"] = serde_json::Value::String(sig.clone());
}
obj
}
Provider::GoogleGenAI => {
let mut obj = serde_json::json!({
"thought": true,
"text": self.content
});
if let Some(ref sig) = self.signature {
obj["thought_signature"] = serde_json::Value::String(sig.clone());
}
obj
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provider_from_model_string() {
let (provider, model) = Provider::from_model_string("anthropic:claude-3-opus").unwrap();
assert_eq!(provider, Provider::Anthropic);
assert_eq!(model, "claude-3-opus");
let (provider, model) = Provider::from_model_string("openai:gpt-4").unwrap();
assert_eq!(provider, Provider::OpenAI);
assert_eq!(model, "gpt-4");
let (provider, model) = Provider::from_model_string("openai:resp:gpt-4").unwrap();
assert_eq!(provider, Provider::OpenAIResponses);
assert_eq!(model, "gpt-4");
let (provider, model) =
Provider::from_model_string("openrouter:resp:openai/o4-mini").unwrap();
assert_eq!(provider, Provider::OpenRouterResponses);
assert_eq!(model, "openai/o4-mini");
assert!(Provider::from_model_string("invalid").is_err());
}
#[test]
fn test_provider_supports_native_tools() {
assert!(Provider::Anthropic.supports_native_tools());
assert!(Provider::OpenAI.supports_native_tools());
assert!(Provider::OpenAIResponses.supports_native_tools());
assert!(Provider::GoogleAnthropic.supports_native_tools());
assert!(Provider::GoogleGenAI.supports_native_tools());
assert!(Provider::OpenRouter.supports_native_tools());
assert!(Provider::OpenRouterResponses.supports_native_tools());
assert!(Provider::Bedrock.supports_native_tools());
}
#[test]
fn test_chat_message_constructors() {
let msg = ChatMessage::system("You are helpful");
assert_eq!(msg.role, ChatRole::System);
assert_eq!(msg.content, "You are helpful");
let msg = ChatMessage::user("Hello");
assert_eq!(msg.role, ChatRole::User);
let msg = ChatMessage::assistant("Hi there").with_cache_marker(CacheMarker::Ephemeral);
assert_eq!(msg.role, ChatRole::Assistant);
assert!(msg.cache_marker.is_some());
}
#[test]
fn test_token_usage() {
let usage = TokenUsage::new(100, 50, 25);
assert_eq!(usage.input_tokens, 100);
assert_eq!(usage.output_tokens, 50);
assert_eq!(usage.cached_tokens, 25);
assert_eq!(usage.total_tokens(), 150);
}
#[test]
fn test_tool_call_creation() {
let tool_call = ToolCall::new("get_weather", serde_json::json!({"city": "SF"}));
assert_eq!(tool_call.tool_name, "get_weather");
assert_eq!(tool_call.args["city"], "SF");
assert!(!tool_call.tool_use_id.is_empty());
}
#[test]
fn test_tool_call_from_anthropic_format() {
let format = serde_json::json!({
"id": "toolu_123",
"name": "get_weather",
"input": {"city": "SF"}
});
let tool_call = ToolCall::from_provider_format(format, Provider::Anthropic).unwrap();
assert_eq!(tool_call.tool_use_id, "toolu_123");
assert_eq!(tool_call.tool_name, "get_weather");
assert_eq!(tool_call.args["city"], "SF");
}
#[test]
fn test_tool_call_from_openai_format() {
let format = serde_json::json!({
"id": "call_123",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"SF\"}"
}
});
let tool_call = ToolCall::from_provider_format(format, Provider::OpenAI).unwrap();
assert_eq!(tool_call.tool_use_id, "call_123");
assert_eq!(tool_call.tool_name, "get_weather");
assert_eq!(tool_call.args["city"], "SF");
}
#[test]
fn test_tool_result_success() {
let result = ToolResult::success("toolu_123", "Sunny, 72°F");
assert_eq!(result.tool_use_id, "toolu_123");
assert_eq!(result.content, "Sunny, 72°F");
assert!(!result.is_error);
}
#[test]
fn test_tool_result_error() {
let result = ToolResult::error("toolu_123", "API error");
assert!(result.is_error);
}
#[test]
fn test_tool_result_to_anthropic_format() {
let result = ToolResult::success("toolu_123", "Result");
let format = result.to_provider_format(Provider::Anthropic);
assert_eq!(format["type"], "tool_result");
assert_eq!(format["tool_use_id"], "toolu_123");
assert_eq!(format["content"], "Result");
}
#[test]
fn test_tool_result_to_openai_format() {
let result = ToolResult::success("call_123", "Result");
let format = result.to_provider_format(Provider::OpenAI);
assert_eq!(format["role"], "tool");
assert_eq!(format["tool_call_id"], "call_123");
assert_eq!(format["content"], "Result");
}
#[test]
fn test_reasoning_segment() {
let segment = ReasoningSegment::with_index("Thinking...", 0);
assert_eq!(segment.content, "Thinking...");
assert_eq!(segment.segment_index, 0);
}
#[test]
fn test_reasoning_segment_to_anthropic_format() {
let segment = ReasoningSegment::with_index("Thinking...", 0);
let format = segment.to_provider_format(Provider::Anthropic);
assert_eq!(format["type"], "thinking");
assert_eq!(format["thinking"], "Thinking...");
}
}