Skip to main content

reson_agentic/
types.rs

1//! Core types for Reson
2//!
3//! This module contains the fundamental data structures used throughout
4//! the framework: messages, tool calls, tool results, and reasoning segments.
5
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::error::{Error, Result};
10
11/// Format JSON to match Python's json.dumps default style (space after colon and comma)
12fn 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/// LLM Provider enumeration
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub enum Provider {
37    /// Anthropic Claude (direct API)
38    Anthropic,
39    /// OpenAI GPT models (direct API)
40    OpenAI,
41    /// OpenAI Responses API
42    OpenAIResponses,
43    /// AWS Bedrock (Claude and others)
44    Bedrock,
45    /// Google Gemini via GenAI SDK
46    GoogleGenAI,
47    /// Anthropic Claude via Google Vertex AI
48    GoogleAnthropic,
49    /// OpenRouter proxy service
50    OpenRouter,
51    /// OpenRouter Responses API
52    OpenRouterResponses,
53}
54
55impl Provider {
56    /// Parse provider from model string (e.g., "anthropic:claude-3-opus")
57    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    /// Check if this provider supports native tool calling
90    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/// Chat message role
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "lowercase")]
108pub enum ChatRole {
109    /// System message (instructions)
110    System,
111    /// User message
112    User,
113    /// Assistant message
114    Assistant,
115    /// Tool result message
116    #[serde(rename = "tool")]
117    Tool,
118}
119
120/// Cache marker for Anthropic prompt caching
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "lowercase")]
123pub enum CacheMarker {
124    /// Ephemeral cache point
125    Ephemeral,
126}
127
128/// A chat message in the conversation
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ChatMessage {
131    /// Message role
132    pub role: ChatRole,
133
134    /// Message content (text)
135    pub content: String,
136
137    /// Optional cache marker (for Anthropic caching)
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub cache_marker: Option<CacheMarker>,
140
141    /// Optional model family filtering
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub model_families: Option<Vec<String>>,
144
145    /// Provider-specific signature for reasoning preservation
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub signature: Option<String>,
148}
149
150impl ChatMessage {
151    /// Create a new system message
152    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    /// Create a new user message
163    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    /// Create a new assistant message
174    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    /// Add a cache marker to this message
185    pub fn with_cache_marker(mut self, marker: CacheMarker) -> Self {
186        self.cache_marker = Some(marker);
187        self
188    }
189}
190
191/// Token usage statistics
192#[derive(Debug, Clone, Default, Serialize, Deserialize)]
193pub struct TokenUsage {
194    /// Input tokens consumed
195    pub input_tokens: u64,
196
197    /// Output tokens generated
198    pub output_tokens: u64,
199
200    /// Cached tokens (for Anthropic caching)
201    pub cached_tokens: u64,
202}
203
204impl TokenUsage {
205    /// Create new token usage stats
206    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    /// Total tokens (input + output)
215    pub fn total_tokens(&self) -> u64 {
216        self.input_tokens + self.output_tokens
217    }
218}
219
220/// Cost information for inference requests
221///
222/// Stores costs in microdollars ($1 = 1,000,000 microdollars) for billing-level precision.
223/// This avoids floating-point rounding errors when accumulating many small costs.
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
225pub struct CostInfo {
226    /// Input tokens consumed
227    pub input_tokens: u64,
228
229    /// Output tokens generated
230    pub output_tokens: u64,
231
232    /// Cache read tokens (prompt caching hits)
233    pub cache_read_input_tokens: u64,
234
235    /// Cache write tokens (prompt caching misses that were cached)
236    pub cache_write_input_tokens: u64,
237
238    /// Cost in microdollars ($1 = 1,000,000) - calculated from pricing tables
239    pub microdollar_cost: u64,
240
241    /// Provider-reported cost in microdollars (e.g., from OpenRouter)
242    /// When present, this takes precedence over calculated cost
243    pub provider_cost_microdollars: Option<u64>,
244
245    /// Manual adjustment in microdollars (can be negative via wrapping)
246    pub microdollar_adjust: u64,
247}
248
249impl CostInfo {
250    /// Create CostInfo from TokenUsage and model name
251    ///
252    /// Calculates cost based on per-model pricing. Uses microdollars for precision.
253    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, // Not tracked in TokenUsage currently
259            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    /// Create CostInfo with provider-reported cost (e.g., from OpenRouter)
269    ///
270    /// When the provider returns cost directly, we use that instead of calculating.
271    /// The provider cost takes precedence in `total_microdollars()`.
272    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        // Convert dollars to microdollars
279        info.provider_cost_microdollars = Some((provider_cost_dollars * 1_000_000.0) as u64);
280        info
281    }
282
283    /// Set provider-reported cost in dollars
284    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    /// Calculate cost in microdollars based on model pricing
290    ///
291    /// Pricing per million tokens, converted to microdollars per token:
292    /// - $X per 1M tokens = X microdollars per token
293    fn calculate_microdollar_cost(&self, model: &str) -> u64 {
294        let model_lower = model.to_lowercase();
295
296        // Anthropic models
297        if model_lower.contains("claude") || model_lower.contains("anthropic") {
298            if model_lower.contains("haiku") {
299                // Haiku: $0.80/1M input, $4/1M output, $0.08/1M cache read
300                // In microdollars: 0.80 input, 4.0 output, 0.08 cache read
301                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                // Opus: $15/1M input, $75/1M output, $1.50/1M cache read
307                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                // Sonnet (default): $3/1M input, $15/1M output, $0.30/1M cache read
313                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        // OpenAI models
320        else if model_lower.contains("gpt-4o") || model_lower.contains("o4-mini") {
321            if model_lower.contains("mini") {
322                // GPT-4o-mini / o4-mini: $1.10/1M input, $4.40/1M output
323                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                // GPT-4o: $5/1M input, $15/1M output
329                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            // o3: $10/1M input, $40/1M output
336            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            // o1: $15/1M input, $60/1M output
342            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        // Google models
347        else if model_lower.contains("gemini") {
348            if model_lower.contains("flash") {
349                // Gemini Flash: $0.075/1M input, $0.30/1M output
350                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                // Gemini Pro: $1.25/1M input, $5/1M output
355                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            // Unknown model - return 0 cost
363            0
364        }
365    }
366
367    /// Total cost in microdollars (including adjustments)
368    ///
369    /// Prefers provider-reported cost when available (e.g., from OpenRouter),
370    /// falls back to calculated cost from pricing tables.
371    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    /// Get the calculated cost (from pricing tables), ignoring provider cost
379    pub fn calculated_microdollars(&self) -> u64 {
380        self.microdollar_cost.wrapping_add(self.microdollar_adjust)
381    }
382
383    /// Check if provider reported cost directly
384    pub fn has_provider_cost(&self) -> bool {
385        self.provider_cost_microdollars.is_some()
386    }
387
388    /// Convert to dollars (for display)
389    pub fn total_dollars(&self) -> f64 {
390        (self.total_microdollars() as f64) / 1_000_000.0
391    }
392
393    /// Convert to cents (for legacy compatibility)
394    pub fn total_cents(&self) -> u64 {
395        self.total_microdollars() / 10_000
396    }
397}
398
399// ============================================================================
400// Media Types - for multimodal content (images, audio, video)
401// ============================================================================
402
403/// Source of media content - where the data comes from
404#[derive(Debug, Clone, Serialize, Deserialize)]
405#[serde(tag = "type", rename_all = "snake_case")]
406pub enum MediaSource {
407    /// Base64-encoded data with MIME type
408    Base64 { data: String, mime_type: String },
409    /// URL to the media (public URL)
410    Url { url: String },
411    /// Provider file ID (uploaded via provider's File API)
412    FileId { file_id: String },
413    /// Provider file URI (Google-style, includes YouTube URLs)
414    FileUri {
415        uri: String,
416        #[serde(skip_serializing_if = "Option::is_none")]
417        mime_type: Option<String>,
418    },
419}
420
421impl MediaSource {
422    /// Create from base64 data
423    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    /// Create from URL
431    pub fn url(url: impl Into<String>) -> Self {
432        Self::Url { url: url.into() }
433    }
434
435    /// Create from provider file ID
436    pub fn file_id(file_id: impl Into<String>) -> Self {
437        Self::FileId {
438            file_id: file_id.into(),
439        }
440    }
441
442    /// Create from provider file URI (Google)
443    pub fn file_uri(uri: impl Into<String>) -> Self {
444        Self::FileUri {
445            uri: uri.into(),
446            mime_type: None,
447        }
448    }
449
450    /// Create from YouTube URL (Google)
451    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/// Video-specific metadata for processing options
460#[derive(Debug, Clone, Default, Serialize, Deserialize)]
461pub struct VideoMetadata {
462    /// Start offset for clipping (e.g., "1250s" or "00:30")
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub start_offset: Option<String>,
465
466    /// End offset for clipping
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub end_offset: Option<String>,
469
470    /// Frames per second for sampling (default 1 FPS)
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub fps: Option<f32>,
473}
474
475impl VideoMetadata {
476    /// Create new video metadata
477    pub fn new() -> Self {
478        Self::default()
479    }
480
481    /// Set start offset
482    pub fn with_start(mut self, offset: impl Into<String>) -> Self {
483        self.start_offset = Some(offset.into());
484        self
485    }
486
487    /// Set end offset
488    pub fn with_end(mut self, offset: impl Into<String>) -> Self {
489        self.end_offset = Some(offset.into());
490        self
491    }
492
493    /// Set FPS for video sampling
494    pub fn with_fps(mut self, fps: f32) -> Self {
495        self.fps = Some(fps);
496        self
497    }
498
499    /// Set clipping range
500    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/// A media content part that can be attached to messages
508#[derive(Debug, Clone, Serialize, Deserialize)]
509#[serde(tag = "type", rename_all = "snake_case")]
510pub enum MediaPart {
511    /// Text content
512    Text { text: String },
513
514    /// Image content
515    Image {
516        source: MediaSource,
517        /// Detail level for processing (OpenAI: "high", "low", "auto")
518        #[serde(skip_serializing_if = "Option::is_none")]
519        detail: Option<String>,
520    },
521
522    /// Audio content
523    Audio {
524        source: MediaSource,
525        /// Audio format hint (e.g., "wav", "mp3")
526        #[serde(skip_serializing_if = "Option::is_none")]
527        format: Option<String>,
528    },
529
530    /// Video content
531    Video {
532        source: MediaSource,
533        /// Video processing metadata (clipping, FPS)
534        #[serde(skip_serializing_if = "Option::is_none")]
535        metadata: Option<VideoMetadata>,
536    },
537
538    /// PDF document (Anthropic, Google)
539    Document { source: MediaSource },
540}
541
542impl MediaPart {
543    /// Create a text part
544    pub fn text(text: impl Into<String>) -> Self {
545        Self::Text { text: text.into() }
546    }
547
548    /// Create an image part from a source
549    pub fn image(source: MediaSource) -> Self {
550        Self::Image {
551            source,
552            detail: None,
553        }
554    }
555
556    /// Create an image part with detail level
557    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    /// Create an audio part
565    pub fn audio(source: MediaSource) -> Self {
566        Self::Audio {
567            source,
568            format: None,
569        }
570    }
571
572    /// Create an audio part with format hint
573    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    /// Create a video part
581    pub fn video(source: MediaSource) -> Self {
582        Self::Video {
583            source,
584            metadata: None,
585        }
586    }
587
588    /// Create a video part with metadata
589    pub fn video_with_metadata(source: MediaSource, metadata: VideoMetadata) -> Self {
590        Self::Video {
591            source,
592            metadata: Some(metadata),
593        }
594    }
595
596    /// Create a video from YouTube URL
597    pub fn youtube(url: impl Into<String>) -> Self {
598        Self::Video {
599            source: MediaSource::youtube(url),
600            metadata: None,
601        }
602    }
603
604    /// Create a video from YouTube URL with clipping
605    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    /// Create a document part
617    pub fn document(source: MediaSource) -> Self {
618        Self::Document { source }
619    }
620}
621
622/// A multimodal message containing multiple parts (text, images, video, etc.)
623#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct MultimodalMessage {
625    /// Message role
626    pub role: ChatRole,
627
628    /// Content parts (text, images, video, audio, documents)
629    pub parts: Vec<MediaPart>,
630
631    /// Optional cache marker (for Anthropic caching)
632    #[serde(skip_serializing_if = "Option::is_none")]
633    pub cache_marker: Option<CacheMarker>,
634}
635
636impl MultimodalMessage {
637    /// Create a new multimodal user message
638    pub fn user(parts: Vec<MediaPart>) -> Self {
639        Self {
640            role: ChatRole::User,
641            parts,
642            cache_marker: None,
643        }
644    }
645
646    /// Create a user message with text and media
647    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    /// Create a user message with text and video
656    pub fn user_with_video(text: impl Into<String>, source: MediaSource) -> Self {
657        Self::user_with_media(text, MediaPart::video(source))
658    }
659
660    /// Create a user message with text and image
661    pub fn user_with_image(text: impl Into<String>, source: MediaSource) -> Self {
662        Self::user_with_media(text, MediaPart::image(source))
663    }
664
665    /// Create a user message with text and YouTube video
666    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    /// Add a cache marker
671    pub fn with_cache_marker(mut self, marker: CacheMarker) -> Self {
672        self.cache_marker = Some(marker);
673        self
674    }
675}
676
677// ============================================================================
678// Tool Types
679// ============================================================================
680
681/// Result of ToolCall::create() - handles single, multiple, or empty results
682#[derive(Debug, Clone, Serialize, Deserialize)]
683pub enum CreateResult {
684    /// Single tool call
685    Single(ToolCall),
686    /// Multiple tool calls
687    Multiple(Vec<ToolCall>),
688    /// Empty input (no tool calls)
689    Empty,
690}
691
692/// A tool call from the LLM
693#[derive(Debug, Clone, Serialize, Deserialize)]
694pub struct ToolCall {
695    /// Unique ID for this tool use
696    pub tool_use_id: String,
697
698    /// Name of the tool to call
699    pub tool_name: String,
700
701    /// Tool arguments (parsed JSON)
702    pub args: serde_json::Value,
703
704    /// Raw argument string (for debugging)
705    #[serde(skip_serializing_if = "Option::is_none")]
706    pub raw_arguments: Option<String>,
707
708    /// Provider signature for reasoning preservation
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub signature: Option<String>,
711
712    /// Original tool call object (for preserving provider format)
713    #[serde(skip_serializing_if = "Option::is_none")]
714    pub tool_obj: Option<serde_json::Value>,
715}
716
717impl ToolCall {
718    /// Create a new tool call
719    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    /// Create from provider-specific format with auto-detection
731    ///
732    /// Detects the provider format automatically based on the structure:
733    /// - OpenAI: has "function" key with "name" and "arguments"
734    /// - Anthropic: has "name" and "input" keys (no "function")
735    /// - Google: has "functionCall" key
736    /// - Deserializable object: has "_tool_name" attribute
737    /// - List: returns Vec of ToolCalls
738    pub fn create(data: serde_json::Value) -> Result<CreateResult> {
739        // Handle list input
740        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        // Detect format and parse
756        if data.get("function").is_some() {
757            // OpenAI format
758            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            // OpenAI/OpenRouter Responses format
767            Self::from_provider_format(data, Provider::OpenAIResponses).map(CreateResult::Single)
768        } else if data.get("functionCall").is_some() {
769            // Google format
770            Self::from_provider_format(data, Provider::GoogleGenAI).map(CreateResult::Single)
771        } else if data.get("name").is_some() && data.get("input").is_some() {
772            // Anthropic format
773            Self::from_provider_format(data, Provider::Anthropic).map(CreateResult::Single)
774        } else if data.get("_tool_name").is_some() {
775            // Deserializable object format
776            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            // Extract args by filtering out internal fields
796            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    /// Create from provider-specific format
819    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                // Handle both string (real API) and object (streaming accumulator) arguments
843                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                // Google format generates tool_use_id based on name + args hash (matches Python)
889                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                // Generate ID matching Python: google_{name}_{hash}
899                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    /// Convert to provider-specific assistant message format
919    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                // Use custom formatting to match Python's json.dumps (space after colon/comma)
934                let args_str = self.raw_arguments.clone().unwrap_or_else(|| {
935                    // Format to match Python's json.dumps with separators=(", ", ": ")
936                    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/// Result of a tool execution
978#[derive(Debug, Clone, Serialize, Deserialize)]
979pub struct ToolResult {
980    /// ID matching the original tool call
981    pub tool_use_id: String,
982
983    /// Name of the tool that was called (needed for Google API)
984    #[serde(skip_serializing_if = "Option::is_none")]
985    pub tool_name: Option<String>,
986
987    /// Result content (stringified)
988    pub content: String,
989
990    /// Whether the tool execution resulted in an error
991    pub is_error: bool,
992
993    /// Provider signature for reasoning preservation
994    #[serde(skip_serializing_if = "Option::is_none")]
995    pub signature: Option<String>,
996
997    /// Original tool call object for reference (as JSON)
998    #[serde(skip_serializing_if = "Option::is_none")]
999    pub tool_obj: Option<serde_json::Value>,
1000}
1001
1002impl ToolResult {
1003    /// Create a successful tool result
1004    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    /// Create a successful tool result with tool name (for Google API)
1016    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    /// Create an error tool result
1032    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    /// Set the tool name
1044    pub fn with_tool_name(mut self, name: impl Into<String>) -> Self {
1045        self.tool_name = Some(name.into());
1046        self
1047    }
1048
1049    /// Set the tool object (JSON representation)
1050    pub fn with_tool_obj(mut self, obj: serde_json::Value) -> Self {
1051        self.tool_obj = Some(obj);
1052        self
1053    }
1054
1055    /// Convert to provider-specific format
1056    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                // Try to get tool_name from: 1) tool_name field, 2) tool_obj["_tool_name"], 3) "unknown"
1082                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/// A reasoning segment (extended thinking)
1105#[derive(Debug, Clone, Serialize, Deserialize)]
1106pub struct ReasoningSegment {
1107    /// Reasoning content
1108    pub content: String,
1109
1110    /// Index in sequence of reasoning segments
1111    pub segment_index: usize,
1112
1113    /// Provider signature for reasoning preservation
1114    #[serde(skip_serializing_if = "Option::is_none")]
1115    pub signature: Option<String>,
1116
1117    /// Provider-specific metadata
1118    #[serde(skip_serializing_if = "Option::is_none")]
1119    pub provider_metadata: Option<serde_json::Value>,
1120}
1121
1122impl ReasoningSegment {
1123    /// Create a new reasoning segment with just content (index defaults to 0)
1124    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    /// Create a new reasoning segment with content and index
1134    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    /// Set the signature
1144    pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
1145        self.signature = Some(signature.into());
1146        self
1147    }
1148
1149    /// Set provider metadata
1150    pub fn with_provider_metadata(mut self, metadata: serde_json::Value) -> Self {
1151        self.provider_metadata = Some(metadata);
1152        self
1153    }
1154
1155    /// Set segment index
1156    pub fn with_segment_index(mut self, index: usize) -> Self {
1157        self.segment_index = index;
1158        self
1159    }
1160
1161    /// Convert to provider-specific format
1162    pub fn to_provider_format(&self, provider: Provider) -> serde_json::Value {
1163        match provider {
1164            // Anthropic, Bedrock, and GoogleAnthropic (Vertex AI Claude) all use Anthropic format
1165            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}