Skip to main content

rig_core/completion/
message.rs

1use serde::{Deserialize, Serialize};
2use std::{convert::Infallible, str::FromStr};
3use thiserror::Error;
4
5use super::CompletionError;
6
7// ================================================================
8// Message models
9// ================================================================
10
11/// A provider-agnostic chat message.
12///
13/// Messages are role-tagged and may contain one or many content items, including
14/// text, images, audio, documents, tool calls, and tool results. Provider modules
15/// are responsible for translating these generic messages into provider-native
16/// request bodies. That conversion may be lossy when a provider does not support
17/// a particular content type.
18#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
19#[serde(tag = "role", rename_all = "lowercase")]
20pub enum Message {
21    /// System message containing instruction text.
22    System { content: String },
23
24    /// User message containing one or more content types defined by `UserContent`.
25    User { content: Vec<UserContent> },
26
27    /// Assistant message containing one or more content types defined by `AssistantContent`.
28    Assistant {
29        /// Provider-assigned assistant message ID, when available.
30        id: Option<String>,
31        content: Vec<AssistantContent>,
32    },
33}
34
35/// The shared wording for a response whose converted choice is empty.
36///
37/// Every provider decode rejects that state through
38/// [`require_non_empty_response`]; sharing the literal keeps a wording
39/// change from silently forking the error text across wires. A guard
40/// rejecting a *different* state (a role mismatch, a missing message) keeps
41/// its own text instead.
42pub const EMPTY_RESPONSE_ERROR: &str = "Response contained no message or tool call (empty)";
43
44/// Reject an empty content list, with the error the call site chose.
45///
46/// Message content is a `Vec`, so "no content" is representable in the type.
47/// Most wires nevertheless reject it — a completion that carried no message and
48/// no tool call is a provider defect, and a history message with no blocks has
49/// nothing to send — and at least one call site depends on that rejection as
50/// control flow rather than as a diagnostic.
51///
52/// These guards used to be a side effect of the non-empty container's
53/// constructor, which meant every site borrowed the same context-free "cannot
54/// create with an empty vector". Stated explicitly here, each site keeps its own
55/// message, which is where the useful detail lives.
56///
57/// Two rules for anyone extending this:
58///
59/// - It is **mostly** a guard for the **response** direction. Empty assistant
60///   content is legal at the rig level — a tool-call-only turn, a truncated
61///   stream — but a provider returning nothing where its protocol promises
62///   content is malformed, and that is what most of these call sites detect.
63///   Request-direction emptiness at the rig level is rejected once, at the
64///   request boundary — but a few request-conversion sites also use this guard,
65///   because non-empty rig content can still convert to zero *wire* blocks
66///   (e.g. assistant content whose only parts have no representation on that
67///   wire), and only the provider's own conversion can see that. If your
68///   request `TryFrom` can drop parts, guard the converted list too.
69/// - The check is on the **whole list**, never on individual items. A visibly
70///   empty block can still carry data that must survive a round trip: reasoning
71///   signatures and encrypted reasoning attach to blocks whose text is empty.
72///   Emptiness is a property of the list, not of its members.
73pub fn require_non_empty<T, E>(items: Vec<T>, error: impl FnOnce() -> E) -> Result<Vec<T>, E> {
74    if items.is_empty() {
75        return Err(error());
76    }
77    Ok(items)
78}
79
80/// [`require_non_empty`] with the shared response-direction rejection — the
81/// one-line guard for a provider decode whose converted choice is empty.
82/// Pairing the guard with [`EMPTY_RESPONSE_ERROR`] here keeps the wording
83/// from forking per wire. A decode with a *legal* empty case (anthropic's
84/// documented empty `end_turn`) branches around the guard for that case and
85/// still routes every other empty through it.
86pub fn require_non_empty_response<T>(items: Vec<T>) -> Result<Vec<T>, CompletionError> {
87    require_non_empty(items, || {
88        CompletionError::ResponseError(EMPTY_RESPONSE_ERROR.to_owned())
89    })
90}
91
92/// The `Option` sibling of [`require_non_empty`]: `None` for an empty list,
93/// `Some(items)` otherwise.
94///
95/// This is the one home for the "an empty list means absent" rule, wherever a
96/// list is being placed into an `Option`-shaped slot (an optional response
97/// content, an optional message) rather than validated. The same whole-list
98/// rule applies: never decide emptiness per item.
99pub fn non_empty<T>(items: Vec<T>) -> Option<Vec<T>> {
100    if items.is_empty() { None } else { Some(items) }
101}
102
103/// Describes the content of a message, which can be text, a tool result, an image, audio, or
104///  a document. Dependent on provider supporting the content type. Multimedia content is generally
105///  base64 (defined by it's format) encoded but additionally supports urls (for some providers).
106#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
107#[serde(tag = "type", rename_all = "lowercase")]
108pub enum UserContent {
109    /// Plain text user content.
110    Text(Text),
111    /// Result of a tool call returned as user-visible context to the model.
112    ToolResult(ToolResult),
113    /// Image content.
114    Image(Image),
115    /// Audio content.
116    Audio(Audio),
117    /// Video content.
118    Video(Video),
119    /// Document content.
120    Document(Document),
121}
122
123/// Describes responses from a provider which is either text or a tool call.
124///
125/// Tagged with `"type"`, exactly like [`UserContent`]. The tag is required on
126/// deserialize — there is no fallback to the tagless shape 0.41 serialized,
127/// so a bare `{"text": …}` block does not load; see MIGRATING for the
128/// tag-insertion recipe.
129#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
130#[serde(tag = "type", rename_all = "lowercase")]
131pub enum AssistantContent {
132    /// Plain assistant text.
133    Text(Text),
134    /// Tool call requested by the assistant.
135    ToolCall(ToolCall),
136    /// Structured reasoning emitted by the assistant.
137    Reasoning(Reasoning),
138    /// Image content emitted by the assistant.
139    Image(Image),
140}
141
142#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
143#[serde(tag = "type", content = "content", rename_all = "snake_case")]
144/// A typed reasoning block used by providers that emit structured thinking data.
145pub enum ReasoningContent {
146    /// Plain reasoning text with an optional provider signature.
147    Text {
148        text: String,
149        #[serde(skip_serializing_if = "Option::is_none")]
150        signature: Option<String>,
151    },
152    /// Provider-encrypted reasoning payload.
153    Encrypted(String),
154    /// Redacted reasoning payload preserved as opaque data.
155    Redacted { data: String },
156    /// Provider-generated reasoning summary text.
157    Summary(String),
158}
159
160#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
161/// Assistant reasoning payload with an optional provider-supplied identifier.
162pub struct Reasoning {
163    /// Provider reasoning identifier, when supplied by the upstream API.
164    pub id: Option<String>,
165    /// Ordered reasoning content blocks.
166    pub content: Vec<ReasoningContent>,
167}
168
169impl Reasoning {
170    /// Create a new reasoning item from a single item
171    pub fn new(input: &str) -> Self {
172        Self::new_with_signature(input, None)
173    }
174
175    /// Create a new reasoning item from a single text item and optional signature.
176    pub fn new_with_signature(input: &str, signature: Option<String>) -> Self {
177        Self {
178            id: None,
179            content: vec![ReasoningContent::Text {
180                text: input.to_string(),
181                signature,
182            }],
183        }
184    }
185
186    /// Set a provider reasoning ID.
187    pub fn with_id(mut self, id: String) -> Self {
188        self.id = Some(id);
189        self
190    }
191
192    /// Create reasoning content from multiple text blocks.
193    pub fn multi(input: Vec<String>) -> Self {
194        Self {
195            id: None,
196            content: input
197                .into_iter()
198                .map(|text| ReasoningContent::Text {
199                    text,
200                    signature: None,
201                })
202                .collect(),
203        }
204    }
205
206    /// Create a redacted reasoning block.
207    pub fn redacted(data: impl Into<String>) -> Self {
208        Self {
209            id: None,
210            content: vec![ReasoningContent::Redacted { data: data.into() }],
211        }
212    }
213
214    /// Create an encrypted reasoning block.
215    pub fn encrypted(data: impl Into<String>) -> Self {
216        Self {
217            id: None,
218            content: vec![ReasoningContent::Encrypted(data.into())],
219        }
220    }
221
222    /// Create one reasoning block containing summary items.
223    pub fn summaries(input: Vec<String>) -> Self {
224        Self {
225            id: None,
226            content: input.into_iter().map(ReasoningContent::Summary).collect(),
227        }
228    }
229
230    /// Render reasoning as displayable text by joining text-like blocks with newlines.
231    pub fn display_text(&self) -> String {
232        self.content
233            .iter()
234            .filter_map(|content| match content {
235                ReasoningContent::Text { text, .. } => Some(text.as_str()),
236                ReasoningContent::Summary(summary) => Some(summary.as_str()),
237                ReasoningContent::Redacted { data } => Some(data.as_str()),
238                ReasoningContent::Encrypted(_) => None,
239            })
240            .collect::<Vec<_>>()
241            .join("\n")
242    }
243
244    /// Return the first text reasoning block, if present.
245    pub fn first_text(&self) -> Option<&str> {
246        self.content.iter().find_map(|content| match content {
247            ReasoningContent::Text { text, .. } => Some(text.as_str()),
248            _ => None,
249        })
250    }
251
252    /// Return the first signature from text reasoning, if present.
253    pub fn first_signature(&self) -> Option<&str> {
254        self.content.iter().find_map(|content| match content {
255            ReasoningContent::Text {
256                signature: Some(signature),
257                ..
258            } => Some(signature.as_str()),
259            _ => None,
260        })
261    }
262
263    /// Return the first encrypted reasoning payload, if present.
264    pub fn encrypted_content(&self) -> Option<&str> {
265        self.content.iter().find_map(|content| match content {
266            ReasoningContent::Encrypted(data) => Some(data.as_str()),
267            _ => None,
268        })
269    }
270}
271
272/// Tool result content containing information about a tool call and it's resulting content.
273#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
274pub struct ToolResult {
275    /// Which call this result answers — rig's correlation handle, always
276    /// present. Copied from the answered [`ToolCall::id`], which is minted
277    /// at the provider boundary when the provider issued no identifier.
278    pub call: ToolCallId,
279    /// What the provider issued for the answered call, if anything — the
280    /// only identifiers that may travel back on that provider's wire.
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub provider: Option<ProviderCallId>,
283    /// Name of the tool that produced this result — the *executed* tool,
284    /// which can differ from the model's call when a hook repaired it.
285    ///
286    /// Required: several wires key the replay on it (Gemini's
287    /// `functionResponse.name`, Ollama's tool messages), and an identifier
288    /// is not a name — rig used to smuggle the name through the id, which
289    /// collided two calls to the same tool and misnamed cross-provider
290    /// replays (review 84a43e9e #5).
291    pub name: String,
292    /// One or more content items produced by the tool.
293    pub content: Vec<ToolResultContent>,
294}
295
296impl ToolResult {
297    /// The identifier for a wire whose call-id slot is *required*: the
298    /// provider-issued `call_id` when the provider issued one, else rig's
299    /// minted handle — always non-empty.
300    ///
301    /// Wires whose id slot is *optional* (Gemini REST, gRPC) must read
302    /// [`ToolResult::provider`] directly instead: minted handles never
303    /// travel upstream there.
304    pub fn wire_call_id(&self) -> &str {
305        self.provider
306            .as_ref()
307            .map_or(self.call.as_str(), |provider| provider.call_id.as_str())
308    }
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 } => serde_json::from_value(value.clone()),
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/// Error adopting the empty string as a tool-call identifier.
364///
365/// Absence is `None` on [`ToolCall::provider`] (or a minted [`ToolCallId`]),
366/// never `""` — the empty-string sentinel is unrepresentable on these types.
367#[derive(Debug, thiserror::Error)]
368#[error("a tool-call identifier cannot be the empty string; absence is `None` or a minted id")]
369pub struct EmptyToolCallId;
370
371/// Rig's tool-call correlation handle: non-empty by construction, minted at
372/// the provider boundary when the provider issued no identifier.
373///
374/// Mirrors the streaming layer's `WireId` idiom — one idiom, not two —
375/// except that it is *required and minted* rather than optional: correlation
376/// must always work, since a [`ToolResult`] must always name the call it
377/// answers. Provider provenance lives on [`ToolCall::provider`], so a
378/// consumer can still see that the provider issued nothing.
379#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
380#[serde(try_from = "String", into = "String")]
381pub struct ToolCallId(String);
382
383impl ToolCallId {
384    /// Adopt a provider-issued identifier. `None` for the empty string:
385    /// absence is not an id.
386    pub fn new(id: impl Into<String>) -> Option<Self> {
387        let id = id.into();
388        if id.is_empty() { None } else { Some(Self(id)) }
389    }
390
391    /// Mint a fresh, unique handle (21-character URL-safe id).
392    pub fn mint() -> Self {
393        Self(crate::id::generate())
394    }
395
396    /// Adopt `id` when non-empty, mint otherwise — the boundary guard for
397    /// wires that may omit the identifier.
398    pub fn new_or_mint(id: impl Into<String>) -> Self {
399        Self::new(id).unwrap_or_else(Self::mint)
400    }
401
402    /// The correlation handle for the given provider identity: the
403    /// provider's `call_id` when the provider issued one, minted when it
404    /// did not. The single derivation the message and streaming layers
405    /// share — a result correlates with its call because both derive the
406    /// handle from the same provider identity.
407    ///
408    /// (`ProviderCallId`'s constructors reject the empty string, but its
409    /// fields are public, so an empty `call_id` from a literal
410    /// construction still mints rather than producing an empty handle.)
411    pub fn for_provider(provider: Option<&ProviderCallId>) -> Self {
412        provider
413            .and_then(|provider| Self::new(provider.call_id.clone()))
414            .unwrap_or_else(Self::mint)
415    }
416
417    /// Borrow the identifier.
418    pub fn as_str(&self) -> &str {
419        &self.0
420    }
421
422    /// Consume into the underlying string.
423    pub fn into_string(self) -> String {
424        self.0
425    }
426}
427
428impl std::fmt::Display for ToolCallId {
429    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430        f.write_str(&self.0)
431    }
432}
433
434impl AsRef<str> for ToolCallId {
435    fn as_ref(&self) -> &str {
436        &self.0
437    }
438}
439
440impl std::ops::Deref for ToolCallId {
441    type Target = str;
442
443    fn deref(&self) -> &str {
444        &self.0
445    }
446}
447
448impl std::borrow::Borrow<str> for ToolCallId {
449    fn borrow(&self) -> &str {
450        &self.0
451    }
452}
453
454impl TryFrom<String> for ToolCallId {
455    type Error = EmptyToolCallId;
456
457    fn try_from(id: String) -> Result<Self, Self::Error> {
458        Self::new(id).ok_or(EmptyToolCallId)
459    }
460}
461
462impl From<ToolCallId> for String {
463    fn from(id: ToolCallId) -> Self {
464        id.0
465    }
466}
467
468impl PartialEq<str> for ToolCallId {
469    fn eq(&self, other: &str) -> bool {
470        self.0 == other
471    }
472}
473
474impl PartialEq<&str> for ToolCallId {
475    fn eq(&self, other: &&str) -> bool {
476        self.0 == *other
477    }
478}
479
480/// Wire shape for [`ProviderCallId`], so deserialization enforces the
481/// non-empty `call_id` invariant.
482#[derive(Deserialize)]
483struct ProviderCallIdWire {
484    call_id: String,
485    #[serde(default)]
486    item_id: Option<String>,
487}
488
489/// What the provider issued for a call — the only identifiers that may
490/// travel back on that provider's wire.
491///
492/// Dual-identifier wires need both: OpenAI Responses issues an item id
493/// (`fc_…`) *and* a `call_id` (`call_…`), and expects the right one in each
494/// position. Single-identifier wires carry their id in `call_id` and leave
495/// `item_id` empty.
496#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
497#[serde(try_from = "ProviderCallIdWire")]
498pub struct ProviderCallId {
499    /// The call-correlation identifier the provider expects echoed back.
500    pub call_id: String,
501    /// The output-item id issued alongside `call_id` on dual-identifier
502    /// wires (OpenAI Responses `fc_…`).
503    #[serde(default, skip_serializing_if = "Option::is_none")]
504    pub item_id: Option<String>,
505}
506
507impl ProviderCallId {
508    /// Adopt a provider-issued call identifier. `None` for the empty
509    /// string: absence is not an id.
510    pub fn new(call_id: impl Into<String>) -> Option<Self> {
511        let call_id = call_id.into();
512        if call_id.is_empty() {
513            None
514        } else {
515            Some(Self {
516                call_id,
517                item_id: None,
518            })
519        }
520    }
521
522    /// Attach the dual-wire output-item id (empty strings are dropped).
523    pub fn with_item_id(mut self, item_id: impl Into<String>) -> Self {
524        let item_id = item_id.into();
525        self.item_id = (!item_id.is_empty()).then_some(item_id);
526        self
527    }
528
529    /// Derive the provider identity from a streaming part's optional wire
530    /// handles. A dual wire carries `(call_id, item id)`; a single wire's id
531    /// arrives as the tool/part id alone and becomes the `call_id`; with
532    /// neither, the identity is absent. The empty-string filtering is
533    /// load-bearing: [`ProviderCallId::new`] returns `None` on empty, so an
534    /// empty `call_id` must fall through to the single-id arm rather than
535    /// erase a real tool id.
536    ///
537    /// Both streaming surfaces (the parts accumulator and the raw
538    /// `ToolCall` lift) derive through here so they cannot disagree.
539    /// [`ToolCall::from_dual_wire`] is deliberately different — a dual wire
540    /// that omits its `call_id` has no single-id fallback — and stays
541    /// separate.
542    pub fn from_optional_wire(call_id: Option<String>, tool_id: Option<String>) -> Option<Self> {
543        let call_id = call_id.filter(|call_id| !call_id.is_empty());
544        match (call_id, tool_id) {
545            (Some(call_id), tool_id) => Self::new(call_id).map(|provider| match tool_id {
546                Some(tool_id) => provider.with_item_id(tool_id),
547                None => provider,
548            }),
549            (None, Some(tool_id)) => Self::new(tool_id),
550            (None, None) => None,
551        }
552    }
553}
554
555impl TryFrom<ProviderCallIdWire> for ProviderCallId {
556    type Error = EmptyToolCallId;
557
558    fn try_from(wire: ProviderCallIdWire) -> Result<Self, Self::Error> {
559        let Some(provider) = Self::new(wire.call_id) else {
560            return Err(EmptyToolCallId);
561        };
562        Ok(match wire.item_id {
563            Some(item_id) => provider.with_item_id(item_id),
564            None => provider,
565        })
566    }
567}
568
569/// Describes a tool call with an id and function to call, generally produced by a provider.
570#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
571pub struct ToolCall {
572    /// Rig's correlation handle. Always present; minted when the provider
573    /// issued none.
574    pub id: ToolCallId,
575    /// What the provider issued, if anything — the only identifiers that
576    /// may go back on the wire as *that provider's* handles. `None` means
577    /// the provider issued no id (id-less wires such as Gemini REST or
578    /// older Ollama daemons).
579    #[serde(default, skip_serializing_if = "Option::is_none")]
580    pub provider: Option<ProviderCallId>,
581    /// Function name and JSON arguments requested by the model.
582    pub function: ToolFunction,
583    /// Optional cryptographic signature for the tool call.
584    ///
585    /// This field is used by some providers (e.g., Google) to provide a signature
586    /// that can verify the authenticity and integrity of the tool call. When present,
587    /// it allows verification that the tool call was actually generated by the model
588    /// and has not been tampered with.
589    ///
590    /// This is an optional, provider-specific feature and will be `None` for providers
591    /// that don't support tool call signatures.
592    #[serde(default)]
593    pub signature: Option<String>,
594    /// Additional provider-specific parameters to be sent to the completion model provider
595    #[serde(default)]
596    pub additional_params: Option<serde_json::Value>,
597}
598
599impl ToolCall {
600    fn assemble(provider: Option<ProviderCallId>, function: ToolFunction) -> Self {
601        Self {
602            id: ToolCallId::for_provider(provider.as_ref()),
603            provider,
604            function,
605            signature: None,
606            additional_params: None,
607        }
608    }
609
610    /// A call with an explicit correlation handle and no provider-issued id.
611    pub fn new(id: ToolCallId, function: ToolFunction) -> Self {
612        Self {
613            id,
614            ..Self::assemble(None, function)
615        }
616    }
617
618    /// The single-identifier provider boundary: adopt the wire's id when it
619    /// issued one, mint when it did not (empty or absent ids mint).
620    pub fn from_wire(wire_id: impl Into<String>, function: ToolFunction) -> Self {
621        Self::assemble(ProviderCallId::new(wire_id), function)
622    }
623
624    /// The dual-identifier provider boundary (OpenAI Responses): `item_id`
625    /// is the output-item handle (`fc_…`), `call_id` the correlator
626    /// (`call_…`). The correlator drives rig's id; empty ids mint.
627    pub fn from_dual_wire(
628        item_id: impl Into<String>,
629        call_id: impl Into<String>,
630        function: ToolFunction,
631    ) -> Self {
632        let provider =
633            ProviderCallId::new(call_id).map(|provider| provider.with_item_id(item_id.into()));
634        Self::assemble(provider, function)
635    }
636
637    /// Attach provider-issued identifiers.
638    pub fn with_provider(mut self, provider: ProviderCallId) -> Self {
639        self.provider = Some(provider);
640        self
641    }
642
643    /// The identifier for a wire whose call-id slot is *required*: the
644    /// provider-issued `call_id` when the provider issued one, else rig's
645    /// minted handle — always non-empty.
646    ///
647    /// Wires whose id slot is *optional* (Gemini REST, gRPC) must read
648    /// [`ToolCall::provider`] directly instead: minted handles never travel
649    /// upstream there.
650    pub fn wire_call_id(&self) -> &str {
651        self.provider
652            .as_ref()
653            .map_or(self.id.as_str(), |provider| provider.call_id.as_str())
654    }
655
656    pub fn with_signature(mut self, signature: Option<String>) -> Self {
657        self.signature = signature;
658        self
659    }
660
661    pub fn with_additional_params(mut self, additional_params: Option<serde_json::Value>) -> Self {
662        self.additional_params = additional_params;
663        self
664    }
665}
666
667/// Describes a tool function to call with a name and arguments, generally produced by a provider.
668#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
669pub struct ToolFunction {
670    /// Tool/function name to invoke.
671    pub name: String,
672    /// JSON arguments for the tool/function.
673    pub arguments: serde_json::Value,
674}
675
676impl ToolFunction {
677    /// Create a tool function call payload.
678    pub fn new(name: String, arguments: serde_json::Value) -> Self {
679        Self { name, arguments }
680    }
681}
682
683// ================================================================
684// Base content models
685// ================================================================
686
687/// Provider extras on a content block: a non-empty JSON object, by
688/// construction.
689///
690/// The serialized form is the bare object (`#[serde(transparent)]`), so the
691/// wire shape of an `additional_params` field is a named key carrying an
692/// object — never flattened into the block's own key namespace. The type
693/// carries the whole params contract, so no call-site convention is needed:
694///
695/// - `Some(AdditionalParams)` always carries data. The constructors collapse
696///   an empty map to `None` and the inner map is private, so emptiness checks
697///   on a params field are a plain `is_none()`/`is_some()` — no tolerant
698///   shim, in-tree or out.
699/// - A non-object params value is unrepresentable in memory, so serialization
700///   can never emit a value deserialization rejects: what a live run writes,
701///   a restored run loads.
702/// - On decode, `null` and `{}` canonicalize to an absent field (see
703///   [`optional_additional_params`]) and any other non-object value is a loud
704///   error.
705///
706/// The block structs themselves follow the complementary tolerance doctrine:
707/// a known field with the wrong shape is a loud decode error, an *unknown*
708/// key on a block is ignored (never captured, never replayed), and an unknown
709/// content-block tag is a loud error. The params are provider-specific: a
710/// serializer replays only params it recognizes as its own wire's.
711#[derive(Clone, Debug, PartialEq, Serialize)]
712#[serde(transparent)]
713pub struct AdditionalParams(serde_json::Map<String, serde_json::Value>);
714
715impl AdditionalParams {
716    /// The canonical constructor: `None` when the map is empty.
717    pub fn new(map: serde_json::Map<String, serde_json::Value>) -> Option<Self> {
718        if map.is_empty() {
719            None
720        } else {
721            Some(Self(map))
722        }
723    }
724
725    /// Build from `(key, value)` entries; `None` when the iterator yields
726    /// none. `Option<(K, Value)>` is such an iterator, so a conditional
727    /// single-key params reads as
728    /// `AdditionalParams::from_entries(guard.then(|| (key, value)))`.
729    pub fn from_entries<K, I>(entries: I) -> Option<Self>
730    where
731        K: Into<String>,
732        I: IntoIterator<Item = (K, serde_json::Value)>,
733    {
734        Self::new(
735            entries
736                .into_iter()
737                .map(|(key, value)| (key.into(), value))
738                .collect(),
739        )
740    }
741
742    /// The value stored under `key`, when present.
743    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
744        self.0.get(key)
745    }
746
747    /// The underlying (non-empty) object.
748    pub fn as_map(&self) -> &serde_json::Map<String, serde_json::Value> {
749        &self.0
750    }
751
752    /// The params as a bare JSON object value.
753    pub fn into_value(self) -> serde_json::Value {
754        serde_json::Value::Object(self.0)
755    }
756
757    /// Deep-merge `incoming` into `self`: arrays concatenate (streamed
758    /// citation deltas), objects merge recursively, scalars take the
759    /// incoming value.
760    pub fn merge(&mut self, incoming: Self) {
761        // One merge routine at every depth: the top level delegates to the
762        // same map merge the nested Object case uses, so the semantics
763        // cannot drift between single-level and nested params.
764        fn merge_maps(
765            existing: &mut serde_json::Map<String, serde_json::Value>,
766            incoming: serde_json::Map<String, serde_json::Value>,
767        ) {
768            for (key, incoming_value) in incoming {
769                match existing.get_mut(&key) {
770                    Some(existing_value) => merge_value(existing_value, incoming_value),
771                    None => {
772                        existing.insert(key, incoming_value);
773                    }
774                }
775            }
776        }
777        fn merge_value(existing: &mut serde_json::Value, incoming: serde_json::Value) {
778            match (existing, incoming) {
779                (
780                    serde_json::Value::Object(existing_map),
781                    serde_json::Value::Object(incoming_map),
782                ) => merge_maps(existing_map, incoming_map),
783                (
784                    serde_json::Value::Array(existing_array),
785                    serde_json::Value::Array(mut incoming_array),
786                ) => existing_array.append(&mut incoming_array),
787                (existing, incoming) => *existing = incoming,
788            }
789        }
790        merge_maps(&mut self.0, incoming.0);
791    }
792
793    /// The extras stored under a wire's own key, when present — the
794    /// replay-side gate: a serializer asks for its key and never sees
795    /// another wire's extras (capture is unconditional at ingest; replay is
796    /// gated here). A non-object value under the key yields `None` (it is
797    /// not that wire's extras); a caller that must *distinguish* malformed
798    /// from absent — a warn path — pairs this with [`Self::get`]. Never a
799    /// hard error: extras were written by a previous turn, and failing
800    /// serialization over them would turn a persistence blemish into a
801    /// broken conversation.
802    pub fn wire_extras(
803        &self,
804        wire_key: &str,
805    ) -> Option<&serde_json::Map<String, serde_json::Value>> {
806        self.0.get(wire_key).and_then(serde_json::Value::as_object)
807    }
808
809    /// Owned counterpart of [`Self::wire_extras`] for serialization paths
810    /// that already own the params (the common replay case): extracts the
811    /// wire's object without cloning. Same gate semantics.
812    pub fn into_wire_extras(
813        mut self,
814        wire_key: &str,
815    ) -> Option<serde_json::Map<String, serde_json::Value>> {
816        match self.0.remove(wire_key) {
817            Some(serde_json::Value::Object(map)) => Some(map),
818            _ => None,
819        }
820    }
821
822    /// Build from a JSON value: `Ok(None)` for `null` and the empty object
823    /// (canonical absence), `Ok(Some)` for a non-empty object, and `Err`
824    /// handing the value back otherwise — a non-object is never silently
825    /// swallowed; the caller decides loud versus lossy.
826    pub fn try_from_value(value: serde_json::Value) -> Result<Option<Self>, serde_json::Value> {
827        match value {
828            serde_json::Value::Null => Ok(None),
829            serde_json::Value::Object(map) => Ok(Self::new(map)),
830            other => Err(other),
831        }
832    }
833}
834
835impl From<AdditionalParams> for serde_json::Value {
836    fn from(params: AdditionalParams) -> Self {
837        params.into_value()
838    }
839}
840
841impl std::ops::Index<&str> for AdditionalParams {
842    type Output = serde_json::Value;
843
844    /// Panics when the key is absent — the mirror of `serde_json::Map`'s
845    /// `Index`, for test assertions and quick extraction.
846    #[allow(clippy::indexing_slicing)]
847    fn index(&self, key: &str) -> &serde_json::Value {
848        &self.0[key]
849    }
850}
851
852impl<'de> Deserialize<'de> for AdditionalParams {
853    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
854    where
855        D: serde::Deserializer<'de>,
856    {
857        match Self::try_from_value(serde_json::Value::deserialize(deserializer)?) {
858            Ok(Some(params)) => Ok(params),
859            // `null` and `{}` canonicalize to absence, which a bare
860            // (non-`Option`) slot cannot express.
861            Ok(None) => Err(serde::de::Error::custom(
862                "`additional_params` carries no data — omit the field (an `Option` \
863                 field routed through `optional_additional_params` canonicalizes \
864                 `{}` and `null` to absent)",
865            )),
866            Err(_) => Err(serde::de::Error::custom(
867                "`additional_params` must be a non-empty JSON object",
868            )),
869        }
870    }
871}
872
873/// Migration verification: every key path present in `original` (with a
874/// non-`null` value) that is missing or unequal after a tolerant
875/// load-and-reserialize round trip.
876///
877/// The runtime load path ignores unknown keys on content blocks, so a 0.41
878/// history whose flattened provider extras were never re-nested under
879/// `additional_params` loads *silently minus those keys*. This is the opt-in
880/// detector MIGRATING's recipe runs over persisted history once, at
881/// migration time: load a message tolerantly, re-serialize it, and every
882/// dropped key surfaces here by path. An empty result means the history
883/// survives the round trip; keys the current writer *adds* (defaults such as
884/// an explicit `null`) are not differences, and neither is a value the
885/// loader canonicalizes to absence (`null`, the empty object).
886///
887/// # Example
888///
889/// MIGRATING's verification recipe, compiled here so the documented snippet
890/// and the behavior cannot drift — run it once over persisted history at
891/// migration time:
892///
893/// ```
894/// use rig_core::message;
895///
896/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
897/// let original = serde_json::json!({
898///     "role": "assistant",
899///     "content": [{"type": "text", "text": "cited", "citations": ["not re-nested"]}],
900/// });
901/// let loaded: message::Message = serde_json::from_value(original.clone())?;
902/// let round_tripped = serde_json::to_value(&loaded)?;
903/// let lost = message::keys_lost_in_round_trip(&original, &round_tripped);
904/// assert_eq!(lost, vec!["content.0.citations".to_string()]);
905/// # Ok(())
906/// # }
907/// ```
908pub fn keys_lost_in_round_trip(
909    original: &serde_json::Value,
910    round_tripped: &serde_json::Value,
911) -> Vec<String> {
912    fn walk(
913        original: &serde_json::Value,
914        round_tripped: &serde_json::Value,
915        path: &mut String,
916        lost: &mut Vec<String>,
917    ) {
918        match (original, round_tripped) {
919            (serde_json::Value::Object(original_map), serde_json::Value::Object(round_map)) => {
920                for (key, original_value) in original_map {
921                    if original_value.is_null() {
922                        continue;
923                    }
924                    let checkpoint = path.len();
925                    if !path.is_empty() {
926                        path.push('.');
927                    }
928                    path.push_str(key);
929                    match round_map.get(key) {
930                        Some(round_value) => walk(original_value, round_value, path, lost),
931                        // A missing key whose original value the loader
932                        // canonicalizes to absence (the empty object —
933                        // MIGRATING's blessed `"additional_params": {}`
934                        // spelling; `null` is skipped above) is not a loss.
935                        None => {
936                            if !original_value
937                                .as_object()
938                                .is_some_and(serde_json::Map::is_empty)
939                            {
940                                lost.push(path.clone());
941                            }
942                        }
943                    }
944                    path.truncate(checkpoint);
945                }
946            }
947            (serde_json::Value::Array(original_items), serde_json::Value::Array(round_items)) => {
948                for (index, original_value) in original_items.iter().enumerate() {
949                    let checkpoint = path.len();
950                    if !path.is_empty() {
951                        path.push('.');
952                    }
953                    path.push_str(&index.to_string());
954                    match round_items.get(index) {
955                        Some(round_value) => walk(original_value, round_value, path, lost),
956                        None => lost.push(path.clone()),
957                    }
958                    path.truncate(checkpoint);
959                }
960            }
961            (original, round_tripped) => {
962                if original != round_tripped {
963                    lost.push(path.clone());
964                }
965            }
966        }
967    }
968
969    let mut lost = Vec::new();
970    walk(original, round_tripped, &mut String::new(), &mut lost);
971    lost
972}
973
974/// Serde route for `Option<AdditionalParams>` fields: an explicit `null` or
975/// `{}` decodes as `None`, exactly like an absent field — a mechanically
976/// migrated block that wrote `"additional_params": {}` classifies identically
977/// to one that omitted the key. Any other non-object value is a loud decode
978/// error: extras are a keyed namespace (every producer stores an object,
979/// every extractor `get`s a key), so a mis-migrated `[]` or bare string is
980/// malformed data, not a phantom annotation no reader can see.
981pub fn optional_additional_params<'de, D>(
982    deserializer: D,
983) -> Result<Option<AdditionalParams>, D::Error>
984where
985    D: serde::Deserializer<'de>,
986{
987    match Option::<serde_json::Value>::deserialize(deserializer)? {
988        None => Ok(None),
989        Some(value) => AdditionalParams::try_from_value(value).map_err(|_| {
990            serde::de::Error::custom("`additional_params` must be a JSON object (or null)")
991        }),
992    }
993}
994
995/// Basic text content.
996///
997/// `additional_params` carries provider-specific fields that arrive on text
998/// content blocks (e.g. Anthropic returns citation metadata on assistant text
999/// blocks). It is a **named** field in the serialized form — never flattened
1000/// into the block's own key namespace, so a stray key can neither shadow the
1001/// enum tag nor be silently captured, and an absent field decodes as `None`
1002/// (no empty-map artifact). An unknown key on the block itself is ignored on
1003/// decode — tolerated, never captured — so histories written by a newer rig
1004/// stay loadable; [`AdditionalParams`] documents the full doctrine.
1005#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1006pub struct Text {
1007    /// Text content.
1008    pub text: String,
1009    /// Provider-specific text fields.
1010    #[serde(
1011        default,
1012        deserialize_with = "optional_additional_params",
1013        skip_serializing_if = "Option::is_none"
1014    )]
1015    pub additional_params: Option<AdditionalParams>,
1016}
1017
1018impl Text {
1019    /// Construct a new text block with no provider-specific fields.
1020    pub fn new(text: impl Into<String>) -> Self {
1021        Self {
1022            text: text.into(),
1023            additional_params: None,
1024        }
1025    }
1026
1027    /// Returns the inner text string.
1028    pub fn text(&self) -> &str {
1029        &self.text
1030    }
1031}
1032
1033impl std::fmt::Display for Text {
1034    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1035        let Self { text, .. } = self;
1036        write!(f, "{text}")
1037    }
1038}
1039
1040/// Image content containing image data and metadata about it.
1041#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1042pub struct Image {
1043    /// Image source data.
1044    pub data: DocumentSourceKind,
1045    /// Image media type, if known.
1046    #[serde(skip_serializing_if = "Option::is_none")]
1047    pub media_type: Option<ImageMediaType>,
1048    /// Provider-specific image detail preference.
1049    #[serde(skip_serializing_if = "Option::is_none")]
1050    pub detail: Option<ImageDetail>,
1051    /// Provider-specific image fields.
1052    #[serde(
1053        default,
1054        deserialize_with = "optional_additional_params",
1055        skip_serializing_if = "Option::is_none"
1056    )]
1057    pub additional_params: Option<AdditionalParams>,
1058}
1059
1060/// The kind of image source (to be used).
1061#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1062#[serde(tag = "type", content = "value", rename_all = "camelCase")]
1063pub enum DocumentSourceKind {
1064    /// A file URL/URI.
1065    Url(String),
1066    /// A base-64 encoded string.
1067    Base64(String),
1068    /// A provider-side uploaded file identifier.
1069    FileId(String),
1070    /// Raw bytes
1071    Raw(Vec<u8>),
1072    /// A string (or a string literal).
1073    String(String),
1074    #[default]
1075    /// An unknown file source (there's nothing there).
1076    Unknown,
1077}
1078
1079impl DocumentSourceKind {
1080    /// Create a URL-backed source.
1081    pub fn url(url: &str) -> Self {
1082        Self::Url(url.to_string())
1083    }
1084
1085    /// Create a base64-backed source.
1086    pub fn base64(base64_string: &str) -> Self {
1087        Self::Base64(base64_string.to_string())
1088    }
1089
1090    /// Create a provider file ID-backed source.
1091    pub fn file_id(file_id: &str) -> Self {
1092        Self::FileId(file_id.to_string())
1093    }
1094
1095    /// Create a string-backed source.
1096    pub fn string(input: &str) -> Self {
1097        Self::String(input.into())
1098    }
1099
1100    /// Return the contained URL, base64 string, or file ID, if this source stores one.
1101    pub fn try_into_inner(self) -> Option<String> {
1102        match self {
1103            Self::Url(s) | Self::Base64(s) | Self::FileId(s) => Some(s),
1104            _ => None,
1105        }
1106    }
1107}
1108
1109impl std::fmt::Display for DocumentSourceKind {
1110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1111        match self {
1112            Self::Url(string) => write!(f, "{string}"),
1113            Self::Base64(string) => write!(f, "{string}"),
1114            Self::FileId(string) => write!(f, "{string}"),
1115            Self::String(string) => write!(f, "{string}"),
1116            Self::Raw(_) => write!(f, "<binary data>"),
1117            Self::Unknown => write!(f, "<unknown>"),
1118        }
1119    }
1120}
1121
1122/// Audio content containing audio data and metadata about it.
1123#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1124pub struct Audio {
1125    /// Audio source data.
1126    pub data: DocumentSourceKind,
1127    /// Audio media type, if known.
1128    #[serde(skip_serializing_if = "Option::is_none")]
1129    pub media_type: Option<AudioMediaType>,
1130    /// Provider-specific audio fields.
1131    #[serde(
1132        default,
1133        deserialize_with = "optional_additional_params",
1134        skip_serializing_if = "Option::is_none"
1135    )]
1136    pub additional_params: Option<AdditionalParams>,
1137}
1138
1139/// Video content containing video data and metadata about it.
1140#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1141pub struct Video {
1142    /// Video source data.
1143    pub data: DocumentSourceKind,
1144    /// Video media type, if known.
1145    #[serde(skip_serializing_if = "Option::is_none")]
1146    pub media_type: Option<VideoMediaType>,
1147    /// Provider-specific video fields.
1148    #[serde(
1149        default,
1150        deserialize_with = "optional_additional_params",
1151        skip_serializing_if = "Option::is_none"
1152    )]
1153    pub additional_params: Option<AdditionalParams>,
1154}
1155
1156/// Document content containing document data and metadata about it.
1157#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1158pub struct Document {
1159    /// Document source data.
1160    pub data: DocumentSourceKind,
1161    /// Document media type, if known.
1162    #[serde(skip_serializing_if = "Option::is_none")]
1163    pub media_type: Option<DocumentMediaType>,
1164    /// Provider-specific document fields.
1165    #[serde(
1166        default,
1167        deserialize_with = "optional_additional_params",
1168        skip_serializing_if = "Option::is_none"
1169    )]
1170    pub additional_params: Option<AdditionalParams>,
1171}
1172
1173/// Describes the format of the content, which can be base64 or string.
1174#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1175#[serde(rename_all = "lowercase")]
1176pub enum ContentFormat {
1177    #[default]
1178    Base64,
1179    String,
1180    Url,
1181}
1182
1183/// Helper enum that tracks the media type of the content.
1184#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1185pub enum MediaType {
1186    Image(ImageMediaType),
1187    Audio(AudioMediaType),
1188    Document(DocumentMediaType),
1189    Video(VideoMediaType),
1190}
1191
1192/// Describes the image media type of the content. Not every provider supports every media type.
1193/// Convertible to and from MIME type strings.
1194#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1195#[serde(rename_all = "lowercase")]
1196pub enum ImageMediaType {
1197    JPEG,
1198    PNG,
1199    GIF,
1200    WEBP,
1201    HEIC,
1202    HEIF,
1203    SVG,
1204}
1205
1206/// Describes the document media type of the content. Not every provider supports every media type.
1207/// Includes also programming languages as document types for providers who support code running.
1208/// Convertible to and from MIME type strings.
1209#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1210#[serde(rename_all = "lowercase")]
1211pub enum DocumentMediaType {
1212    PDF,
1213    TXT,
1214    RTF,
1215    HTML,
1216    CSS,
1217    MARKDOWN,
1218    CSV,
1219    XML,
1220    Javascript,
1221    Python,
1222}
1223
1224impl DocumentMediaType {
1225    pub fn is_code(&self) -> bool {
1226        matches!(self, Self::Javascript | Self::Python)
1227    }
1228}
1229
1230/// Describes the audio media type of the content. Not every provider supports every media type.
1231/// Convertible to and from MIME type strings.
1232#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1233#[serde(rename_all = "lowercase")]
1234pub enum AudioMediaType {
1235    WAV,
1236    MP3,
1237    AIFF,
1238    AAC,
1239    OGG,
1240    FLAC,
1241    M4A,
1242    PCM16,
1243    PCM24,
1244}
1245
1246/// Describes the video media type of the content. Not every provider supports every media type.
1247/// Convertible to and from MIME type strings.
1248#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1249#[serde(rename_all = "lowercase")]
1250pub enum VideoMediaType {
1251    AVI,
1252    MP4,
1253    MPEG,
1254    MOV,
1255    WEBM,
1256}
1257
1258/// Describes the detail of the image content, which can be low, high, or auto (open-ai specific).
1259#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1260#[serde(rename_all = "lowercase")]
1261pub enum ImageDetail {
1262    Low,
1263    High,
1264    #[default]
1265    Auto,
1266}
1267
1268// ================================================================
1269// Impl. for message models
1270// ================================================================
1271
1272impl Message {
1273    /// This helper method is primarily used to extract the first string prompt from a `Message`.
1274    /// Since `Message` might have more than just text content, we need to find the first text.
1275    pub fn rag_text(&self) -> Option<String> {
1276        match self {
1277            Message::User { content } => {
1278                for item in content.iter() {
1279                    if let UserContent::Text(Text { text, .. }) = item {
1280                        return Some(text.clone());
1281                    }
1282                }
1283                None
1284            }
1285            Message::System { .. } => None,
1286            _ => None,
1287        }
1288    }
1289
1290    /// Helper constructor to make creating system messages easier.
1291    pub fn system(text: impl Into<String>) -> Self {
1292        Message::System {
1293            content: text.into(),
1294        }
1295    }
1296
1297    /// Helper constructor to make creating user messages easier.
1298    pub fn user(text: impl Into<String>) -> Self {
1299        Message::User {
1300            content: vec![UserContent::text(text)],
1301        }
1302    }
1303
1304    /// Helper constructor to make creating assistant messages easier.
1305    pub fn assistant(text: impl Into<String>) -> Self {
1306        Message::Assistant {
1307            id: None,
1308            content: vec![AssistantContent::text(text)],
1309        }
1310    }
1311
1312    /// Helper constructor to make creating tool result messages easier.
1313    /// `call` is the answered call's correlation handle — echo
1314    /// [`ToolCall::id`]; it is never recorded as a provider-issued
1315    /// identifier (see [`UserContent::tool_result`]). `name` is the
1316    /// executed tool's name.
1317    pub fn tool_result(
1318        call: impl Into<String>,
1319        name: impl Into<String>,
1320        content: impl Into<String>,
1321    ) -> Self {
1322        Message::User {
1323            content: vec![UserContent::tool_result(
1324                call,
1325                name,
1326                vec![ToolResultContent::text(content)],
1327            )],
1328        }
1329    }
1330}
1331
1332// The media helper constructors differ only in the wrapped content variant,
1333// the `DocumentSourceKind` constructor, and the accepted data type; the macro
1334// stamps them out while keeping each method's public signature and rustdoc
1335// intact. `Image(...)` rows additionally take the `detail` parameter.
1336macro_rules! media_ctors {
1337    () => {};
1338    (
1339        $(#[$meta:meta])* $name:ident => Image($kind:ident: $data:ty);
1340        $($rest:tt)*
1341    ) => {
1342        $(#[$meta])*
1343        pub fn $name(
1344            data: impl Into<$data>,
1345            media_type: Option<ImageMediaType>,
1346            detail: Option<ImageDetail>,
1347        ) -> Self {
1348            Self::Image(Image {
1349                data: DocumentSourceKind::$kind(data.into()),
1350                media_type,
1351                detail,
1352                additional_params: None,
1353            })
1354        }
1355        media_ctors! { $($rest)* }
1356    };
1357    (
1358        $(#[$meta:meta])* $name:ident => $variant:ident($mt:ty, $kind:ident: $data:ty);
1359        $($rest:tt)*
1360    ) => {
1361        $(#[$meta])*
1362        pub fn $name(data: impl Into<$data>, media_type: Option<$mt>) -> Self {
1363            Self::$variant($variant {
1364                data: DocumentSourceKind::$kind(data.into()),
1365                media_type,
1366                additional_params: None,
1367            })
1368        }
1369        media_ctors! { $($rest)* }
1370    };
1371}
1372
1373impl UserContent {
1374    /// Helper constructor to make creating user text content easier.
1375    pub fn text(text: impl Into<String>) -> Self {
1376        UserContent::Text(text.into().into())
1377    }
1378
1379    media_ctors! {
1380        /// Helper constructor to make creating user image content easier.
1381        image_base64 => Image(Base64: String);
1382        /// Helper constructor to make creating user image content from raw unencoded bytes easier.
1383        image_raw => Image(Raw: Vec<u8>);
1384        /// Helper constructor to make creating user image content easier.
1385        image_url => Image(Url: String);
1386        /// Helper constructor to make creating user audio content easier.
1387        audio => Audio(AudioMediaType, Base64: String);
1388        /// Helper constructor to make creating user audio content from raw unencoded bytes easier.
1389        audio_raw => Audio(AudioMediaType, Raw: Vec<u8>);
1390        /// Helper to create an audio resource from a URL
1391        audio_url => Audio(AudioMediaType, Url: String);
1392        /// Helper constructor to make creating user video content easier.
1393        video => Video(VideoMediaType, Base64: String);
1394        /// Helper constructor to make creating user video content from raw unencoded bytes easier.
1395        video_raw => Video(VideoMediaType, Raw: Vec<u8>);
1396        /// Helper to create a video resource from a URL
1397        video_url => Video(VideoMediaType, Url: String);
1398        /// Helper to create a document from raw unencoded bytes
1399        document_raw => Document(DocumentMediaType, Raw: Vec<u8>);
1400        /// Helper to create a document from a URL
1401        document_url => Document(DocumentMediaType, Url: String);
1402    }
1403
1404    /// Helper constructor to make creating user document content easier.
1405    /// This creates a document that assumes the data being passed in is a raw string.
1406    pub fn document(data: impl Into<String>, media_type: Option<DocumentMediaType>) -> Self {
1407        let data: String = data.into();
1408        UserContent::Document(Document {
1409            data: DocumentSourceKind::string(&data),
1410            media_type,
1411            additional_params: None,
1412        })
1413    }
1414
1415    /// Helper constructor to make creating user tool result content easier.
1416    ///
1417    /// `call` is the answered call's correlation handle — echo
1418    /// [`ToolCall::id`] (an empty string mints a fresh handle). It is
1419    /// recorded as the handle only, never as a provider-issued identifier:
1420    /// a bare string cannot prove provider provenance, and stamping a
1421    /// minted handle as one would send it upstream on wires whose id slot
1422    /// is optional. When you hold provider identifiers, use
1423    /// [`UserContent::tool_result_for`] (from an executed [`ToolCall`]) or
1424    /// [`UserContent::tool_result_from_wire`] (from the provider's wire).
1425    /// `name` is the executed tool's name (required — several wires key
1426    /// the replay on it).
1427    pub fn tool_result(
1428        call: impl Into<String>,
1429        name: impl Into<String>,
1430        content: Vec<ToolResultContent>,
1431    ) -> Self {
1432        UserContent::ToolResult(ToolResult {
1433            call: ToolCallId::new_or_mint(call),
1434            provider: None,
1435            name: name.into(),
1436            content,
1437        })
1438    }
1439
1440    /// Tool result content at the single-identifier provider boundary —
1441    /// the inbound-converter form, mirroring [`ToolCall::from_wire`]:
1442    /// `wire_id` came off the provider's wire, so it is recorded as the
1443    /// provider-issued identifier (empty records none and mints the
1444    /// handle).
1445    pub fn tool_result_from_wire(
1446        wire_id: impl Into<String>,
1447        name: impl Into<String>,
1448        content: Vec<ToolResultContent>,
1449    ) -> Self {
1450        let provider = ProviderCallId::new(wire_id);
1451        let call = ToolCallId::for_provider(provider.as_ref());
1452        Self::tool_result_for(call, provider, name, content)
1453    }
1454
1455    /// Tool result content answering a specific call — the form the agent
1456    /// drivers use: `call`/`provider` come from the executed [`ToolCall`],
1457    /// `name` is the *executed* tool's name (which can differ from the
1458    /// model's call when a hook repaired it).
1459    pub fn tool_result_for(
1460        call: ToolCallId,
1461        provider: Option<ProviderCallId>,
1462        name: impl Into<String>,
1463        content: Vec<ToolResultContent>,
1464    ) -> Self {
1465        UserContent::ToolResult(ToolResult {
1466            call,
1467            provider,
1468            name: name.into(),
1469            content,
1470        })
1471    }
1472
1473    /// Tool result content for a dual-identifier wire (OpenAI Responses):
1474    /// `item_id` is the output-item handle (`fc_…`), `call_id` the
1475    /// correlator (`call_…`). Empty ids record no provider id and mint.
1476    pub fn tool_result_with_call_id(
1477        item_id: impl Into<String>,
1478        call_id: impl Into<String>,
1479        name: impl Into<String>,
1480        content: Vec<ToolResultContent>,
1481    ) -> Self {
1482        let provider = ProviderCallId::new(call_id).map(|provider| provider.with_item_id(item_id));
1483        let call = ToolCallId::for_provider(provider.as_ref());
1484        Self::tool_result_for(call, provider, name, content)
1485    }
1486}
1487
1488impl AssistantContent {
1489    /// Helper constructor to make creating assistant text content easier.
1490    pub fn text(text: impl Into<String>) -> Self {
1491        AssistantContent::Text(text.into().into())
1492    }
1493
1494    media_ctors! {
1495        /// Helper constructor to make creating assistant image content easier.
1496        image_base64 => Image(Base64: String);
1497    }
1498
1499    /// Helper constructor to make creating assistant tool call content easier.
1500    ///
1501    /// `id` is the provider-issued identifier when one exists; an empty
1502    /// `id` records no provider id and mints the correlation handle.
1503    pub fn tool_call(
1504        id: impl Into<String>,
1505        name: impl Into<String>,
1506        arguments: serde_json::Value,
1507    ) -> Self {
1508        AssistantContent::ToolCall(ToolCall::from_wire(
1509            id,
1510            ToolFunction {
1511                name: name.into(),
1512                arguments,
1513            },
1514        ))
1515    }
1516
1517    /// Dual-identifier variant (OpenAI Responses): `id` is the output-item
1518    /// handle (`fc_…`), `call_id` the correlator (`call_…`).
1519    pub fn tool_call_with_call_id(
1520        id: impl Into<String>,
1521        call_id: String,
1522        name: impl Into<String>,
1523        arguments: serde_json::Value,
1524    ) -> Self {
1525        AssistantContent::ToolCall(ToolCall::from_dual_wire(
1526            id,
1527            call_id,
1528            ToolFunction {
1529                name: name.into(),
1530                arguments,
1531            },
1532        ))
1533    }
1534
1535    pub fn reasoning(reasoning: impl AsRef<str>) -> Self {
1536        AssistantContent::Reasoning(Reasoning::new(reasoning.as_ref()))
1537    }
1538}
1539
1540impl ToolResultContent {
1541    /// Helper constructor to make creating tool result text content easier.
1542    pub fn text(text: impl Into<String>) -> Self {
1543        ToolResultContent::Text(text.into().into())
1544    }
1545
1546    /// Helper constructor for structured JSON tool-result content.
1547    pub fn json(value: serde_json::Value) -> Self {
1548        ToolResultContent::Json { value }
1549    }
1550
1551    media_ctors! {
1552        /// Helper constructor to make tool result images from a base64-encoded string.
1553        image_base64 => Image(Base64: String);
1554        /// Helper constructor to make tool result images from a base64-encoded string.
1555        image_raw => Image(Raw: Vec<u8>);
1556        /// Helper constructor to make tool result images from a URL.
1557        image_url => Image(Url: String);
1558    }
1559}
1560
1561/// Trait for converting between MIME types and media types.
1562pub trait MimeType {
1563    fn from_mime_type(mime_type: &str) -> Option<Self>
1564    where
1565        Self: Sized;
1566    fn to_mime_type(&self) -> &'static str;
1567}
1568
1569impl MimeType for MediaType {
1570    fn from_mime_type(mime_type: &str) -> Option<Self> {
1571        ImageMediaType::from_mime_type(mime_type)
1572            .map(MediaType::Image)
1573            .or_else(|| DocumentMediaType::from_mime_type(mime_type).map(MediaType::Document))
1574            .or_else(|| AudioMediaType::from_mime_type(mime_type).map(MediaType::Audio))
1575            .or_else(|| VideoMediaType::from_mime_type(mime_type).map(MediaType::Video))
1576    }
1577
1578    fn to_mime_type(&self) -> &'static str {
1579        match self {
1580            MediaType::Image(media_type) => media_type.to_mime_type(),
1581            MediaType::Audio(media_type) => media_type.to_mime_type(),
1582            MediaType::Document(media_type) => media_type.to_mime_type(),
1583            MediaType::Video(media_type) => media_type.to_mime_type(),
1584        }
1585    }
1586}
1587
1588// Emits both directions of a [`MimeType`] impl from a single pair list, so a
1589// variant's parse and emit spellings cannot drift apart. Extra `| "alias"`
1590// spellings parse to the same variant; only the first (canonical) string is
1591// emitted by `to_mime_type`.
1592macro_rules! impl_mime_type {
1593    ($ty:ident { $($variant:ident => $canonical:literal $(| $alias:literal)*),+ $(,)? }) => {
1594        impl MimeType for $ty {
1595            fn from_mime_type(mime_type: &str) -> Option<Self> {
1596                match mime_type {
1597                    $($canonical $(| $alias)* => Some($ty::$variant),)+
1598                    _ => None,
1599                }
1600            }
1601
1602            fn to_mime_type(&self) -> &'static str {
1603                match self {
1604                    $($ty::$variant => $canonical,)+
1605                }
1606            }
1607        }
1608    };
1609}
1610
1611impl_mime_type!(ImageMediaType {
1612    JPEG => "image/jpeg",
1613    PNG => "image/png",
1614    GIF => "image/gif",
1615    WEBP => "image/webp",
1616    HEIC => "image/heic",
1617    HEIF => "image/heif",
1618    SVG => "image/svg+xml",
1619});
1620
1621impl_mime_type!(DocumentMediaType {
1622    PDF => "application/pdf",
1623    TXT => "text/plain",
1624    RTF => "text/rtf",
1625    HTML => "text/html",
1626    CSS => "text/css",
1627    MARKDOWN => "text/markdown" | "text/md",
1628    CSV => "text/csv",
1629    XML => "text/xml",
1630    Javascript => "application/x-javascript" | "text/x-javascript",
1631    Python => "application/x-python" | "text/x-python",
1632});
1633
1634impl_mime_type!(AudioMediaType {
1635    WAV => "audio/wav",
1636    MP3 => "audio/mp3",
1637    AIFF => "audio/aiff",
1638    AAC => "audio/aac",
1639    OGG => "audio/ogg",
1640    FLAC => "audio/flac",
1641    M4A => "audio/m4a",
1642    PCM16 => "audio/pcm16",
1643    PCM24 => "audio/pcm24",
1644});
1645
1646impl_mime_type!(VideoMediaType {
1647    AVI => "video/avi",
1648    MP4 => "video/mp4",
1649    MPEG => "video/mpeg",
1650    MOV => "video/mov",
1651    WEBM => "video/webm",
1652});
1653
1654impl std::str::FromStr for ImageDetail {
1655    type Err = ();
1656
1657    fn from_str(s: &str) -> Result<Self, Self::Err> {
1658        match s.to_lowercase().as_str() {
1659            "low" => Ok(ImageDetail::Low),
1660            "high" => Ok(ImageDetail::High),
1661            "auto" => Ok(ImageDetail::Auto),
1662            _ => Err(()),
1663        }
1664    }
1665}
1666
1667// ================================================================
1668// FromStr, From<String>, and From<&str> impls
1669// ================================================================
1670
1671/// `From` impls for [`Text`] from string-like types.
1672macro_rules! text_from {
1673    ($($src:ty),+ $(,)?) => {$(
1674        impl From<$src> for Text {
1675            fn from(text: $src) -> Self {
1676                Text {
1677                    text: text.into(),
1678                    additional_params: None,
1679                }
1680            }
1681        }
1682    )+};
1683}
1684
1685text_from!(String, &String, &str);
1686
1687/// `From<String>` impls that forward into a content type's `text` constructor.
1688macro_rules! text_content_from_string {
1689    ($($ty:ident),+ $(,)?) => {$(
1690        impl From<String> for $ty {
1691            fn from(text: String) -> Self {
1692                $ty::text(text)
1693            }
1694        }
1695    )+};
1696}
1697
1698text_content_from_string!(ToolResultContent, AssistantContent, UserContent);
1699
1700/// One-line `From<T> for Message` forwards: convert the value, wrap it in the
1701/// named content variant, and build a single-content message.
1702macro_rules! single_content_message_from {
1703    (User { $($src:ty => $variant:ident),+ $(,)? }) => {$(
1704        impl From<$src> for Message {
1705            fn from(value: $src) -> Self {
1706                Message::User {
1707                    content: vec![UserContent::$variant(value.into())],
1708                }
1709            }
1710        }
1711    )+};
1712    (Assistant { $($src:ty => $variant:ident),+ $(,)? }) => {$(
1713        impl From<$src> for Message {
1714            fn from(value: $src) -> Self {
1715                Message::Assistant {
1716                    id: None,
1717                    content: vec![AssistantContent::$variant(value.into())],
1718                }
1719            }
1720        }
1721    )+};
1722}
1723
1724single_content_message_from!(User {
1725    String => Text,
1726    &str => Text,
1727    &String => Text,
1728    Text => Text,
1729    Image => Image,
1730    Audio => Audio,
1731    Document => Document,
1732    ToolResult => ToolResult,
1733});
1734
1735single_content_message_from!(Assistant {
1736    ToolCall => ToolCall,
1737});
1738
1739impl FromStr for Text {
1740    type Err = Infallible;
1741
1742    fn from_str(s: &str) -> Result<Self, Self::Err> {
1743        Ok(s.into())
1744    }
1745}
1746
1747impl From<&Message> for Message {
1748    fn from(msg: &Message) -> Self {
1749        msg.clone()
1750    }
1751}
1752
1753impl From<AssistantContent> for Message {
1754    fn from(content: AssistantContent) -> Self {
1755        Message::Assistant {
1756            id: None,
1757            content: vec![content],
1758        }
1759    }
1760}
1761
1762impl From<UserContent> for Message {
1763    fn from(content: UserContent) -> Self {
1764        Message::User {
1765            content: vec![content],
1766        }
1767    }
1768}
1769
1770impl From<Vec<AssistantContent>> for Message {
1771    fn from(content: Vec<AssistantContent>) -> Self {
1772        Message::Assistant { id: None, content }
1773    }
1774}
1775
1776impl From<Vec<UserContent>> for Message {
1777    fn from(content: Vec<UserContent>) -> Self {
1778        Message::User { content }
1779    }
1780}
1781
1782impl From<ToolResultContent> for Message {
1783    fn from(tool_result_content: ToolResultContent) -> Self {
1784        Message::User {
1785            content: vec![UserContent::ToolResult(ToolResult {
1786                call: ToolCallId::mint(),
1787                provider: None,
1788                name: String::new(),
1789                content: vec![tool_result_content],
1790            })],
1791        }
1792    }
1793}
1794
1795#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1796#[serde(rename_all = "snake_case")]
1797pub enum ToolChoice {
1798    #[default]
1799    Auto,
1800    None,
1801    Required,
1802    Specific {
1803        function_names: Vec<String>,
1804    },
1805}
1806
1807// ================================================================
1808// Error types
1809// ================================================================
1810
1811/// Error type to represent issues with converting messages to and from specific provider messages.
1812#[derive(Debug, Error)]
1813pub enum MessageError {
1814    #[error("Message conversion error: {0}")]
1815    ConversionError(String),
1816}
1817
1818impl From<MessageError> for CompletionError {
1819    fn from(error: MessageError) -> Self {
1820        CompletionError::RequestError(error.into())
1821    }
1822}
1823
1824#[cfg(test)]
1825mod tests {
1826    use serde::{Deserialize, Serialize};
1827
1828    use super::{AdditionalParams, Message, Reasoning, ReasoningContent, Text, ToolResultContent};
1829
1830    mod vec_content_serde {
1831        use super::super::{AssistantContent, Message, UserContent};
1832
1833        #[test]
1834        fn message_content_still_serializes_as_a_plain_sequence() {
1835            // The removed container serialized as a bare sequence, which is why
1836            // this migration changes no persisted history and no recorded
1837            // provider fixture. Pin the wire shape so that stays true.
1838            let message = Message::User {
1839                content: vec![UserContent::text("hi")],
1840            };
1841            let json = serde_json::to_value(&message).expect("serialize");
1842            assert_eq!(
1843                json,
1844                serde_json::json!({
1845                    "role": "user",
1846                    "content": [{"type": "text", "text": "hi"}],
1847                })
1848            );
1849        }
1850
1851        #[test]
1852        fn message_content_round_trips_byte_identically() {
1853            let message = Message::Assistant {
1854                id: Some("msg_1".to_owned()),
1855                content: vec![AssistantContent::text("hello")],
1856            };
1857            let encoded = serde_json::to_string(&message).expect("serialize");
1858            let decoded: Message = serde_json::from_str(&encoded).expect("deserialize");
1859            assert_eq!(
1860                serde_json::to_string(&decoded).expect("re-serialize"),
1861                encoded
1862            );
1863        }
1864
1865        #[test]
1866        fn an_empty_content_array_now_deserializes() {
1867            // The container's `Deserialize` implemented only `visit_seq` and
1868            // rejected `[]`. That is the single input whose behaviour this
1869            // migration changes: it was an error, and it is now an empty list.
1870            let message: Message =
1871                serde_json::from_value(serde_json::json!({"role": "user", "content": []}))
1872                    .expect("an empty content list is representable now");
1873            let Message::User { content } = message else {
1874                panic!("expected a user message");
1875            };
1876            assert!(content.is_empty());
1877        }
1878    }
1879
1880    #[test]
1881    fn reasoning_constructors_and_accessors_work() {
1882        let single = Reasoning::new("think");
1883        assert_eq!(single.first_text(), Some("think"));
1884        assert_eq!(single.first_signature(), None);
1885
1886        let signed = Reasoning::new_with_signature("signed", Some("sig-1".to_string()));
1887        assert_eq!(signed.first_text(), Some("signed"));
1888        assert_eq!(signed.first_signature(), Some("sig-1"));
1889
1890        let multi = Reasoning::multi(vec!["a".to_string(), "b".to_string()]);
1891        assert_eq!(multi.display_text(), "a\nb");
1892        assert_eq!(multi.first_text(), Some("a"));
1893
1894        let redacted = Reasoning::redacted("redacted-value");
1895        assert_eq!(redacted.display_text(), "redacted-value");
1896        assert_eq!(redacted.first_text(), None);
1897
1898        let encrypted = Reasoning::encrypted("enc");
1899        assert_eq!(encrypted.encrypted_content(), Some("enc"));
1900        assert_eq!(encrypted.display_text(), "");
1901
1902        let summaries = Reasoning::summaries(vec!["s1".to_string(), "s2".to_string()]);
1903        assert_eq!(summaries.display_text(), "s1\ns2");
1904        assert_eq!(summaries.encrypted_content(), None);
1905    }
1906
1907    #[test]
1908    fn reasoning_content_serde_roundtrip() {
1909        let variants = vec![
1910            ReasoningContent::Text {
1911                text: "plain".to_string(),
1912                signature: Some("sig".to_string()),
1913            },
1914            ReasoningContent::Encrypted("opaque".to_string()),
1915            ReasoningContent::Redacted {
1916                data: "redacted".to_string(),
1917            },
1918            ReasoningContent::Summary("summary".to_string()),
1919        ];
1920
1921        for variant in variants {
1922            let json = serde_json::to_string(&variant).expect("serialize");
1923            let roundtrip: ReasoningContent = serde_json::from_str(&json).expect("deserialize");
1924            assert_eq!(roundtrip, variant);
1925        }
1926    }
1927
1928    #[test]
1929    fn system_message_constructor_and_serde_roundtrip() {
1930        let message = Message::system("You are concise.");
1931
1932        match &message {
1933            Message::System { content } => assert_eq!(content, "You are concise."),
1934            _ => panic!("Expected system message"),
1935        }
1936
1937        let json = serde_json::to_string(&message).expect("serialize");
1938        let roundtrip: Message = serde_json::from_str(&json).expect("deserialize");
1939        assert_eq!(roundtrip, message);
1940    }
1941
1942    #[test]
1943    fn current_schema_tool_call_json_round_trips_without_provider_promotion() {
1944        // A minted handle with no provider must stay provider-less —
1945        // nothing in the round trip may invent provider provenance.
1946        let call = super::ToolCall::new(
1947            super::ToolCallId::new("minted-handle").expect("non-empty"),
1948            super::ToolFunction {
1949                name: "add".to_string(),
1950                arguments: serde_json::json!({}),
1951            },
1952        );
1953
1954        let json = serde_json::to_value(&call).expect("serialize");
1955        assert!(json.get("call_id").is_none());
1956        let roundtrip: super::ToolCall = serde_json::from_value(json).expect("deserialize");
1957        assert_eq!(roundtrip.provider, None);
1958        assert_eq!(roundtrip, call);
1959    }
1960
1961    #[test]
1962    fn empty_params_canonicalize_to_none_in_both_serde_directions() {
1963        // The `AdditionalParams` contract, pinned where it lives. One
1964        // fixture, every direction: canonicalization, round-trip, tolerance,
1965        // and rejection.
1966
1967        // An explicit `{}` or `null` decodes as `None` exactly like an
1968        // absent field.
1969        for empty_spelling in [serde_json::json!({}), serde_json::Value::Null] {
1970            let text: Text = serde_json::from_value(
1971                serde_json::json!({"text": "x", "additional_params": empty_spelling}),
1972            )
1973            .expect("deserialize");
1974            assert_eq!(text.additional_params, None);
1975        }
1976
1977        // Data survives a round trip value-identically, and `Some` params
1978        // always carry data — `AdditionalParams` has no empty value, so the
1979        // old uncanonicalized-`Some({})` hazard is unrepresentable rather
1980        // than tolerated.
1981        let text: Text = serde_json::from_value(
1982            serde_json::json!({"text": "x", "additional_params": {"citations": [1]}}),
1983        )
1984        .expect("deserialize");
1985        assert_eq!(
1986            text.additional_params,
1987            AdditionalParams::from_entries([("citations", serde_json::json!([1]))])
1988        );
1989        assert_eq!(
1990            text.additional_params
1991                .as_ref()
1992                .and_then(|params| params.get("citations")),
1993            Some(&serde_json::json!([1]))
1994        );
1995        let round: Text = serde_json::from_value(serde_json::to_value(&text).expect("serialize"))
1996            .expect("round trip");
1997        assert_eq!(round, text);
1998
1999        // The empty map canonicalizes to `None` at the constructor, so it
2000        // never reaches serialization at all.
2001        assert_eq!(AdditionalParams::new(serde_json::Map::new()), None);
2002        assert_eq!(
2003            AdditionalParams::try_from_value(serde_json::json!({})).expect("object"),
2004            None
2005        );
2006
2007        // An unknown key on the block itself is tolerated and dropped —
2008        // never an error, never captured into params — so histories written
2009        // by a newer rig (or 0.41 flattened extras that were never
2010        // re-nested) still load; MIGRATING's strict-decode recipe is the
2011        // opt-in detector for the dropped keys.
2012        let tolerant: Text = serde_json::from_value(
2013            serde_json::json!({"text": "x", "citations": ["stray"], "future_field": 1}),
2014        )
2015        .expect("unknown keys on a block must not fail the decode");
2016        assert_eq!(tolerant.text, "x");
2017        assert_eq!(tolerant.additional_params, None);
2018
2019        // Extras are a keyed namespace: a non-object carrier (the shape a
2020        // mis-firing migration script writes) is malformed data and fails
2021        // loudly instead of loading as a phantom annotation no extractor
2022        // can read.
2023        for malformed in [serde_json::json!([]), serde_json::json!("title")] {
2024            let err = serde_json::from_value::<Text>(
2025                serde_json::json!({"text": "x", "additional_params": malformed}),
2026            )
2027            .expect_err("non-object params must be a decode error");
2028            assert!(
2029                err.to_string().contains("must be a JSON object"),
2030                "unexpected error: {err}"
2031            );
2032            assert!(
2033                AdditionalParams::try_from_value(serde_json::json!([])).is_err(),
2034                "try_from_value must hand a non-object back, not swallow it"
2035            );
2036        }
2037    }
2038
2039    #[test]
2040    fn round_trip_diff_recipe_detects_every_dropped_key() {
2041        // Pins MIGRATING's opt-in verification recipe: the runtime load
2042        // path tolerates unknown keys (see the tolerance case in
2043        // `empty_params_canonicalize_to_none_in_both_serde_directions`),
2044        // and a migration script detects what tolerance dropped by loading,
2045        // re-serializing, and asking `keys_lost_in_round_trip` — a
2046        // serde_ignored-based recipe cannot serve here, because the
2047        // internally tagged enums buffer their content and hide ignored
2048        // keys from its callback.
2049        let migrated = serde_json::json!({
2050            "role": "assistant",
2051            "content": [
2052                {"type": "text", "text": "cited", "citations": ["not re-nested"]},
2053                {"type": "text", "text": "clean",
2054                 "additional_params": {"citations": ["re-nested"]}},
2055            ],
2056        });
2057        let loaded: Message =
2058            serde_json::from_value(migrated.clone()).expect("tolerant decode must succeed");
2059        let reserialized = serde_json::to_value(&loaded).expect("serialize");
2060        assert_eq!(
2061            super::keys_lost_in_round_trip(&migrated, &reserialized),
2062            vec!["content.0.citations".to_string()],
2063            "every dropped key must be reported by path, and only dropped keys \
2064             — writer-added defaults are not differences"
2065        );
2066
2067        // A fully re-nested history survives whole: the recipe's success
2068        // condition is an empty list. MIGRATING's blessed
2069        // `"additional_params": {}` spelling canonicalizes to absence and
2070        // must not read as a loss.
2071        let clean = serde_json::json!({
2072            "role": "assistant",
2073            "content": [
2074                {"type": "text", "text": "clean",
2075                 "additional_params": {"citations": ["re-nested"]}},
2076                {"type": "text", "text": "mechanically migrated",
2077                 "additional_params": {}},
2078            ],
2079        });
2080        let loaded: Message = serde_json::from_value(clean.clone()).expect("decode");
2081        let reserialized = serde_json::to_value(&loaded).expect("serialize");
2082        assert_eq!(
2083            super::keys_lost_in_round_trip(&clean, &reserialized),
2084            Vec::<String>::new(),
2085            "clean history must survive the round trip whole"
2086        );
2087    }
2088
2089    #[test]
2090    fn legacy_call_id_key_is_ignored_not_lifted() {
2091        // The pre-provider-split lift is deleted: a legacy `call_id` key is
2092        // an unknown field, so it deserializes with the key ignored — `id`
2093        // is read as rig's handle and `provider` stays absent. Pinned so a
2094        // future change (e.g. making the key a hard error) is a decision,
2095        // not an accident; the hand-migration recipe lives in MIGRATING.
2096        let legacy = serde_json::json!({
2097            "id": "fc_123",
2098            "call_id": "call_abc",
2099            "function": {"name": "add", "arguments": {"x": 1}},
2100        });
2101
2102        let call: super::ToolCall = serde_json::from_value(legacy).expect("deserialize");
2103        assert_eq!(call.id, "fc_123");
2104        assert_eq!(call.provider, None);
2105    }
2106
2107    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2108    struct ExecutorLikeResponse {
2109        output: serde_json::Value,
2110        logs: Vec<String>,
2111        execution_time_ms: u64,
2112    }
2113
2114    #[test]
2115    fn tool_result_content_decodes_structured_and_legacy_json() {
2116        let response = ExecutorLikeResponse {
2117            output: serde_json::json!({"answer": 42}),
2118            logs: vec!["computed".to_string()],
2119            execution_time_ms: 7,
2120        };
2121        let value = serde_json::to_value(&response).expect("serialize response");
2122
2123        let structured = ToolResultContent::json(value.clone());
2124        assert_eq!(structured.as_json(), Some(&value));
2125        assert_eq!(structured.as_text(), None);
2126        assert_eq!(
2127            structured
2128                .deserialize_json::<ExecutorLikeResponse>()
2129                .expect("decode structured response"),
2130            response
2131        );
2132
2133        let legacy_json = value.to_string();
2134        let legacy_text = ToolResultContent::Text(Text::new(legacy_json.clone()));
2135        assert_eq!(legacy_text.as_text(), Some(legacy_json.as_str()));
2136        assert_eq!(legacy_text.as_json(), None);
2137        assert_eq!(
2138            legacy_text
2139                .deserialize_json::<ExecutorLikeResponse>()
2140                .expect("decode legacy response"),
2141            response
2142        );
2143
2144        let image = ToolResultContent::image_url("https://example.com/result.png", None, None);
2145        let image_error = image.deserialize_json::<ExecutorLikeResponse>();
2146        assert!(image_error.is_err());
2147        if let Err(error) = image_error {
2148            assert_eq!(
2149                error.to_string(),
2150                "cannot decode image tool-result content as JSON"
2151            );
2152        }
2153    }
2154}