Skip to main content

rig_core/completion/
message.rs

1use crate::error::ProviderError;
2use serde::{Deserialize, Serialize};
3use std::{convert::Infallible, str::FromStr};
4use thiserror::Error;
5
6/// A provider-agnostic chat message.
7///
8/// Messages are role-tagged and may contain one or many content items, including
9/// text, images, audio, documents, tool calls, and tool results. Provider modules
10/// are responsible for translating these generic messages into provider-native
11/// request bodies. That conversion may be lossy when a provider does not support
12/// a particular content type.
13#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
14#[serde(tag = "role", rename_all = "lowercase")]
15pub enum Message {
16    /// System message containing instruction text.
17    System { content: String },
18
19    /// User message containing one or more content types defined by `UserContent`.
20    User { content: Vec<UserContent> },
21
22    /// Assistant message containing one or more content types defined by `AssistantContent`.
23    Assistant {
24        /// Provider-assigned assistant message ID, when available.
25        id: Option<String>,
26        content: Vec<AssistantContent>,
27    },
28}
29
30mod identity;
31
32pub use identity::{
33    CallId, EmptyCallId, EmptyToolName, Issuer, LocalCallId, ProviderCallId, Sealed, ToolName,
34};
35
36/// Shared error text for an invalid empty response choice.
37/// Provider decoders must exempt legal empty outcomes, including recognized
38/// output truncation, before calling [`require_non_empty_response`].
39pub const EMPTY_RESPONSE_ERROR: &str = "Response contained no message or tool call (empty)";
40
41/// Returns `items` unchanged unless the list is empty, then calls `error` once.
42/// Does not inspect individual items: empty text can carry replay signatures.
43/// Request conversions that discard content must validate the converted list
44/// when their wire requires at least one block.
45pub fn require_non_empty<T, E>(items: Vec<T>, error: impl FnOnce() -> E) -> Result<Vec<T>, E> {
46    if items.is_empty() {
47        return Err(error());
48    }
49    Ok(items)
50}
51
52/// Returns a response error using [`EMPTY_RESPONSE_ERROR`] for an empty list.
53/// Callers must handle provider-legal empty outcomes before invoking this guard.
54pub fn require_non_empty_response<T>(items: Vec<T>) -> Result<Vec<T>, ProviderError> {
55    require_non_empty(items, || {
56        ProviderError::Response(EMPTY_RESPONSE_ERROR.to_owned())
57    })
58}
59
60/// Returns `None` for an empty list or `Some(items)` otherwise.
61/// Individual items are not inspected.
62pub fn non_empty<T>(items: Vec<T>) -> Option<Vec<T>> {
63    if items.is_empty() { None } else { Some(items) }
64}
65
66/// Concatenates reasoning, text, and trailing content in that order without
67/// dropping items. Each group's input order is preserved.
68pub fn ordered_assistant_content(
69    reasoning_items: impl IntoIterator<Item = Sealed<Reasoning>>,
70    text_items: impl IntoIterator<Item = AssistantContent>,
71    trailing_items: impl IntoIterator<Item = AssistantContent>,
72) -> Vec<AssistantContent> {
73    let mut content_items = reasoning_items
74        .into_iter()
75        .map(AssistantContent::Reasoning)
76        .collect::<Vec<_>>();
77    content_items.extend(text_items);
78    content_items.extend(trailing_items);
79    content_items
80}
81
82/// Returns whether the choice contains no nonempty text, tool call, or image.
83/// Reasoning alone is not an answer, even when retained in history.
84pub fn turn_delivered_no_answer(choice: &[AssistantContent]) -> bool {
85    !choice.iter().any(|content| match content {
86        // Real text is an answer; an empty block delivers nothing.
87        AssistantContent::Text(text) => !text.text.is_empty(),
88        AssistantContent::ToolCall(_) => true,
89        AssistantContent::Image(_) => true,
90        // The one exclusion: scratch work, not an answer.
91        AssistantContent::Reasoning(_) => false,
92    })
93}
94
95/// Groups streamed choices as reasoning, text, tool calls, then images,
96/// preserving order within each group. Choices without reasoning or tool calls
97/// retain their original order.
98pub fn canonical_streamed_choice(choice: Vec<AssistantContent>) -> Vec<AssistantContent> {
99    let regroup = choice.iter().any(|part| {
100        matches!(
101            part,
102            AssistantContent::Reasoning(_) | AssistantContent::ToolCall(_)
103        )
104    });
105    if !regroup {
106        return choice;
107    }
108    let mut reasoning = Vec::new();
109    let mut text = Vec::new();
110    let mut calls = Vec::new();
111    let mut images = Vec::new();
112    for part in choice {
113        match part {
114            AssistantContent::Reasoning(block) => reasoning.push(block),
115            AssistantContent::Text(_) => text.push(part),
116            AssistantContent::ToolCall(_) => calls.push(part),
117            AssistantContent::Image(_) => images.push(part),
118        }
119    }
120    ordered_assistant_content(reasoning, text, calls.into_iter().chain(images))
121}
122
123/// User text, tool results, or media. Supported source kinds and media types
124/// depend on the target provider.
125#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
126#[serde(tag = "type", rename_all = "lowercase")]
127pub enum UserContent {
128    /// Plain text user content.
129    Text(Text),
130    /// Result of a tool call returned as user-visible context to the model.
131    ToolResult(ToolResult),
132    /// Image content.
133    Image(Image),
134    /// Audio content.
135    Audio(Audio),
136    /// Video content.
137    Video(Video),
138    /// Document content.
139    Document(Document),
140}
141
142/// Assistant text, tool calls, reasoning, or images.
143/// Deserialization requires the lowercase `type` tag.
144#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
145#[serde(tag = "type", rename_all = "lowercase")]
146pub enum AssistantContent {
147    /// Plain assistant text.
148    Text(Text),
149    /// Tool call requested by the assistant.
150    ToolCall(ToolCall),
151    /// Structured reasoning emitted by the assistant, readable only by the
152    /// service that issued it.
153    Reasoning(Sealed<Reasoning>),
154    /// Image content emitted by the assistant.
155    Image(Image),
156}
157
158#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
159#[serde(tag = "type", content = "content", rename_all = "snake_case")]
160/// A typed reasoning block used by providers that emit structured thinking data.
161pub enum ReasoningContent {
162    /// Plain reasoning text with an optional provider signature.
163    Text {
164        text: String,
165        #[serde(skip_serializing_if = "Option::is_none")]
166        signature: Option<String>,
167    },
168    /// Provider-encrypted reasoning payload.
169    Encrypted(String),
170    /// Redacted reasoning payload preserved as opaque data.
171    Redacted { data: String },
172    /// Provider-generated reasoning summary text.
173    Summary(String),
174}
175
176#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
177/// Assistant reasoning payload with an optional provider-supplied identifier.
178/// A message carries it [`Sealed`] to the service that issued it:
179/// signatures, encrypted and redacted payloads and reasoning ids only mean
180/// something there.
181pub struct Reasoning {
182    /// Provider reasoning identifier, when supplied by the upstream API.
183    pub id: Option<String>,
184    /// Ordered reasoning content blocks.
185    pub content: Vec<ReasoningContent>,
186}
187
188impl Reasoning {
189    /// Create a new reasoning item from a single item
190    pub fn new(input: &str) -> Self {
191        Self::new_with_signature(input, None)
192    }
193
194    /// Create a new reasoning item from a single text item and optional signature.
195    pub fn new_with_signature(input: &str, signature: Option<String>) -> Self {
196        Self {
197            id: None,
198            content: vec![ReasoningContent::Text {
199                text: input.to_string(),
200                signature,
201            }],
202        }
203    }
204
205    /// This reasoning, readable only by `issuer`.
206    pub fn sealed(self, issuer: impl Into<Issuer>) -> Sealed<Self> {
207        Sealed::new(issuer, self)
208    }
209
210    /// Set a provider reasoning ID.
211    pub fn with_id(mut self, id: String) -> Self {
212        self.id = Some(id);
213        self
214    }
215
216    /// Create reasoning content from multiple text blocks.
217    pub fn multi(input: Vec<String>) -> Self {
218        Self {
219            id: None,
220            content: input
221                .into_iter()
222                .map(|text| ReasoningContent::Text {
223                    text,
224                    signature: None,
225                })
226                .collect(),
227        }
228    }
229
230    /// Create a redacted reasoning block.
231    pub fn redacted(data: impl Into<String>) -> Self {
232        Self {
233            id: None,
234            content: vec![ReasoningContent::Redacted { data: data.into() }],
235        }
236    }
237
238    /// Create an encrypted reasoning block.
239    pub fn encrypted(data: impl Into<String>) -> Self {
240        Self {
241            id: None,
242            content: vec![ReasoningContent::Encrypted(data.into())],
243        }
244    }
245
246    /// Create one reasoning block containing summary items.
247    pub fn summaries(input: Vec<String>) -> Self {
248        Self {
249            id: None,
250            content: input.into_iter().map(ReasoningContent::Summary).collect(),
251        }
252    }
253
254    /// Render reasoning as displayable text by joining text-like blocks with newlines.
255    pub fn display_text(&self) -> String {
256        self.content
257            .iter()
258            .filter_map(|content| match content {
259                ReasoningContent::Text { text, .. } => Some(text.as_str()),
260                ReasoningContent::Summary(summary) => Some(summary.as_str()),
261                ReasoningContent::Redacted { data } => Some(data.as_str()),
262                ReasoningContent::Encrypted(_) => None,
263            })
264            .collect::<Vec<_>>()
265            .join("\n")
266    }
267
268    /// Return the first text reasoning block, if present.
269    pub fn first_text(&self) -> Option<&str> {
270        self.content.iter().find_map(|content| match content {
271            ReasoningContent::Text { text, .. } => Some(text.as_str()),
272            _ => None,
273        })
274    }
275
276    /// Return the first signature from text reasoning, if present.
277    pub fn first_signature(&self) -> Option<&str> {
278        self.content.iter().find_map(|content| match content {
279            ReasoningContent::Text {
280                signature: Some(signature),
281                ..
282            } => Some(signature.as_str()),
283            _ => None,
284        })
285    }
286
287    /// Return the first encrypted reasoning payload, if present.
288    pub fn encrypted_content(&self) -> Option<&str> {
289        self.content.iter().find_map(|content| match content {
290            ReasoningContent::Encrypted(data) => Some(data.as_str()),
291            _ => None,
292        })
293    }
294}
295
296/// The result of a tool call, sent back to the model.
297///
298/// Build it from the call it answers with [`ToolCall::result`], so its id
299/// and name match the call.
300#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
301pub struct ToolResult {
302    /// The id of the answered call.
303    pub call: CallId,
304    /// Executed tool name, which may differ from the model-requested name after
305    /// hook repair. Required for provider replay independently of call identity.
306    pub name: ToolName,
307    /// One or more content items produced by the tool.
308    pub content: Vec<ToolResultContent>,
309}
310
311/// Describes one typed item in a tool result.
312#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
313#[serde(tag = "type", rename_all = "lowercase")]
314pub enum ToolResultContent {
315    /// Literal text. Providers must not reinterpret it as structured JSON.
316    Text(Text),
317    /// An image supplied explicitly by the tool.
318    Image(Image),
319    /// Structured JSON supplied explicitly by the tool runtime.
320    Json {
321        /// The structured value.
322        value: serde_json::Value,
323    },
324}
325
326impl ToolResultContent {
327    /// Borrow literal text content.
328    pub fn as_text(&self) -> Option<&str> {
329        match self {
330            Self::Text(text) => Some(&text.text),
331            Self::Image(_) | Self::Json { .. } => None,
332        }
333    }
334
335    /// Borrow structured JSON content.
336    pub fn as_json(&self) -> Option<&serde_json::Value> {
337        match self {
338            Self::Json { value } => Some(value),
339            Self::Text(_) | Self::Image(_) => None,
340        }
341    }
342
343    /// Deserialize JSON content into a typed value.
344    ///
345    /// Structured JSON is decoded directly. Literal text is parsed only because
346    /// the caller explicitly requested JSON decoding, which supports transcripts
347    /// recorded before structured tool output was preserved canonically. This
348    /// helper never changes the content sent to a model or provider.
349    pub fn deserialize_json<T>(&self) -> Result<T, serde_json::Error>
350    where
351        T: serde::de::DeserializeOwned,
352    {
353        match self {
354            Self::Json { value } => T::deserialize(value),
355            Self::Text(text) => serde_json::from_str(&text.text),
356            Self::Image(_) => Err(<serde_json::Error as serde::de::Error>::custom(
357                "cannot decode image tool-result content as JSON",
358            )),
359        }
360    }
361}
362
363/// Describes a tool call with an id and function to call, generally produced by a provider.
364#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
365pub struct ToolCall {
366    /// The call's one identity: the provider's id, or one rig issued when
367    /// the provider sent none.
368    pub id: CallId,
369    /// Function name and JSON arguments requested by the model.
370    pub function: ToolFunction,
371    /// Opaque provider signature preserved for replay. Rig does not verify it.
372    #[serde(default)]
373    pub signature: Option<String>,
374    /// Additional provider-specific parameters to be sent to the completion model provider
375    #[serde(default)]
376    pub additional_params: Option<serde_json::Value>,
377}
378
379impl ToolCall {
380    /// A call with `id`.
381    pub fn new(id: CallId, function: ToolFunction) -> Self {
382        Self {
383            id,
384            function,
385            signature: None,
386            additional_params: None,
387        }
388    }
389
390    /// A call the provider identified by `wire_id`; rig issues an id when it
391    /// is empty.
392    pub fn from_wire(wire_id: impl Into<String>, function: ToolFunction) -> Self {
393        Self::new(CallId::from_wire(wire_id), function)
394    }
395
396    /// The dual-identifier provider boundary (OpenAI Responses): `item_id`
397    /// is the output-item handle (`fc_…`), `call_id` the correlator
398    /// (`call_…`). Rig issues an id when `call_id` is empty.
399    pub fn from_dual_wire(
400        item_id: impl Into<String>,
401        call_id: impl Into<String>,
402        function: ToolFunction,
403    ) -> Self {
404        Self::new(CallId::from_dual_wire(item_id, call_id), function)
405    }
406
407    /// The result answering this call: its id and name, and `content`.
408    pub fn result(&self, content: Vec<ToolResultContent>) -> ToolResult {
409        ToolResult {
410            call: self.id.clone(),
411            name: self.function.name.clone(),
412            content,
413        }
414    }
415
416    pub fn with_signature(mut self, signature: Option<String>) -> Self {
417        self.signature = signature;
418        self
419    }
420
421    pub fn with_additional_params(mut self, additional_params: Option<serde_json::Value>) -> Self {
422        self.additional_params = additional_params;
423        self
424    }
425}
426
427/// Describes a tool function to call with a name and arguments, generally produced by a provider.
428#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
429pub struct ToolFunction {
430    /// Tool/function name to invoke.
431    pub name: ToolName,
432    /// JSON arguments for the tool/function.
433    pub arguments: serde_json::Value,
434}
435
436impl ToolFunction {
437    /// Create a tool function call payload.
438    pub fn new(name: ToolName, arguments: serde_json::Value) -> Self {
439        Self { name, arguments }
440    }
441}
442
443/// Nonempty JSON object of provider-specific content metadata, serialized as
444/// an object under a named `additional_params` field rather than flattened.
445/// Constructors return `None` for empty maps. Bare deserialization rejects
446/// empty or non-object values; [`optional_additional_params`] maps null and
447/// empty objects to absence. Providers must replay only their own metadata.
448#[derive(Clone, Debug, PartialEq, Serialize)]
449#[serde(transparent)]
450pub struct AdditionalParams(serde_json::Map<String, serde_json::Value>);
451
452impl AdditionalParams {
453    /// The canonical constructor: `None` when the map is empty.
454    pub fn new(map: serde_json::Map<String, serde_json::Value>) -> Option<Self> {
455        if map.is_empty() {
456            None
457        } else {
458            Some(Self(map))
459        }
460    }
461
462    /// Build from `(key, value)` entries; `None` when the iterator yields
463    /// none. `Option<(K, Value)>` is such an iterator, so a conditional
464    /// single-key params reads as
465    /// `AdditionalParams::from_entries(guard.then(|| (key, value)))`.
466    pub fn from_entries<K, I>(entries: I) -> Option<Self>
467    where
468        K: Into<String>,
469        I: IntoIterator<Item = (K, serde_json::Value)>,
470    {
471        Self::new(
472            entries
473                .into_iter()
474                .map(|(key, value)| (key.into(), value))
475                .collect(),
476        )
477    }
478
479    /// The value stored under `key`, when present.
480    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
481        self.0.get(key)
482    }
483
484    /// The underlying (non-empty) object.
485    pub fn as_map(&self) -> &serde_json::Map<String, serde_json::Value> {
486        &self.0
487    }
488
489    /// The params as a bare JSON object value.
490    pub fn into_value(self) -> serde_json::Value {
491        serde_json::Value::Object(self.0)
492    }
493
494    /// Deep-merge `incoming` into `self`: arrays concatenate (streamed
495    /// citation deltas), objects merge recursively, scalars take the
496    /// incoming value.
497    pub fn merge(&mut self, incoming: Self) {
498        fn merge_maps(
499            existing: &mut serde_json::Map<String, serde_json::Value>,
500            incoming: serde_json::Map<String, serde_json::Value>,
501        ) {
502            for (key, incoming_value) in incoming {
503                match existing.get_mut(&key) {
504                    Some(existing_value) => merge_value(existing_value, incoming_value),
505                    None => {
506                        existing.insert(key, incoming_value);
507                    }
508                }
509            }
510        }
511        fn merge_value(existing: &mut serde_json::Value, incoming: serde_json::Value) {
512            match (existing, incoming) {
513                (
514                    serde_json::Value::Object(existing_map),
515                    serde_json::Value::Object(incoming_map),
516                ) => merge_maps(existing_map, incoming_map),
517                (
518                    serde_json::Value::Array(existing_array),
519                    serde_json::Value::Array(mut incoming_array),
520                ) => existing_array.append(&mut incoming_array),
521                (existing, incoming) => *existing = incoming,
522            }
523        }
524        merge_maps(&mut self.0, incoming.0);
525    }
526
527    /// Returns the object under the provider's own key, or `None` for absent
528    /// or non-object values. Use [`Self::get`] to diagnose malformed values.
529    pub fn wire_extras(
530        &self,
531        wire_key: &str,
532    ) -> Option<&serde_json::Map<String, serde_json::Value>> {
533        self.0.get(wire_key).and_then(serde_json::Value::as_object)
534    }
535
536    /// Owned counterpart of [`Self::wire_extras`] for serialization paths
537    /// that already own the params (the common replay case): extracts the
538    /// wire's object without cloning. Same gate semantics.
539    pub fn into_wire_extras(
540        mut self,
541        wire_key: &str,
542    ) -> Option<serde_json::Map<String, serde_json::Value>> {
543        match self.0.remove(wire_key) {
544            Some(serde_json::Value::Object(map)) => Some(map),
545            _ => None,
546        }
547    }
548
549    /// Returns absence for null or empty objects, metadata for nonempty objects,
550    /// or the original value as an error for other shapes.
551    pub fn try_from_value(value: serde_json::Value) -> Result<Option<Self>, serde_json::Value> {
552        match value {
553            serde_json::Value::Null => Ok(None),
554            serde_json::Value::Object(map) => Ok(Self::new(map)),
555            other => Err(other),
556        }
557    }
558}
559
560impl From<AdditionalParams> for serde_json::Value {
561    fn from(params: AdditionalParams) -> Self {
562        params.into_value()
563    }
564}
565
566impl std::ops::Index<&str> for AdditionalParams {
567    type Output = serde_json::Value;
568
569    /// Returns the value under `key`.
570    ///
571    /// # Panics
572    /// Panics if the key is absent.
573    #[allow(clippy::indexing_slicing)]
574    fn index(&self, key: &str) -> &serde_json::Value {
575        &self.0[key]
576    }
577}
578
579impl<'de> Deserialize<'de> for AdditionalParams {
580    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
581    where
582        D: serde::Deserializer<'de>,
583    {
584        match Self::try_from_value(serde_json::Value::deserialize(deserializer)?) {
585            Ok(Some(params)) => Ok(params),
586            // `null` and `{}` canonicalize to absence, which a bare
587            // (non-`Option`) slot cannot express.
588            Ok(None) => Err(serde::de::Error::custom(
589                "`additional_params` carries no data — omit the field (an `Option` \
590                 field routed through `optional_additional_params` canonicalizes \
591                 `{}` and `null` to absent)",
592            )),
593            Err(_) => Err(serde::de::Error::custom(
594                "`additional_params` must be a non-empty JSON object",
595            )),
596        }
597    }
598}
599
600/// Returns dot-separated paths whose original values are missing or changed
601/// after a round trip. Ignores added keys, null object members, and missing
602/// object members whose original value was an empty object. Array positions
603/// are compared individually.
604///
605/// ```
606/// use rig_core::message;
607///
608/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
609/// let original = serde_json::json!({
610///     "role": "assistant",
611///     "content": [{"type": "text", "text": "cited", "citations": ["not re-nested"]}],
612/// });
613/// let loaded: message::Message = serde_json::from_value(original.clone())?;
614/// let round_tripped = serde_json::to_value(&loaded)?;
615/// let lost = message::keys_lost_in_round_trip(&original, &round_tripped);
616/// assert_eq!(lost, vec!["content.0.citations".to_string()]);
617/// # Ok(())
618/// # }
619/// ```
620pub fn keys_lost_in_round_trip(
621    original: &serde_json::Value,
622    round_tripped: &serde_json::Value,
623) -> Vec<String> {
624    fn walk(
625        original: &serde_json::Value,
626        round_tripped: &serde_json::Value,
627        path: &mut String,
628        lost: &mut Vec<String>,
629    ) {
630        match (original, round_tripped) {
631            (serde_json::Value::Object(original_map), serde_json::Value::Object(round_map)) => {
632                for (key, original_value) in original_map {
633                    if original_value.is_null() {
634                        continue;
635                    }
636                    let checkpoint = path.len();
637                    if !path.is_empty() {
638                        path.push('.');
639                    }
640                    path.push_str(key);
641                    match round_map.get(key) {
642                        Some(round_value) => walk(original_value, round_value, path, lost),
643                        // Empty objects may canonicalize to absent metadata.
644                        None => {
645                            if !original_value
646                                .as_object()
647                                .is_some_and(serde_json::Map::is_empty)
648                            {
649                                lost.push(path.clone());
650                            }
651                        }
652                    }
653                    path.truncate(checkpoint);
654                }
655            }
656            (serde_json::Value::Array(original_items), serde_json::Value::Array(round_items)) => {
657                for (index, original_value) in original_items.iter().enumerate() {
658                    let checkpoint = path.len();
659                    if !path.is_empty() {
660                        path.push('.');
661                    }
662                    path.push_str(&index.to_string());
663                    match round_items.get(index) {
664                        Some(round_value) => walk(original_value, round_value, path, lost),
665                        None => lost.push(path.clone()),
666                    }
667                    path.truncate(checkpoint);
668                }
669            }
670            (original, round_tripped) => {
671                if original != round_tripped {
672                    lost.push(path.clone());
673                }
674            }
675        }
676    }
677
678    let mut lost = Vec::new();
679    walk(original, round_tripped, &mut String::new(), &mut lost);
680    lost
681}
682
683/// Deserializes optional metadata, mapping null and empty objects to `None`.
684/// Nonempty objects produce metadata; other shapes return a deserialization error.
685pub fn optional_additional_params<'de, D>(
686    deserializer: D,
687) -> Result<Option<AdditionalParams>, D::Error>
688where
689    D: serde::Deserializer<'de>,
690{
691    match Option::<serde_json::Value>::deserialize(deserializer)? {
692        None => Ok(None),
693        Some(value) => AdditionalParams::try_from_value(value).map_err(|_| {
694            serde::de::Error::custom("`additional_params` must be a JSON object (or null)")
695        }),
696    }
697}
698
699/// Text with optional provider metadata under the named `additional_params` key.
700/// Unknown sibling fields are ignored on decode, not captured for replay.
701#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
702pub struct Text {
703    /// Text content.
704    pub text: String,
705    /// Provider-specific text fields.
706    #[serde(
707        default,
708        deserialize_with = "optional_additional_params",
709        skip_serializing_if = "Option::is_none"
710    )]
711    pub additional_params: Option<AdditionalParams>,
712}
713
714impl Text {
715    /// Construct a new text block with no provider-specific fields.
716    pub fn new(text: impl Into<String>) -> Self {
717        Self {
718            text: text.into(),
719            additional_params: None,
720        }
721    }
722
723    /// Returns the inner text string.
724    pub fn text(&self) -> &str {
725        &self.text
726    }
727}
728
729impl std::fmt::Display for Text {
730    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
731        let Self { text, .. } = self;
732        write!(f, "{text}")
733    }
734}
735
736/// Image content containing image data and metadata about it.
737#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
738pub struct Image {
739    /// Image source data.
740    pub data: DocumentSourceKind,
741    /// Image media type, if known.
742    #[serde(skip_serializing_if = "Option::is_none")]
743    pub media_type: Option<ImageMediaType>,
744    /// Provider-specific image detail preference.
745    #[serde(skip_serializing_if = "Option::is_none")]
746    pub detail: Option<ImageDetail>,
747    /// Provider-specific image fields.
748    #[serde(
749        default,
750        deserialize_with = "optional_additional_params",
751        skip_serializing_if = "Option::is_none"
752    )]
753    pub additional_params: Option<AdditionalParams>,
754}
755
756/// The kind of image source (to be used).
757#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
758#[serde(tag = "type", content = "value", rename_all = "camelCase")]
759pub enum DocumentSourceKind {
760    /// A file URL/URI.
761    Url(String),
762    /// A base-64 encoded string.
763    Base64(String),
764    /// A provider-side uploaded file identifier.
765    FileId(String),
766    /// Raw bytes
767    Raw(Vec<u8>),
768    /// A string (or a string literal).
769    String(String),
770    #[default]
771    /// An unknown file source (there's nothing there).
772    Unknown,
773}
774
775impl DocumentSourceKind {
776    /// Create a URL-backed source.
777    pub fn url(url: &str) -> Self {
778        Self::Url(url.to_string())
779    }
780
781    /// Create a base64-backed source.
782    pub fn base64(base64_string: &str) -> Self {
783        Self::Base64(base64_string.to_string())
784    }
785
786    /// Create a provider file ID-backed source.
787    pub fn file_id(file_id: &str) -> Self {
788        Self::FileId(file_id.to_string())
789    }
790
791    /// Create a string-backed source.
792    pub fn string(input: &str) -> Self {
793        Self::String(input.into())
794    }
795
796    /// Return the contained URL, base64 string, or file ID, if this source stores one.
797    pub fn try_into_inner(self) -> Option<String> {
798        match self {
799            Self::Url(s) | Self::Base64(s) | Self::FileId(s) => Some(s),
800            _ => None,
801        }
802    }
803}
804
805impl std::fmt::Display for DocumentSourceKind {
806    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
807        match self {
808            Self::Url(string) => write!(f, "{string}"),
809            Self::Base64(string) => write!(f, "{string}"),
810            Self::FileId(string) => write!(f, "{string}"),
811            Self::String(string) => write!(f, "{string}"),
812            Self::Raw(_) => write!(f, "<binary data>"),
813            Self::Unknown => write!(f, "<unknown>"),
814        }
815    }
816}
817
818/// Audio content containing audio data and metadata about it.
819#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
820pub struct Audio {
821    /// Audio source data.
822    pub data: DocumentSourceKind,
823    /// Audio media type, if known.
824    #[serde(skip_serializing_if = "Option::is_none")]
825    pub media_type: Option<AudioMediaType>,
826    /// Provider-specific audio fields.
827    #[serde(
828        default,
829        deserialize_with = "optional_additional_params",
830        skip_serializing_if = "Option::is_none"
831    )]
832    pub additional_params: Option<AdditionalParams>,
833}
834
835/// Video content containing video data and metadata about it.
836#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
837pub struct Video {
838    /// Video source data.
839    pub data: DocumentSourceKind,
840    /// Video media type, if known.
841    #[serde(skip_serializing_if = "Option::is_none")]
842    pub media_type: Option<VideoMediaType>,
843    /// Provider-specific video fields.
844    #[serde(
845        default,
846        deserialize_with = "optional_additional_params",
847        skip_serializing_if = "Option::is_none"
848    )]
849    pub additional_params: Option<AdditionalParams>,
850}
851
852/// Document content containing document data and metadata about it.
853#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
854pub struct Document {
855    /// Document source data.
856    pub data: DocumentSourceKind,
857    /// Document media type, if known.
858    #[serde(skip_serializing_if = "Option::is_none")]
859    pub media_type: Option<DocumentMediaType>,
860    /// Provider-specific document fields.
861    #[serde(
862        default,
863        deserialize_with = "optional_additional_params",
864        skip_serializing_if = "Option::is_none"
865    )]
866    pub additional_params: Option<AdditionalParams>,
867}
868
869/// Content representation as base64, text, or a URL.
870#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
871#[serde(rename_all = "lowercase")]
872pub enum ContentFormat {
873    #[default]
874    Base64,
875    String,
876    Url,
877}
878
879/// Helper enum that tracks the media type of the content.
880#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
881pub enum MediaType {
882    Image(ImageMediaType),
883    Audio(AudioMediaType),
884    Document(DocumentMediaType),
885    Video(VideoMediaType),
886}
887
888/// Describes the image media type of the content. Not every provider supports every media type.
889/// Convertible to and from MIME type strings.
890#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
891#[serde(rename_all = "lowercase")]
892pub enum ImageMediaType {
893    JPEG,
894    PNG,
895    GIF,
896    WEBP,
897    HEIC,
898    HEIF,
899    SVG,
900}
901
902/// Describes the document media type of the content. Not every provider supports every media type.
903/// Includes also programming languages as document types for providers who support code running.
904/// Convertible to and from MIME type strings.
905#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
906#[serde(rename_all = "lowercase")]
907pub enum DocumentMediaType {
908    PDF,
909    TXT,
910    RTF,
911    HTML,
912    CSS,
913    MARKDOWN,
914    CSV,
915    XML,
916    Javascript,
917    Python,
918}
919
920impl DocumentMediaType {
921    pub fn is_code(&self) -> bool {
922        matches!(self, Self::Javascript | Self::Python)
923    }
924}
925
926/// Describes the audio media type of the content. Not every provider supports every media type.
927/// Convertible to and from MIME type strings.
928#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
929#[serde(rename_all = "lowercase")]
930pub enum AudioMediaType {
931    WAV,
932    MP3,
933    AIFF,
934    AAC,
935    OGG,
936    FLAC,
937    M4A,
938    PCM16,
939    PCM24,
940}
941
942/// Describes the video media type of the content. Not every provider supports every media type.
943/// Convertible to and from MIME type strings.
944#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
945#[serde(rename_all = "lowercase")]
946pub enum VideoMediaType {
947    AVI,
948    MP4,
949    MPEG,
950    MOV,
951    WEBM,
952}
953
954/// Describes the detail of the image content, which can be low, high, or auto (open-ai specific).
955#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
956#[serde(rename_all = "lowercase")]
957pub enum ImageDetail {
958    Low,
959    High,
960    #[default]
961    Auto,
962}
963
964impl Message {
965    /// Clones the first text block of a user message, or returns `None`.
966    pub fn rag_text(&self) -> Option<String> {
967        match self {
968            Message::User { content } => {
969                for item in content.iter() {
970                    if let UserContent::Text(Text { text, .. }) = item {
971                        return Some(text.clone());
972                    }
973                }
974                None
975            }
976            Message::System { .. } => None,
977            _ => None,
978        }
979    }
980
981    /// Whether a service replaying reasoning `issuers` issued has anything to
982    /// read in this message: false only for an assistant message whose every
983    /// part is reasoning none of them opens.
984    pub fn replays_to(&self, issuers: &[Issuer]) -> bool {
985        match self {
986            Message::Assistant { content, .. } => content.iter().any(|part| match part {
987                AssistantContent::Reasoning(reasoning) => reasoning.open_for(issuers).is_some(),
988                _ => true,
989            }),
990            Message::System { .. } | Message::User { .. } => true,
991        }
992    }
993
994    /// Creates a system instruction message.
995    pub fn system(text: impl Into<String>) -> Self {
996        Message::System {
997            content: text.into(),
998        }
999    }
1000
1001    /// Creates a user message containing one text block.
1002    pub fn user(text: impl Into<String>) -> Self {
1003        Message::User {
1004            content: vec![UserContent::text(text)],
1005        }
1006    }
1007
1008    /// Creates an assistant message containing one text block and no provider ID.
1009    pub fn assistant(text: impl Into<String>) -> Self {
1010        Message::Assistant {
1011            id: None,
1012            content: vec![AssistantContent::text(text)],
1013        }
1014    }
1015
1016    /// Creates a user message containing a text tool result answering the
1017    /// call `call` to the tool `name`. To answer a call you hold, prefer
1018    /// [`ToolCall::result`] with [`Self::tool_results`].
1019    pub fn tool_result(call: CallId, name: ToolName, content: impl Into<String>) -> Self {
1020        Message::User {
1021            content: vec![UserContent::tool_result(
1022                call,
1023                name,
1024                vec![ToolResultContent::text(content)],
1025            )],
1026        }
1027    }
1028
1029    /// Creates a user message carrying `results`, in order.
1030    pub fn tool_results(results: Vec<ToolResult>) -> Self {
1031        Message::User {
1032            content: results.into_iter().map(UserContent::ToolResult).collect(),
1033        }
1034    }
1035}
1036
1037/// Generates media constructors without fetching or decoding source data.
1038macro_rules! media_ctors {
1039    () => {};
1040    (
1041        $(#[$meta:meta])* $name:ident => Image($kind:ident: $data:ty);
1042        $($rest:tt)*
1043    ) => {
1044        $(#[$meta])*
1045        pub fn $name(
1046            data: impl Into<$data>,
1047            media_type: Option<ImageMediaType>,
1048            detail: Option<ImageDetail>,
1049        ) -> Self {
1050            Self::Image(Image {
1051                data: DocumentSourceKind::$kind(data.into()),
1052                media_type,
1053                detail,
1054                additional_params: None,
1055            })
1056        }
1057        media_ctors! { $($rest)* }
1058    };
1059    (
1060        $(#[$meta:meta])* $name:ident => $variant:ident($mt:ty, $kind:ident: $data:ty);
1061        $($rest:tt)*
1062    ) => {
1063        $(#[$meta])*
1064        pub fn $name(data: impl Into<$data>, media_type: Option<$mt>) -> Self {
1065            Self::$variant($variant {
1066                data: DocumentSourceKind::$kind(data.into()),
1067                media_type,
1068                additional_params: None,
1069            })
1070        }
1071        media_ctors! { $($rest)* }
1072    };
1073}
1074
1075impl UserContent {
1076    /// Creates user text content.
1077    pub fn text(text: impl Into<String>) -> Self {
1078        UserContent::Text(text.into().into())
1079    }
1080
1081    media_ctors! {
1082        /// Creates user image content from base64-encoded data.
1083        image_base64 => Image(Base64: String);
1084        /// Creates user image content from unencoded bytes.
1085        image_raw => Image(Raw: Vec<u8>);
1086        /// Creates user image content referencing a URL.
1087        image_url => Image(Url: String);
1088        /// Creates user audio content from base64-encoded data.
1089        audio => Audio(AudioMediaType, Base64: String);
1090        /// Creates user audio content from unencoded bytes.
1091        audio_raw => Audio(AudioMediaType, Raw: Vec<u8>);
1092        /// Creates user audio content referencing a URL.
1093        audio_url => Audio(AudioMediaType, Url: String);
1094        /// Creates user video content from base64-encoded data.
1095        video => Video(VideoMediaType, Base64: String);
1096        /// Creates user video content from unencoded bytes.
1097        video_raw => Video(VideoMediaType, Raw: Vec<u8>);
1098        /// Creates user video content referencing a URL.
1099        video_url => Video(VideoMediaType, Url: String);
1100        /// Creates user document content from unencoded bytes.
1101        document_raw => Document(DocumentMediaType, Raw: Vec<u8>);
1102        /// Creates user document content referencing a URL.
1103        document_url => Document(DocumentMediaType, Url: String);
1104    }
1105
1106    /// Creates document content from a string without decoding or fetching it.
1107    pub fn document(data: impl Into<String>, media_type: Option<DocumentMediaType>) -> Self {
1108        let data: String = data.into();
1109        UserContent::Document(Document {
1110            data: DocumentSourceKind::string(&data),
1111            media_type,
1112            additional_params: None,
1113        })
1114    }
1115
1116    /// Creates a tool result answering the call `call` to the tool `name`.
1117    pub fn tool_result(call: CallId, name: ToolName, content: Vec<ToolResultContent>) -> Self {
1118        UserContent::ToolResult(ToolResult {
1119            call,
1120            name,
1121            content,
1122        })
1123    }
1124}
1125
1126impl AssistantContent {
1127    /// Creates assistant text content.
1128    pub fn text(text: impl Into<String>) -> Self {
1129        AssistantContent::Text(text.into().into())
1130    }
1131
1132    media_ctors! {
1133        /// Creates assistant image content from base64-encoded data.
1134        image_base64 => Image(Base64: String);
1135    }
1136
1137    /// Creates a tool call from a provider-issued ID, name, and arguments.
1138    /// Rig issues an id when `id` is empty.
1139    pub fn tool_call(id: impl Into<String>, name: ToolName, arguments: serde_json::Value) -> Self {
1140        AssistantContent::ToolCall(ToolCall::from_wire(id, ToolFunction { name, arguments }))
1141    }
1142
1143    /// Dual-identifier variant (OpenAI Responses): `id` is the output-item
1144    /// handle (`fc_…`), `call_id` the correlator (`call_…`).
1145    pub fn tool_call_with_call_id(
1146        id: impl Into<String>,
1147        call_id: String,
1148        name: ToolName,
1149        arguments: serde_json::Value,
1150    ) -> Self {
1151        AssistantContent::ToolCall(ToolCall::from_dual_wire(
1152            id,
1153            call_id,
1154            ToolFunction { name, arguments },
1155        ))
1156    }
1157
1158    /// Creates reasoning text issued by `issuer`.
1159    pub fn reasoning(issuer: impl Into<Issuer>, reasoning: impl AsRef<str>) -> Self {
1160        AssistantContent::Reasoning(Reasoning::new(reasoning.as_ref()).sealed(issuer))
1161    }
1162}
1163
1164impl ToolResultContent {
1165    /// Creates literal text tool-result content.
1166    pub fn text(text: impl Into<String>) -> Self {
1167        ToolResultContent::Text(text.into().into())
1168    }
1169
1170    /// Creates structured JSON tool-result content.
1171    pub fn json(value: serde_json::Value) -> Self {
1172        ToolResultContent::Json { value }
1173    }
1174
1175    media_ctors! {
1176        /// Creates tool-result image content from base64-encoded data.
1177        image_base64 => Image(Base64: String);
1178        /// Creates tool-result image content from raw, unencoded bytes.
1179        image_raw => Image(Raw: Vec<u8>);
1180        /// Creates tool-result image content referencing a URL.
1181        image_url => Image(Url: String);
1182    }
1183}
1184
1185/// Trait for converting between MIME types and media types.
1186pub trait MimeType {
1187    fn from_mime_type(mime_type: &str) -> Option<Self>
1188    where
1189        Self: Sized;
1190    fn to_mime_type(&self) -> &'static str;
1191}
1192
1193impl MimeType for MediaType {
1194    fn from_mime_type(mime_type: &str) -> Option<Self> {
1195        ImageMediaType::from_mime_type(mime_type)
1196            .map(MediaType::Image)
1197            .or_else(|| DocumentMediaType::from_mime_type(mime_type).map(MediaType::Document))
1198            .or_else(|| AudioMediaType::from_mime_type(mime_type).map(MediaType::Audio))
1199            .or_else(|| VideoMediaType::from_mime_type(mime_type).map(MediaType::Video))
1200    }
1201
1202    fn to_mime_type(&self) -> &'static str {
1203        match self {
1204            MediaType::Image(media_type) => media_type.to_mime_type(),
1205            MediaType::Audio(media_type) => media_type.to_mime_type(),
1206            MediaType::Document(media_type) => media_type.to_mime_type(),
1207            MediaType::Video(media_type) => media_type.to_mime_type(),
1208        }
1209    }
1210}
1211
1212// Emits both directions of a [`MimeType`] impl from a single pair list, so a
1213// variant's parse and emit spellings cannot drift apart. Extra `| "alias"`
1214// spellings parse to the same variant; only the first (canonical) string is
1215// emitted by `to_mime_type`.
1216macro_rules! impl_mime_type {
1217    ($ty:ident { $($variant:ident => $canonical:literal $(| $alias:literal)*),+ $(,)? }) => {
1218        impl MimeType for $ty {
1219            fn from_mime_type(mime_type: &str) -> Option<Self> {
1220                match mime_type {
1221                    $($canonical $(| $alias)* => Some($ty::$variant),)+
1222                    _ => None,
1223                }
1224            }
1225
1226            fn to_mime_type(&self) -> &'static str {
1227                match self {
1228                    $($ty::$variant => $canonical,)+
1229                }
1230            }
1231        }
1232    };
1233}
1234
1235impl_mime_type!(ImageMediaType {
1236    JPEG => "image/jpeg",
1237    PNG => "image/png",
1238    GIF => "image/gif",
1239    WEBP => "image/webp",
1240    HEIC => "image/heic",
1241    HEIF => "image/heif",
1242    SVG => "image/svg+xml",
1243});
1244
1245impl_mime_type!(DocumentMediaType {
1246    PDF => "application/pdf",
1247    TXT => "text/plain",
1248    RTF => "text/rtf",
1249    HTML => "text/html",
1250    CSS => "text/css",
1251    MARKDOWN => "text/markdown" | "text/md",
1252    CSV => "text/csv",
1253    XML => "text/xml",
1254    Javascript => "application/x-javascript" | "text/x-javascript",
1255    Python => "application/x-python" | "text/x-python",
1256});
1257
1258impl_mime_type!(AudioMediaType {
1259    WAV => "audio/wav",
1260    MP3 => "audio/mp3",
1261    AIFF => "audio/aiff",
1262    AAC => "audio/aac",
1263    OGG => "audio/ogg",
1264    FLAC => "audio/flac",
1265    M4A => "audio/m4a",
1266    PCM16 => "audio/pcm16",
1267    PCM24 => "audio/pcm24",
1268});
1269
1270impl_mime_type!(VideoMediaType {
1271    AVI => "video/avi",
1272    MP4 => "video/mp4",
1273    MPEG => "video/mpeg",
1274    MOV => "video/mov",
1275    WEBM => "video/webm",
1276});
1277
1278impl std::str::FromStr for ImageDetail {
1279    type Err = ();
1280
1281    fn from_str(s: &str) -> Result<Self, Self::Err> {
1282        match s.to_lowercase().as_str() {
1283            "low" => Ok(ImageDetail::Low),
1284            "high" => Ok(ImageDetail::High),
1285            "auto" => Ok(ImageDetail::Auto),
1286            _ => Err(()),
1287        }
1288    }
1289}
1290
1291/// `From` impls for [`Text`] from string-like types.
1292macro_rules! text_from {
1293    ($($src:ty),+ $(,)?) => {$(
1294        impl From<$src> for Text {
1295            fn from(text: $src) -> Self {
1296                Text {
1297                    text: text.into(),
1298                    additional_params: None,
1299                }
1300            }
1301        }
1302    )+};
1303}
1304
1305text_from!(String, &String, &str);
1306
1307/// `From<String>` impls that forward into a content type's `text` constructor.
1308macro_rules! text_content_from_string {
1309    ($($ty:ident),+ $(,)?) => {$(
1310        impl From<String> for $ty {
1311            fn from(text: String) -> Self {
1312                $ty::text(text)
1313            }
1314        }
1315    )+};
1316}
1317
1318text_content_from_string!(ToolResultContent, AssistantContent, UserContent);
1319
1320/// One-line `From<T> for Message` forwards: convert the value, wrap it in the
1321/// named content variant, and build a single-content message.
1322macro_rules! single_content_message_from {
1323    (User { $($src:ty => $variant:ident),+ $(,)? }) => {$(
1324        impl From<$src> for Message {
1325            fn from(value: $src) -> Self {
1326                Message::User {
1327                    content: vec![UserContent::$variant(value.into())],
1328                }
1329            }
1330        }
1331    )+};
1332    (Assistant { $($src:ty => $variant:ident),+ $(,)? }) => {$(
1333        impl From<$src> for Message {
1334            fn from(value: $src) -> Self {
1335                Message::Assistant {
1336                    id: None,
1337                    content: vec![AssistantContent::$variant(value.into())],
1338                }
1339            }
1340        }
1341    )+};
1342}
1343
1344single_content_message_from!(User {
1345    String => Text,
1346    &str => Text,
1347    &String => Text,
1348    Text => Text,
1349    Image => Image,
1350    Audio => Audio,
1351    Document => Document,
1352    ToolResult => ToolResult,
1353});
1354
1355single_content_message_from!(Assistant {
1356    ToolCall => ToolCall,
1357});
1358
1359impl FromStr for Text {
1360    type Err = Infallible;
1361
1362    fn from_str(s: &str) -> Result<Self, Self::Err> {
1363        Ok(s.into())
1364    }
1365}
1366
1367impl From<&Message> for Message {
1368    fn from(msg: &Message) -> Self {
1369        msg.clone()
1370    }
1371}
1372
1373impl From<AssistantContent> for Message {
1374    fn from(content: AssistantContent) -> Self {
1375        Message::Assistant {
1376            id: None,
1377            content: vec![content],
1378        }
1379    }
1380}
1381
1382impl From<UserContent> for Message {
1383    fn from(content: UserContent) -> Self {
1384        Message::User {
1385            content: vec![content],
1386        }
1387    }
1388}
1389
1390impl From<Vec<AssistantContent>> for Message {
1391    fn from(content: Vec<AssistantContent>) -> Self {
1392        Message::Assistant { id: None, content }
1393    }
1394}
1395
1396impl From<Vec<UserContent>> for Message {
1397    fn from(content: Vec<UserContent>) -> Self {
1398        Message::User { content }
1399    }
1400}
1401
1402#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1403#[serde(rename_all = "snake_case")]
1404pub enum ToolChoice {
1405    #[default]
1406    Auto,
1407    None,
1408    Required,
1409    Specific {
1410        function_names: Vec<String>,
1411    },
1412}
1413
1414/// Error type to represent issues with converting messages to and from specific provider messages.
1415#[derive(Debug, Error)]
1416pub enum MessageError {
1417    #[error("Message conversion error: {0}")]
1418    ConversionError(String),
1419}
1420
1421impl From<MessageError> for ProviderError {
1422    fn from(error: MessageError) -> Self {
1423        ProviderError::request(error)
1424    }
1425}
1426
1427#[cfg(test)]
1428mod tests;