Skip to main content

rig_core/completion/
message.rs

1use std::{convert::Infallible, str::FromStr};
2
3use crate::OneOrMany;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use super::CompletionError;
8
9// ================================================================
10// Message models
11// ================================================================
12
13/// A provider-agnostic chat message.
14///
15/// Messages are role-tagged and may contain one or many content items, including
16/// text, images, audio, documents, tool calls, and tool results. Provider modules
17/// are responsible for translating these generic messages into provider-native
18/// request bodies. That conversion may be lossy when a provider does not support
19/// a particular content type.
20#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
21#[serde(tag = "role", rename_all = "lowercase")]
22pub enum Message {
23    /// System message containing instruction text.
24    System { content: String },
25
26    /// User message containing one or more content types defined by `UserContent`.
27    User { content: OneOrMany<UserContent> },
28
29    /// Assistant message containing one or more content types defined by `AssistantContent`.
30    Assistant {
31        /// Provider-assigned assistant message ID, when available.
32        id: Option<String>,
33        content: OneOrMany<AssistantContent>,
34    },
35}
36
37/// Describes the content of a message, which can be text, a tool result, an image, audio, or
38///  a document. Dependent on provider supporting the content type. Multimedia content is generally
39///  base64 (defined by it's format) encoded but additionally supports urls (for some providers).
40#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
41#[serde(tag = "type", rename_all = "lowercase")]
42pub enum UserContent {
43    /// Plain text user content.
44    Text(Text),
45    /// Result of a tool call returned as user-visible context to the model.
46    ToolResult(ToolResult),
47    /// Image content.
48    Image(Image),
49    /// Audio content.
50    Audio(Audio),
51    /// Video content.
52    Video(Video),
53    /// Document content.
54    Document(Document),
55}
56
57/// Describes responses from a provider which is either text or a tool call.
58#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
59#[serde(untagged)]
60pub enum AssistantContent {
61    /// Plain assistant text.
62    Text(Text),
63    /// Tool call requested by the assistant.
64    ToolCall(ToolCall),
65    /// Structured reasoning emitted by the assistant.
66    Reasoning(Reasoning),
67    /// Image content emitted by the assistant.
68    Image(Image),
69}
70
71#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
72#[serde(tag = "type", content = "content", rename_all = "snake_case")]
73#[non_exhaustive]
74/// A typed reasoning block used by providers that emit structured thinking data.
75pub enum ReasoningContent {
76    /// Plain reasoning text with an optional provider signature.
77    Text {
78        text: String,
79        #[serde(skip_serializing_if = "Option::is_none")]
80        signature: Option<String>,
81    },
82    /// Provider-encrypted reasoning payload.
83    Encrypted(String),
84    /// Redacted reasoning payload preserved as opaque data.
85    Redacted { data: String },
86    /// Provider-generated reasoning summary text.
87    Summary(String),
88}
89
90#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
91#[non_exhaustive]
92/// Assistant reasoning payload with an optional provider-supplied identifier.
93pub struct Reasoning {
94    /// Provider reasoning identifier, when supplied by the upstream API.
95    pub id: Option<String>,
96    /// Ordered reasoning content blocks.
97    pub content: Vec<ReasoningContent>,
98}
99
100impl Reasoning {
101    /// Create a new reasoning item from a single item
102    pub fn new(input: &str) -> Self {
103        Self::new_with_signature(input, None)
104    }
105
106    /// Create a new reasoning item from a single text item and optional signature.
107    pub fn new_with_signature(input: &str, signature: Option<String>) -> Self {
108        Self {
109            id: None,
110            content: vec![ReasoningContent::Text {
111                text: input.to_string(),
112                signature,
113            }],
114        }
115    }
116
117    /// Set or clear the provider reasoning ID.
118    pub fn optional_id(mut self, id: Option<String>) -> Self {
119        self.id = id;
120        self
121    }
122
123    /// Set a provider reasoning ID.
124    pub fn with_id(mut self, id: String) -> Self {
125        self.id = Some(id);
126        self
127    }
128
129    /// Create reasoning content from multiple text blocks.
130    pub fn multi(input: Vec<String>) -> Self {
131        Self {
132            id: None,
133            content: input
134                .into_iter()
135                .map(|text| ReasoningContent::Text {
136                    text,
137                    signature: None,
138                })
139                .collect(),
140        }
141    }
142
143    /// Create a redacted reasoning block.
144    pub fn redacted(data: impl Into<String>) -> Self {
145        Self {
146            id: None,
147            content: vec![ReasoningContent::Redacted { data: data.into() }],
148        }
149    }
150
151    /// Create an encrypted reasoning block.
152    pub fn encrypted(data: impl Into<String>) -> Self {
153        Self {
154            id: None,
155            content: vec![ReasoningContent::Encrypted(data.into())],
156        }
157    }
158
159    /// Create one reasoning block containing summary items.
160    pub fn summaries(input: Vec<String>) -> Self {
161        Self {
162            id: None,
163            content: input.into_iter().map(ReasoningContent::Summary).collect(),
164        }
165    }
166
167    /// Render reasoning as displayable text by joining text-like blocks with newlines.
168    pub fn display_text(&self) -> String {
169        self.content
170            .iter()
171            .filter_map(|content| match content {
172                ReasoningContent::Text { text, .. } => Some(text.as_str()),
173                ReasoningContent::Summary(summary) => Some(summary.as_str()),
174                ReasoningContent::Redacted { data } => Some(data.as_str()),
175                ReasoningContent::Encrypted(_) => None,
176            })
177            .collect::<Vec<_>>()
178            .join("\n")
179    }
180
181    /// Return the first text reasoning block, if present.
182    pub fn first_text(&self) -> Option<&str> {
183        self.content.iter().find_map(|content| match content {
184            ReasoningContent::Text { text, .. } => Some(text.as_str()),
185            _ => None,
186        })
187    }
188
189    /// Return the first signature from text reasoning, if present.
190    pub fn first_signature(&self) -> Option<&str> {
191        self.content.iter().find_map(|content| match content {
192            ReasoningContent::Text {
193                signature: Some(signature),
194                ..
195            } => Some(signature.as_str()),
196            _ => None,
197        })
198    }
199
200    /// Return the first encrypted reasoning payload, if present.
201    pub fn encrypted_content(&self) -> Option<&str> {
202        self.content.iter().find_map(|content| match content {
203            ReasoningContent::Encrypted(data) => Some(data.as_str()),
204            _ => None,
205        })
206    }
207}
208
209/// Tool result content containing information about a tool call and it's resulting content.
210#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
211pub struct ToolResult {
212    /// Tool call ID that this result answers.
213    pub id: String,
214    /// Provider-specific call ID, when distinct from `id`.
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub call_id: Option<String>,
217    /// One or more content items produced by the tool.
218    pub content: OneOrMany<ToolResultContent>,
219}
220
221/// Describes one typed item in a tool result.
222#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
223#[serde(tag = "type", rename_all = "lowercase")]
224pub enum ToolResultContent {
225    /// Literal text. Providers must not reinterpret it as structured JSON.
226    Text(Text),
227    /// An image supplied explicitly by the tool.
228    Image(Image),
229    /// Structured JSON supplied explicitly by the tool runtime.
230    Json {
231        /// The structured value.
232        value: serde_json::Value,
233    },
234}
235
236impl ToolResultContent {
237    /// Borrow literal text content.
238    pub fn as_text(&self) -> Option<&str> {
239        match self {
240            Self::Text(text) => Some(&text.text),
241            Self::Image(_) | Self::Json { .. } => None,
242        }
243    }
244
245    /// Borrow structured JSON content.
246    pub fn as_json(&self) -> Option<&serde_json::Value> {
247        match self {
248            Self::Json { value } => Some(value),
249            Self::Text(_) | Self::Image(_) => None,
250        }
251    }
252
253    /// Deserialize JSON content into a typed value.
254    ///
255    /// Structured JSON is decoded directly. Literal text is parsed only because
256    /// the caller explicitly requested JSON decoding, which supports transcripts
257    /// recorded before structured tool output was preserved canonically. This
258    /// helper never changes the content sent to a model or provider.
259    pub fn deserialize_json<T>(&self) -> Result<T, serde_json::Error>
260    where
261        T: serde::de::DeserializeOwned,
262    {
263        match self {
264            Self::Json { value } => serde_json::from_value(value.clone()),
265            Self::Text(text) => serde_json::from_str(&text.text),
266            Self::Image(_) => Err(<serde_json::Error as serde::de::Error>::custom(
267                "cannot decode image tool-result content as JSON",
268            )),
269        }
270    }
271}
272
273/// Describes a tool call with an id and function to call, generally produced by a provider.
274#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
275pub struct ToolCall {
276    /// Provider-supplied tool call ID.
277    pub id: String,
278    /// Provider-specific call ID used by some APIs for tool result correlation.
279    pub call_id: Option<String>,
280    /// Function name and JSON arguments requested by the model.
281    pub function: ToolFunction,
282    /// Optional cryptographic signature for the tool call.
283    ///
284    /// This field is used by some providers (e.g., Google) to provide a signature
285    /// that can verify the authenticity and integrity of the tool call. When present,
286    /// it allows verification that the tool call was actually generated by the model
287    /// and has not been tampered with.
288    ///
289    /// This is an optional, provider-specific feature and will be `None` for providers
290    /// that don't support tool call signatures.
291    pub signature: Option<String>,
292    /// Additional provider-specific parameters to be sent to the completion model provider
293    pub additional_params: Option<serde_json::Value>,
294}
295
296impl ToolCall {
297    pub fn new(id: String, function: ToolFunction) -> Self {
298        Self {
299            id,
300            call_id: None,
301            function,
302            signature: None,
303            additional_params: None,
304        }
305    }
306
307    pub fn with_call_id(mut self, call_id: String) -> Self {
308        self.call_id = Some(call_id);
309        self
310    }
311
312    pub fn with_signature(mut self, signature: Option<String>) -> Self {
313        self.signature = signature;
314        self
315    }
316
317    pub fn with_additional_params(mut self, additional_params: Option<serde_json::Value>) -> Self {
318        self.additional_params = additional_params;
319        self
320    }
321}
322
323/// Describes a tool function to call with a name and arguments, generally produced by a provider.
324#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
325pub struct ToolFunction {
326    /// Tool/function name to invoke.
327    pub name: String,
328    /// JSON arguments for the tool/function.
329    pub arguments: serde_json::Value,
330}
331
332impl ToolFunction {
333    /// Create a tool function call payload.
334    pub fn new(name: String, arguments: serde_json::Value) -> Self {
335        Self { name, arguments }
336    }
337}
338
339// ================================================================
340// Base content models
341// ================================================================
342
343/// Basic text content.
344///
345/// `additional_params` carries provider-specific fields that arrive on text
346/// content blocks (e.g. Anthropic returns citation metadata on assistant text
347/// blocks). It is flattened during serialization so the inner JSON keys appear
348/// at the same level as `text`, matching the wire format of provider APIs.
349#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
350pub struct Text {
351    /// Text content.
352    pub text: String,
353    /// Provider-specific text fields.
354    #[serde(flatten, skip_serializing_if = "Option::is_none")]
355    pub additional_params: Option<serde_json::Value>,
356}
357
358impl Text {
359    /// Construct a new text block with no provider-specific fields.
360    pub fn new(text: impl Into<String>) -> Self {
361        Self {
362            text: text.into(),
363            additional_params: None,
364        }
365    }
366
367    /// Returns the inner text string.
368    pub fn text(&self) -> &str {
369        &self.text
370    }
371}
372
373impl std::fmt::Display for Text {
374    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375        let Self { text, .. } = self;
376        write!(f, "{text}")
377    }
378}
379
380/// Image content containing image data and metadata about it.
381#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
382pub struct Image {
383    /// Image source data.
384    pub data: DocumentSourceKind,
385    /// Image media type, if known.
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub media_type: Option<ImageMediaType>,
388    /// Provider-specific image detail preference.
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub detail: Option<ImageDetail>,
391    /// Provider-specific image fields.
392    #[serde(flatten, skip_serializing_if = "Option::is_none")]
393    pub additional_params: Option<serde_json::Value>,
394}
395
396impl Image {
397    pub fn try_into_url(self) -> Result<String, MessageError> {
398        match self.data {
399            DocumentSourceKind::Url(url) => Ok(url),
400            DocumentSourceKind::Base64(data) => {
401                let Some(media_type) = self.media_type else {
402                    return Err(MessageError::ConversionError(
403                        "A media type is required to create a valid base64-encoded image URL"
404                            .to_string(),
405                    ));
406                };
407
408                Ok(format!(
409                    "data:image/{ty};base64,{data}",
410                    ty = media_type.to_mime_type()
411                ))
412            }
413            unknown => Err(MessageError::ConversionError(format!(
414                "Tried to convert unknown type to a URL: {unknown:?}"
415            ))),
416        }
417    }
418}
419
420/// The kind of image source (to be used).
421#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
422#[serde(tag = "type", content = "value", rename_all = "camelCase")]
423#[non_exhaustive]
424pub enum DocumentSourceKind {
425    /// A file URL/URI.
426    Url(String),
427    /// A base-64 encoded string.
428    Base64(String),
429    /// A provider-side uploaded file identifier.
430    FileId(String),
431    /// Raw bytes
432    Raw(Vec<u8>),
433    /// A string (or a string literal).
434    String(String),
435    #[default]
436    /// An unknown file source (there's nothing there).
437    Unknown,
438}
439
440impl DocumentSourceKind {
441    /// Create a URL-backed source.
442    pub fn url(url: &str) -> Self {
443        Self::Url(url.to_string())
444    }
445
446    /// Create a base64-backed source.
447    pub fn base64(base64_string: &str) -> Self {
448        Self::Base64(base64_string.to_string())
449    }
450
451    /// Create a provider file ID-backed source.
452    pub fn file_id(file_id: &str) -> Self {
453        Self::FileId(file_id.to_string())
454    }
455
456    /// Create a raw byte source.
457    pub fn raw(bytes: impl Into<Vec<u8>>) -> Self {
458        Self::Raw(bytes.into())
459    }
460
461    /// Create a string-backed source.
462    pub fn string(input: &str) -> Self {
463        Self::String(input.into())
464    }
465
466    /// Create an unknown source placeholder.
467    pub fn unknown() -> Self {
468        Self::Unknown
469    }
470
471    /// Return the contained URL, base64 string, or file ID, if this source stores one.
472    pub fn try_into_inner(self) -> Option<String> {
473        match self {
474            Self::Url(s) | Self::Base64(s) | Self::FileId(s) => Some(s),
475            _ => None,
476        }
477    }
478}
479
480impl std::fmt::Display for DocumentSourceKind {
481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482        match self {
483            Self::Url(string) => write!(f, "{string}"),
484            Self::Base64(string) => write!(f, "{string}"),
485            Self::FileId(string) => write!(f, "{string}"),
486            Self::String(string) => write!(f, "{string}"),
487            Self::Raw(_) => write!(f, "<binary data>"),
488            Self::Unknown => write!(f, "<unknown>"),
489        }
490    }
491}
492
493/// Audio content containing audio data and metadata about it.
494#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
495pub struct Audio {
496    /// Audio source data.
497    pub data: DocumentSourceKind,
498    /// Audio media type, if known.
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub media_type: Option<AudioMediaType>,
501    /// Provider-specific audio fields.
502    #[serde(flatten, skip_serializing_if = "Option::is_none")]
503    pub additional_params: Option<serde_json::Value>,
504}
505
506/// Video content containing video data and metadata about it.
507#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
508pub struct Video {
509    /// Video source data.
510    pub data: DocumentSourceKind,
511    /// Video media type, if known.
512    #[serde(skip_serializing_if = "Option::is_none")]
513    pub media_type: Option<VideoMediaType>,
514    /// Provider-specific video fields.
515    #[serde(flatten, skip_serializing_if = "Option::is_none")]
516    pub additional_params: Option<serde_json::Value>,
517}
518
519/// Document content containing document data and metadata about it.
520#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
521pub struct Document {
522    /// Document source data.
523    pub data: DocumentSourceKind,
524    /// Document media type, if known.
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub media_type: Option<DocumentMediaType>,
527    /// Provider-specific document fields.
528    #[serde(flatten, skip_serializing_if = "Option::is_none")]
529    pub additional_params: Option<serde_json::Value>,
530}
531
532/// Describes the format of the content, which can be base64 or string.
533#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
534#[serde(rename_all = "lowercase")]
535pub enum ContentFormat {
536    #[default]
537    Base64,
538    String,
539    Url,
540}
541
542/// Helper enum that tracks the media type of the content.
543#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
544pub enum MediaType {
545    Image(ImageMediaType),
546    Audio(AudioMediaType),
547    Document(DocumentMediaType),
548    Video(VideoMediaType),
549}
550
551/// Describes the image media type of the content. Not every provider supports every media type.
552/// Convertible to and from MIME type strings.
553#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
554#[serde(rename_all = "lowercase")]
555pub enum ImageMediaType {
556    JPEG,
557    PNG,
558    GIF,
559    WEBP,
560    HEIC,
561    HEIF,
562    SVG,
563}
564
565/// Describes the document media type of the content. Not every provider supports every media type.
566/// Includes also programming languages as document types for providers who support code running.
567/// Convertible to and from MIME type strings.
568#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
569#[serde(rename_all = "lowercase")]
570pub enum DocumentMediaType {
571    PDF,
572    TXT,
573    RTF,
574    HTML,
575    CSS,
576    MARKDOWN,
577    CSV,
578    XML,
579    Javascript,
580    Python,
581}
582
583impl DocumentMediaType {
584    pub fn is_code(&self) -> bool {
585        matches!(self, Self::Javascript | Self::Python)
586    }
587}
588
589/// Describes the audio media type of the content. Not every provider supports every media type.
590/// Convertible to and from MIME type strings.
591#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
592#[serde(rename_all = "lowercase")]
593pub enum AudioMediaType {
594    WAV,
595    MP3,
596    AIFF,
597    AAC,
598    OGG,
599    FLAC,
600    M4A,
601    PCM16,
602    PCM24,
603}
604
605/// Describes the video media type of the content. Not every provider supports every media type.
606/// Convertible to and from MIME type strings.
607#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
608#[serde(rename_all = "lowercase")]
609pub enum VideoMediaType {
610    AVI,
611    MP4,
612    MPEG,
613    MOV,
614    WEBM,
615}
616
617/// Describes the detail of the image content, which can be low, high, or auto (open-ai specific).
618#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
619#[serde(rename_all = "lowercase")]
620pub enum ImageDetail {
621    Low,
622    High,
623    #[default]
624    Auto,
625}
626
627// ================================================================
628// Impl. for message models
629// ================================================================
630
631impl Message {
632    /// This helper method is primarily used to extract the first string prompt from a `Message`.
633    /// Since `Message` might have more than just text content, we need to find the first text.
634    pub fn rag_text(&self) -> Option<String> {
635        match self {
636            Message::User { content } => {
637                for item in content.iter() {
638                    if let UserContent::Text(Text { text, .. }) = item {
639                        return Some(text.clone());
640                    }
641                }
642                None
643            }
644            Message::System { .. } => None,
645            _ => None,
646        }
647    }
648
649    /// Helper constructor to make creating system messages easier.
650    pub fn system(text: impl Into<String>) -> Self {
651        Message::System {
652            content: text.into(),
653        }
654    }
655
656    /// Helper constructor to make creating user messages easier.
657    pub fn user(text: impl Into<String>) -> Self {
658        Message::User {
659            content: OneOrMany::one(UserContent::text(text)),
660        }
661    }
662
663    /// Helper constructor to make creating assistant messages easier.
664    pub fn assistant(text: impl Into<String>) -> Self {
665        Message::Assistant {
666            id: None,
667            content: OneOrMany::one(AssistantContent::text(text)),
668        }
669    }
670
671    /// Helper constructor to make creating assistant messages easier.
672    pub fn assistant_with_id(id: String, text: impl Into<String>) -> Self {
673        Message::Assistant {
674            id: Some(id),
675            content: OneOrMany::one(AssistantContent::text(text)),
676        }
677    }
678
679    /// Helper constructor to make creating tool result messages easier.
680    pub fn tool_result(id: impl Into<String>, content: impl Into<String>) -> Self {
681        Message::User {
682            content: OneOrMany::one(UserContent::ToolResult(ToolResult {
683                id: id.into(),
684                call_id: None,
685                content: OneOrMany::one(ToolResultContent::text(content)),
686            })),
687        }
688    }
689
690    pub fn tool_result_with_call_id(
691        id: impl Into<String>,
692        call_id: Option<String>,
693        content: impl Into<String>,
694    ) -> Self {
695        Message::User {
696            content: OneOrMany::one(UserContent::ToolResult(ToolResult {
697                id: id.into(),
698                call_id,
699                content: OneOrMany::one(ToolResultContent::text(content)),
700            })),
701        }
702    }
703}
704
705impl UserContent {
706    /// Helper constructor to make creating user text content easier.
707    pub fn text(text: impl Into<String>) -> Self {
708        UserContent::Text(text.into().into())
709    }
710
711    /// Helper constructor to make creating user image content easier.
712    pub fn image_base64(
713        data: impl Into<String>,
714        media_type: Option<ImageMediaType>,
715        detail: Option<ImageDetail>,
716    ) -> Self {
717        UserContent::Image(Image {
718            data: DocumentSourceKind::Base64(data.into()),
719            media_type,
720            detail,
721            additional_params: None,
722        })
723    }
724
725    /// Helper constructor to make creating user image content from raw unencoded bytes easier.
726    pub fn image_raw(
727        data: impl Into<Vec<u8>>,
728        media_type: Option<ImageMediaType>,
729        detail: Option<ImageDetail>,
730    ) -> Self {
731        UserContent::Image(Image {
732            data: DocumentSourceKind::Raw(data.into()),
733            media_type,
734            detail,
735            ..Default::default()
736        })
737    }
738
739    /// Helper constructor to make creating user image content easier.
740    pub fn image_url(
741        url: impl Into<String>,
742        media_type: Option<ImageMediaType>,
743        detail: Option<ImageDetail>,
744    ) -> Self {
745        UserContent::Image(Image {
746            data: DocumentSourceKind::Url(url.into()),
747            media_type,
748            detail,
749            additional_params: None,
750        })
751    }
752
753    /// Helper constructor to make creating user audio content easier.
754    pub fn audio(data: impl Into<String>, media_type: Option<AudioMediaType>) -> Self {
755        UserContent::Audio(Audio {
756            data: DocumentSourceKind::Base64(data.into()),
757            media_type,
758            additional_params: None,
759        })
760    }
761
762    /// Helper constructor to make creating user audio content from raw unencoded bytes easier.
763    pub fn audio_raw(data: impl Into<Vec<u8>>, media_type: Option<AudioMediaType>) -> Self {
764        UserContent::Audio(Audio {
765            data: DocumentSourceKind::Raw(data.into()),
766            media_type,
767            ..Default::default()
768        })
769    }
770
771    /// Helper to create an audio resource from a URL
772    pub fn audio_url(url: impl Into<String>, media_type: Option<AudioMediaType>) -> Self {
773        UserContent::Audio(Audio {
774            data: DocumentSourceKind::Url(url.into()),
775            media_type,
776            ..Default::default()
777        })
778    }
779
780    /// Helper constructor to make creating user video content easier.
781    pub fn video(data: impl Into<String>, media_type: Option<VideoMediaType>) -> Self {
782        UserContent::Video(Video {
783            data: DocumentSourceKind::Base64(data.into()),
784            media_type,
785            additional_params: None,
786        })
787    }
788
789    /// Helper constructor to make creating user video content from raw unencoded bytes easier.
790    pub fn video_raw(data: impl Into<Vec<u8>>, media_type: Option<VideoMediaType>) -> Self {
791        UserContent::Video(Video {
792            data: DocumentSourceKind::Raw(data.into()),
793            media_type,
794            ..Default::default()
795        })
796    }
797
798    /// Helper to create a video resource from a URL
799    pub fn video_url(url: impl Into<String>, media_type: Option<VideoMediaType>) -> Self {
800        UserContent::Video(Video {
801            data: DocumentSourceKind::Url(url.into()),
802            media_type,
803            ..Default::default()
804        })
805    }
806
807    /// Helper constructor to make creating user document content easier.
808    /// This creates a document that assumes the data being passed in is a raw string.
809    pub fn document(data: impl Into<String>, media_type: Option<DocumentMediaType>) -> Self {
810        let data: String = data.into();
811        UserContent::Document(Document {
812            data: DocumentSourceKind::string(&data),
813            media_type,
814            additional_params: None,
815        })
816    }
817
818    /// Helper to create a document from raw unencoded bytes
819    pub fn document_raw(data: impl Into<Vec<u8>>, media_type: Option<DocumentMediaType>) -> Self {
820        UserContent::Document(Document {
821            data: DocumentSourceKind::Raw(data.into()),
822            media_type,
823            ..Default::default()
824        })
825    }
826
827    /// Helper to create a document from a URL
828    pub fn document_url(url: impl Into<String>, media_type: Option<DocumentMediaType>) -> Self {
829        UserContent::Document(Document {
830            data: DocumentSourceKind::Url(url.into()),
831            media_type,
832            ..Default::default()
833        })
834    }
835
836    /// Helper constructor to make creating user tool result content easier.
837    pub fn tool_result(id: impl Into<String>, content: OneOrMany<ToolResultContent>) -> Self {
838        UserContent::ToolResult(ToolResult {
839            id: id.into(),
840            call_id: None,
841            content,
842        })
843    }
844
845    /// Helper constructor to make creating user tool result content easier.
846    pub fn tool_result_with_call_id(
847        id: impl Into<String>,
848        call_id: String,
849        content: OneOrMany<ToolResultContent>,
850    ) -> Self {
851        UserContent::ToolResult(ToolResult {
852            id: id.into(),
853            call_id: Some(call_id),
854            content,
855        })
856    }
857}
858
859impl AssistantContent {
860    /// Helper constructor to make creating assistant text content easier.
861    pub fn text(text: impl Into<String>) -> Self {
862        AssistantContent::Text(text.into().into())
863    }
864
865    /// Helper constructor to make creating assistant image content easier.
866    pub fn image_base64(
867        data: impl Into<String>,
868        media_type: Option<ImageMediaType>,
869        detail: Option<ImageDetail>,
870    ) -> Self {
871        AssistantContent::Image(Image {
872            data: DocumentSourceKind::Base64(data.into()),
873            media_type,
874            detail,
875            additional_params: None,
876        })
877    }
878
879    /// Helper constructor to make creating assistant tool call content easier.
880    pub fn tool_call(
881        id: impl Into<String>,
882        name: impl Into<String>,
883        arguments: serde_json::Value,
884    ) -> Self {
885        AssistantContent::ToolCall(ToolCall::new(
886            id.into(),
887            ToolFunction {
888                name: name.into(),
889                arguments,
890            },
891        ))
892    }
893
894    pub fn tool_call_with_call_id(
895        id: impl Into<String>,
896        call_id: String,
897        name: impl Into<String>,
898        arguments: serde_json::Value,
899    ) -> Self {
900        AssistantContent::ToolCall(
901            ToolCall::new(
902                id.into(),
903                ToolFunction {
904                    name: name.into(),
905                    arguments,
906                },
907            )
908            .with_call_id(call_id),
909        )
910    }
911
912    pub fn reasoning(reasoning: impl AsRef<str>) -> Self {
913        AssistantContent::Reasoning(Reasoning::new(reasoning.as_ref()))
914    }
915}
916
917impl ToolResultContent {
918    /// Helper constructor to make creating tool result text content easier.
919    pub fn text(text: impl Into<String>) -> Self {
920        ToolResultContent::Text(text.into().into())
921    }
922
923    /// Helper constructor for structured JSON tool-result content.
924    pub fn json(value: serde_json::Value) -> Self {
925        ToolResultContent::Json { value }
926    }
927
928    /// Helper constructor to make tool result images from a base64-encoded string.
929    pub fn image_base64(
930        data: impl Into<String>,
931        media_type: Option<ImageMediaType>,
932        detail: Option<ImageDetail>,
933    ) -> Self {
934        ToolResultContent::Image(Image {
935            data: DocumentSourceKind::Base64(data.into()),
936            media_type,
937            detail,
938            additional_params: None,
939        })
940    }
941
942    /// Helper constructor to make tool result images from a base64-encoded string.
943    pub fn image_raw(
944        data: impl Into<Vec<u8>>,
945        media_type: Option<ImageMediaType>,
946        detail: Option<ImageDetail>,
947    ) -> Self {
948        ToolResultContent::Image(Image {
949            data: DocumentSourceKind::Raw(data.into()),
950            media_type,
951            detail,
952            ..Default::default()
953        })
954    }
955
956    /// Helper constructor to make tool result images from a URL.
957    pub fn image_url(
958        url: impl Into<String>,
959        media_type: Option<ImageMediaType>,
960        detail: Option<ImageDetail>,
961    ) -> Self {
962        ToolResultContent::Image(Image {
963            data: DocumentSourceKind::Url(url.into()),
964            media_type,
965            detail,
966            additional_params: None,
967        })
968    }
969}
970
971/// Trait for converting between MIME types and media types.
972pub trait MimeType {
973    fn from_mime_type(mime_type: &str) -> Option<Self>
974    where
975        Self: Sized;
976    fn to_mime_type(&self) -> &'static str;
977}
978
979impl MimeType for MediaType {
980    fn from_mime_type(mime_type: &str) -> Option<Self> {
981        ImageMediaType::from_mime_type(mime_type)
982            .map(MediaType::Image)
983            .or_else(|| {
984                DocumentMediaType::from_mime_type(mime_type)
985                    .map(MediaType::Document)
986                    .or_else(|| {
987                        AudioMediaType::from_mime_type(mime_type)
988                            .map(MediaType::Audio)
989                            .or_else(|| {
990                                VideoMediaType::from_mime_type(mime_type).map(MediaType::Video)
991                            })
992                    })
993            })
994    }
995
996    fn to_mime_type(&self) -> &'static str {
997        match self {
998            MediaType::Image(media_type) => media_type.to_mime_type(),
999            MediaType::Audio(media_type) => media_type.to_mime_type(),
1000            MediaType::Document(media_type) => media_type.to_mime_type(),
1001            MediaType::Video(media_type) => media_type.to_mime_type(),
1002        }
1003    }
1004}
1005
1006impl MimeType for ImageMediaType {
1007    fn from_mime_type(mime_type: &str) -> Option<Self> {
1008        match mime_type {
1009            "image/jpeg" => Some(ImageMediaType::JPEG),
1010            "image/png" => Some(ImageMediaType::PNG),
1011            "image/gif" => Some(ImageMediaType::GIF),
1012            "image/webp" => Some(ImageMediaType::WEBP),
1013            "image/heic" => Some(ImageMediaType::HEIC),
1014            "image/heif" => Some(ImageMediaType::HEIF),
1015            "image/svg+xml" => Some(ImageMediaType::SVG),
1016            _ => None,
1017        }
1018    }
1019
1020    fn to_mime_type(&self) -> &'static str {
1021        match self {
1022            ImageMediaType::JPEG => "image/jpeg",
1023            ImageMediaType::PNG => "image/png",
1024            ImageMediaType::GIF => "image/gif",
1025            ImageMediaType::WEBP => "image/webp",
1026            ImageMediaType::HEIC => "image/heic",
1027            ImageMediaType::HEIF => "image/heif",
1028            ImageMediaType::SVG => "image/svg+xml",
1029        }
1030    }
1031}
1032
1033impl MimeType for DocumentMediaType {
1034    fn from_mime_type(mime_type: &str) -> Option<Self> {
1035        match mime_type {
1036            "application/pdf" => Some(DocumentMediaType::PDF),
1037            "text/plain" => Some(DocumentMediaType::TXT),
1038            "text/rtf" => Some(DocumentMediaType::RTF),
1039            "text/html" => Some(DocumentMediaType::HTML),
1040            "text/css" => Some(DocumentMediaType::CSS),
1041            "text/md" | "text/markdown" => Some(DocumentMediaType::MARKDOWN),
1042            "text/csv" => Some(DocumentMediaType::CSV),
1043            "text/xml" => Some(DocumentMediaType::XML),
1044            "application/x-javascript" | "text/x-javascript" => Some(DocumentMediaType::Javascript),
1045            "application/x-python" | "text/x-python" => Some(DocumentMediaType::Python),
1046            _ => None,
1047        }
1048    }
1049
1050    fn to_mime_type(&self) -> &'static str {
1051        match self {
1052            DocumentMediaType::PDF => "application/pdf",
1053            DocumentMediaType::TXT => "text/plain",
1054            DocumentMediaType::RTF => "text/rtf",
1055            DocumentMediaType::HTML => "text/html",
1056            DocumentMediaType::CSS => "text/css",
1057            DocumentMediaType::MARKDOWN => "text/markdown",
1058            DocumentMediaType::CSV => "text/csv",
1059            DocumentMediaType::XML => "text/xml",
1060            DocumentMediaType::Javascript => "application/x-javascript",
1061            DocumentMediaType::Python => "application/x-python",
1062        }
1063    }
1064}
1065
1066impl MimeType for AudioMediaType {
1067    fn from_mime_type(mime_type: &str) -> Option<Self> {
1068        match mime_type {
1069            "audio/wav" => Some(AudioMediaType::WAV),
1070            "audio/mp3" => Some(AudioMediaType::MP3),
1071            "audio/aiff" => Some(AudioMediaType::AIFF),
1072            "audio/aac" => Some(AudioMediaType::AAC),
1073            "audio/ogg" => Some(AudioMediaType::OGG),
1074            "audio/flac" => Some(AudioMediaType::FLAC),
1075            "audio/m4a" => Some(AudioMediaType::M4A),
1076            "audio/pcm16" => Some(AudioMediaType::PCM16),
1077            "audio/pcm24" => Some(AudioMediaType::PCM24),
1078            _ => None,
1079        }
1080    }
1081
1082    fn to_mime_type(&self) -> &'static str {
1083        match self {
1084            AudioMediaType::WAV => "audio/wav",
1085            AudioMediaType::MP3 => "audio/mp3",
1086            AudioMediaType::AIFF => "audio/aiff",
1087            AudioMediaType::AAC => "audio/aac",
1088            AudioMediaType::OGG => "audio/ogg",
1089            AudioMediaType::FLAC => "audio/flac",
1090            AudioMediaType::M4A => "audio/m4a",
1091            AudioMediaType::PCM16 => "audio/pcm16",
1092            AudioMediaType::PCM24 => "audio/pcm24",
1093        }
1094    }
1095}
1096
1097impl MimeType for VideoMediaType {
1098    fn from_mime_type(mime_type: &str) -> Option<Self>
1099    where
1100        Self: Sized,
1101    {
1102        match mime_type {
1103            "video/avi" => Some(VideoMediaType::AVI),
1104            "video/mp4" => Some(VideoMediaType::MP4),
1105            "video/mpeg" => Some(VideoMediaType::MPEG),
1106            "video/mov" => Some(VideoMediaType::MOV),
1107            "video/webm" => Some(VideoMediaType::WEBM),
1108            &_ => None,
1109        }
1110    }
1111
1112    fn to_mime_type(&self) -> &'static str {
1113        match self {
1114            VideoMediaType::AVI => "video/avi",
1115            VideoMediaType::MP4 => "video/mp4",
1116            VideoMediaType::MPEG => "video/mpeg",
1117            VideoMediaType::MOV => "video/mov",
1118            VideoMediaType::WEBM => "video/webm",
1119        }
1120    }
1121}
1122
1123impl std::str::FromStr for ImageDetail {
1124    type Err = ();
1125
1126    fn from_str(s: &str) -> Result<Self, Self::Err> {
1127        match s.to_lowercase().as_str() {
1128            "low" => Ok(ImageDetail::Low),
1129            "high" => Ok(ImageDetail::High),
1130            "auto" => Ok(ImageDetail::Auto),
1131            _ => Err(()),
1132        }
1133    }
1134}
1135
1136// ================================================================
1137// FromStr, From<String>, and From<&str> impls
1138// ================================================================
1139
1140impl From<String> for Text {
1141    fn from(text: String) -> Self {
1142        Text {
1143            text,
1144            additional_params: None,
1145        }
1146    }
1147}
1148
1149impl From<&String> for Text {
1150    fn from(text: &String) -> Self {
1151        text.to_owned().into()
1152    }
1153}
1154
1155impl From<&str> for Text {
1156    fn from(text: &str) -> Self {
1157        text.to_owned().into()
1158    }
1159}
1160
1161impl FromStr for Text {
1162    type Err = Infallible;
1163
1164    fn from_str(s: &str) -> Result<Self, Self::Err> {
1165        Ok(s.into())
1166    }
1167}
1168
1169impl From<&Message> for Message {
1170    fn from(msg: &Message) -> Self {
1171        msg.clone()
1172    }
1173}
1174
1175impl From<String> for Message {
1176    fn from(text: String) -> Self {
1177        Message::User {
1178            content: OneOrMany::one(UserContent::Text(text.into())),
1179        }
1180    }
1181}
1182
1183impl From<&str> for Message {
1184    fn from(text: &str) -> Self {
1185        Message::User {
1186            content: OneOrMany::one(UserContent::Text(text.into())),
1187        }
1188    }
1189}
1190
1191impl From<&String> for Message {
1192    fn from(text: &String) -> Self {
1193        Message::User {
1194            content: OneOrMany::one(UserContent::Text(text.into())),
1195        }
1196    }
1197}
1198
1199impl From<Text> for Message {
1200    fn from(text: Text) -> Self {
1201        Message::User {
1202            content: OneOrMany::one(UserContent::Text(text)),
1203        }
1204    }
1205}
1206
1207impl From<Image> for Message {
1208    fn from(image: Image) -> Self {
1209        Message::User {
1210            content: OneOrMany::one(UserContent::Image(image)),
1211        }
1212    }
1213}
1214
1215impl From<Audio> for Message {
1216    fn from(audio: Audio) -> Self {
1217        Message::User {
1218            content: OneOrMany::one(UserContent::Audio(audio)),
1219        }
1220    }
1221}
1222
1223impl From<Document> for Message {
1224    fn from(document: Document) -> Self {
1225        Message::User {
1226            content: OneOrMany::one(UserContent::Document(document)),
1227        }
1228    }
1229}
1230
1231impl From<String> for ToolResultContent {
1232    fn from(text: String) -> Self {
1233        ToolResultContent::text(text)
1234    }
1235}
1236
1237impl From<String> for AssistantContent {
1238    fn from(text: String) -> Self {
1239        AssistantContent::text(text)
1240    }
1241}
1242
1243impl From<String> for UserContent {
1244    fn from(text: String) -> Self {
1245        UserContent::text(text)
1246    }
1247}
1248
1249impl From<AssistantContent> for Message {
1250    fn from(content: AssistantContent) -> Self {
1251        Message::Assistant {
1252            id: None,
1253            content: OneOrMany::one(content),
1254        }
1255    }
1256}
1257
1258impl From<UserContent> for Message {
1259    fn from(content: UserContent) -> Self {
1260        Message::User {
1261            content: OneOrMany::one(content),
1262        }
1263    }
1264}
1265
1266impl From<OneOrMany<AssistantContent>> for Message {
1267    fn from(content: OneOrMany<AssistantContent>) -> Self {
1268        Message::Assistant { id: None, content }
1269    }
1270}
1271
1272impl From<OneOrMany<UserContent>> for Message {
1273    fn from(content: OneOrMany<UserContent>) -> Self {
1274        Message::User { content }
1275    }
1276}
1277
1278impl From<ToolCall> for Message {
1279    fn from(tool_call: ToolCall) -> Self {
1280        Message::Assistant {
1281            id: None,
1282            content: OneOrMany::one(AssistantContent::ToolCall(tool_call)),
1283        }
1284    }
1285}
1286
1287impl From<ToolResult> for Message {
1288    fn from(tool_result: ToolResult) -> Self {
1289        Message::User {
1290            content: OneOrMany::one(UserContent::ToolResult(tool_result)),
1291        }
1292    }
1293}
1294
1295impl From<ToolResultContent> for Message {
1296    fn from(tool_result_content: ToolResultContent) -> Self {
1297        Message::User {
1298            content: OneOrMany::one(UserContent::ToolResult(ToolResult {
1299                id: String::new(),
1300                call_id: None,
1301                content: OneOrMany::one(tool_result_content),
1302            })),
1303        }
1304    }
1305}
1306
1307#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1308#[serde(rename_all = "snake_case")]
1309pub enum ToolChoice {
1310    #[default]
1311    Auto,
1312    None,
1313    Required,
1314    Specific {
1315        function_names: Vec<String>,
1316    },
1317}
1318
1319// ================================================================
1320// Error types
1321// ================================================================
1322
1323/// Error type to represent issues with converting messages to and from specific provider messages.
1324#[derive(Debug, Error)]
1325pub enum MessageError {
1326    #[error("Message conversion error: {0}")]
1327    ConversionError(String),
1328}
1329
1330impl From<MessageError> for CompletionError {
1331    fn from(error: MessageError) -> Self {
1332        CompletionError::RequestError(error.into())
1333    }
1334}
1335
1336#[cfg(test)]
1337mod tests {
1338    use serde::{Deserialize, Serialize};
1339
1340    use super::{Message, Reasoning, ReasoningContent, Text, ToolResultContent};
1341
1342    #[test]
1343    fn reasoning_constructors_and_accessors_work() {
1344        let single = Reasoning::new("think");
1345        assert_eq!(single.first_text(), Some("think"));
1346        assert_eq!(single.first_signature(), None);
1347
1348        let signed = Reasoning::new_with_signature("signed", Some("sig-1".to_string()));
1349        assert_eq!(signed.first_text(), Some("signed"));
1350        assert_eq!(signed.first_signature(), Some("sig-1"));
1351
1352        let multi = Reasoning::multi(vec!["a".to_string(), "b".to_string()]);
1353        assert_eq!(multi.display_text(), "a\nb");
1354        assert_eq!(multi.first_text(), Some("a"));
1355
1356        let redacted = Reasoning::redacted("redacted-value");
1357        assert_eq!(redacted.display_text(), "redacted-value");
1358        assert_eq!(redacted.first_text(), None);
1359
1360        let encrypted = Reasoning::encrypted("enc");
1361        assert_eq!(encrypted.encrypted_content(), Some("enc"));
1362        assert_eq!(encrypted.display_text(), "");
1363
1364        let summaries = Reasoning::summaries(vec!["s1".to_string(), "s2".to_string()]);
1365        assert_eq!(summaries.display_text(), "s1\ns2");
1366        assert_eq!(summaries.encrypted_content(), None);
1367    }
1368
1369    #[test]
1370    fn reasoning_content_serde_roundtrip() {
1371        let variants = vec![
1372            ReasoningContent::Text {
1373                text: "plain".to_string(),
1374                signature: Some("sig".to_string()),
1375            },
1376            ReasoningContent::Encrypted("opaque".to_string()),
1377            ReasoningContent::Redacted {
1378                data: "redacted".to_string(),
1379            },
1380            ReasoningContent::Summary("summary".to_string()),
1381        ];
1382
1383        for variant in variants {
1384            let json = serde_json::to_string(&variant).expect("serialize");
1385            let roundtrip: ReasoningContent = serde_json::from_str(&json).expect("deserialize");
1386            assert_eq!(roundtrip, variant);
1387        }
1388    }
1389
1390    #[test]
1391    fn system_message_constructor_and_serde_roundtrip() {
1392        let message = Message::system("You are concise.");
1393
1394        match &message {
1395            Message::System { content } => assert_eq!(content, "You are concise."),
1396            _ => panic!("Expected system message"),
1397        }
1398
1399        let json = serde_json::to_string(&message).expect("serialize");
1400        let roundtrip: Message = serde_json::from_str(&json).expect("deserialize");
1401        assert_eq!(roundtrip, message);
1402    }
1403
1404    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1405    struct ExecutorLikeResponse {
1406        output: serde_json::Value,
1407        logs: Vec<String>,
1408        execution_time_ms: u64,
1409    }
1410
1411    #[test]
1412    fn tool_result_content_decodes_structured_and_legacy_json() {
1413        let response = ExecutorLikeResponse {
1414            output: serde_json::json!({"answer": 42}),
1415            logs: vec!["computed".to_string()],
1416            execution_time_ms: 7,
1417        };
1418        let value = serde_json::to_value(&response).expect("serialize response");
1419
1420        let structured = ToolResultContent::json(value.clone());
1421        assert_eq!(structured.as_json(), Some(&value));
1422        assert_eq!(structured.as_text(), None);
1423        assert_eq!(
1424            structured
1425                .deserialize_json::<ExecutorLikeResponse>()
1426                .expect("decode structured response"),
1427            response
1428        );
1429
1430        let legacy_json = value.to_string();
1431        let legacy_text = ToolResultContent::Text(Text::new(legacy_json.clone()));
1432        assert_eq!(legacy_text.as_text(), Some(legacy_json.as_str()));
1433        assert_eq!(legacy_text.as_json(), None);
1434        assert_eq!(
1435            legacy_text
1436                .deserialize_json::<ExecutorLikeResponse>()
1437                .expect("decode legacy response"),
1438            response
1439        );
1440
1441        let image = ToolResultContent::image_url("https://example.com/result.png", None, None);
1442        let image_error = image.deserialize_json::<ExecutorLikeResponse>();
1443        assert!(image_error.is_err());
1444        if let Err(error) = image_error {
1445            assert_eq!(
1446                error.to_string(),
1447                "cannot decode image tool-result content as JSON"
1448            );
1449        }
1450    }
1451}