Skip to main content

pi_ai/
types.rs

1//! Wire-compatible model, message, tool, and streaming event contracts.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use serde::{Deserialize, Serialize};
7use serde_json::{Map, Number, Value};
8
9use crate::text::SharedText;
10
11/// Open API identifier used to select a provider transport implementation.
12pub type Api = String;
13
14/// Open provider identifier used to select credentials and model catalogs.
15pub type ProviderId = String;
16
17/// User-selectable reasoning effort.
18#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
19#[serde(rename_all = "lowercase")]
20pub enum ThinkingLevel {
21    /// Minimal reasoning effort.
22    Minimal,
23    /// Low reasoning effort.
24    Low,
25    /// Medium reasoning effort.
26    Medium,
27    /// High reasoning effort.
28    High,
29    /// Extra-high reasoning effort.
30    Xhigh,
31    /// Maximum reasoning effort.
32    Max,
33}
34
35/// Reasoning effort supported by a model, including disabled reasoning.
36#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
37#[serde(rename_all = "lowercase")]
38pub enum ModelThinkingLevel {
39    /// Reasoning is disabled.
40    Off,
41    /// Minimal reasoning effort.
42    Minimal,
43    /// Low reasoning effort.
44    Low,
45    /// Medium reasoning effort.
46    Medium,
47    /// High reasoning effort.
48    High,
49    /// Extra-high reasoning effort.
50    Xhigh,
51    /// Maximum reasoning effort.
52    Max,
53}
54
55/// Provider-specific values for model thinking levels.
56///
57/// A missing key uses the provider default, while a present `None` value marks
58/// that level as unsupported and is encoded as JSON `null`.
59pub type ThinkingLevelMap = BTreeMap<ModelThinkingLevel, Option<String>>;
60
61/// Prompt-cache retention preference.
62#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
63#[serde(rename_all = "lowercase")]
64pub enum CacheRetention {
65    /// Disable prompt caching.
66    None,
67    /// Request short-lived prompt caching.
68    Short,
69    /// Request long-lived prompt caching.
70    Long,
71}
72
73/// Streaming transport preference.
74#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
75#[serde(rename_all = "kebab-case")]
76pub enum Transport {
77    /// Server-sent events.
78    Sse,
79    /// A fresh WebSocket connection.
80    Websocket,
81    /// A cached WebSocket connection.
82    WebsocketCached,
83    /// Let the provider choose the transport.
84    Auto,
85}
86
87/// Input modality accepted by a model.
88#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
89#[serde(rename_all = "lowercase")]
90pub enum ModelInput {
91    /// Plain text input.
92    Text,
93    /// Image input.
94    Image,
95}
96
97/// Terminal reason recorded on an assistant message.
98#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
99pub enum StopReason {
100    /// The provider completed normally.
101    #[serde(rename = "stop")]
102    Stop,
103    /// The provider reached its output limit.
104    #[serde(rename = "length")]
105    Length,
106    /// The provider requested one or more tools.
107    #[serde(rename = "toolUse")]
108    ToolUse,
109    /// The provider failed.
110    #[serde(rename = "error")]
111    Error,
112    /// The request was cancelled.
113    #[serde(rename = "aborted")]
114    Aborted,
115}
116
117/// Successful stream termination reason.
118#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
119pub enum DoneReason {
120    /// The provider completed normally.
121    #[serde(rename = "stop")]
122    Stop,
123    /// The provider reached its output limit.
124    #[serde(rename = "length")]
125    Length,
126    /// The provider requested one or more tools.
127    #[serde(rename = "toolUse")]
128    ToolUse,
129}
130
131/// Failed stream termination reason.
132#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
133#[serde(rename_all = "lowercase")]
134pub enum ErrorReason {
135    /// The request was cancelled.
136    Aborted,
137    /// The provider failed.
138    Error,
139}
140
141#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
142enum TextContentType {
143    #[serde(rename = "text")]
144    Text,
145}
146
147/// A text block in a message.
148#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub struct TextContent {
151    #[serde(rename = "type")]
152    kind: TextContentType,
153    /// UTF-8 text carried by the block.
154    pub text: SharedText,
155    /// Provider-specific text signature or response metadata.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub text_signature: Option<Arc<str>>,
158}
159
160impl TextContent {
161    /// Creates a text block with no provider signature.
162    #[must_use]
163    pub fn new(text: impl Into<SharedText>) -> Self {
164        Self {
165            kind: TextContentType::Text,
166            text: text.into(),
167            text_signature: None,
168        }
169    }
170}
171
172#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
173enum ThinkingContentType {
174    #[serde(rename = "thinking")]
175    Thinking,
176}
177
178/// A provider reasoning block.
179#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct ThinkingContent {
182    #[serde(rename = "type")]
183    kind: ThinkingContentType,
184    /// Human-readable or redacted reasoning text.
185    pub thinking: SharedText,
186    /// Provider-specific reasoning signature or encrypted payload.
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub thinking_signature: Option<Arc<str>>,
189    /// Whether safety filters redacted the reasoning text.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub redacted: Option<bool>,
192}
193
194impl ThinkingContent {
195    /// Creates a reasoning block with no signature or redaction marker.
196    #[must_use]
197    pub fn new(thinking: impl Into<SharedText>) -> Self {
198        Self {
199            kind: ThinkingContentType::Thinking,
200            thinking: thinking.into(),
201            thinking_signature: None,
202            redacted: None,
203        }
204    }
205}
206
207#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
208enum ImageContentType {
209    #[serde(rename = "image")]
210    Image,
211}
212
213/// A base64-encoded image block.
214#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
215#[serde(rename_all = "camelCase")]
216pub struct ImageContent {
217    #[serde(rename = "type")]
218    kind: ImageContentType,
219    /// Base64-encoded image bytes.
220    pub data: String,
221    /// MIME type of the encoded image.
222    pub mime_type: String,
223}
224
225impl ImageContent {
226    /// Creates an image block.
227    #[must_use]
228    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
229        Self {
230            kind: ImageContentType::Image,
231            data: data.into(),
232            mime_type: mime_type.into(),
233        }
234    }
235}
236
237#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
238enum ToolCallType {
239    #[serde(rename = "toolCall")]
240    ToolCall,
241}
242
243/// A tool invocation emitted by an assistant.
244#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
245#[serde(rename_all = "camelCase")]
246pub struct ToolCall {
247    #[serde(rename = "type")]
248    kind: ToolCallType,
249    /// Provider-assigned invocation identifier.
250    pub id: String,
251    /// Registered tool name.
252    pub name: String,
253    /// JSON object passed to the tool.
254    pub arguments: Arc<Map<String, Value>>,
255    /// Google-specific opaque signature for reusing thought context.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub thought_signature: Option<Arc<str>>,
258}
259
260impl ToolCall {
261    /// Creates a tool invocation with object arguments and no thought signature.
262    #[must_use]
263    pub fn new(
264        id: impl Into<String>,
265        name: impl Into<String>,
266        arguments: impl Into<Arc<Map<String, Value>>>,
267    ) -> Self {
268        Self {
269            kind: ToolCallType::ToolCall,
270            id: id.into(),
271            name: name.into(),
272            arguments: arguments.into(),
273            thought_signature: None,
274        }
275    }
276}
277
278/// Monetary cost associated with one usage record.
279#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct UsageCost {
282    /// Input-token cost in US dollars.
283    #[serde(default)]
284    pub input: f64,
285    /// Output-token cost in US dollars.
286    #[serde(default)]
287    pub output: f64,
288    /// Cache-read cost in US dollars.
289    #[serde(default)]
290    pub cache_read: f64,
291    /// Cache-write cost in US dollars.
292    #[serde(default)]
293    pub cache_write: f64,
294    /// Total request cost in US dollars.
295    #[serde(default)]
296    pub total: f64,
297}
298
299/// Token usage and monetary cost for an assistant response.
300#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
301#[serde(rename_all = "camelCase")]
302pub struct Usage {
303    /// Input tokens consumed.
304    #[serde(default)]
305    pub input: u64,
306    /// Output tokens produced, including reasoning tokens.
307    #[serde(default)]
308    pub output: u64,
309    /// Cached input tokens read.
310    #[serde(default)]
311    pub cache_read: u64,
312    /// Input tokens written to cache.
313    #[serde(default)]
314    pub cache_write: u64,
315    /// Cache-write tokens stored with one-hour retention, when reported.
316    #[serde(default, skip_serializing_if = "Option::is_none")]
317    pub cache_write1h: Option<u64>,
318    /// Reasoning tokens, as a subset of output tokens, when reported.
319    #[serde(default, skip_serializing_if = "Option::is_none")]
320    pub reasoning: Option<u64>,
321    /// Total tokens reported by the provider.
322    #[serde(default)]
323    pub total_tokens: u64,
324    /// Monetary cost for the request.
325    #[serde(default)]
326    pub cost: UsageCost,
327}
328
329/// String-or-number error code included in a diagnostic.
330#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
331#[serde(untagged)]
332pub enum DiagnosticCode {
333    /// Textual error code.
334    String(String),
335    /// Numeric error code.
336    Number(Number),
337}
338
339/// Redacted details about an error associated with an assistant message.
340#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
341pub struct DiagnosticErrorInfo {
342    /// Error class or runtime name.
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub name: Option<String>,
345    /// Human-readable error message.
346    pub message: String,
347    /// Redacted stack trace, when available.
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub stack: Option<String>,
350    /// Provider or runtime error code.
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub code: Option<DiagnosticCode>,
353}
354
355/// Redacted provider or runtime diagnostic attached to an assistant message.
356#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
357pub struct AssistantMessageDiagnostic {
358    /// Diagnostic category.
359    #[serde(rename = "type")]
360    pub kind: String,
361    /// Unix timestamp in milliseconds.
362    pub timestamp: i64,
363    /// Structured error information, when the diagnostic represents an error.
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub error: Option<DiagnosticErrorInfo>,
366    /// Additional diagnostic properties.
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub details: Option<Map<String, Value>>,
369}
370
371/// A user message array element.
372#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
373#[serde(untagged)]
374pub enum UserContent {
375    /// Text input.
376    Text(TextContent),
377    /// Image input.
378    Image(ImageContent),
379}
380
381/// Content accepted by a user message.
382#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
383#[serde(untagged)]
384pub enum UserMessageContent {
385    /// A plain text prompt.
386    Text(String),
387    /// Structured text and image blocks.
388    Blocks(Vec<UserContent>),
389}
390
391/// Content emitted by an assistant message.
392#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
393#[serde(untagged)]
394pub enum AssistantContent {
395    /// Text output.
396    Text(TextContent),
397    /// Provider reasoning output.
398    Thinking(ThinkingContent),
399    /// A requested tool invocation.
400    ToolCall(ToolCall),
401}
402
403/// Content returned by a tool.
404#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
405#[serde(untagged)]
406pub enum ToolResultContent {
407    /// Text output from a tool.
408    Text(TextContent),
409    /// Image output from a tool.
410    Image(ImageContent),
411}
412
413#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
414enum UserRole {
415    #[serde(rename = "user")]
416    User,
417}
418
419/// A user-authored conversation message.
420#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
421pub struct UserMessage {
422    role: UserRole,
423    /// User prompt content.
424    pub content: UserMessageContent,
425    /// Unix timestamp in milliseconds.
426    pub timestamp: i64,
427}
428
429impl UserMessage {
430    /// Creates a user message with the required literal role.
431    #[must_use]
432    pub fn new(content: UserMessageContent, timestamp: i64) -> Self {
433        Self {
434            role: UserRole::User,
435            content,
436            timestamp,
437        }
438    }
439}
440
441#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
442enum AssistantRole {
443    #[serde(rename = "assistant")]
444    Assistant,
445}
446
447/// A provider-produced assistant message.
448#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
449#[serde(rename_all = "camelCase")]
450pub struct AssistantMessage {
451    role: AssistantRole,
452    /// Ordered assistant content blocks.
453    pub content: Vec<AssistantContent>,
454    /// API shape used for the request.
455    pub api: Api,
456    /// Provider used for the request.
457    pub provider: ProviderId,
458    /// Requested model identifier.
459    pub model: String,
460    /// Concrete response model when it differs from the requested model.
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub response_model: Option<String>,
463    /// Provider-specific response or message identifier.
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub response_id: Option<String>,
466    /// Redacted provider and runtime diagnostics.
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub diagnostics: Option<Vec<AssistantMessageDiagnostic>>,
469    /// Token usage and cost.
470    pub usage: Usage,
471    /// Terminal response reason.
472    pub stop_reason: StopReason,
473    /// Error description for failed or aborted responses.
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub error_message: Option<String>,
476    /// Unix timestamp in milliseconds.
477    pub timestamp: i64,
478}
479
480impl AssistantMessage {
481    /// Creates an assistant message with the required literal role.
482    #[must_use]
483    pub fn new(
484        api: impl Into<Api>,
485        provider: impl Into<ProviderId>,
486        model: impl Into<String>,
487        timestamp: i64,
488    ) -> Self {
489        Self {
490            role: AssistantRole::Assistant,
491            content: Vec::new(),
492            api: api.into(),
493            provider: provider.into(),
494            model: model.into(),
495            response_model: None,
496            response_id: None,
497            diagnostics: None,
498            usage: Usage::default(),
499            stop_reason: StopReason::Stop,
500            error_message: None,
501            timestamp,
502        }
503    }
504}
505
506#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
507enum ToolResultRole {
508    #[serde(rename = "toolResult")]
509    ToolResult,
510}
511
512/// A tool execution result added to the conversation.
513#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
514#[serde(rename_all = "camelCase")]
515pub struct ToolResultMessage {
516    role: ToolResultRole,
517    /// Identifier of the corresponding tool call.
518    pub tool_call_id: String,
519    /// Name of the invoked tool.
520    pub tool_name: String,
521    /// Text and image output returned by the tool.
522    pub content: Vec<ToolResultContent>,
523    /// Tool-specific structured details.
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub details: Option<Value>,
526    /// Tool names made available after this result.
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub added_tool_names: Option<Vec<String>>,
529    /// Whether tool execution failed.
530    pub is_error: bool,
531    /// Unix timestamp in milliseconds.
532    pub timestamp: i64,
533}
534
535impl ToolResultMessage {
536    /// Creates a tool result with the required literal role.
537    #[must_use]
538    pub fn new(
539        tool_call_id: impl Into<String>,
540        tool_name: impl Into<String>,
541        content: Vec<ToolResultContent>,
542        is_error: bool,
543        timestamp: i64,
544    ) -> Self {
545        Self {
546            role: ToolResultRole::ToolResult,
547            tool_call_id: tool_call_id.into(),
548            tool_name: tool_name.into(),
549            content,
550            details: None,
551            added_tool_names: None,
552            is_error,
553            timestamp,
554        }
555    }
556}
557
558/// A conversation message.
559#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
560#[serde(untagged)]
561pub enum Message {
562    /// User-authored message.
563    User(UserMessage),
564    /// Provider-produced assistant message.
565    Assistant(AssistantMessage),
566    /// Tool execution result.
567    ToolResult(ToolResultMessage),
568}
569
570/// Tool definition made available to a provider.
571#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
572pub struct Tool {
573    /// Unique tool name.
574    pub name: String,
575    /// Human-readable tool description.
576    pub description: String,
577    /// TypeBox-compatible JSON Schema for tool arguments.
578    pub parameters: Value,
579}
580
581/// Complete provider input context.
582#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
583#[serde(rename_all = "camelCase")]
584pub struct Context {
585    /// Optional system instruction.
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub system_prompt: Option<String>,
588    /// Ordered conversation history.
589    pub messages: Vec<Message>,
590    /// Optional tool definitions.
591    #[serde(skip_serializing_if = "Option::is_none")]
592    pub tools: Option<Vec<Tool>>,
593}
594
595/// Per-million-token model prices.
596#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
597#[serde(rename_all = "camelCase")]
598pub struct ModelCostRates {
599    /// Input-token price in US dollars per million tokens.
600    pub input: f64,
601    /// Output-token price in US dollars per million tokens.
602    pub output: f64,
603    /// Cache-read price in US dollars per million tokens.
604    pub cache_read: f64,
605    /// Cache-write price in US dollars per million tokens.
606    pub cache_write: f64,
607}
608
609/// Request-wide model pricing tier.
610#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
611#[serde(rename_all = "camelCase")]
612pub struct ModelCostTier {
613    /// Input-token price in US dollars per million tokens.
614    pub input: f64,
615    /// Output-token price in US dollars per million tokens.
616    pub output: f64,
617    /// Cache-read price in US dollars per million tokens.
618    pub cache_read: f64,
619    /// Cache-write price in US dollars per million tokens.
620    pub cache_write: f64,
621    /// Input-token threshold above which this tier applies.
622    pub input_tokens_above: u64,
623}
624
625/// Model pricing, optionally including request-wide tiers.
626#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
627#[serde(rename_all = "camelCase")]
628pub struct ModelCost {
629    /// Input-token price in US dollars per million tokens.
630    pub input: f64,
631    /// Output-token price in US dollars per million tokens.
632    pub output: f64,
633    /// Cache-read price in US dollars per million tokens.
634    pub cache_read: f64,
635    /// Cache-write price in US dollars per million tokens.
636    pub cache_write: f64,
637    /// Request-wide pricing tiers.
638    #[serde(skip_serializing_if = "Option::is_none")]
639    pub tiers: Option<Vec<ModelCostTier>>,
640}
641
642/// Provider model metadata, including preserved compatibility extensions.
643#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
644#[serde(rename_all = "camelCase")]
645pub struct Model {
646    /// Provider model identifier.
647    pub id: String,
648    /// Display name.
649    pub name: String,
650    /// API shape used by the model.
651    pub api: Api,
652    /// Provider identifier.
653    pub provider: ProviderId,
654    /// Provider endpoint base URL.
655    pub base_url: String,
656    /// Whether the model supports reasoning.
657    pub reasoning: bool,
658    /// Provider-specific mapping of supported thinking levels.
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub thinking_level_map: Option<ThinkingLevelMap>,
661    /// Accepted input modalities.
662    pub input: Vec<ModelInput>,
663    /// Model pricing.
664    pub cost: ModelCost,
665    /// Context-window size in tokens.
666    pub context_window: u64,
667    /// Maximum output tokens.
668    pub max_tokens: u64,
669    /// Additional static request headers.
670    #[serde(skip_serializing_if = "Option::is_none")]
671    pub headers: Option<BTreeMap<String, String>>,
672    /// API-specific compatibility settings preserved without reshaping.
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub compat: Option<Value>,
675    /// Unknown catalog fields preserved across round trips.
676    #[serde(flatten)]
677    pub extra: BTreeMap<String, Value>,
678}
679
680/// Semantic events emitted while assembling an assistant message.
681#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
682#[serde(tag = "type")]
683pub enum AssistantMessageEvent {
684    /// Begins a response stream.
685    #[serde(rename = "start")]
686    Start {
687        /// Current assistant message snapshot.
688        partial: AssistantMessage,
689    },
690    /// Begins a text block.
691    #[serde(rename = "text_start")]
692    TextStart {
693        /// Index of the content block being updated.
694        #[serde(rename = "contentIndex")]
695        content_index: u64,
696        /// Current assistant message snapshot.
697        partial: AssistantMessage,
698    },
699    /// Appends text to a text block.
700    #[serde(rename = "text_delta")]
701    TextDelta {
702        /// Index of the content block being updated.
703        #[serde(rename = "contentIndex")]
704        content_index: u64,
705        /// Newly emitted text.
706        delta: String,
707        /// Current assistant message snapshot.
708        partial: AssistantMessage,
709    },
710    /// Completes a text block.
711    #[serde(rename = "text_end")]
712    TextEnd {
713        /// Index of the completed content block.
714        #[serde(rename = "contentIndex")]
715        content_index: u64,
716        /// Complete text content.
717        content: String,
718        /// Current assistant message snapshot.
719        partial: AssistantMessage,
720    },
721    /// Begins a reasoning block.
722    #[serde(rename = "thinking_start")]
723    ThinkingStart {
724        /// Index of the content block being updated.
725        #[serde(rename = "contentIndex")]
726        content_index: u64,
727        /// Current assistant message snapshot.
728        partial: AssistantMessage,
729    },
730    /// Appends text to a reasoning block.
731    #[serde(rename = "thinking_delta")]
732    ThinkingDelta {
733        /// Index of the content block being updated.
734        #[serde(rename = "contentIndex")]
735        content_index: u64,
736        /// Newly emitted reasoning text.
737        delta: String,
738        /// Current assistant message snapshot.
739        partial: AssistantMessage,
740    },
741    /// Completes a reasoning block.
742    #[serde(rename = "thinking_end")]
743    ThinkingEnd {
744        /// Index of the completed content block.
745        #[serde(rename = "contentIndex")]
746        content_index: u64,
747        /// Complete reasoning text.
748        content: String,
749        /// Current assistant message snapshot.
750        partial: AssistantMessage,
751    },
752    /// Begins a tool call block.
753    #[serde(rename = "toolcall_start")]
754    ToolCallStart {
755        /// Index of the content block being updated.
756        #[serde(rename = "contentIndex")]
757        content_index: u64,
758        /// Current assistant message snapshot.
759        partial: AssistantMessage,
760    },
761    /// Appends serialized arguments to a tool call block.
762    #[serde(rename = "toolcall_delta")]
763    ToolCallDelta {
764        /// Index of the content block being updated.
765        #[serde(rename = "contentIndex")]
766        content_index: u64,
767        /// Newly emitted serialized argument fragment.
768        delta: String,
769        /// Current assistant message snapshot.
770        partial: AssistantMessage,
771    },
772    /// Completes a tool call block.
773    #[serde(rename = "toolcall_end")]
774    ToolCallEnd {
775        /// Index of the completed content block.
776        #[serde(rename = "contentIndex")]
777        content_index: u64,
778        /// Completed tool call.
779        #[serde(rename = "toolCall")]
780        tool_call: ToolCall,
781        /// Current assistant message snapshot.
782        partial: AssistantMessage,
783    },
784    /// Completes a successful response stream.
785    #[serde(rename = "done")]
786    Done {
787        /// Successful termination reason.
788        reason: DoneReason,
789        /// Final assistant message.
790        message: AssistantMessage,
791    },
792    /// Completes a failed or cancelled response stream.
793    #[serde(rename = "error")]
794    Error {
795        /// Failure termination reason.
796        reason: ErrorReason,
797        /// Final assistant message.
798        error: AssistantMessage,
799    },
800}
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805    use serde_json::json;
806
807    fn assistant() -> AssistantMessage {
808        AssistantMessage::new("custom-api", "custom-provider", "model", 1_700_000_000_000)
809    }
810
811    #[test]
812    fn sibling_content_tags_are_literal() -> Result<(), Box<dyn std::error::Error>> {
813        let text = TextContent::new("hello");
814        assert_eq!(
815            serde_json::to_value(text)?,
816            json!({"type": "text", "text": "hello"})
817        );
818        assert!(
819            serde_json::from_value::<TextContent>(json!({
820                "type": "image",
821                "text": "hello"
822            }))
823            .is_err()
824        );
825
826        let image = ImageContent::new("AA==", "image/png");
827        assert_eq!(
828            serde_json::to_value(image)?,
829            json!({"type": "image", "data": "AA==", "mimeType": "image/png"})
830        );
831        Ok(())
832    }
833
834    #[test]
835    fn sibling_message_roles_are_literal() -> Result<(), Box<dyn std::error::Error>> {
836        let message = UserMessage::new(UserMessageContent::Text("hi".into()), 7);
837        assert_eq!(
838            serde_json::to_value(message)?,
839            json!({"role": "user", "content": "hi", "timestamp": 7})
840        );
841        assert!(
842            serde_json::from_value::<UserMessage>(json!({
843                "role": "assistant",
844                "content": "hi",
845                "timestamp": 7
846            }))
847            .is_err()
848        );
849
850        let assistant = Message::Assistant(assistant());
851        let assistant_json = serde_json::to_value(&assistant)?;
852        assert_eq!(assistant_json["role"], "assistant");
853        assert_eq!(
854            serde_json::from_value::<Message>(assistant_json)?,
855            assistant
856        );
857
858        let tool_result = Message::ToolResult(ToolResultMessage::new(
859            "call-1",
860            "read",
861            Vec::new(),
862            false,
863            8,
864        ));
865        let tool_result_json = serde_json::to_value(&tool_result)?;
866        assert_eq!(tool_result_json["role"], "toolResult");
867        assert_eq!(
868            serde_json::from_value::<Message>(tool_result_json)?,
869            tool_result
870        );
871        Ok(())
872    }
873
874    #[test]
875    fn events_use_exact_tags_fields_and_tool_use() -> Result<(), Box<dyn std::error::Error>> {
876        let events = [
877            AssistantMessageEvent::Start {
878                partial: assistant(),
879            },
880            AssistantMessageEvent::TextStart {
881                content_index: 0,
882                partial: assistant(),
883            },
884            AssistantMessageEvent::TextDelta {
885                content_index: 0,
886                delta: "x".into(),
887                partial: assistant(),
888            },
889            AssistantMessageEvent::TextEnd {
890                content_index: 0,
891                content: "x".into(),
892                partial: assistant(),
893            },
894            AssistantMessageEvent::ThinkingStart {
895                content_index: 1,
896                partial: assistant(),
897            },
898            AssistantMessageEvent::ThinkingDelta {
899                content_index: 1,
900                delta: "x".into(),
901                partial: assistant(),
902            },
903            AssistantMessageEvent::ThinkingEnd {
904                content_index: 1,
905                content: "x".into(),
906                partial: assistant(),
907            },
908            AssistantMessageEvent::ToolCallStart {
909                content_index: 2,
910                partial: assistant(),
911            },
912            AssistantMessageEvent::ToolCallDelta {
913                content_index: 2,
914                delta: "{}".into(),
915                partial: assistant(),
916            },
917            AssistantMessageEvent::ToolCallEnd {
918                content_index: 2,
919                tool_call: ToolCall::new("call-1", "read", Map::new()),
920                partial: assistant(),
921            },
922            AssistantMessageEvent::Done {
923                reason: DoneReason::ToolUse,
924                message: assistant(),
925            },
926            AssistantMessageEvent::Error {
927                reason: ErrorReason::Error,
928                error: assistant(),
929            },
930        ];
931        let encoded = events
932            .into_iter()
933            .map(serde_json::to_value)
934            .collect::<Result<Vec<_>, _>>()?;
935        let tags = encoded
936            .iter()
937            .map(|event| &event["type"])
938            .collect::<Vec<_>>();
939        assert_eq!(
940            tags,
941            [
942                "start",
943                "text_start",
944                "text_delta",
945                "text_end",
946                "thinking_start",
947                "thinking_delta",
948                "thinking_end",
949                "toolcall_start",
950                "toolcall_delta",
951                "toolcall_end",
952                "done",
953                "error",
954            ]
955        );
956        assert_eq!(encoded[9]["contentIndex"], 2);
957        assert_eq!(encoded[9]["toolCall"]["type"], "toolCall");
958        assert_eq!(encoded[10]["reason"], "toolUse");
959        assert_eq!(encoded[10]["message"]["role"], "assistant");
960        assert!(encoded[10].get("error").is_none());
961        assert_eq!(encoded[11]["error"]["role"], "assistant");
962        assert!(encoded[11].get("message").is_none());
963        Ok(())
964    }
965
966    #[test]
967    fn optional_fields_are_omitted() -> Result<(), Box<dyn std::error::Error>> {
968        let value = serde_json::to_value(assistant())?;
969        for key in ["responseModel", "responseId", "diagnostics", "errorMessage"] {
970            assert!(value.get(key).is_none(), "unexpected field {key}");
971        }
972        assert!(value["usage"].get("cacheWrite1h").is_none());
973        assert!(value["usage"].get("reasoning").is_none());
974        Ok(())
975    }
976
977    #[test]
978    fn thinking_level_map_preserves_null_values() -> Result<(), Box<dyn std::error::Error>> {
979        let map: ThinkingLevelMap = serde_json::from_value(json!({
980            "off": null,
981            "high": "high"
982        }))?;
983        assert_eq!(
984            serde_json::to_value(map)?,
985            json!({"off": null, "high": "high"})
986        );
987        Ok(())
988    }
989
990    #[test]
991    fn model_preserves_unknown_fields() -> Result<(), Box<dyn std::error::Error>> {
992        let input = json!({
993            "id": "m",
994            "name": "Model",
995            "api": "future-api",
996            "provider": "future-provider",
997            "baseUrl": "https://example.test",
998            "reasoning": false,
999            "input": ["text"],
1000            "cost": {"input": 0.0, "output": 0.0, "cacheRead": 0.0, "cacheWrite": 0.0},
1001            "contextWindow": 1000,
1002            "maxTokens": 100,
1003            "futureField": {"nested": true}
1004        });
1005        let model: Model = serde_json::from_value(input.clone())?;
1006        assert_eq!(serde_json::to_value(model)?, input);
1007        Ok(())
1008    }
1009
1010    #[test]
1011    fn tool_arguments_must_be_objects() -> Result<(), Box<dyn std::error::Error>> {
1012        let input = json!({
1013            "type": "toolCall",
1014            "id": "1",
1015            "name": "read",
1016            "arguments": {"path": "a.txt"}
1017        });
1018        let valid: ToolCall = serde_json::from_value(input.clone())?;
1019        assert_eq!(serde_json::to_value(valid)?, input);
1020
1021        for invalid in [json!(null), json!([]), json!("x"), json!(1)] {
1022            assert!(
1023                serde_json::from_value::<ToolCall>(json!({
1024                    "type": "toolCall",
1025                    "id": "1",
1026                    "name": "read",
1027                    "arguments": invalid
1028                }))
1029                .is_err()
1030            );
1031        }
1032        Ok(())
1033    }
1034
1035    #[test]
1036    fn done_and_error_reasons_reject_the_other_domain() -> Result<(), Box<dyn std::error::Error>> {
1037        assert!(serde_json::from_value::<DoneReason>(json!("error")).is_err());
1038        assert!(serde_json::from_value::<DoneReason>(json!("aborted")).is_err());
1039        assert!(serde_json::from_value::<ErrorReason>(json!("stop")).is_err());
1040        assert!(serde_json::from_value::<ErrorReason>(json!("toolUse")).is_err());
1041
1042        let mut invalid_done = serde_json::to_value(AssistantMessageEvent::Done {
1043            reason: DoneReason::Stop,
1044            message: assistant(),
1045        })?;
1046        invalid_done["reason"] = json!("error");
1047        assert!(serde_json::from_value::<AssistantMessageEvent>(invalid_done).is_err());
1048        Ok(())
1049    }
1050}