Skip to main content

serdes_ai_core/messages/
events.rs

1//! Streaming events for model responses.
2//!
3//! This module defines the event types used during streaming responses,
4//! including part start, delta, and end events.
5
6use serde::{Deserialize, Serialize};
7use serde_json::{Map, Value};
8
9use super::parts::{
10    BuiltinToolCallPart, FilePart, TextPart, ThinkingPart, ToolCallArgs, ToolCallPart,
11};
12use super::response::ModelResponsePart;
13
14/// Stream event for model responses.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16#[serde(tag = "event_kind", rename_all = "snake_case")]
17pub enum ModelResponseStreamEvent {
18    /// A new part has started.
19    PartStart(PartStartEvent),
20    /// Delta for an existing part.
21    PartDelta(PartDeltaEvent),
22    /// A part has ended.
23    PartEnd(PartEndEvent),
24}
25
26impl ModelResponseStreamEvent {
27    /// Create a part start event.
28    #[must_use]
29    pub fn part_start(index: usize, part: ModelResponsePart) -> Self {
30        Self::PartStart(PartStartEvent { index, part })
31    }
32
33    /// Create a text delta event.
34    #[must_use]
35    pub fn text_delta(index: usize, content_delta: impl Into<String>) -> Self {
36        Self::PartDelta(PartDeltaEvent {
37            index,
38            delta: ModelResponsePartDelta::Text(TextPartDelta::new(content_delta)),
39        })
40    }
41
42    /// Create a tool call delta event.
43    #[must_use]
44    pub fn tool_call_delta(index: usize, args_delta: impl Into<String>) -> Self {
45        Self::PartDelta(PartDeltaEvent {
46            index,
47            delta: ModelResponsePartDelta::ToolCall(ToolCallPartDelta::new(args_delta)),
48        })
49    }
50
51    /// Create a thinking delta event.
52    #[must_use]
53    pub fn thinking_delta(index: usize, content_delta: impl Into<String>) -> Self {
54        Self::PartDelta(PartDeltaEvent {
55            index,
56            delta: ModelResponsePartDelta::Thinking(ThinkingPartDelta::new(content_delta)),
57        })
58    }
59
60    /// Create a builtin tool call delta event.
61    #[must_use]
62    pub fn builtin_tool_call_delta(index: usize, args_delta: impl Into<String>) -> Self {
63        Self::PartDelta(PartDeltaEvent {
64            index,
65            delta: ModelResponsePartDelta::BuiltinToolCall(BuiltinToolCallPartDelta::new(
66                args_delta,
67            )),
68        })
69    }
70
71    /// Create a file part start event.
72    ///
73    /// Files arrive complete (no deltas), so this creates a start event
74    /// with the full file content.
75    #[must_use]
76    pub fn file_part(index: usize, part: FilePart) -> Self {
77        Self::PartStart(PartStartEvent {
78            index,
79            part: ModelResponsePart::File(part),
80        })
81    }
82
83    /// Create a builtin tool call start event.
84    #[must_use]
85    pub fn builtin_tool_call_start(index: usize, part: BuiltinToolCallPart) -> Self {
86        Self::PartStart(PartStartEvent {
87            index,
88            part: ModelResponsePart::BuiltinToolCall(part),
89        })
90    }
91
92    /// Create a part end event.
93    #[must_use]
94    pub fn part_end(index: usize) -> Self {
95        Self::PartEnd(PartEndEvent { index })
96    }
97
98    /// Get the part index.
99    #[must_use]
100    pub fn index(&self) -> usize {
101        match self {
102            Self::PartStart(e) => e.index,
103            Self::PartDelta(e) => e.index,
104            Self::PartEnd(e) => e.index,
105        }
106    }
107
108    /// Check if this is a start event.
109    #[must_use]
110    pub fn is_start(&self) -> bool {
111        matches!(self, Self::PartStart(_))
112    }
113
114    /// Check if this is a delta event.
115    #[must_use]
116    pub fn is_delta(&self) -> bool {
117        matches!(self, Self::PartDelta(_))
118    }
119
120    /// Check if this is an end event.
121    #[must_use]
122    pub fn is_end(&self) -> bool {
123        matches!(self, Self::PartEnd(_))
124    }
125}
126
127/// Event indicating a new part has started.
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129pub struct PartStartEvent {
130    /// Index of the part in the response.
131    pub index: usize,
132    /// The initial part data.
133    pub part: ModelResponsePart,
134}
135
136impl PartStartEvent {
137    /// Create a new part start event.
138    #[must_use]
139    pub fn new(index: usize, part: ModelResponsePart) -> Self {
140        Self { index, part }
141    }
142
143    /// Create a text part start.
144    #[must_use]
145    pub fn text(index: usize, content: impl Into<String>) -> Self {
146        Self::new(index, ModelResponsePart::Text(TextPart::new(content)))
147    }
148
149    /// Create a tool call part start.
150    #[must_use]
151    pub fn tool_call(index: usize, tool_name: impl Into<String>) -> Self {
152        Self::new(
153            index,
154            ModelResponsePart::ToolCall(ToolCallPart::new(tool_name, serde_json::Value::Null)),
155        )
156    }
157
158    /// Create a thinking part start.
159    #[must_use]
160    pub fn thinking(index: usize, content: impl Into<String>) -> Self {
161        Self::new(
162            index,
163            ModelResponsePart::Thinking(ThinkingPart::new(content)),
164        )
165    }
166
167    /// Create a file part start.
168    #[must_use]
169    pub fn file(index: usize, part: FilePart) -> Self {
170        Self::new(index, ModelResponsePart::File(part))
171    }
172
173    /// Create a builtin tool call part start.
174    #[must_use]
175    pub fn builtin_tool_call(
176        index: usize,
177        tool_name: impl Into<String>,
178        args: impl Into<ToolCallArgs>,
179    ) -> Self {
180        Self::new(
181            index,
182            ModelResponsePart::BuiltinToolCall(BuiltinToolCallPart::new(tool_name, args)),
183        )
184    }
185}
186
187/// Event containing a delta update for a part.
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct PartDeltaEvent {
190    /// Index of the part being updated.
191    pub index: usize,
192    /// The delta content.
193    pub delta: ModelResponsePartDelta,
194}
195
196impl PartDeltaEvent {
197    /// Create a new delta event.
198    #[must_use]
199    pub fn new(index: usize, delta: ModelResponsePartDelta) -> Self {
200        Self { index, delta }
201    }
202
203    /// Create a text delta.
204    #[must_use]
205    pub fn text(index: usize, content: impl Into<String>) -> Self {
206        Self::new(
207            index,
208            ModelResponsePartDelta::Text(TextPartDelta::new(content)),
209        )
210    }
211
212    /// Create a tool call args delta.
213    #[must_use]
214    pub fn tool_call_args(index: usize, args: impl Into<String>) -> Self {
215        Self::new(
216            index,
217            ModelResponsePartDelta::ToolCall(ToolCallPartDelta::new(args)),
218        )
219    }
220
221    /// Create a thinking delta.
222    #[must_use]
223    pub fn thinking(index: usize, content: impl Into<String>) -> Self {
224        Self::new(
225            index,
226            ModelResponsePartDelta::Thinking(ThinkingPartDelta::new(content)),
227        )
228    }
229
230    /// Create a builtin tool call args delta.
231    #[must_use]
232    pub fn builtin_tool_call_args(index: usize, args: impl Into<String>) -> Self {
233        Self::new(
234            index,
235            ModelResponsePartDelta::BuiltinToolCall(BuiltinToolCallPartDelta::new(args)),
236        )
237    }
238}
239
240/// Delta content for different part types.
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242#[serde(tag = "delta_kind", rename_all = "snake_case")]
243pub enum ModelResponsePartDelta {
244    /// Text content delta.
245    Text(TextPartDelta),
246    /// Tool call arguments delta.
247    ToolCall(ToolCallPartDelta),
248    /// Thinking content delta.
249    Thinking(ThinkingPartDelta),
250    /// Builtin tool call arguments delta.
251    BuiltinToolCall(BuiltinToolCallPartDelta),
252}
253
254impl ModelResponsePartDelta {
255    /// Check if this is a text delta.
256    #[must_use]
257    pub fn is_text(&self) -> bool {
258        matches!(self, Self::Text(_))
259    }
260
261    /// Check if this is a tool call delta.
262    #[must_use]
263    pub fn is_tool_call(&self) -> bool {
264        matches!(self, Self::ToolCall(_))
265    }
266
267    /// Check if this is a thinking delta.
268    #[must_use]
269    pub fn is_thinking(&self) -> bool {
270        matches!(self, Self::Thinking(_))
271    }
272
273    /// Check if this is a builtin tool call delta.
274    #[must_use]
275    pub fn is_builtin_tool_call(&self) -> bool {
276        matches!(self, Self::BuiltinToolCall(_))
277    }
278
279    /// Get the content delta if applicable.
280    #[must_use]
281    pub fn content_delta(&self) -> Option<&str> {
282        match self {
283            Self::Text(d) => Some(&d.content_delta),
284            Self::Thinking(d) => Some(&d.content_delta),
285            Self::ToolCall(_) => None,
286            Self::BuiltinToolCall(_) => None,
287        }
288    }
289
290    /// Apply this delta to a matching ModelResponsePart.
291    ///
292    /// Returns `true` if the delta was successfully applied (types matched),
293    /// `false` if the types didn't match.
294    #[must_use]
295    pub fn apply(&self, part: &mut ModelResponsePart) -> bool {
296        match (self, part) {
297            (Self::Text(delta), ModelResponsePart::Text(text_part)) => {
298                delta.apply(text_part);
299                true
300            }
301            (Self::ToolCall(delta), ModelResponsePart::ToolCall(tool_part)) => {
302                delta.apply(tool_part);
303                true
304            }
305            (Self::Thinking(delta), ModelResponsePart::Thinking(thinking_part)) => {
306                delta.apply(thinking_part);
307                true
308            }
309            (Self::BuiltinToolCall(delta), ModelResponsePart::BuiltinToolCall(builtin_part)) => {
310                delta.apply(builtin_part);
311                true
312            }
313            _ => false,
314        }
315    }
316}
317
318/// Delta for text content.
319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320pub struct TextPartDelta {
321    /// The text content delta.
322    pub content_delta: String,
323    /// Provider-specific details/metadata delta.
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub provider_details: Option<Map<String, Value>>,
326}
327
328impl TextPartDelta {
329    /// Create a new text delta.
330    #[must_use]
331    pub fn new(content: impl Into<String>) -> Self {
332        Self {
333            content_delta: content.into(),
334            provider_details: None,
335        }
336    }
337
338    /// Set provider-specific details.
339    #[must_use]
340    pub fn with_provider_details(mut self, details: Map<String, Value>) -> Self {
341        self.provider_details = Some(details);
342        self
343    }
344
345    /// Check if the delta is empty.
346    #[must_use]
347    pub fn is_empty(&self) -> bool {
348        self.content_delta.is_empty() && self.provider_details.is_none()
349    }
350
351    /// Apply this delta to an existing TextPart.
352    pub fn apply(&self, part: &mut TextPart) {
353        if !self.content_delta.is_empty() {
354            part.content.push_str(&self.content_delta);
355        }
356        if let Some(ref details) = self.provider_details {
357            match &mut part.provider_details {
358                Some(existing) => {
359                    // Merge new details into existing
360                    for (key, value) in details {
361                        existing.insert(key.clone(), value.clone());
362                    }
363                }
364                None => part.provider_details = Some(details.clone()),
365            }
366        }
367    }
368}
369
370impl Default for TextPartDelta {
371    fn default() -> Self {
372        Self::new("")
373    }
374}
375
376/// Delta for tool call arguments.
377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
378pub struct ToolCallPartDelta {
379    /// The arguments JSON delta.
380    pub args_delta: String,
381    /// Provider-assigned tool call ID (may arrive in delta).
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub tool_call_id: Option<String>,
384    /// Provider-specific details/metadata delta.
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub provider_details: Option<Map<String, Value>>,
387}
388
389impl ToolCallPartDelta {
390    /// Create a new tool call delta.
391    #[must_use]
392    pub fn new(args: impl Into<String>) -> Self {
393        Self {
394            args_delta: args.into(),
395            tool_call_id: None,
396            provider_details: None,
397        }
398    }
399
400    /// Set the tool call ID.
401    #[must_use]
402    pub fn with_tool_call_id(mut self, id: impl Into<String>) -> Self {
403        self.tool_call_id = Some(id.into());
404        self
405    }
406
407    /// Set provider-specific details.
408    #[must_use]
409    pub fn with_provider_details(mut self, details: Map<String, Value>) -> Self {
410        self.provider_details = Some(details);
411        self
412    }
413
414    /// Check if the delta is empty.
415    #[must_use]
416    pub fn is_empty(&self) -> bool {
417        self.args_delta.is_empty() && self.tool_call_id.is_none() && self.provider_details.is_none()
418    }
419
420    /// Apply this delta to an existing ToolCallPart.
421    pub fn apply(&self, part: &mut ToolCallPart) {
422        if !self.args_delta.is_empty() {
423            // Check if current args are empty (just "{}") - if so, replace instead of append
424            let current = part
425                .args
426                .to_json_string()
427                .unwrap_or_else(|_| part.args.to_json().to_string());
428
429            let new_args = if current == "{}" || current.is_empty() {
430                // Start fresh with the delta
431                self.args_delta.clone()
432            } else {
433                // Append to existing args
434                format!("{}{}", current, self.args_delta)
435            };
436            part.args = ToolCallArgs::String(new_args);
437        }
438        if self.tool_call_id.is_some() && part.tool_call_id.is_none() {
439            part.tool_call_id = self.tool_call_id.clone();
440        }
441        if let Some(ref details) = self.provider_details {
442            match &mut part.provider_details {
443                Some(existing) => {
444                    // Merge new details into existing
445                    for (key, value) in details {
446                        existing.insert(key.clone(), value.clone());
447                    }
448                }
449                None => part.provider_details = Some(details.clone()),
450            }
451        }
452    }
453}
454
455impl Default for ToolCallPartDelta {
456    fn default() -> Self {
457        Self::new("")
458    }
459}
460
461/// Delta for thinking content.
462#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
463pub struct ThinkingPartDelta {
464    /// The thinking content delta.
465    pub content_delta: String,
466    /// Signature delta (for Anthropic's thinking blocks).
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub signature_delta: Option<String>,
469    /// Provider name (set once, not accumulated).
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub provider_name: Option<String>,
472    /// Provider-specific details/metadata delta.
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub provider_details: Option<Map<String, Value>>,
475}
476
477impl ThinkingPartDelta {
478    /// Create a new thinking delta.
479    #[must_use]
480    pub fn new(content: impl Into<String>) -> Self {
481        Self {
482            content_delta: content.into(),
483            signature_delta: None,
484            provider_name: None,
485            provider_details: None,
486        }
487    }
488
489    /// Set the signature delta.
490    #[must_use]
491    pub fn with_signature_delta(mut self, sig: impl Into<String>) -> Self {
492        self.signature_delta = Some(sig.into());
493        self
494    }
495
496    /// Set the provider name.
497    #[must_use]
498    pub fn with_provider_name(mut self, name: impl Into<String>) -> Self {
499        self.provider_name = Some(name.into());
500        self
501    }
502
503    /// Set provider-specific details.
504    #[must_use]
505    pub fn with_provider_details(mut self, details: Map<String, Value>) -> Self {
506        self.provider_details = Some(details);
507        self
508    }
509
510    /// Check if the delta is empty.
511    #[must_use]
512    pub fn is_empty(&self) -> bool {
513        self.content_delta.is_empty()
514            && self.signature_delta.is_none()
515            && self.provider_name.is_none()
516            && self.provider_details.is_none()
517    }
518
519    /// Apply this delta to an existing ThinkingPart.
520    pub fn apply(&self, part: &mut ThinkingPart) {
521        if !self.content_delta.is_empty() {
522            part.content.push_str(&self.content_delta);
523        }
524        if let Some(ref sig_delta) = self.signature_delta {
525            match &mut part.signature {
526                Some(existing) => existing.push_str(sig_delta),
527                None => part.signature = Some(sig_delta.clone()),
528            }
529        }
530        if self.provider_name.is_some() {
531            part.provider_name = self.provider_name.clone();
532        }
533        if let Some(ref details) = self.provider_details {
534            match &mut part.provider_details {
535                Some(existing) => {
536                    // Merge new details into existing
537                    for (key, value) in details {
538                        existing.insert(key.clone(), value.clone());
539                    }
540                }
541                None => part.provider_details = Some(details.clone()),
542            }
543        }
544    }
545}
546
547impl Default for ThinkingPartDelta {
548    fn default() -> Self {
549        Self::new("")
550    }
551}
552
553/// Delta for builtin tool call arguments.
554///
555/// Similar to `ToolCallPartDelta` but for builtin tools like web search,
556/// code execution, and file search.
557#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
558pub struct BuiltinToolCallPartDelta {
559    /// The arguments JSON delta.
560    pub args_delta: String,
561    /// Provider-assigned tool call ID (may arrive in delta).
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub tool_call_id: Option<String>,
564    /// Provider-specific details/metadata delta.
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub provider_details: Option<Map<String, Value>>,
567}
568
569impl BuiltinToolCallPartDelta {
570    /// Create a new builtin tool call delta.
571    #[must_use]
572    pub fn new(args: impl Into<String>) -> Self {
573        Self {
574            args_delta: args.into(),
575            tool_call_id: None,
576            provider_details: None,
577        }
578    }
579
580    /// Set the tool call ID.
581    #[must_use]
582    pub fn with_tool_call_id(mut self, id: impl Into<String>) -> Self {
583        self.tool_call_id = Some(id.into());
584        self
585    }
586
587    /// Set provider-specific details.
588    #[must_use]
589    pub fn with_provider_details(mut self, details: Map<String, Value>) -> Self {
590        self.provider_details = Some(details);
591        self
592    }
593
594    /// Check if the delta is empty.
595    #[must_use]
596    pub fn is_empty(&self) -> bool {
597        self.args_delta.is_empty() && self.tool_call_id.is_none() && self.provider_details.is_none()
598    }
599
600    /// Apply this delta to an existing BuiltinToolCallPart.
601    pub fn apply(&self, part: &mut BuiltinToolCallPart) {
602        if !self.args_delta.is_empty() {
603            // Check if current args are empty (just "{}") - if so, replace instead of append
604            let current = part
605                .args
606                .to_json_string()
607                .unwrap_or_else(|_| part.args.to_json().to_string());
608
609            let new_args = if current == "{}" || current.is_empty() {
610                // Start fresh with the delta
611                self.args_delta.clone()
612            } else {
613                // Append to existing args
614                format!("{}{}", current, self.args_delta)
615            };
616            part.args = ToolCallArgs::String(new_args);
617        }
618        if self.tool_call_id.is_some() && part.tool_call_id.is_none() {
619            part.tool_call_id = self.tool_call_id.clone();
620        }
621        if let Some(ref details) = self.provider_details {
622            match &mut part.provider_details {
623                Some(existing) => {
624                    // Merge new details into existing
625                    for (key, value) in details {
626                        existing.insert(key.clone(), value.clone());
627                    }
628                }
629                None => part.provider_details = Some(details.clone()),
630            }
631        }
632    }
633}
634
635impl Default for BuiltinToolCallPartDelta {
636    fn default() -> Self {
637        Self::new("")
638    }
639}
640
641/// Event indicating a part has ended.
642#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
643pub struct PartEndEvent {
644    /// Index of the part that ended.
645    pub index: usize,
646}
647
648impl PartEndEvent {
649    /// Create a new part end event.
650    #[must_use]
651    pub fn new(index: usize) -> Self {
652        Self { index }
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    #[test]
661    fn test_part_start_event() {
662        let event = PartStartEvent::text(0, "Hello");
663        assert_eq!(event.index, 0);
664        assert!(matches!(event.part, ModelResponsePart::Text(_)));
665    }
666
667    #[test]
668    fn test_part_delta_event() {
669        let event = PartDeltaEvent::text(0, " world");
670        assert_eq!(event.index, 0);
671        assert!(event.delta.is_text());
672        assert_eq!(event.delta.content_delta(), Some(" world"));
673    }
674
675    #[test]
676    fn test_stream_event_helpers() {
677        let start = ModelResponseStreamEvent::part_start(0, ModelResponsePart::text("Hello"));
678        assert!(start.is_start());
679        assert_eq!(start.index(), 0);
680
681        let delta = ModelResponseStreamEvent::text_delta(0, " world");
682        assert!(delta.is_delta());
683
684        let end = ModelResponseStreamEvent::part_end(0);
685        assert!(end.is_end());
686    }
687
688    #[test]
689    fn test_serde_roundtrip() {
690        let event = ModelResponseStreamEvent::text_delta(0, "Hello");
691        let json = serde_json::to_string(&event).unwrap();
692        let parsed: ModelResponseStreamEvent = serde_json::from_str(&json).unwrap();
693        assert_eq!(event, parsed);
694    }
695
696    #[test]
697    fn test_text_delta_apply() {
698        let mut part = TextPart::new("Hello");
699        let delta = TextPartDelta::new(" world");
700        delta.apply(&mut part);
701        assert_eq!(part.content, "Hello world");
702    }
703
704    #[test]
705    fn test_text_delta_apply_with_provider_details() {
706        let mut part = TextPart::new("Hello");
707
708        let mut details = Map::new();
709        details.insert("model".to_string(), Value::String("gpt-4".to_string()));
710
711        let delta = TextPartDelta::new(" world").with_provider_details(details);
712        delta.apply(&mut part);
713
714        assert_eq!(part.content, "Hello world");
715        assert!(part.provider_details.is_some());
716        assert_eq!(
717            part.provider_details.as_ref().unwrap().get("model"),
718            Some(&Value::String("gpt-4".to_string()))
719        );
720    }
721
722    #[test]
723    fn test_text_delta_merge_provider_details() {
724        let mut initial_details = Map::new();
725        initial_details.insert("key1".to_string(), Value::String("value1".to_string()));
726
727        let mut part = TextPart::new("Hello").with_provider_details(initial_details);
728
729        let mut new_details = Map::new();
730        new_details.insert("key2".to_string(), Value::String("value2".to_string()));
731
732        let delta = TextPartDelta::new("").with_provider_details(new_details);
733        delta.apply(&mut part);
734
735        let details = part.provider_details.as_ref().unwrap();
736        assert_eq!(details.len(), 2);
737        assert_eq!(
738            details.get("key1"),
739            Some(&Value::String("value1".to_string()))
740        );
741        assert_eq!(
742            details.get("key2"),
743            Some(&Value::String("value2".to_string()))
744        );
745    }
746
747    #[test]
748    fn test_thinking_delta_apply() {
749        let mut part = ThinkingPart::new("Initial thought");
750        let delta = ThinkingPartDelta::new(" continued...");
751        delta.apply(&mut part);
752        assert_eq!(part.content, "Initial thought continued...");
753    }
754
755    #[test]
756    fn test_thinking_delta_apply_with_signature() {
757        let mut part = ThinkingPart::new("Thinking");
758
759        let delta = ThinkingPartDelta::new("")
760            .with_signature_delta("sig123")
761            .with_provider_name("anthropic");
762        delta.apply(&mut part);
763
764        assert_eq!(part.signature, Some("sig123".to_string()));
765        assert_eq!(part.provider_name, Some("anthropic".to_string()));
766    }
767
768    #[test]
769    fn test_thinking_delta_signature_accumulation() {
770        let mut part = ThinkingPart::new("Thinking").with_signature("sig1");
771
772        let delta = ThinkingPartDelta::new("").with_signature_delta("23");
773        delta.apply(&mut part);
774
775        assert_eq!(part.signature, Some("sig123".to_string()));
776    }
777
778    #[test]
779    fn test_tool_call_delta_apply() {
780        let mut part = ToolCallPart::new("get_weather", serde_json::json!({}));
781
782        let delta = ToolCallPartDelta::new(r#"{"city":"NYC"}"#).with_tool_call_id("call_123");
783        delta.apply(&mut part);
784
785        assert_eq!(part.tool_call_id, Some("call_123".to_string()));
786        // Args should have the delta appended
787        assert!(part.args.to_json_string().unwrap().contains("city"));
788    }
789
790    #[test]
791    fn test_tool_call_delta_doesnt_overwrite_id() {
792        let mut part =
793            ToolCallPart::new("search", serde_json::json!({})).with_tool_call_id("original_id");
794
795        let delta = ToolCallPartDelta::new("").with_tool_call_id("new_id");
796        delta.apply(&mut part);
797
798        // Should keep original ID
799        assert_eq!(part.tool_call_id, Some("original_id".to_string()));
800    }
801
802    #[test]
803    fn test_model_response_part_delta_apply() {
804        let mut text_part = ModelResponsePart::Text(TextPart::new("Hello"));
805        let delta = ModelResponsePartDelta::Text(TextPartDelta::new(" world"));
806
807        assert!(delta.apply(&mut text_part));
808
809        if let ModelResponsePart::Text(ref text) = text_part {
810            assert_eq!(text.content, "Hello world");
811        } else {
812            panic!("Expected Text part");
813        }
814    }
815
816    #[test]
817    fn test_model_response_part_delta_apply_type_mismatch() {
818        let mut text_part = ModelResponsePart::Text(TextPart::new("Hello"));
819        let delta = ModelResponsePartDelta::Thinking(ThinkingPartDelta::new("thinking"));
820
821        // Should return false for type mismatch
822        assert!(!delta.apply(&mut text_part));
823
824        // Part should be unchanged
825        if let ModelResponsePart::Text(ref text) = text_part {
826            assert_eq!(text.content, "Hello");
827        } else {
828            panic!("Expected Text part");
829        }
830    }
831
832    #[test]
833    fn test_delta_is_empty() {
834        // Empty text delta
835        let text_delta = TextPartDelta::default();
836        assert!(text_delta.is_empty());
837
838        // Non-empty text delta
839        let text_delta = TextPartDelta::new("content");
840        assert!(!text_delta.is_empty());
841
842        // Text delta with only provider details
843        let mut details = Map::new();
844        details.insert("key".to_string(), Value::Null);
845        let text_delta = TextPartDelta::new("").with_provider_details(details);
846        assert!(!text_delta.is_empty());
847
848        // Empty thinking delta
849        let thinking_delta = ThinkingPartDelta::default();
850        assert!(thinking_delta.is_empty());
851
852        // Thinking delta with only signature
853        let thinking_delta = ThinkingPartDelta::new("").with_signature_delta("sig");
854        assert!(!thinking_delta.is_empty());
855
856        // Empty tool call delta
857        let tool_delta = ToolCallPartDelta::default();
858        assert!(tool_delta.is_empty());
859
860        // Tool call delta with only tool_call_id
861        let tool_delta = ToolCallPartDelta::new("").with_tool_call_id("id");
862        assert!(!tool_delta.is_empty());
863    }
864
865    #[test]
866    fn test_delta_builders() {
867        let text_delta = TextPartDelta::new("content").with_provider_details(Map::new());
868        assert!(!text_delta.is_empty());
869
870        let thinking_delta = ThinkingPartDelta::new("thought")
871            .with_signature_delta("sig")
872            .with_provider_name("provider")
873            .with_provider_details(Map::new());
874        assert!(!thinking_delta.is_empty());
875
876        let tool_delta = ToolCallPartDelta::new(r#"{}"#)
877            .with_tool_call_id("call_1")
878            .with_provider_details(Map::new());
879        assert!(!tool_delta.is_empty());
880    }
881
882    #[test]
883    fn test_serde_roundtrip_with_new_fields() {
884        // Text delta with provider_details
885        let mut details = Map::new();
886        details.insert("key".to_string(), Value::String("value".to_string()));
887        let delta = TextPartDelta::new("hello").with_provider_details(details);
888        let json = serde_json::to_string(&delta).unwrap();
889        let parsed: TextPartDelta = serde_json::from_str(&json).unwrap();
890        assert_eq!(delta, parsed);
891
892        // Thinking delta with all fields
893        let thinking_delta = ThinkingPartDelta::new("thought")
894            .with_signature_delta("sig")
895            .with_provider_name("anthropic");
896        let json = serde_json::to_string(&thinking_delta).unwrap();
897        let parsed: ThinkingPartDelta = serde_json::from_str(&json).unwrap();
898        assert_eq!(thinking_delta, parsed);
899
900        // Tool call delta with all fields
901        let tool_delta = ToolCallPartDelta::new(r#"{"a":1}"#).with_tool_call_id("call_123");
902        let json = serde_json::to_string(&tool_delta).unwrap();
903        let parsed: ToolCallPartDelta = serde_json::from_str(&json).unwrap();
904        assert_eq!(tool_delta, parsed);
905    }
906
907    #[test]
908    fn test_serde_skip_none_fields() {
909        // Verify that None fields are not serialized
910        let delta = TextPartDelta::new("hello");
911        let json = serde_json::to_string(&delta).unwrap();
912
913        assert!(json.contains("content_delta"));
914        assert!(!json.contains("provider_details"));
915
916        let thinking_delta = ThinkingPartDelta::new("thought");
917        let json = serde_json::to_string(&thinking_delta).unwrap();
918
919        assert!(!json.contains("signature_delta"));
920        assert!(!json.contains("provider_name"));
921        assert!(!json.contains("provider_details"));
922    }
923
924    #[test]
925    fn test_backward_compat_deserialization() {
926        // Verify we can deserialize old JSON without the new fields
927        let old_json = r#"{"content_delta":"hello"}"#;
928        let delta: TextPartDelta = serde_json::from_str(old_json).unwrap();
929        assert_eq!(delta.content_delta, "hello");
930        assert!(delta.provider_details.is_none());
931
932        let old_json = r#"{"content_delta":"thinking"}"#;
933        let delta: ThinkingPartDelta = serde_json::from_str(old_json).unwrap();
934        assert_eq!(delta.content_delta, "thinking");
935        assert!(delta.signature_delta.is_none());
936        assert!(delta.provider_name.is_none());
937
938        let old_json = r#"{"args_delta":"{}"}"#;
939        let delta: ToolCallPartDelta = serde_json::from_str(old_json).unwrap();
940        assert_eq!(delta.args_delta, "{}");
941        assert!(delta.tool_call_id.is_none());
942    }
943
944    #[test]
945    fn test_builtin_tool_call_delta() {
946        let delta =
947            BuiltinToolCallPartDelta::new(r#"{"query":"rust"}"#).with_tool_call_id("builtin_123");
948
949        assert!(!delta.is_empty());
950        assert_eq!(delta.args_delta, r#"{"query":"rust"}"#);
951        assert_eq!(delta.tool_call_id, Some("builtin_123".to_string()));
952    }
953
954    #[test]
955    fn test_builtin_tool_call_delta_apply() {
956        let mut part = BuiltinToolCallPart::new("web_search", serde_json::json!({}));
957
958        let delta = BuiltinToolCallPartDelta::new(r#"{"q":"test"}"#).with_tool_call_id("call_456");
959        delta.apply(&mut part);
960
961        assert_eq!(part.tool_call_id, Some("call_456".to_string()));
962        assert!(part.args.to_json_string().unwrap().contains("test"));
963    }
964
965    #[test]
966    fn test_file_part_event() {
967        let file = FilePart::from_bytes(vec![0x89, 0x50, 0x4E, 0x47], "image/png");
968        let event = ModelResponseStreamEvent::file_part(0, file.clone());
969
970        assert!(event.is_start());
971        assert_eq!(event.index(), 0);
972
973        if let ModelResponseStreamEvent::PartStart(start) = event {
974            assert!(matches!(start.part, ModelResponsePart::File(_)));
975        } else {
976            panic!("Expected PartStart");
977        }
978    }
979
980    #[test]
981    fn test_builtin_tool_call_start_event() {
982        let part = BuiltinToolCallPart::new("web_search", serde_json::json!({"query": "rust"}))
983            .with_tool_call_id("call_123");
984        let event = ModelResponseStreamEvent::builtin_tool_call_start(0, part);
985
986        assert!(event.is_start());
987
988        if let ModelResponseStreamEvent::PartStart(start) = event {
989            assert!(matches!(start.part, ModelResponsePart::BuiltinToolCall(_)));
990        } else {
991            panic!("Expected PartStart");
992        }
993    }
994
995    #[test]
996    fn test_builtin_tool_call_delta_event() {
997        let event = ModelResponseStreamEvent::builtin_tool_call_delta(0, r#"{"q":"rust"}"#);
998
999        assert!(event.is_delta());
1000
1001        if let ModelResponseStreamEvent::PartDelta(delta_event) = event {
1002            assert!(delta_event.delta.is_builtin_tool_call());
1003        } else {
1004            panic!("Expected PartDelta");
1005        }
1006    }
1007
1008    #[test]
1009    fn test_part_start_event_builtin_tool_call() {
1010        let event = PartStartEvent::builtin_tool_call(
1011            0,
1012            "code_execution",
1013            serde_json::json!({"code": "print(1)"}),
1014        );
1015
1016        assert_eq!(event.index, 0);
1017        assert!(matches!(event.part, ModelResponsePart::BuiltinToolCall(_)));
1018    }
1019
1020    #[test]
1021    fn test_part_delta_event_builtin_tool_call() {
1022        let event = PartDeltaEvent::builtin_tool_call_args(0, r#"{"more":"args"}"#);
1023
1024        assert_eq!(event.index, 0);
1025        assert!(event.delta.is_builtin_tool_call());
1026        assert_eq!(event.delta.content_delta(), None); // Builtin tool calls have no content delta
1027    }
1028
1029    #[test]
1030    fn test_serde_roundtrip_builtin_tool_call_delta() {
1031        let delta = BuiltinToolCallPartDelta::new(r#"{"q":"test"}"#).with_tool_call_id("call_123");
1032
1033        let json = serde_json::to_string(&delta).unwrap();
1034        let parsed: BuiltinToolCallPartDelta = serde_json::from_str(&json).unwrap();
1035
1036        assert_eq!(delta, parsed);
1037    }
1038
1039    #[test]
1040    fn test_model_response_part_delta_apply_builtin() {
1041        let mut part = ModelResponsePart::BuiltinToolCall(BuiltinToolCallPart::new(
1042            "search",
1043            serde_json::json!({}),
1044        ));
1045        let delta = ModelResponsePartDelta::BuiltinToolCall(BuiltinToolCallPartDelta::new(
1046            r#"{"q":"rust"}"#,
1047        ));
1048
1049        assert!(delta.apply(&mut part));
1050
1051        if let ModelResponsePart::BuiltinToolCall(ref builtin) = part {
1052            assert!(builtin.args.to_json_string().unwrap().contains("rust"));
1053        } else {
1054            panic!("Expected BuiltinToolCall part");
1055        }
1056    }
1057}