Skip to main content

serdes_ai_core/messages/
parts.rs

1//! Message part types for model responses.
2//!
3//! This module defines the individual parts that can appear in model responses,
4//! including text, tool calls, and thinking/reasoning content.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9/// Text content part.
10#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
11pub struct TextPart {
12    /// The text content.
13    pub content: String,
14    /// Optional unique identifier for this part.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub id: Option<String>,
17    /// Provider-specific details/metadata.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub provider_details: Option<serde_json::Map<String, serde_json::Value>>,
20}
21
22impl TextPart {
23    /// Part kind identifier.
24    pub const PART_KIND: &'static str = "text";
25
26    /// Create a new text part.
27    #[must_use]
28    pub fn new(content: impl Into<String>) -> Self {
29        Self {
30            content: content.into(),
31            id: None,
32            provider_details: None,
33        }
34    }
35
36    /// Set the part ID.
37    #[must_use]
38    pub fn with_id(mut self, id: impl Into<String>) -> Self {
39        self.id = Some(id.into());
40        self
41    }
42
43    /// Set provider-specific details.
44    #[must_use]
45    pub fn with_provider_details(
46        mut self,
47        details: serde_json::Map<String, serde_json::Value>,
48    ) -> Self {
49        self.provider_details = Some(details);
50        self
51    }
52
53    /// Get the part kind.
54    #[must_use]
55    pub fn part_kind(&self) -> &'static str {
56        Self::PART_KIND
57    }
58
59    /// Check if the content is empty.
60    #[must_use]
61    pub fn is_empty(&self) -> bool {
62        self.content.is_empty()
63    }
64
65    /// Get the content length.
66    #[must_use]
67    pub fn len(&self) -> usize {
68        self.content.len()
69    }
70}
71
72// Note: Eq is not derived because serde_json::Map doesn't implement Eq
73// (due to potential NaN values in JSON numbers). Use PartialEq for comparisons.
74
75impl From<String> for TextPart {
76    fn from(s: String) -> Self {
77        Self::new(s)
78    }
79}
80
81impl From<&str> for TextPart {
82    fn from(s: &str) -> Self {
83        Self::new(s)
84    }
85}
86
87/// Tool call arguments - can be either parsed JSON or raw string.
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89#[serde(untagged)]
90pub enum ToolCallArgs {
91    /// Parsed JSON arguments.
92    Json(serde_json::Value),
93    /// Raw string arguments (for streaming or parse failures).
94    String(String),
95}
96
97/// Attempt to repair common JSON malformations from LLM outputs.
98///
99/// This function tries various repairs on malformed JSON strings:
100/// - Removes trailing commas before `}` or `]`
101/// - Fixes unquoted keys: `{foo: "bar"}` -> `{"foo": "bar"}`
102/// - Replaces single quotes with double quotes
103/// - Closes unclosed braces/brackets
104///
105/// Returns `Some(Value)` if parsing succeeds (before or after repair),
106/// or `None` if the string cannot be salvaged.
107fn repair_json(s: &str) -> Option<serde_json::Value> {
108    let s = s.trim();
109
110    // Already valid JSON?
111    if let Ok(v) = serde_json::from_str::<serde_json::Value>(s) {
112        return Some(v);
113    }
114
115    let mut repaired = s.to_string();
116
117    // 1. Remove trailing commas before } or ]
118    repaired = remove_trailing_commas(&repaired);
119
120    // 2. Try to fix unquoted keys: {foo: "bar"} -> {"foo": "bar"}
121    repaired = quote_unquoted_keys(&repaired);
122
123    // 3. Replace single quotes with double quotes (only if no double quotes present)
124    if repaired.contains('\'') && !repaired.contains('"') {
125        repaired = repaired.replace('\'', "\"");
126    }
127
128    // 4. Try to close unclosed braces/brackets
129    let open_braces = repaired.matches('{').count();
130    let close_braces = repaired.matches('}').count();
131    if open_braces > close_braces {
132        repaired.push_str(&"}".repeat(open_braces - close_braces));
133    }
134
135    let open_brackets = repaired.matches('[').count();
136    let close_brackets = repaired.matches(']').count();
137    if open_brackets > close_brackets {
138        repaired.push_str(&"]".repeat(open_brackets - close_brackets));
139    }
140
141    // Try parsing the repaired string
142    serde_json::from_str(&repaired).ok()
143}
144
145/// Remove trailing commas before `}` or `]`.
146/// E.g., `{"a": 1,}` -> `{"a": 1}`
147fn remove_trailing_commas(s: &str) -> String {
148    let mut result = String::with_capacity(s.len());
149    let chars: Vec<char> = s.chars().collect();
150    let len = chars.len();
151
152    let mut i = 0;
153    while i < len {
154        let c = chars[i];
155        if c == ',' {
156            // Look ahead for whitespace followed by } or ]
157            let mut j = i + 1;
158            while j < len && chars[j].is_whitespace() {
159                j += 1;
160            }
161            if j < len && (chars[j] == '}' || chars[j] == ']') {
162                // Skip this comma
163                i += 1;
164                continue;
165            }
166        }
167        result.push(c);
168        i += 1;
169    }
170    result
171}
172
173/// Attempt to quote unquoted keys in JSON-like strings.
174/// E.g., `{foo: "bar"}` -> `{"foo": "bar"}`
175fn quote_unquoted_keys(s: &str) -> String {
176    let mut result = String::with_capacity(s.len() + 32);
177    let chars: Vec<char> = s.chars().collect();
178    let len = chars.len();
179
180    let mut i = 0;
181    while i < len {
182        let c = chars[i];
183
184        // After { or , we might have an unquoted key
185        if c == '{' || c == ',' {
186            result.push(c);
187            i += 1;
188
189            // Skip whitespace
190            while i < len && chars[i].is_whitespace() {
191                result.push(chars[i]);
192                i += 1;
193            }
194
195            // Check if we have an unquoted identifier followed by :
196            if i < len && is_ident_start(chars[i]) {
197                let key_start = i;
198                while i < len && is_ident_char(chars[i]) {
199                    i += 1;
200                }
201                let key = &s[key_start..i];
202
203                // Skip whitespace after key
204                while i < len && chars[i].is_whitespace() {
205                    i += 1;
206                }
207
208                // If followed by :, this was an unquoted key
209                if i < len && chars[i] == ':' {
210                    result.push('"');
211                    result.push_str(key);
212                    result.push('"');
213                } else {
214                    // Not a key, just push what we read
215                    result.push_str(key);
216                }
217            }
218        } else {
219            result.push(c);
220            i += 1;
221        }
222    }
223    result
224}
225
226/// Check if a character can start an identifier.
227#[inline]
228fn is_ident_start(c: char) -> bool {
229    c.is_ascii_alphabetic() || c == '_'
230}
231
232/// Check if a character can be part of an identifier.
233#[inline]
234fn is_ident_char(c: char) -> bool {
235    c.is_ascii_alphanumeric() || c == '_'
236}
237
238impl ToolCallArgs {
239    /// Create from JSON value.
240    #[must_use]
241    pub fn json(value: serde_json::Value) -> Self {
242        Self::Json(value)
243    }
244
245    /// Create from string.
246    #[must_use]
247    pub fn string(s: impl Into<String>) -> Self {
248        Self::String(s.into())
249    }
250
251    /// Try to get as JSON object.
252    #[must_use]
253    pub fn as_object(&self) -> Option<serde_json::Map<String, serde_json::Value>> {
254        match self {
255            Self::Json(serde_json::Value::Object(obj)) => Some(obj.clone()),
256            Self::String(s) => {
257                serde_json::from_str::<serde_json::Value>(s)
258                    .ok()
259                    .and_then(|value| match value {
260                        serde_json::Value::Object(map) => Some(map),
261                        _ => None,
262                    })
263            }
264            _ => None,
265        }
266    }
267
268    /// Convert to JSON value, guaranteeing a valid JSON object.
269    ///
270    /// This method ensures the result is always a JSON object (dictionary),
271    /// which is required by APIs like Anthropic's `tool_use.input` field.
272    ///
273    /// # Behavior
274    ///
275    /// - If already a JSON object, returns it as-is
276    /// - If a JSON array or primitive, wraps in `{"_value": ...}`
277    /// - If a string, attempts to parse and repair malformed JSON
278    /// - If all parsing fails, returns `{"_raw": "<original>", "_error": "parse_failed"}`
279    #[must_use]
280    pub fn to_json(&self) -> serde_json::Value {
281        match self {
282            Self::Json(v) => {
283                if v.is_object() {
284                    v.clone()
285                } else {
286                    // Wrap non-objects
287                    serde_json::json!({ "_value": v })
288                }
289            }
290            Self::String(s) => {
291                // Try direct parse first
292                if let Ok(v) = serde_json::from_str::<serde_json::Value>(s) {
293                    if v.is_object() {
294                        return v;
295                    }
296                    return serde_json::json!({ "_value": v });
297                }
298
299                // Try to repair malformed JSON
300                if let Some(v) = repair_json(s) {
301                    if v.is_object() {
302                        return v;
303                    }
304                    return serde_json::json!({ "_value": v });
305                }
306
307                // All parsing failed - wrap the raw string
308                serde_json::json!({
309                    "_raw": s,
310                    "_error": "parse_failed"
311                })
312            }
313        }
314    }
315
316    /// Convert to a JSON object map, guaranteed.
317    ///
318    /// Similar to [`to_json()`](Self::to_json) but returns the inner `Map` directly.
319    /// This is useful when you need to work with the map directly.
320    #[must_use]
321    pub fn to_json_object(&self) -> serde_json::Map<String, serde_json::Value> {
322        match self.to_json() {
323            serde_json::Value::Object(map) => map,
324            // SAFETY: to_json() guarantees an object, so this is unreachable
325            _ => unreachable!("to_json() guarantees an object"),
326        }
327    }
328
329    /// Convert to JSON string.
330    ///
331    /// # Errors
332    ///
333    /// Returns an error if serialization fails.
334    pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
335        match self {
336            Self::Json(v) => serde_json::to_string(v),
337            Self::String(s) => Ok(s.clone()),
338        }
339    }
340
341    /// Check if this is valid JSON.
342    #[must_use]
343    pub fn is_valid_json(&self) -> bool {
344        match self {
345            Self::Json(_) => true,
346            Self::String(s) => serde_json::from_str::<serde_json::Value>(s).is_ok(),
347        }
348    }
349}
350
351impl Default for ToolCallArgs {
352    fn default() -> Self {
353        Self::Json(serde_json::Value::Object(serde_json::Map::new()))
354    }
355}
356
357impl From<serde_json::Value> for ToolCallArgs {
358    fn from(v: serde_json::Value) -> Self {
359        Self::Json(v)
360    }
361}
362
363impl From<String> for ToolCallArgs {
364    fn from(s: String) -> Self {
365        // Try to parse as JSON first
366        match serde_json::from_str(&s) {
367            Ok(v) => Self::Json(v),
368            Err(_) => Self::String(s),
369        }
370    }
371}
372
373impl From<&str> for ToolCallArgs {
374    fn from(s: &str) -> Self {
375        Self::from(s.to_string())
376    }
377}
378
379/// Tool call part.
380#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
381pub struct ToolCallPart {
382    /// Name of the tool being called.
383    pub tool_name: String,
384    /// Arguments for the tool call.
385    pub args: ToolCallArgs,
386    /// Unique identifier for this tool call (provider-assigned).
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub tool_call_id: Option<String>,
389    /// Optional unique identifier for this message part.
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub id: Option<String>,
392    /// Provider-specific details/metadata.
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub provider_details: Option<serde_json::Map<String, serde_json::Value>>,
395}
396
397impl ToolCallPart {
398    /// Part kind identifier.
399    pub const PART_KIND: &'static str = "tool-call";
400
401    /// Create a new tool call part.
402    #[must_use]
403    pub fn new(tool_name: impl Into<String>, args: impl Into<ToolCallArgs>) -> Self {
404        Self {
405            tool_name: tool_name.into(),
406            args: args.into(),
407            tool_call_id: None,
408            id: None,
409            provider_details: None,
410        }
411    }
412
413    /// Get the part kind.
414    #[must_use]
415    pub fn part_kind(&self) -> &'static str {
416        Self::PART_KIND
417    }
418
419    /// Set the tool call ID (provider-assigned identifier for the tool call).
420    #[must_use]
421    pub fn with_tool_call_id(mut self, id: impl Into<String>) -> Self {
422        self.tool_call_id = Some(id.into());
423        self
424    }
425
426    /// Alias for `with_tool_call_id` - kept for backward compatibility.
427    #[must_use]
428    #[deprecated(since = "0.2.0", note = "Use with_tool_call_id() instead for clarity")]
429    pub fn with_id(mut self, id: impl Into<String>) -> Self {
430        self.tool_call_id = Some(id.into());
431        self
432    }
433
434    /// Set the part ID (unique identifier for this message part).
435    #[must_use]
436    pub fn with_part_id(mut self, id: impl Into<String>) -> Self {
437        self.id = Some(id.into());
438        self
439    }
440
441    /// Set provider-specific details.
442    #[must_use]
443    pub fn with_provider_details(
444        mut self,
445        details: serde_json::Map<String, serde_json::Value>,
446    ) -> Self {
447        self.provider_details = Some(details);
448        self
449    }
450
451    /// Get arguments as a dictionary/object.
452    #[must_use]
453    pub fn args_as_dict(&self) -> serde_json::Value {
454        self.args.to_json()
455    }
456
457    /// Get arguments as JSON string.
458    ///
459    /// # Errors
460    ///
461    /// Returns an error if serialization fails.
462    pub fn args_as_json_str(&self) -> Result<String, serde_json::Error> {
463        self.args.to_json_string()
464    }
465
466    /// Try to deserialize arguments into a typed struct.
467    pub fn parse_args<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
468        let json = self.args.to_json();
469        serde_json::from_value(json)
470    }
471}
472
473/// Thinking/reasoning content part.
474///
475/// Used for models that support "thinking" or chain-of-thought reasoning,
476/// like Claude's extended thinking feature.
477#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
478pub struct ThinkingPart {
479    /// The thinking content.
480    pub content: String,
481    /// Optional unique identifier for this part.
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub id: Option<String>,
484    /// Optional signature for verification.
485    #[serde(skip_serializing_if = "Option::is_none")]
486    pub signature: Option<String>,
487    /// Provider name that generated this thinking content.
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub provider_name: Option<String>,
490    /// Provider-specific details/metadata.
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub provider_details: Option<serde_json::Map<String, serde_json::Value>>,
493}
494
495impl ThinkingPart {
496    /// Part kind identifier.
497    pub const PART_KIND: &'static str = "thinking";
498
499    /// ID used for Anthropic redacted thinking blocks.
500    pub const REDACTED_THINKING_ID: &'static str = "redacted_thinking";
501
502    /// ID used for Bedrock redacted content blocks.
503    pub const REDACTED_CONTENT_ID: &'static str = "redacted_content";
504
505    /// Create a new thinking part.
506    #[must_use]
507    pub fn new(content: impl Into<String>) -> Self {
508        Self {
509            content: content.into(),
510            id: None,
511            signature: None,
512            provider_name: None,
513            provider_details: None,
514        }
515    }
516
517    /// Create a redacted thinking part.
518    ///
519    /// Redacted thinking blocks contain encrypted/hidden content. The signature
520    /// must be preserved and sent back to the API in subsequent requests.
521    ///
522    /// # Arguments
523    /// * `signature` - The encrypted/encoded data from the provider
524    /// * `provider_name` - The provider that generated this (e.g., "anthropic", "bedrock")
525    #[must_use]
526    pub fn redacted(signature: impl Into<String>, provider_name: impl Into<String>) -> Self {
527        let provider = provider_name.into();
528        let id = if provider.contains("bedrock") {
529            Self::REDACTED_CONTENT_ID
530        } else {
531            Self::REDACTED_THINKING_ID
532        };
533        Self {
534            content: String::new(),
535            id: Some(id.to_string()),
536            signature: Some(signature.into()),
537            provider_name: Some(provider),
538            provider_details: None,
539        }
540    }
541
542    /// Create a redacted thinking part with a specific ID.
543    #[must_use]
544    pub fn redacted_with_id(
545        id: impl Into<String>,
546        signature: impl Into<String>,
547        provider_name: impl Into<String>,
548    ) -> Self {
549        Self {
550            content: String::new(),
551            id: Some(id.into()),
552            signature: Some(signature.into()),
553            provider_name: Some(provider_name.into()),
554            provider_details: None,
555        }
556    }
557
558    /// Get the part kind.
559    #[must_use]
560    pub fn part_kind(&self) -> &'static str {
561        Self::PART_KIND
562    }
563
564    /// Set the part ID.
565    #[must_use]
566    pub fn with_id(mut self, id: impl Into<String>) -> Self {
567        self.id = Some(id.into());
568        self
569    }
570
571    /// Set the signature.
572    #[must_use]
573    pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
574        self.signature = Some(signature.into());
575        self
576    }
577
578    /// Set the provider name.
579    #[must_use]
580    pub fn with_provider_name(mut self, name: impl Into<String>) -> Self {
581        self.provider_name = Some(name.into());
582        self
583    }
584
585    /// Set provider-specific details.
586    #[must_use]
587    pub fn with_provider_details(
588        mut self,
589        details: serde_json::Map<String, serde_json::Value>,
590    ) -> Self {
591        self.provider_details = Some(details);
592        self
593    }
594
595    /// Check if the content is empty.
596    #[must_use]
597    pub fn is_empty(&self) -> bool {
598        self.content.is_empty()
599    }
600
601    /// Check if this is a redacted thinking block.
602    ///
603    /// Redacted blocks have their content hidden/encrypted by the provider.
604    /// The `signature` field contains the encrypted data that must be
605    /// round-tripped back to the API.
606    #[must_use]
607    pub fn is_redacted(&self) -> bool {
608        self.id
609            .as_ref()
610            .map(|id| id.starts_with("redacted"))
611            .unwrap_or(false)
612    }
613
614    /// Get the signature if this is a redacted block.
615    #[must_use]
616    pub fn redacted_signature(&self) -> Option<&str> {
617        if self.is_redacted() {
618            self.signature.as_deref()
619        } else {
620            None
621        }
622    }
623}
624
625/// Binary content container for file data.
626///
627/// Encapsulates raw binary data along with its MIME type for type-safe
628/// handling of file content like images, audio, or documents.
629#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
630pub struct BinaryContent {
631    /// The raw binary data.
632    #[serde(with = "base64_serde")]
633    pub data: Vec<u8>,
634    /// The MIME type of the content (e.g., "image/png", "audio/wav").
635    pub media_type: String,
636}
637
638impl BinaryContent {
639    /// Create new binary content.
640    #[must_use]
641    pub fn new(data: Vec<u8>, media_type: impl Into<String>) -> Self {
642        Self {
643            data,
644            media_type: media_type.into(),
645        }
646    }
647
648    /// Check if the content is empty.
649    #[must_use]
650    pub fn is_empty(&self) -> bool {
651        self.data.is_empty()
652    }
653
654    /// Get the length of the binary data.
655    #[must_use]
656    pub fn len(&self) -> usize {
657        self.data.len()
658    }
659}
660
661/// Custom serde module for base64 encoding/decoding of binary data.
662mod base64_serde {
663    use base64::{engine::general_purpose::STANDARD, Engine};
664    use serde::{Deserialize, Deserializer, Serializer};
665
666    pub fn serialize<S>(data: &[u8], serializer: S) -> Result<S::Ok, S::Error>
667    where
668        S: Serializer,
669    {
670        serializer.serialize_str(&STANDARD.encode(data))
671    }
672
673    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
674    where
675        D: Deserializer<'de>,
676    {
677        let s = String::deserialize(deserializer)?;
678        STANDARD.decode(&s).map_err(serde::de::Error::custom)
679    }
680}
681
682/// A file response from a model.
683///
684/// Represents file content generated by models, such as images from
685/// image generation APIs. This is the Rust equivalent of pydantic-ai's
686/// `FilePart` type.
687#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
688pub struct FilePart {
689    /// The binary content of the file.
690    pub content: BinaryContent,
691    /// Optional unique identifier for this part.
692    #[serde(skip_serializing_if = "Option::is_none")]
693    pub id: Option<String>,
694    /// Provider name that generated this file.
695    #[serde(skip_serializing_if = "Option::is_none")]
696    pub provider_name: Option<String>,
697    /// Provider-specific details/metadata.
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub provider_details: Option<serde_json::Map<String, serde_json::Value>>,
700}
701
702impl FilePart {
703    /// Part kind identifier.
704    pub const PART_KIND: &'static str = "file";
705
706    /// Create a new file part.
707    #[must_use]
708    pub fn new(content: BinaryContent) -> Self {
709        Self {
710            content,
711            id: None,
712            provider_name: None,
713            provider_details: None,
714        }
715    }
716
717    /// Create a new file part from raw data and media type.
718    #[must_use]
719    pub fn from_bytes(data: Vec<u8>, media_type: impl Into<String>) -> Self {
720        Self::new(BinaryContent::new(data, media_type))
721    }
722
723    /// Get the part kind.
724    #[must_use]
725    pub fn part_kind(&self) -> &'static str {
726        Self::PART_KIND
727    }
728
729    /// Set the part ID.
730    #[must_use]
731    pub fn with_id(mut self, id: impl Into<String>) -> Self {
732        self.id = Some(id.into());
733        self
734    }
735
736    /// Set the provider name.
737    #[must_use]
738    pub fn with_provider_name(mut self, name: impl Into<String>) -> Self {
739        self.provider_name = Some(name.into());
740        self
741    }
742
743    /// Set provider-specific details.
744    #[must_use]
745    pub fn with_provider_details(
746        mut self,
747        details: serde_json::Map<String, serde_json::Value>,
748    ) -> Self {
749        self.provider_details = Some(details);
750        self
751    }
752
753    /// Check if the file has content.
754    #[must_use]
755    pub fn has_content(&self) -> bool {
756        !self.content.is_empty()
757    }
758
759    /// Get the media type of the file content.
760    #[must_use]
761    pub fn media_type(&self) -> &str {
762        &self.content.media_type
763    }
764
765    /// Get the raw binary data.
766    #[must_use]
767    pub fn data(&self) -> &[u8] {
768        &self.content.data
769    }
770
771    /// Get the size of the file in bytes.
772    #[must_use]
773    pub fn size(&self) -> usize {
774        self.content.len()
775    }
776}
777
778/// A builtin tool call from a model (web search, code execution, etc.).
779///
780/// Builtin tools are provider-native capabilities like web search, code execution,
781/// and file search. They are distinct from user-defined function tools and require
782/// special handling for UI rendering and history management.
783#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
784pub struct BuiltinToolCallPart {
785    /// Name of the builtin tool being called (e.g., "web_search", "code_execution").
786    pub tool_name: String,
787    /// Arguments for the tool call.
788    pub args: ToolCallArgs,
789    /// Unique identifier for this tool call (provider-assigned).
790    #[serde(skip_serializing_if = "Option::is_none")]
791    pub tool_call_id: Option<String>,
792    /// Optional unique identifier for this message part.
793    #[serde(skip_serializing_if = "Option::is_none")]
794    pub id: Option<String>,
795    /// Provider-specific details/metadata.
796    #[serde(skip_serializing_if = "Option::is_none")]
797    pub provider_details: Option<serde_json::Map<String, serde_json::Value>>,
798}
799
800impl BuiltinToolCallPart {
801    /// Part kind identifier.
802    pub const PART_KIND: &'static str = "builtin-tool-call";
803
804    /// Create a new builtin tool call part.
805    #[must_use]
806    pub fn new(tool_name: impl Into<String>, args: impl Into<ToolCallArgs>) -> Self {
807        Self {
808            tool_name: tool_name.into(),
809            args: args.into(),
810            tool_call_id: None,
811            id: None,
812            provider_details: None,
813        }
814    }
815
816    /// Get the part kind.
817    #[must_use]
818    pub fn part_kind(&self) -> &'static str {
819        Self::PART_KIND
820    }
821
822    /// Set the tool call ID (provider-assigned identifier for the tool call).
823    #[must_use]
824    pub fn with_tool_call_id(mut self, id: impl Into<String>) -> Self {
825        self.tool_call_id = Some(id.into());
826        self
827    }
828
829    /// Set the part ID (unique identifier for this message part).
830    #[must_use]
831    pub fn with_part_id(mut self, id: impl Into<String>) -> Self {
832        self.id = Some(id.into());
833        self
834    }
835
836    /// Set provider-specific details.
837    #[must_use]
838    pub fn with_provider_details(
839        mut self,
840        details: serde_json::Map<String, serde_json::Value>,
841    ) -> Self {
842        self.provider_details = Some(details);
843        self
844    }
845
846    /// Get arguments as a dictionary/object.
847    #[must_use]
848    pub fn args_as_dict(&self) -> serde_json::Value {
849        self.args.to_json()
850    }
851
852    /// Get arguments as JSON string.
853    ///
854    /// # Errors
855    ///
856    /// Returns an error if serialization fails.
857    pub fn args_as_json_str(&self) -> Result<String, serde_json::Error> {
858        self.args.to_json_string()
859    }
860
861    /// Try to deserialize arguments into a typed struct.
862    pub fn parse_args<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
863        let json = self.args.to_json();
864        serde_json::from_value(json)
865    }
866}
867
868/// A single web search result.
869#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
870pub struct WebSearchResult {
871    /// The title of the search result.
872    pub title: String,
873    /// The URL of the search result.
874    pub url: String,
875    /// A snippet/summary of the content.
876    #[serde(skip_serializing_if = "Option::is_none")]
877    pub snippet: Option<String>,
878    /// The full content if available.
879    #[serde(skip_serializing_if = "Option::is_none")]
880    pub content: Option<String>,
881}
882
883impl WebSearchResult {
884    /// Create a new web search result.
885    #[must_use]
886    pub fn new(title: impl Into<String>, url: impl Into<String>) -> Self {
887        Self {
888            title: title.into(),
889            url: url.into(),
890            snippet: None,
891            content: None,
892        }
893    }
894
895    /// Set the snippet.
896    #[must_use]
897    pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
898        self.snippet = Some(snippet.into());
899        self
900    }
901
902    /// Set the full content.
903    #[must_use]
904    pub fn with_content(mut self, content: impl Into<String>) -> Self {
905        self.content = Some(content.into());
906        self
907    }
908}
909
910/// Web search results from a builtin search tool.
911#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
912pub struct WebSearchResults {
913    /// The search query that was executed.
914    pub query: String,
915    /// The list of search results.
916    pub results: Vec<WebSearchResult>,
917    /// Total number of results (may be more than returned).
918    #[serde(skip_serializing_if = "Option::is_none")]
919    pub total_results: Option<u64>,
920}
921
922impl WebSearchResults {
923    /// Create new web search results.
924    #[must_use]
925    pub fn new(query: impl Into<String>, results: Vec<WebSearchResult>) -> Self {
926        Self {
927            query: query.into(),
928            results,
929            total_results: None,
930        }
931    }
932
933    /// Set the total results count.
934    #[must_use]
935    pub fn with_total_results(mut self, total: u64) -> Self {
936        self.total_results = Some(total);
937        self
938    }
939
940    /// Check if there are any results.
941    #[must_use]
942    pub fn is_empty(&self) -> bool {
943        self.results.is_empty()
944    }
945
946    /// Get the number of results.
947    #[must_use]
948    pub fn len(&self) -> usize {
949        self.results.len()
950    }
951}
952
953/// Result from code execution.
954#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
955pub struct CodeExecutionResult {
956    /// The code that was executed.
957    pub code: String,
958    /// Standard output from the execution.
959    #[serde(skip_serializing_if = "Option::is_none")]
960    pub stdout: Option<String>,
961    /// Standard error from the execution.
962    #[serde(skip_serializing_if = "Option::is_none")]
963    pub stderr: Option<String>,
964    /// Exit code from the execution.
965    #[serde(skip_serializing_if = "Option::is_none")]
966    pub exit_code: Option<i32>,
967    /// Any images/files generated by the code.
968    #[serde(skip_serializing_if = "Vec::is_empty", default)]
969    pub output_files: Vec<BinaryContent>,
970    /// Error message if execution failed.
971    #[serde(skip_serializing_if = "Option::is_none")]
972    pub error: Option<String>,
973}
974
975impl CodeExecutionResult {
976    /// Create a new code execution result.
977    #[must_use]
978    pub fn new(code: impl Into<String>) -> Self {
979        Self {
980            code: code.into(),
981            stdout: None,
982            stderr: None,
983            exit_code: None,
984            output_files: Vec::new(),
985            error: None,
986        }
987    }
988
989    /// Set the stdout.
990    #[must_use]
991    pub fn with_stdout(mut self, stdout: impl Into<String>) -> Self {
992        self.stdout = Some(stdout.into());
993        self
994    }
995
996    /// Set the stderr.
997    #[must_use]
998    pub fn with_stderr(mut self, stderr: impl Into<String>) -> Self {
999        self.stderr = Some(stderr.into());
1000        self
1001    }
1002
1003    /// Set the exit code.
1004    #[must_use]
1005    pub fn with_exit_code(mut self, code: i32) -> Self {
1006        self.exit_code = Some(code);
1007        self
1008    }
1009
1010    /// Add an output file.
1011    #[must_use]
1012    pub fn with_output_file(mut self, file: BinaryContent) -> Self {
1013        self.output_files.push(file);
1014        self
1015    }
1016
1017    /// Set the error message.
1018    #[must_use]
1019    pub fn with_error(mut self, error: impl Into<String>) -> Self {
1020        self.error = Some(error.into());
1021        self
1022    }
1023
1024    /// Check if execution succeeded (exit_code == 0 and no error).
1025    #[must_use]
1026    pub fn is_success(&self) -> bool {
1027        self.error.is_none() && self.exit_code.map_or(true, |c| c == 0)
1028    }
1029}
1030
1031/// A single file search result.
1032#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1033pub struct FileSearchResult {
1034    /// The file name or path.
1035    pub file_name: String,
1036    /// The matched content or snippet.
1037    pub content: String,
1038    /// Relevance score if available.
1039    #[serde(skip_serializing_if = "Option::is_none")]
1040    pub score: Option<f64>,
1041    /// File metadata.
1042    #[serde(skip_serializing_if = "Option::is_none")]
1043    pub metadata: Option<serde_json::Map<String, serde_json::Value>>,
1044}
1045
1046impl FileSearchResult {
1047    /// Create a new file search result.
1048    #[must_use]
1049    pub fn new(file_name: impl Into<String>, content: impl Into<String>) -> Self {
1050        Self {
1051            file_name: file_name.into(),
1052            content: content.into(),
1053            score: None,
1054            metadata: None,
1055        }
1056    }
1057
1058    /// Set the relevance score.
1059    #[must_use]
1060    pub fn with_score(mut self, score: f64) -> Self {
1061        self.score = Some(score);
1062        self
1063    }
1064
1065    /// Set file metadata.
1066    #[must_use]
1067    pub fn with_metadata(mut self, metadata: serde_json::Map<String, serde_json::Value>) -> Self {
1068        self.metadata = Some(metadata);
1069        self
1070    }
1071}
1072
1073/// File search results from a builtin file search tool.
1074#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1075pub struct FileSearchResults {
1076    /// The search query that was executed.
1077    pub query: String,
1078    /// The list of file search results.
1079    pub results: Vec<FileSearchResult>,
1080}
1081
1082impl FileSearchResults {
1083    /// Create new file search results.
1084    #[must_use]
1085    pub fn new(query: impl Into<String>, results: Vec<FileSearchResult>) -> Self {
1086        Self {
1087            query: query.into(),
1088            results,
1089        }
1090    }
1091
1092    /// Check if there are any results.
1093    #[must_use]
1094    pub fn is_empty(&self) -> bool {
1095        self.results.is_empty()
1096    }
1097
1098    /// Get the number of results.
1099    #[must_use]
1100    pub fn len(&self) -> usize {
1101        self.results.len()
1102    }
1103}
1104
1105/// Content returned from builtin tools.
1106///
1107/// This enum represents the different types of structured content that can be
1108/// returned from builtin tools like web search, code execution, and file search.
1109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1110#[serde(tag = "type", rename_all = "snake_case")]
1111pub enum BuiltinToolReturnContent {
1112    /// Web search results.
1113    WebSearch(WebSearchResults),
1114    /// Code execution result.
1115    CodeExecution(CodeExecutionResult),
1116    /// File search results.
1117    FileSearch(FileSearchResults),
1118    /// Generic/other structured content for provider-specific results.
1119    Other {
1120        /// The type identifier for this content.
1121        kind: String,
1122        /// The content data.
1123        data: serde_json::Value,
1124    },
1125}
1126
1127impl BuiltinToolReturnContent {
1128    /// Create web search content.
1129    #[must_use]
1130    pub fn web_search(results: WebSearchResults) -> Self {
1131        Self::WebSearch(results)
1132    }
1133
1134    /// Create code execution content.
1135    #[must_use]
1136    pub fn code_execution(result: CodeExecutionResult) -> Self {
1137        Self::CodeExecution(result)
1138    }
1139
1140    /// Create file search content.
1141    #[must_use]
1142    pub fn file_search(results: FileSearchResults) -> Self {
1143        Self::FileSearch(results)
1144    }
1145
1146    /// Create other/generic content.
1147    #[must_use]
1148    pub fn other(kind: impl Into<String>, data: serde_json::Value) -> Self {
1149        Self::Other {
1150            kind: kind.into(),
1151            data,
1152        }
1153    }
1154
1155    /// Get the content type name.
1156    #[must_use]
1157    pub fn content_type(&self) -> &str {
1158        match self {
1159            Self::WebSearch(_) => "web_search",
1160            Self::CodeExecution(_) => "code_execution",
1161            Self::FileSearch(_) => "file_search",
1162            Self::Other { kind, .. } => kind,
1163        }
1164    }
1165}
1166
1167/// Return from a builtin tool with structured content.
1168///
1169/// This part represents the result of a builtin tool execution, containing
1170/// structured content appropriate for the tool type (search results, code output, etc.).
1171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1172pub struct BuiltinToolReturnPart {
1173    /// Name of the builtin tool that was called.
1174    pub tool_name: String,
1175    /// The structured content returned by the tool.
1176    pub content: BuiltinToolReturnContent,
1177    /// ID of the tool call this is responding to.
1178    pub tool_call_id: String,
1179    /// When this return was generated.
1180    pub timestamp: DateTime<Utc>,
1181    /// Optional unique identifier for this part.
1182    #[serde(skip_serializing_if = "Option::is_none")]
1183    pub id: Option<String>,
1184    /// Provider-specific details/metadata.
1185    #[serde(skip_serializing_if = "Option::is_none")]
1186    pub provider_details: Option<serde_json::Map<String, serde_json::Value>>,
1187}
1188
1189impl BuiltinToolReturnPart {
1190    /// Part kind identifier.
1191    pub const PART_KIND: &'static str = "builtin-tool-return";
1192
1193    /// Create a new builtin tool return part.
1194    #[must_use]
1195    pub fn new(
1196        tool_name: impl Into<String>,
1197        content: BuiltinToolReturnContent,
1198        tool_call_id: impl Into<String>,
1199    ) -> Self {
1200        Self {
1201            tool_name: tool_name.into(),
1202            content,
1203            tool_call_id: tool_call_id.into(),
1204            timestamp: Utc::now(),
1205            id: None,
1206            provider_details: None,
1207        }
1208    }
1209
1210    /// Get the part kind.
1211    #[must_use]
1212    pub fn part_kind(&self) -> &'static str {
1213        Self::PART_KIND
1214    }
1215
1216    /// Set the timestamp.
1217    #[must_use]
1218    pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
1219        self.timestamp = timestamp;
1220        self
1221    }
1222
1223    /// Set the part ID.
1224    #[must_use]
1225    pub fn with_id(mut self, id: impl Into<String>) -> Self {
1226        self.id = Some(id.into());
1227        self
1228    }
1229
1230    /// Set provider-specific details.
1231    #[must_use]
1232    pub fn with_provider_details(
1233        mut self,
1234        details: serde_json::Map<String, serde_json::Value>,
1235    ) -> Self {
1236        self.provider_details = Some(details);
1237        self
1238    }
1239
1240    /// Get the content type of the return.
1241    #[must_use]
1242    pub fn content_type(&self) -> &str {
1243        self.content.content_type()
1244    }
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249    use super::*;
1250
1251    #[test]
1252    fn test_text_part() {
1253        let part = TextPart::new("Hello, world!");
1254        assert_eq!(part.content, "Hello, world!");
1255        assert_eq!(part.part_kind(), "text");
1256        assert!(!part.is_empty());
1257        assert_eq!(part.len(), 13);
1258        assert!(part.id.is_none());
1259        assert!(part.provider_details.is_none());
1260    }
1261
1262    #[test]
1263    fn test_text_part_with_builders() {
1264        let mut details = serde_json::Map::new();
1265        details.insert("model".to_string(), serde_json::json!("gpt-4"));
1266
1267        let part = TextPart::new("Hello!")
1268            .with_id("part-123")
1269            .with_provider_details(details.clone());
1270
1271        assert_eq!(part.id, Some("part-123".to_string()));
1272        assert_eq!(part.provider_details, Some(details));
1273    }
1274
1275    #[test]
1276    fn test_tool_call_args_from_json() {
1277        let args = ToolCallArgs::json(serde_json::json!({"location": "NYC"}));
1278        assert!(args.is_valid_json());
1279        assert_eq!(args.to_json_string().unwrap(), r#"{"location":"NYC"}"#);
1280    }
1281
1282    #[test]
1283    fn test_tool_call_args_from_string() {
1284        let args: ToolCallArgs = r#"{"x": 1}"#.into();
1285        assert!(args.is_valid_json());
1286        // Should parse into Json variant
1287        if let ToolCallArgs::Json(v) = &args {
1288            assert_eq!(v["x"], 1);
1289        } else {
1290            panic!("Expected Json variant");
1291        }
1292    }
1293
1294    // ==================== JSON Repair Tests ====================
1295
1296    #[test]
1297    fn test_to_json_always_returns_object() {
1298        // Valid JSON object should pass through
1299        let args = ToolCallArgs::json(serde_json::json!({"foo": "bar"}));
1300        let result = args.to_json();
1301        assert!(result.is_object());
1302        assert_eq!(result["foo"], "bar");
1303    }
1304
1305    #[test]
1306    fn test_to_json_wraps_array() {
1307        // Arrays should be wrapped in {"_value": ...}
1308        let args = ToolCallArgs::json(serde_json::json!([1, 2, 3]));
1309        let result = args.to_json();
1310        assert!(result.is_object());
1311        assert_eq!(result["_value"], serde_json::json!([1, 2, 3]));
1312    }
1313
1314    #[test]
1315    fn test_to_json_wraps_primitive() {
1316        // Primitives should be wrapped in {"_value": ...}
1317        let args = ToolCallArgs::json(serde_json::json!(42));
1318        let result = args.to_json();
1319        assert!(result.is_object());
1320        assert_eq!(result["_value"], 42);
1321
1322        let args = ToolCallArgs::json(serde_json::json!("hello"));
1323        let result = args.to_json();
1324        assert!(result.is_object());
1325        assert_eq!(result["_value"], "hello");
1326    }
1327
1328    #[test]
1329    fn test_to_json_repairs_trailing_comma() {
1330        // Trailing commas should be repaired
1331        let args = ToolCallArgs::string(r#"{"a": 1,}"#.to_string());
1332        let result = args.to_json();
1333        assert!(result.is_object());
1334        assert_eq!(result["a"], 1);
1335    }
1336
1337    #[test]
1338    fn test_to_json_repairs_unquoted_keys() {
1339        // Unquoted keys should be repaired
1340        let args = ToolCallArgs::string(r#"{foo: "bar"}"#.to_string());
1341        let result = args.to_json();
1342        assert!(result.is_object());
1343        assert_eq!(result["foo"], "bar");
1344    }
1345
1346    #[test]
1347    fn test_to_json_repairs_single_quotes() {
1348        // Single quotes should be converted to double quotes
1349        let args = ToolCallArgs::string("{'a': 'b'}".to_string());
1350        let result = args.to_json();
1351        assert!(result.is_object());
1352        assert_eq!(result["a"], "b");
1353    }
1354
1355    #[test]
1356    fn test_to_json_repairs_unclosed_braces() {
1357        // Unclosed braces should be closed
1358        let args = ToolCallArgs::string(r#"{"x": 1"#.to_string());
1359        let result = args.to_json();
1360        assert!(result.is_object());
1361        assert_eq!(result["x"], 1);
1362    }
1363
1364    #[test]
1365    fn test_to_json_handles_completely_invalid() {
1366        // Completely invalid JSON should return error object
1367        let args = ToolCallArgs::string("this is not json at all".to_string());
1368        let result = args.to_json();
1369        assert!(result.is_object());
1370        assert_eq!(result["_error"], "parse_failed");
1371        assert_eq!(result["_raw"], "this is not json at all");
1372    }
1373
1374    #[test]
1375    fn test_to_json_object_returns_map() {
1376        let args = ToolCallArgs::json(serde_json::json!({"key": "value"}));
1377        let map = args.to_json_object();
1378        assert_eq!(map.get("key"), Some(&serde_json::json!("value")));
1379    }
1380
1381    #[test]
1382    fn test_repair_json_valid_passthrough() {
1383        // Already valid JSON should pass through
1384        let result = repair_json(r#"{"valid": true}"#);
1385        assert!(result.is_some());
1386        assert_eq!(result.unwrap()["valid"], true);
1387    }
1388
1389    #[test]
1390    fn test_repair_json_nested_trailing_comma() {
1391        let result = repair_json(r#"{"outer": {"inner": 1,},}"#);
1392        assert!(result.is_some());
1393        let v = result.unwrap();
1394        assert_eq!(v["outer"]["inner"], 1);
1395    }
1396
1397    #[test]
1398    fn test_repair_json_array_trailing_comma() {
1399        let result = repair_json(r#"[1, 2, 3,]"#);
1400        assert!(result.is_some());
1401        assert_eq!(result.unwrap(), serde_json::json!([1, 2, 3]));
1402    }
1403
1404    #[test]
1405    fn test_repair_json_multiple_unquoted_keys() {
1406        let result = repair_json(r#"{foo: 1, bar: 2}"#);
1407        assert!(result.is_some());
1408        let v = result.unwrap();
1409        assert_eq!(v["foo"], 1);
1410        assert_eq!(v["bar"], 2);
1411    }
1412
1413    #[test]
1414    fn test_remove_trailing_commas_helper() {
1415        assert_eq!(remove_trailing_commas("{\"a\": 1,}"), "{\"a\": 1}");
1416        assert_eq!(remove_trailing_commas("[1, 2,]"), "[1, 2]");
1417        assert_eq!(remove_trailing_commas("{\"a\": 1,  }"), "{\"a\": 1  }");
1418    }
1419
1420    #[test]
1421    fn test_quote_unquoted_keys_helper() {
1422        assert_eq!(quote_unquoted_keys("{foo: 1}"), "{\"foo\": 1}");
1423        assert_eq!(
1424            quote_unquoted_keys("{foo: 1, bar: 2}"),
1425            "{\"foo\": 1, \"bar\": 2}"
1426        );
1427    }
1428
1429    // ==================== End JSON Repair Tests ====================
1430
1431    #[test]
1432    fn test_tool_call_part() {
1433        let part = ToolCallPart::new("get_weather", serde_json::json!({"city": "NYC"}))
1434            .with_tool_call_id("call_123");
1435        assert_eq!(part.tool_name, "get_weather");
1436        assert_eq!(part.tool_call_id, Some("call_123".to_string()));
1437        assert_eq!(part.part_kind(), "tool-call");
1438        assert!(part.id.is_none());
1439        assert!(part.provider_details.is_none());
1440    }
1441
1442    #[test]
1443    fn test_tool_call_part_with_all_fields() {
1444        let mut details = serde_json::Map::new();
1445        details.insert("temperature".to_string(), serde_json::json!(0.7));
1446
1447        let part = ToolCallPart::new("search", serde_json::json!({"query": "rust"}))
1448            .with_tool_call_id("call_456")
1449            .with_part_id("part-789")
1450            .with_provider_details(details.clone());
1451
1452        assert_eq!(part.tool_call_id, Some("call_456".to_string()));
1453        assert_eq!(part.id, Some("part-789".to_string()));
1454        assert_eq!(part.provider_details, Some(details));
1455    }
1456
1457    #[test]
1458    #[allow(deprecated)]
1459    fn test_tool_call_part_deprecated_with_id() {
1460        // Test backward compatibility with deprecated with_id()
1461        let part = ToolCallPart::new("test", serde_json::json!({})).with_id("call_compat");
1462        assert_eq!(part.tool_call_id, Some("call_compat".to_string()));
1463    }
1464
1465    #[test]
1466    fn test_tool_call_parse_args() {
1467        #[derive(Deserialize, PartialEq, Debug)]
1468        struct WeatherArgs {
1469            city: String,
1470        }
1471
1472        let part = ToolCallPart::new("get_weather", serde_json::json!({"city": "NYC"}));
1473        let args: WeatherArgs = part.parse_args().unwrap();
1474        assert_eq!(args.city, "NYC");
1475    }
1476
1477    #[test]
1478    fn test_thinking_part() {
1479        let part = ThinkingPart::new("Let me think about this...").with_signature("sig123");
1480        assert_eq!(part.content, "Let me think about this...");
1481        assert_eq!(part.signature, Some("sig123".to_string()));
1482        assert!(part.id.is_none());
1483        assert!(part.provider_name.is_none());
1484        assert!(part.provider_details.is_none());
1485    }
1486
1487    #[test]
1488    fn test_thinking_part_with_all_fields() {
1489        let mut details = serde_json::Map::new();
1490        details.insert("thinking_tokens".to_string(), serde_json::json!(1500));
1491
1492        let part = ThinkingPart::new("Deep thoughts...")
1493            .with_id("think-001")
1494            .with_signature("sig456")
1495            .with_provider_name("anthropic")
1496            .with_provider_details(details.clone());
1497
1498        assert_eq!(part.id, Some("think-001".to_string()));
1499        assert_eq!(part.signature, Some("sig456".to_string()));
1500        assert_eq!(part.provider_name, Some("anthropic".to_string()));
1501        assert_eq!(part.provider_details, Some(details));
1502    }
1503
1504    #[test]
1505    fn test_thinking_part_redacted_anthropic() {
1506        let part = ThinkingPart::redacted("encrypted_signature_data", "anthropic");
1507
1508        assert!(part.is_redacted());
1509        assert!(part.content.is_empty());
1510        assert_eq!(part.id, Some("redacted_thinking".to_string()));
1511        assert_eq!(part.signature, Some("encrypted_signature_data".to_string()));
1512        assert_eq!(part.provider_name, Some("anthropic".to_string()));
1513        assert_eq!(part.redacted_signature(), Some("encrypted_signature_data"));
1514    }
1515
1516    #[test]
1517    fn test_thinking_part_redacted_bedrock() {
1518        let part = ThinkingPart::redacted("base64_encoded_content", "aws-bedrock");
1519
1520        assert!(part.is_redacted());
1521        assert!(part.content.is_empty());
1522        assert_eq!(part.id, Some("redacted_content".to_string()));
1523        assert_eq!(part.signature, Some("base64_encoded_content".to_string()));
1524        assert_eq!(part.provider_name, Some("aws-bedrock".to_string()));
1525    }
1526
1527    #[test]
1528    fn test_thinking_part_redacted_with_custom_id() {
1529        let part = ThinkingPart::redacted_with_id(
1530            "redacted_custom_type",
1531            "my_signature",
1532            "custom-provider",
1533        );
1534
1535        assert!(part.is_redacted());
1536        assert_eq!(part.id, Some("redacted_custom_type".to_string()));
1537        assert_eq!(part.signature, Some("my_signature".to_string()));
1538    }
1539
1540    #[test]
1541    fn test_thinking_part_not_redacted() {
1542        let part = ThinkingPart::new("Regular thinking content").with_id("think-123");
1543
1544        assert!(!part.is_redacted());
1545        assert_eq!(part.redacted_signature(), None);
1546    }
1547
1548    #[test]
1549    fn test_thinking_part_redacted_constants() {
1550        assert_eq!(ThinkingPart::REDACTED_THINKING_ID, "redacted_thinking");
1551        assert_eq!(ThinkingPart::REDACTED_CONTENT_ID, "redacted_content");
1552    }
1553
1554    #[test]
1555    fn test_serde_roundtrip_redacted_thinking() {
1556        let part = ThinkingPart::redacted("encrypted_data_here", "anthropic");
1557
1558        let json = serde_json::to_string(&part).unwrap();
1559        let parsed: ThinkingPart = serde_json::from_str(&json).unwrap();
1560
1561        assert_eq!(part, parsed);
1562        assert!(parsed.is_redacted());
1563        assert_eq!(parsed.redacted_signature(), Some("encrypted_data_here"));
1564    }
1565
1566    #[test]
1567    fn test_serde_roundtrip_tool_call() {
1568        let mut details = serde_json::Map::new();
1569        details.insert("key".to_string(), serde_json::json!("value"));
1570
1571        let part = ToolCallPart::new("test", serde_json::json!({"a": 1}))
1572            .with_tool_call_id("call_1")
1573            .with_part_id("part_1")
1574            .with_provider_details(details);
1575
1576        let json = serde_json::to_string(&part).unwrap();
1577        let parsed: ToolCallPart = serde_json::from_str(&json).unwrap();
1578        assert_eq!(part, parsed);
1579    }
1580
1581    #[test]
1582    fn test_serde_roundtrip_text() {
1583        let mut details = serde_json::Map::new();
1584        details.insert("tokens".to_string(), serde_json::json!(42));
1585
1586        let part = TextPart::new("Hello")
1587            .with_id("text-1")
1588            .with_provider_details(details);
1589
1590        let json = serde_json::to_string(&part).unwrap();
1591        let parsed: TextPart = serde_json::from_str(&json).unwrap();
1592        assert_eq!(part, parsed);
1593    }
1594
1595    #[test]
1596    fn test_serde_roundtrip_thinking() {
1597        let mut details = serde_json::Map::new();
1598        details.insert("budget".to_string(), serde_json::json!(10000));
1599
1600        let part = ThinkingPart::new("Thinking...")
1601            .with_id("think-1")
1602            .with_signature("sig")
1603            .with_provider_name("anthropic")
1604            .with_provider_details(details);
1605
1606        let json = serde_json::to_string(&part).unwrap();
1607        let parsed: ThinkingPart = serde_json::from_str(&json).unwrap();
1608        assert_eq!(part, parsed);
1609    }
1610
1611    #[test]
1612    fn test_serde_skip_none_fields() {
1613        // Verify that None fields are not serialized
1614        let part = TextPart::new("Hello");
1615        let json = serde_json::to_string(&part).unwrap();
1616
1617        // Should only have "content" field, not id or provider_details
1618        assert!(json.contains("content"));
1619        assert!(!json.contains("id"));
1620        assert!(!json.contains("provider_details"));
1621    }
1622
1623    #[test]
1624    fn test_backward_compat_deserialization() {
1625        // Verify we can deserialize old JSON without the new fields
1626        let old_json = r#"{"content":"Hello, world!"}"#;
1627        let part: TextPart = serde_json::from_str(old_json).unwrap();
1628        assert_eq!(part.content, "Hello, world!");
1629        assert!(part.id.is_none());
1630        assert!(part.provider_details.is_none());
1631    }
1632
1633    #[test]
1634    fn test_binary_content() {
1635        let data = vec![0x89, 0x50, 0x4E, 0x47]; // PNG magic bytes
1636        let content = BinaryContent::new(data.clone(), "image/png");
1637
1638        assert_eq!(content.data, data);
1639        assert_eq!(content.media_type, "image/png");
1640        assert!(!content.is_empty());
1641        assert_eq!(content.len(), 4);
1642    }
1643
1644    #[test]
1645    fn test_binary_content_empty() {
1646        let content = BinaryContent::default();
1647        assert!(content.is_empty());
1648        assert_eq!(content.len(), 0);
1649        assert!(content.media_type.is_empty());
1650    }
1651
1652    #[test]
1653    fn test_file_part() {
1654        let data = vec![0xFF, 0xD8, 0xFF]; // JPEG magic bytes
1655        let part = FilePart::from_bytes(data.clone(), "image/jpeg");
1656
1657        assert_eq!(part.content.data, data);
1658        assert_eq!(part.media_type(), "image/jpeg");
1659        assert_eq!(part.part_kind(), "file");
1660        assert!(part.has_content());
1661        assert_eq!(part.size(), 3);
1662        assert!(part.id.is_none());
1663        assert!(part.provider_name.is_none());
1664        assert!(part.provider_details.is_none());
1665    }
1666
1667    #[test]
1668    fn test_file_part_with_builders() {
1669        let mut details = serde_json::Map::new();
1670        details.insert("model".to_string(), serde_json::json!("dall-e-3"));
1671        details.insert(
1672            "revised_prompt".to_string(),
1673            serde_json::json!("A cute puppy"),
1674        );
1675
1676        let data = vec![0x89, 0x50, 0x4E, 0x47];
1677        let part = FilePart::from_bytes(data, "image/png")
1678            .with_id("file-123")
1679            .with_provider_name("openai")
1680            .with_provider_details(details.clone());
1681
1682        assert_eq!(part.id, Some("file-123".to_string()));
1683        assert_eq!(part.provider_name, Some("openai".to_string()));
1684        assert_eq!(part.provider_details, Some(details));
1685    }
1686
1687    #[test]
1688    fn test_file_part_empty_content() {
1689        let part = FilePart::from_bytes(vec![], "application/octet-stream");
1690        assert!(!part.has_content());
1691        assert_eq!(part.size(), 0);
1692    }
1693
1694    #[test]
1695    fn test_serde_roundtrip_binary_content() {
1696        let data = vec![0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD];
1697        let content = BinaryContent::new(data.clone(), "application/octet-stream");
1698
1699        let json = serde_json::to_string(&content).unwrap();
1700        let parsed: BinaryContent = serde_json::from_str(&json).unwrap();
1701
1702        assert_eq!(content, parsed);
1703        assert_eq!(parsed.data, data);
1704    }
1705
1706    #[test]
1707    fn test_serde_roundtrip_file_part() {
1708        let mut details = serde_json::Map::new();
1709        details.insert("quality".to_string(), serde_json::json!("hd"));
1710
1711        let data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
1712        let part = FilePart::from_bytes(data.clone(), "image/png")
1713            .with_id("img-001")
1714            .with_provider_name("openai")
1715            .with_provider_details(details);
1716
1717        let json = serde_json::to_string(&part).unwrap();
1718        let parsed: FilePart = serde_json::from_str(&json).unwrap();
1719
1720        assert_eq!(part, parsed);
1721        assert_eq!(parsed.data(), &data);
1722    }
1723
1724    #[test]
1725    fn test_file_part_serde_skip_none() {
1726        // Verify that None fields are not serialized
1727        let part = FilePart::from_bytes(vec![0x00], "application/octet-stream");
1728        let json = serde_json::to_string(&part).unwrap();
1729
1730        assert!(json.contains("content"));
1731        assert!(!json.contains("id"));
1732        assert!(!json.contains("provider_name"));
1733        assert!(!json.contains("provider_details"));
1734    }
1735
1736    #[test]
1737    fn test_builtin_tool_call_part() {
1738        let part = BuiltinToolCallPart::new("web_search", serde_json::json!({"query": "rust"}))
1739            .with_tool_call_id("call_123");
1740
1741        assert_eq!(part.tool_name, "web_search");
1742        assert_eq!(part.tool_call_id, Some("call_123".to_string()));
1743        assert_eq!(part.part_kind(), "builtin-tool-call");
1744    }
1745
1746    #[test]
1747    fn test_builtin_tool_call_part_with_all_fields() {
1748        let mut details = serde_json::Map::new();
1749        details.insert("provider".to_string(), serde_json::json!("google"));
1750
1751        let part =
1752            BuiltinToolCallPart::new("code_execution", serde_json::json!({"code": "print(1)"}))
1753                .with_tool_call_id("call_456")
1754                .with_part_id("part-789")
1755                .with_provider_details(details.clone());
1756
1757        assert_eq!(part.tool_call_id, Some("call_456".to_string()));
1758        assert_eq!(part.id, Some("part-789".to_string()));
1759        assert_eq!(part.provider_details, Some(details));
1760    }
1761
1762    #[test]
1763    fn test_web_search_result() {
1764        let result = WebSearchResult::new("Rust Programming", "https://rust-lang.org")
1765            .with_snippet("A systems programming language")
1766            .with_content("Full article content...");
1767
1768        assert_eq!(result.title, "Rust Programming");
1769        assert_eq!(result.url, "https://rust-lang.org");
1770        assert_eq!(
1771            result.snippet,
1772            Some("A systems programming language".to_string())
1773        );
1774        assert!(result.content.is_some());
1775    }
1776
1777    #[test]
1778    fn test_web_search_results() {
1779        let results = WebSearchResults::new(
1780            "rust programming",
1781            vec![
1782                WebSearchResult::new("Rust", "https://rust-lang.org"),
1783                WebSearchResult::new("Crates.io", "https://crates.io"),
1784            ],
1785        )
1786        .with_total_results(1000);
1787
1788        assert_eq!(results.query, "rust programming");
1789        assert_eq!(results.len(), 2);
1790        assert!(!results.is_empty());
1791        assert_eq!(results.total_results, Some(1000));
1792    }
1793
1794    #[test]
1795    fn test_code_execution_result() {
1796        let result = CodeExecutionResult::new("print('hello')")
1797            .with_stdout("hello\n")
1798            .with_exit_code(0);
1799
1800        assert_eq!(result.code, "print('hello')");
1801        assert_eq!(result.stdout, Some("hello\n".to_string()));
1802        assert!(result.is_success());
1803    }
1804
1805    #[test]
1806    fn test_code_execution_result_with_error() {
1807        let result = CodeExecutionResult::new("invalid code")
1808            .with_stderr("SyntaxError")
1809            .with_exit_code(1)
1810            .with_error("Compilation failed");
1811
1812        assert!(!result.is_success());
1813        assert_eq!(result.error, Some("Compilation failed".to_string()));
1814    }
1815
1816    #[test]
1817    fn test_code_execution_result_with_output_file() {
1818        let image = BinaryContent::new(vec![0x89, 0x50, 0x4E, 0x47], "image/png");
1819        let result = CodeExecutionResult::new("plot()")
1820            .with_stdout("Plot saved")
1821            .with_output_file(image);
1822
1823        assert_eq!(result.output_files.len(), 1);
1824        assert_eq!(result.output_files[0].media_type, "image/png");
1825    }
1826
1827    #[test]
1828    fn test_file_search_result() {
1829        let mut metadata = serde_json::Map::new();
1830        metadata.insert("size".to_string(), serde_json::json!(1024));
1831
1832        let result = FileSearchResult::new("main.rs", "fn main() {}")
1833            .with_score(0.95)
1834            .with_metadata(metadata);
1835
1836        assert_eq!(result.file_name, "main.rs");
1837        assert_eq!(result.score, Some(0.95));
1838        assert!(result.metadata.is_some());
1839    }
1840
1841    #[test]
1842    fn test_file_search_results() {
1843        let results = FileSearchResults::new(
1844            "main function",
1845            vec![FileSearchResult::new("main.rs", "fn main() {}")],
1846        );
1847
1848        assert_eq!(results.query, "main function");
1849        assert_eq!(results.len(), 1);
1850        assert!(!results.is_empty());
1851    }
1852
1853    #[test]
1854    fn test_builtin_tool_return_content() {
1855        let web_content =
1856            BuiltinToolReturnContent::web_search(WebSearchResults::new("test", vec![]));
1857        assert_eq!(web_content.content_type(), "web_search");
1858
1859        let code_content =
1860            BuiltinToolReturnContent::code_execution(CodeExecutionResult::new("x = 1"));
1861        assert_eq!(code_content.content_type(), "code_execution");
1862
1863        let file_content =
1864            BuiltinToolReturnContent::file_search(FileSearchResults::new("query", vec![]));
1865        assert_eq!(file_content.content_type(), "file_search");
1866
1867        let other_content =
1868            BuiltinToolReturnContent::other("custom_tool", serde_json::json!({"result": "data"}));
1869        assert_eq!(other_content.content_type(), "custom_tool");
1870    }
1871
1872    #[test]
1873    fn test_builtin_tool_return_part() {
1874        let content = BuiltinToolReturnContent::web_search(WebSearchResults::new(
1875            "rust",
1876            vec![WebSearchResult::new("Rust", "https://rust-lang.org")],
1877        ));
1878
1879        let part =
1880            BuiltinToolReturnPart::new("web_search", content, "call_123").with_id("return-001");
1881
1882        assert_eq!(part.tool_name, "web_search");
1883        assert_eq!(part.tool_call_id, "call_123");
1884        assert_eq!(part.part_kind(), "builtin-tool-return");
1885        assert_eq!(part.content_type(), "web_search");
1886        assert_eq!(part.id, Some("return-001".to_string()));
1887    }
1888
1889    #[test]
1890    fn test_serde_roundtrip_builtin_tool_call() {
1891        let part = BuiltinToolCallPart::new("web_search", serde_json::json!({"q": "test"}))
1892            .with_tool_call_id("call_1");
1893
1894        let json = serde_json::to_string(&part).unwrap();
1895        let parsed: BuiltinToolCallPart = serde_json::from_str(&json).unwrap();
1896        assert_eq!(part, parsed);
1897    }
1898
1899    #[test]
1900    fn test_serde_roundtrip_web_search_results() {
1901        let results = WebSearchResults::new(
1902            "rust",
1903            vec![WebSearchResult::new("Rust", "https://rust-lang.org")
1904                .with_snippet("Systems programming")],
1905        )
1906        .with_total_results(100);
1907
1908        let json = serde_json::to_string(&results).unwrap();
1909        let parsed: WebSearchResults = serde_json::from_str(&json).unwrap();
1910        assert_eq!(results, parsed);
1911    }
1912
1913    #[test]
1914    fn test_serde_roundtrip_builtin_tool_return() {
1915        let content = BuiltinToolReturnContent::code_execution(
1916            CodeExecutionResult::new("print(1)")
1917                .with_stdout("1\n")
1918                .with_exit_code(0),
1919        );
1920
1921        let part =
1922            BuiltinToolReturnPart::new("code_execution", content, "call_xyz").with_id("ret-1");
1923
1924        let json = serde_json::to_string(&part).unwrap();
1925        let parsed: BuiltinToolReturnPart = serde_json::from_str(&json).unwrap();
1926
1927        assert_eq!(part.tool_name, parsed.tool_name);
1928        assert_eq!(part.tool_call_id, parsed.tool_call_id);
1929        assert_eq!(part.id, parsed.id);
1930    }
1931}