1use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::error::{Error, Result};
10
11fn format_json_python_style(value: &serde_json::Value) -> String {
13 match value {
14 serde_json::Value::Object(map) => {
15 let pairs: Vec<String> = map
16 .iter()
17 .map(|(k, v)| format!("\"{}\": {}", k, format_json_python_style(v)))
18 .collect();
19 format!("{{{}}}", pairs.join(", "))
20 }
21 serde_json::Value::Array(arr) => {
22 let items: Vec<String> = arr.iter().map(format_json_python_style).collect();
23 format!("[{}]", items.join(", "))
24 }
25 serde_json::Value::String(_) => {
26 serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
27 }
28 serde_json::Value::Number(n) => n.to_string(),
29 serde_json::Value::Bool(b) => b.to_string(),
30 serde_json::Value::Null => "null".to_string(),
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub enum Provider {
37 Anthropic,
39 OpenAI,
41 OpenAIResponses,
43 Bedrock,
45 GoogleGenAI,
47 GoogleAnthropic,
49 OpenRouter,
51 OpenRouterResponses,
53}
54
55impl Provider {
56 pub fn from_model_string(model: &str) -> Result<(Self, String)> {
58 let parts: Vec<&str> = model.split(':').collect();
59 if parts.len() < 2 {
60 return Err(Error::InvalidProvider(format!(
61 "Invalid model string format: {}. Expected 'provider:model'",
62 model
63 )));
64 }
65
66 let (provider_str, model_name) = if parts.len() >= 3 && parts[1] == "resp" {
67 let provider = match parts[0] {
68 "openai" => Provider::OpenAIResponses,
69 "openrouter" => Provider::OpenRouterResponses,
70 _ => return Err(Error::InvalidProvider(parts[0].to_string())),
71 };
72 return Ok((provider, parts[2..].join(":")));
73 } else {
74 (parts[0], parts[1..].join(":"))
75 };
76
77 let provider = match provider_str {
78 "anthropic" => Provider::Anthropic,
79 "openai" => Provider::OpenAI,
80 "bedrock" => Provider::Bedrock,
81 "google-genai" | "gemini" | "google-gemini" => Provider::GoogleGenAI,
82 "google-anthropic" | "vertexai" => Provider::GoogleAnthropic,
83 "openrouter" => Provider::OpenRouter,
84 _ => return Err(Error::InvalidProvider(provider_str.to_string())),
85 };
86 Ok((provider, model_name))
87 }
88
89 pub fn supports_native_tools(&self) -> bool {
91 matches!(
92 self,
93 Provider::Anthropic
94 | Provider::OpenAI
95 | Provider::OpenAIResponses
96 | Provider::Bedrock
97 | Provider::GoogleGenAI
98 | Provider::GoogleAnthropic
99 | Provider::OpenRouter
100 | Provider::OpenRouterResponses
101 )
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "lowercase")]
108pub enum ChatRole {
109 System,
111 User,
113 Assistant,
115 #[serde(rename = "tool")]
117 Tool,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "lowercase")]
123pub enum CacheMarker {
124 Ephemeral,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ChatMessage {
131 pub role: ChatRole,
133
134 pub content: String,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub cache_marker: Option<CacheMarker>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub model_families: Option<Vec<String>>,
144
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub signature: Option<String>,
148}
149
150impl ChatMessage {
151 pub fn system(content: impl Into<String>) -> Self {
153 Self {
154 role: ChatRole::System,
155 content: content.into(),
156 cache_marker: None,
157 model_families: None,
158 signature: None,
159 }
160 }
161
162 pub fn user(content: impl Into<String>) -> Self {
164 Self {
165 role: ChatRole::User,
166 content: content.into(),
167 cache_marker: None,
168 model_families: None,
169 signature: None,
170 }
171 }
172
173 pub fn assistant(content: impl Into<String>) -> Self {
175 Self {
176 role: ChatRole::Assistant,
177 content: content.into(),
178 cache_marker: None,
179 model_families: None,
180 signature: None,
181 }
182 }
183
184 pub fn with_cache_marker(mut self, marker: CacheMarker) -> Self {
186 self.cache_marker = Some(marker);
187 self
188 }
189}
190
191#[derive(Debug, Clone, Default, Serialize, Deserialize)]
193pub struct TokenUsage {
194 pub input_tokens: u64,
196
197 pub output_tokens: u64,
199
200 pub cached_tokens: u64,
202}
203
204impl TokenUsage {
205 pub fn new(input_tokens: u64, output_tokens: u64, cached_tokens: u64) -> Self {
207 Self {
208 input_tokens,
209 output_tokens,
210 cached_tokens,
211 }
212 }
213
214 pub fn total_tokens(&self) -> u64 {
216 self.input_tokens + self.output_tokens
217 }
218}
219
220#[derive(Debug, Clone, Default, Serialize, Deserialize)]
225pub struct CostInfo {
226 pub input_tokens: u64,
228
229 pub output_tokens: u64,
231
232 pub cache_read_input_tokens: u64,
234
235 pub cache_write_input_tokens: u64,
237
238 pub microdollar_cost: u64,
240
241 pub provider_cost_microdollars: Option<u64>,
244
245 pub microdollar_adjust: u64,
247}
248
249impl CostInfo {
250 pub fn from_usage(usage: &TokenUsage, model: &str) -> Self {
254 let mut info = Self {
255 input_tokens: usage.input_tokens,
256 output_tokens: usage.output_tokens,
257 cache_read_input_tokens: usage.cached_tokens,
258 cache_write_input_tokens: 0, microdollar_cost: 0,
260 provider_cost_microdollars: None,
261 microdollar_adjust: 0,
262 };
263
264 info.microdollar_cost = info.calculate_microdollar_cost(model);
265 info
266 }
267
268 pub fn from_usage_with_provider_cost(
273 usage: &TokenUsage,
274 model: &str,
275 provider_cost_dollars: f64,
276 ) -> Self {
277 let mut info = Self::from_usage(usage, model);
278 info.provider_cost_microdollars = Some((provider_cost_dollars * 1_000_000.0) as u64);
280 info
281 }
282
283 pub fn with_provider_cost(mut self, cost_dollars: f64) -> Self {
285 self.provider_cost_microdollars = Some((cost_dollars * 1_000_000.0) as u64);
286 self
287 }
288
289 fn calculate_microdollar_cost(&self, model: &str) -> u64 {
294 let model_lower = model.to_lowercase();
295
296 if model_lower.contains("claude") || model_lower.contains("anthropic") {
298 if model_lower.contains("haiku") {
299 let input_cost = (self.input_tokens as f64) * 0.80;
302 let output_cost = (self.output_tokens as f64) * 4.0;
303 let cache_cost = (self.cache_read_input_tokens as f64) * 0.08;
304 (input_cost + output_cost + cache_cost).ceil() as u64
305 } else if model_lower.contains("opus") {
306 let input_cost = (self.input_tokens as f64) * 15.0;
308 let output_cost = (self.output_tokens as f64) * 75.0;
309 let cache_cost = (self.cache_read_input_tokens as f64) * 1.50;
310 (input_cost + output_cost + cache_cost).ceil() as u64
311 } else {
312 let input_cost = (self.input_tokens as f64) * 3.0;
314 let output_cost = (self.output_tokens as f64) * 15.0;
315 let cache_cost = (self.cache_read_input_tokens as f64) * 0.30;
316 (input_cost + output_cost + cache_cost).ceil() as u64
317 }
318 }
319 else if model_lower.contains("gpt-4o") || model_lower.contains("o4-mini") {
321 if model_lower.contains("mini") {
322 let input_cost = (self.input_tokens as f64) * 1.10;
324 let output_cost = (self.output_tokens as f64) * 4.40;
325 let cache_cost = (self.cache_read_input_tokens as f64) * 0.275;
326 (input_cost + output_cost + cache_cost).ceil() as u64
327 } else {
328 let input_cost = (self.input_tokens as f64) * 5.0;
330 let output_cost = (self.output_tokens as f64) * 15.0;
331 let cache_cost = (self.cache_read_input_tokens as f64) * 1.25;
332 (input_cost + output_cost + cache_cost).ceil() as u64
333 }
334 } else if model_lower.contains("o3") {
335 let input_cost = (self.input_tokens as f64) * 10.0;
337 let output_cost = (self.output_tokens as f64) * 40.0;
338 let cache_cost = (self.cache_read_input_tokens as f64) * 2.5;
339 (input_cost + output_cost + cache_cost).ceil() as u64
340 } else if model_lower.contains("o1") {
341 let input_cost = (self.input_tokens as f64) * 15.0;
343 let output_cost = (self.output_tokens as f64) * 60.0;
344 (input_cost + output_cost).ceil() as u64
345 }
346 else if model_lower.contains("gemini") {
348 if model_lower.contains("flash") {
349 let input_cost = (self.input_tokens as f64) * 0.075;
351 let output_cost = (self.output_tokens as f64) * 0.30;
352 (input_cost + output_cost).ceil() as u64
353 } else if model_lower.contains("pro") {
354 let input_cost = (self.input_tokens as f64) * 1.25;
356 let output_cost = (self.output_tokens as f64) * 5.0;
357 (input_cost + output_cost).ceil() as u64
358 } else {
359 0
360 }
361 } else {
362 0
364 }
365 }
366
367 pub fn total_microdollars(&self) -> u64 {
372 let base_cost = self
373 .provider_cost_microdollars
374 .unwrap_or(self.microdollar_cost);
375 base_cost.wrapping_add(self.microdollar_adjust)
376 }
377
378 pub fn calculated_microdollars(&self) -> u64 {
380 self.microdollar_cost.wrapping_add(self.microdollar_adjust)
381 }
382
383 pub fn has_provider_cost(&self) -> bool {
385 self.provider_cost_microdollars.is_some()
386 }
387
388 pub fn total_dollars(&self) -> f64 {
390 (self.total_microdollars() as f64) / 1_000_000.0
391 }
392
393 pub fn total_cents(&self) -> u64 {
395 self.total_microdollars() / 10_000
396 }
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
405#[serde(tag = "type", rename_all = "snake_case")]
406pub enum MediaSource {
407 Base64 { data: String, mime_type: String },
409 Url { url: String },
411 FileId { file_id: String },
413 FileUri {
415 uri: String,
416 #[serde(skip_serializing_if = "Option::is_none")]
417 mime_type: Option<String>,
418 },
419}
420
421impl MediaSource {
422 pub fn base64(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
424 Self::Base64 {
425 data: data.into(),
426 mime_type: mime_type.into(),
427 }
428 }
429
430 pub fn url(url: impl Into<String>) -> Self {
432 Self::Url { url: url.into() }
433 }
434
435 pub fn file_id(file_id: impl Into<String>) -> Self {
437 Self::FileId {
438 file_id: file_id.into(),
439 }
440 }
441
442 pub fn file_uri(uri: impl Into<String>) -> Self {
444 Self::FileUri {
445 uri: uri.into(),
446 mime_type: None,
447 }
448 }
449
450 pub fn youtube(url: impl Into<String>) -> Self {
452 Self::FileUri {
453 uri: url.into(),
454 mime_type: Some("video/*".to_string()),
455 }
456 }
457}
458
459#[derive(Debug, Clone, Default, Serialize, Deserialize)]
461pub struct VideoMetadata {
462 #[serde(skip_serializing_if = "Option::is_none")]
464 pub start_offset: Option<String>,
465
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub end_offset: Option<String>,
469
470 #[serde(skip_serializing_if = "Option::is_none")]
472 pub fps: Option<f32>,
473}
474
475impl VideoMetadata {
476 pub fn new() -> Self {
478 Self::default()
479 }
480
481 pub fn with_start(mut self, offset: impl Into<String>) -> Self {
483 self.start_offset = Some(offset.into());
484 self
485 }
486
487 pub fn with_end(mut self, offset: impl Into<String>) -> Self {
489 self.end_offset = Some(offset.into());
490 self
491 }
492
493 pub fn with_fps(mut self, fps: f32) -> Self {
495 self.fps = Some(fps);
496 self
497 }
498
499 pub fn with_clip(mut self, start: impl Into<String>, end: impl Into<String>) -> Self {
501 self.start_offset = Some(start.into());
502 self.end_offset = Some(end.into());
503 self
504 }
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
509#[serde(tag = "type", rename_all = "snake_case")]
510pub enum MediaPart {
511 Text { text: String },
513
514 Image {
516 source: MediaSource,
517 #[serde(skip_serializing_if = "Option::is_none")]
519 detail: Option<String>,
520 },
521
522 Audio {
524 source: MediaSource,
525 #[serde(skip_serializing_if = "Option::is_none")]
527 format: Option<String>,
528 },
529
530 Video {
532 source: MediaSource,
533 #[serde(skip_serializing_if = "Option::is_none")]
535 metadata: Option<VideoMetadata>,
536 },
537
538 Document { source: MediaSource },
540}
541
542impl MediaPart {
543 pub fn text(text: impl Into<String>) -> Self {
545 Self::Text { text: text.into() }
546 }
547
548 pub fn image(source: MediaSource) -> Self {
550 Self::Image {
551 source,
552 detail: None,
553 }
554 }
555
556 pub fn image_with_detail(source: MediaSource, detail: impl Into<String>) -> Self {
558 Self::Image {
559 source,
560 detail: Some(detail.into()),
561 }
562 }
563
564 pub fn audio(source: MediaSource) -> Self {
566 Self::Audio {
567 source,
568 format: None,
569 }
570 }
571
572 pub fn audio_with_format(source: MediaSource, format: impl Into<String>) -> Self {
574 Self::Audio {
575 source,
576 format: Some(format.into()),
577 }
578 }
579
580 pub fn video(source: MediaSource) -> Self {
582 Self::Video {
583 source,
584 metadata: None,
585 }
586 }
587
588 pub fn video_with_metadata(source: MediaSource, metadata: VideoMetadata) -> Self {
590 Self::Video {
591 source,
592 metadata: Some(metadata),
593 }
594 }
595
596 pub fn youtube(url: impl Into<String>) -> Self {
598 Self::Video {
599 source: MediaSource::youtube(url),
600 metadata: None,
601 }
602 }
603
604 pub fn youtube_clip(
606 url: impl Into<String>,
607 start: impl Into<String>,
608 end: impl Into<String>,
609 ) -> Self {
610 Self::Video {
611 source: MediaSource::youtube(url),
612 metadata: Some(VideoMetadata::new().with_clip(start, end)),
613 }
614 }
615
616 pub fn document(source: MediaSource) -> Self {
618 Self::Document { source }
619 }
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct MultimodalMessage {
625 pub role: ChatRole,
627
628 pub parts: Vec<MediaPart>,
630
631 #[serde(skip_serializing_if = "Option::is_none")]
633 pub cache_marker: Option<CacheMarker>,
634}
635
636impl MultimodalMessage {
637 pub fn user(parts: Vec<MediaPart>) -> Self {
639 Self {
640 role: ChatRole::User,
641 parts,
642 cache_marker: None,
643 }
644 }
645
646 pub fn user_with_media(text: impl Into<String>, media: MediaPart) -> Self {
648 Self {
649 role: ChatRole::User,
650 parts: vec![media, MediaPart::text(text)],
651 cache_marker: None,
652 }
653 }
654
655 pub fn user_with_video(text: impl Into<String>, source: MediaSource) -> Self {
657 Self::user_with_media(text, MediaPart::video(source))
658 }
659
660 pub fn user_with_image(text: impl Into<String>, source: MediaSource) -> Self {
662 Self::user_with_media(text, MediaPart::image(source))
663 }
664
665 pub fn user_with_youtube(text: impl Into<String>, url: impl Into<String>) -> Self {
667 Self::user_with_media(text, MediaPart::youtube(url))
668 }
669
670 pub fn with_cache_marker(mut self, marker: CacheMarker) -> Self {
672 self.cache_marker = Some(marker);
673 self
674 }
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
683pub enum CreateResult {
684 Single(ToolCall),
686 Multiple(Vec<ToolCall>),
688 Empty,
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize)]
694pub struct ToolCall {
695 pub tool_use_id: String,
697
698 pub tool_name: String,
700
701 pub args: serde_json::Value,
703
704 #[serde(skip_serializing_if = "Option::is_none")]
706 pub raw_arguments: Option<String>,
707
708 #[serde(skip_serializing_if = "Option::is_none")]
710 pub signature: Option<String>,
711
712 #[serde(skip_serializing_if = "Option::is_none")]
714 pub tool_obj: Option<serde_json::Value>,
715}
716
717impl ToolCall {
718 pub fn new(tool_name: impl Into<String>, args: serde_json::Value) -> Self {
720 Self {
721 tool_use_id: Uuid::new_v4().to_string(),
722 tool_name: tool_name.into(),
723 args,
724 raw_arguments: None,
725 signature: None,
726 tool_obj: None,
727 }
728 }
729
730 pub fn create(data: serde_json::Value) -> Result<CreateResult> {
739 if let Some(arr) = data.as_array() {
741 if arr.is_empty() {
742 return Ok(CreateResult::Empty);
743 }
744 let mut results = Vec::new();
745 for item in arr {
746 match Self::create(item.clone())? {
747 CreateResult::Single(tc) => results.push(tc),
748 CreateResult::Multiple(tcs) => results.extend(tcs),
749 CreateResult::Empty => {}
750 }
751 }
752 return Ok(CreateResult::Multiple(results));
753 }
754
755 if data.get("function").is_some() {
757 Self::from_provider_format(data, Provider::OpenAI).map(CreateResult::Single)
759 } else if data
760 .get("type")
761 .and_then(|v| v.as_str())
762 .map(|t| t == "function_call")
763 .unwrap_or(false)
764 && data.get("name").is_some()
765 {
766 Self::from_provider_format(data, Provider::OpenAIResponses).map(CreateResult::Single)
768 } else if data.get("functionCall").is_some() {
769 Self::from_provider_format(data, Provider::GoogleGenAI).map(CreateResult::Single)
771 } else if data.get("name").is_some() && data.get("input").is_some() {
772 Self::from_provider_format(data, Provider::Anthropic).map(CreateResult::Single)
774 } else if data.get("_tool_name").is_some() {
775 let tool_name = data["_tool_name"]
777 .as_str()
778 .ok_or_else(|| Error::Parse("_tool_name must be a string".to_string()))?
779 .to_string();
780 let tool_use_id = data
781 .get("_tool_use_id")
782 .and_then(|v| v.as_str())
783 .map(|s| s.to_string())
784 .unwrap_or_else(|| Uuid::new_v4().to_string());
785 let signature = data
786 .get("signature")
787 .and_then(|v| v.as_str())
788 .map(|s| s.to_string())
789 .or_else(|| {
790 data.get("thought_signature")
791 .and_then(|v| v.as_str())
792 .map(|s| s.to_string())
793 });
794
795 let mut args = serde_json::Map::new();
797 if let Some(obj) = data.as_object() {
798 for (k, v) in obj {
799 if !k.starts_with('_') && k != "signature" && k != "thought_signature" {
800 args.insert(k.clone(), v.clone());
801 }
802 }
803 }
804
805 Ok(CreateResult::Single(Self {
806 tool_use_id,
807 tool_name,
808 args: serde_json::Value::Object(args),
809 raw_arguments: None,
810 signature,
811 tool_obj: Some(data),
812 }))
813 } else {
814 Err(Error::Parse("Unknown tool call format".to_string()))
815 }
816 }
817
818 pub fn from_provider_format(
820 provider_format: serde_json::Value,
821 provider: Provider,
822 ) -> Result<Self> {
823 match provider {
824 Provider::Anthropic | Provider::Bedrock => Ok(Self {
825 tool_use_id: provider_format["id"]
826 .as_str()
827 .ok_or_else(|| Error::Parse("Missing 'id' in tool call".to_string()))?
828 .to_string(),
829 tool_name: provider_format["name"]
830 .as_str()
831 .ok_or_else(|| Error::Parse("Missing 'name' in tool call".to_string()))?
832 .to_string(),
833 args: provider_format["input"].clone(),
834 raw_arguments: None,
835 signature: None,
836 tool_obj: Some(provider_format),
837 }),
838 Provider::OpenAI | Provider::OpenRouter => {
839 let function = &provider_format["function"];
840 let arguments = &function["arguments"];
841
842 let (args, raw_arguments) = if let Some(args_str) = arguments.as_str() {
844 let parsed: serde_json::Value = serde_json::from_str(args_str)?;
845 (parsed, Some(args_str.to_string()))
846 } else if arguments.is_object() {
847 let raw = serde_json::to_string(arguments)?;
848 (arguments.clone(), Some(raw))
849 } else {
850 return Err(Error::Parse("Missing 'arguments' in tool call".to_string()));
851 };
852
853 Ok(Self {
854 tool_use_id: provider_format["id"]
855 .as_str()
856 .ok_or_else(|| Error::Parse("Missing 'id' in tool call".to_string()))?
857 .to_string(),
858 tool_name: function["name"]
859 .as_str()
860 .ok_or_else(|| Error::Parse("Missing 'name' in function".to_string()))?
861 .to_string(),
862 args,
863 raw_arguments,
864 signature: None,
865 tool_obj: Some(provider_format),
866 })
867 }
868 Provider::OpenAIResponses | Provider::OpenRouterResponses => {
869 let args_str = provider_format["arguments"].as_str().unwrap_or("");
870 let call_id = provider_format
871 .get("call_id")
872 .and_then(|v| v.as_str())
873 .or_else(|| provider_format.get("id").and_then(|v| v.as_str()))
874 .ok_or_else(|| Error::Parse("Missing 'call_id' in tool call".to_string()))?;
875 Ok(Self {
876 tool_use_id: call_id.to_string(),
877 tool_name: provider_format["name"]
878 .as_str()
879 .ok_or_else(|| Error::Parse("Missing 'name' in tool call".to_string()))?
880 .to_string(),
881 args: serde_json::from_str(args_str).unwrap_or_else(|_| serde_json::json!({})),
882 raw_arguments: Some(args_str.to_string()),
883 signature: None,
884 tool_obj: Some(provider_format),
885 })
886 }
887 Provider::GoogleGenAI | Provider::GoogleAnthropic => {
888 let func_call = &provider_format["functionCall"];
890 let name = func_call["name"]
891 .as_str()
892 .ok_or_else(|| Error::Parse("Missing 'name' in functionCall".to_string()))?;
893 let args = func_call
894 .get("args")
895 .cloned()
896 .unwrap_or(serde_json::json!({}));
897
898 use std::collections::hash_map::DefaultHasher;
900 use std::hash::{Hash, Hasher};
901 let mut hasher = DefaultHasher::new();
902 args.to_string().hash(&mut hasher);
903 let hash = hasher.finish();
904 let tool_use_id = format!("google_{}_{}", name, hash);
905
906 Ok(Self {
907 tool_use_id,
908 tool_name: name.to_string(),
909 args,
910 raw_arguments: None,
911 signature: None,
912 tool_obj: Some(provider_format),
913 })
914 }
915 }
916 }
917
918 pub fn to_provider_assistant_message(&self, provider: Provider) -> serde_json::Value {
920 match provider {
921 Provider::Anthropic | Provider::Bedrock => {
922 serde_json::json!({
923 "role": "assistant",
924 "content": [{
925 "type": "tool_use",
926 "id": self.tool_use_id,
927 "name": self.tool_name,
928 "input": self.args
929 }]
930 })
931 }
932 Provider::OpenAI | Provider::OpenRouter => {
933 let args_str = self.raw_arguments.clone().unwrap_or_else(|| {
935 format_json_python_style(&self.args)
937 });
938 serde_json::json!({
939 "role": "assistant",
940 "tool_calls": [{
941 "id": self.tool_use_id,
942 "type": "function",
943 "function": {
944 "name": self.tool_name,
945 "arguments": args_str
946 }
947 }]
948 })
949 }
950 Provider::OpenAIResponses | Provider::OpenRouterResponses => {
951 let args_str = self.raw_arguments.clone().unwrap_or_else(|| {
952 serde_json::to_string(&self.args).unwrap_or_else(|_| "{}".to_string())
953 });
954 serde_json::json!({
955 "type": "function_call",
956 "id": format!("fc_{}", self.tool_use_id),
957 "call_id": self.tool_use_id,
958 "name": self.tool_name,
959 "arguments": args_str
960 })
961 }
962 Provider::GoogleGenAI | Provider::GoogleAnthropic => {
963 serde_json::json!({
964 "role": "model",
965 "parts": [{
966 "functionCall": {
967 "name": self.tool_name,
968 "args": self.args
969 }
970 }]
971 })
972 }
973 }
974 }
975}
976
977#[derive(Debug, Clone, Serialize, Deserialize)]
979pub struct ToolResult {
980 pub tool_use_id: String,
982
983 #[serde(skip_serializing_if = "Option::is_none")]
985 pub tool_name: Option<String>,
986
987 pub content: String,
989
990 pub is_error: bool,
992
993 #[serde(skip_serializing_if = "Option::is_none")]
995 pub signature: Option<String>,
996
997 #[serde(skip_serializing_if = "Option::is_none")]
999 pub tool_obj: Option<serde_json::Value>,
1000}
1001
1002impl ToolResult {
1003 pub fn success(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
1005 Self {
1006 tool_use_id: tool_use_id.into(),
1007 tool_name: None,
1008 content: content.into(),
1009 is_error: false,
1010 signature: None,
1011 tool_obj: None,
1012 }
1013 }
1014
1015 pub fn success_with_name(
1017 tool_use_id: impl Into<String>,
1018 tool_name: impl Into<String>,
1019 content: impl Into<String>,
1020 ) -> Self {
1021 Self {
1022 tool_use_id: tool_use_id.into(),
1023 tool_name: Some(tool_name.into()),
1024 content: content.into(),
1025 is_error: false,
1026 signature: None,
1027 tool_obj: None,
1028 }
1029 }
1030
1031 pub fn error(tool_use_id: impl Into<String>, error: impl Into<String>) -> Self {
1033 Self {
1034 tool_use_id: tool_use_id.into(),
1035 tool_name: None,
1036 content: error.into(),
1037 is_error: true,
1038 signature: None,
1039 tool_obj: None,
1040 }
1041 }
1042
1043 pub fn with_tool_name(mut self, name: impl Into<String>) -> Self {
1045 self.tool_name = Some(name.into());
1046 self
1047 }
1048
1049 pub fn with_tool_obj(mut self, obj: serde_json::Value) -> Self {
1051 self.tool_obj = Some(obj);
1052 self
1053 }
1054
1055 pub fn to_provider_format(&self, provider: Provider) -> serde_json::Value {
1057 match provider {
1058 Provider::Anthropic | Provider::Bedrock => {
1059 serde_json::json!({
1060 "type": "tool_result",
1061 "tool_use_id": self.tool_use_id,
1062 "content": self.content,
1063 "is_error": self.is_error
1064 })
1065 }
1066 Provider::OpenAI | Provider::OpenRouter => {
1067 serde_json::json!({
1068 "role": "tool",
1069 "tool_call_id": self.tool_use_id,
1070 "content": self.content
1071 })
1072 }
1073 Provider::OpenAIResponses | Provider::OpenRouterResponses => {
1074 serde_json::json!({
1075 "type": "function_call_output",
1076 "call_id": self.tool_use_id,
1077 "output": self.content
1078 })
1079 }
1080 Provider::GoogleGenAI | Provider::GoogleAnthropic => {
1081 let name = self.tool_name.clone().unwrap_or_else(|| {
1083 self.tool_obj
1084 .as_ref()
1085 .and_then(|obj| obj.get("_tool_name"))
1086 .and_then(|v| v.as_str())
1087 .map(|s| s.to_string())
1088 .unwrap_or_else(|| "unknown".to_string())
1089 });
1090
1091 serde_json::json!({
1092 "functionResponse": {
1093 "name": name,
1094 "response": {
1095 "result": self.content
1096 }
1097 }
1098 })
1099 }
1100 }
1101 }
1102}
1103
1104#[derive(Debug, Clone, Serialize, Deserialize)]
1106pub struct ReasoningSegment {
1107 pub content: String,
1109
1110 pub segment_index: usize,
1112
1113 #[serde(skip_serializing_if = "Option::is_none")]
1115 pub signature: Option<String>,
1116
1117 #[serde(skip_serializing_if = "Option::is_none")]
1119 pub provider_metadata: Option<serde_json::Value>,
1120}
1121
1122impl ReasoningSegment {
1123 pub fn new(content: impl Into<String>) -> Self {
1125 Self {
1126 content: content.into(),
1127 segment_index: 0,
1128 signature: None,
1129 provider_metadata: None,
1130 }
1131 }
1132
1133 pub fn with_index(content: impl Into<String>, segment_index: usize) -> Self {
1135 Self {
1136 content: content.into(),
1137 segment_index,
1138 signature: None,
1139 provider_metadata: None,
1140 }
1141 }
1142
1143 pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
1145 self.signature = Some(signature.into());
1146 self
1147 }
1148
1149 pub fn with_provider_metadata(mut self, metadata: serde_json::Value) -> Self {
1151 self.provider_metadata = Some(metadata);
1152 self
1153 }
1154
1155 pub fn with_segment_index(mut self, index: usize) -> Self {
1157 self.segment_index = index;
1158 self
1159 }
1160
1161 pub fn to_provider_format(&self, provider: Provider) -> serde_json::Value {
1163 match provider {
1164 Provider::Anthropic | Provider::Bedrock | Provider::GoogleAnthropic => {
1166 let mut obj = serde_json::json!({
1167 "type": "thinking",
1168 "thinking": self.content
1169 });
1170 if let Some(ref sig) = self.signature {
1171 obj["signature"] = serde_json::Value::String(sig.clone());
1172 }
1173 obj
1174 }
1175 Provider::OpenAI | Provider::OpenRouter => {
1176 let mut obj = serde_json::json!({
1177 "type": "reasoning",
1178 "content": self.content
1179 });
1180 if let Some(ref sig) = self.signature {
1181 obj["signature"] = serde_json::Value::String(sig.clone());
1182 }
1183 obj
1184 }
1185 Provider::OpenAIResponses | Provider::OpenRouterResponses => {
1186 let mut obj = serde_json::json!({
1187 "type": "reasoning",
1188 "content": [{
1189 "type": "output_text",
1190 "text": self.content
1191 }]
1192 });
1193 if let Some(ref sig) = self.signature {
1194 obj["signature"] = serde_json::Value::String(sig.clone());
1195 }
1196 obj
1197 }
1198 Provider::GoogleGenAI => {
1199 let mut obj = serde_json::json!({
1200 "thought": true,
1201 "text": self.content
1202 });
1203 if let Some(ref sig) = self.signature {
1204 obj["thought_signature"] = serde_json::Value::String(sig.clone());
1205 }
1206 obj
1207 }
1208 }
1209 }
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214 use super::*;
1215
1216 #[test]
1217 fn test_provider_from_model_string() {
1218 let (provider, model) = Provider::from_model_string("anthropic:claude-3-opus").unwrap();
1219 assert_eq!(provider, Provider::Anthropic);
1220 assert_eq!(model, "claude-3-opus");
1221
1222 let (provider, model) = Provider::from_model_string("openai:gpt-4").unwrap();
1223 assert_eq!(provider, Provider::OpenAI);
1224 assert_eq!(model, "gpt-4");
1225
1226 let (provider, model) = Provider::from_model_string("openai:resp:gpt-4").unwrap();
1227 assert_eq!(provider, Provider::OpenAIResponses);
1228 assert_eq!(model, "gpt-4");
1229
1230 let (provider, model) =
1231 Provider::from_model_string("openrouter:resp:openai/o4-mini").unwrap();
1232 assert_eq!(provider, Provider::OpenRouterResponses);
1233 assert_eq!(model, "openai/o4-mini");
1234
1235 assert!(Provider::from_model_string("invalid").is_err());
1236 }
1237
1238 #[test]
1239 fn test_provider_supports_native_tools() {
1240 assert!(Provider::Anthropic.supports_native_tools());
1241 assert!(Provider::OpenAI.supports_native_tools());
1242 assert!(Provider::OpenAIResponses.supports_native_tools());
1243 assert!(Provider::GoogleAnthropic.supports_native_tools());
1244 assert!(Provider::GoogleGenAI.supports_native_tools());
1245 assert!(Provider::OpenRouter.supports_native_tools());
1246 assert!(Provider::OpenRouterResponses.supports_native_tools());
1247 assert!(Provider::Bedrock.supports_native_tools());
1248 }
1249
1250 #[test]
1251 fn test_chat_message_constructors() {
1252 let msg = ChatMessage::system("You are helpful");
1253 assert_eq!(msg.role, ChatRole::System);
1254 assert_eq!(msg.content, "You are helpful");
1255
1256 let msg = ChatMessage::user("Hello");
1257 assert_eq!(msg.role, ChatRole::User);
1258
1259 let msg = ChatMessage::assistant("Hi there").with_cache_marker(CacheMarker::Ephemeral);
1260 assert_eq!(msg.role, ChatRole::Assistant);
1261 assert!(msg.cache_marker.is_some());
1262 }
1263
1264 #[test]
1265 fn test_token_usage() {
1266 let usage = TokenUsage::new(100, 50, 25);
1267 assert_eq!(usage.input_tokens, 100);
1268 assert_eq!(usage.output_tokens, 50);
1269 assert_eq!(usage.cached_tokens, 25);
1270 assert_eq!(usage.total_tokens(), 150);
1271 }
1272
1273 #[test]
1274 fn test_tool_call_creation() {
1275 let tool_call = ToolCall::new("get_weather", serde_json::json!({"city": "SF"}));
1276 assert_eq!(tool_call.tool_name, "get_weather");
1277 assert_eq!(tool_call.args["city"], "SF");
1278 assert!(!tool_call.tool_use_id.is_empty());
1279 }
1280
1281 #[test]
1282 fn test_tool_call_from_anthropic_format() {
1283 let format = serde_json::json!({
1284 "id": "toolu_123",
1285 "name": "get_weather",
1286 "input": {"city": "SF"}
1287 });
1288
1289 let tool_call = ToolCall::from_provider_format(format, Provider::Anthropic).unwrap();
1290 assert_eq!(tool_call.tool_use_id, "toolu_123");
1291 assert_eq!(tool_call.tool_name, "get_weather");
1292 assert_eq!(tool_call.args["city"], "SF");
1293 }
1294
1295 #[test]
1296 fn test_tool_call_from_openai_format() {
1297 let format = serde_json::json!({
1298 "id": "call_123",
1299 "function": {
1300 "name": "get_weather",
1301 "arguments": "{\"city\":\"SF\"}"
1302 }
1303 });
1304
1305 let tool_call = ToolCall::from_provider_format(format, Provider::OpenAI).unwrap();
1306 assert_eq!(tool_call.tool_use_id, "call_123");
1307 assert_eq!(tool_call.tool_name, "get_weather");
1308 assert_eq!(tool_call.args["city"], "SF");
1309 }
1310
1311 #[test]
1312 fn test_tool_result_success() {
1313 let result = ToolResult::success("toolu_123", "Sunny, 72°F");
1314 assert_eq!(result.tool_use_id, "toolu_123");
1315 assert_eq!(result.content, "Sunny, 72°F");
1316 assert!(!result.is_error);
1317 }
1318
1319 #[test]
1320 fn test_tool_result_error() {
1321 let result = ToolResult::error("toolu_123", "API error");
1322 assert!(result.is_error);
1323 }
1324
1325 #[test]
1326 fn test_tool_result_to_anthropic_format() {
1327 let result = ToolResult::success("toolu_123", "Result");
1328 let format = result.to_provider_format(Provider::Anthropic);
1329
1330 assert_eq!(format["type"], "tool_result");
1331 assert_eq!(format["tool_use_id"], "toolu_123");
1332 assert_eq!(format["content"], "Result");
1333 }
1334
1335 #[test]
1336 fn test_tool_result_to_openai_format() {
1337 let result = ToolResult::success("call_123", "Result");
1338 let format = result.to_provider_format(Provider::OpenAI);
1339
1340 assert_eq!(format["role"], "tool");
1341 assert_eq!(format["tool_call_id"], "call_123");
1342 assert_eq!(format["content"], "Result");
1343 }
1344
1345 #[test]
1346 fn test_reasoning_segment() {
1347 let segment = ReasoningSegment::with_index("Thinking...", 0);
1348 assert_eq!(segment.content, "Thinking...");
1349 assert_eq!(segment.segment_index, 0);
1350 }
1351
1352 #[test]
1353 fn test_reasoning_segment_to_anthropic_format() {
1354 let segment = ReasoningSegment::with_index("Thinking...", 0);
1355 let format = segment.to_provider_format(Provider::Anthropic);
1356
1357 assert_eq!(format["type"], "thinking");
1358 assert_eq!(format["thinking"], "Thinking...");
1359 }
1360}