Skip to main content

aimux_core/
content.rs

1//! Content parts shared between user-facing and provider-facing messages.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use ts_rs::TS;
6
7/// A part of a multi-part message.
8///
9/// Shared between `ModelMessage` (user-facing) and `LanguageModelPrompt` (provider-facing).
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
11#[serde(tag = "type", rename_all = "snake_case")]
12#[ts(export)]
13pub enum ContentPart {
14    /// A text segment.
15    Text {
16        text: String,
17        /// Provider-specific options for this part (e.g. `openai.promptCacheBreakpoint`).
18        #[serde(default, skip_serializing_if = "Option::is_none")]
19        provider_options: Option<Value>,
20    },
21
22    /// An image (raw bytes + MIME type).
23    Image {
24        image: Vec<u8>,
25        media_type: String,
26        #[serde(default, skip_serializing_if = "Option::is_none")]
27        provider_options: Option<Value>,
28    },
29
30    /// A file (raw bytes + MIME type).
31    File {
32        data: Vec<u8>,
33        media_type: String,
34        #[serde(default, skip_serializing_if = "Option::is_none")]
35        filename: Option<String>,
36        #[serde(default, skip_serializing_if = "Option::is_none")]
37        provider_options: Option<Value>,
38    },
39
40    /// A file whose inline data is an already-base64-encoded string.
41    ///
42    /// Mirrors the Vercel AI SDK `file` part with `data: { type: 'data', data:
43    /// '<base64>' }`: the `data` field holds the raw base64 string verbatim
44    /// (it is NOT decoded into bytes), so it round-trips through providers that
45    /// emit a `base64` source unchanged.
46    FileBase64 {
47        data: String,
48        media_type: String,
49        #[serde(default, skip_serializing_if = "Option::is_none")]
50        filename: Option<String>,
51        #[serde(default, skip_serializing_if = "Option::is_none")]
52        provider_options: Option<Value>,
53    },
54
55    /// A file referenced by URL.
56    ///
57    /// Mirrors the Vercel AI SDK `file` part with `data: { type: 'url', url }`.
58    FileUrl {
59        url: String,
60        media_type: String,
61        #[serde(default, skip_serializing_if = "Option::is_none")]
62        provider_options: Option<Value>,
63    },
64
65    /// A file referenced by a provider-specific reference (e.g. an OpenAI file ID).
66    ///
67    /// Mirrors the Vercel AI SDK `file` part with `data: { type: 'reference',
68    /// reference: { openai: 'file-xxx' } }`. The `reference` field is a JSON
69    /// object keyed by provider name.
70    FileReference {
71        media_type: String,
72        reference: Value,
73        #[serde(default, skip_serializing_if = "Option::is_none")]
74        filename: Option<String>,
75        #[serde(default, skip_serializing_if = "Option::is_none")]
76        provider_options: Option<Value>,
77    },
78
79    /// A reasoning / thinking segment produced by the model.
80    ///
81    /// Mirrors the Vercel AI SDK `reasoning` part. `signature` is the Anthropic
82    /// thinking-block signature (from `providerOptions.anthropic.signature`);
83    /// when absent the part is treated as having unsupported metadata (unless
84    /// `providerOptions.anthropic.redactedData` is present, in which case a
85    /// `redacted_thinking` block is emitted).
86    Reasoning {
87        text: String,
88        signature: Option<String>,
89        /// Provider-specific options for this part (e.g.
90        /// `anthropic.signature`, `anthropic.redactedData`,
91        /// `anthropic.cacheControl`).
92        #[serde(default, skip_serializing_if = "Option::is_none")]
93        provider_options: Option<Value>,
94    },
95
96    /// A tool call requested by the model.
97    ToolCall {
98        tool_call_id: String,
99        tool_name: String,
100        /// Arguments as a JSON value (usually an object).
101        input: Value,
102        /// Whether the tool call is executed by the provider rather than by
103        /// the client. This is part of the prompt contract because providers
104        /// need it to replay server tool calls on a later turn.
105        #[serde(default, skip_serializing_if = "Option::is_none")]
106        provider_executed: Option<bool>,
107        /// Provider-assigned thought signature (e.g. Google Gemini
108        /// `thoughtSignature`). Must be echoed back verbatim on the follow-up
109        /// turn when the tool result is sent.
110        #[serde(default, skip_serializing_if = "Option::is_none")]
111        thought_signature: Option<String>,
112        /// Provider-specific options for this part (e.g.
113        /// `anthropic.cacheControl`).
114        #[serde(default, skip_serializing_if = "Option::is_none")]
115        provider_options: Option<Value>,
116    },
117
118    /// The result of executing a tool call.
119    ToolResult {
120        tool_call_id: String,
121        /// The tool's output (usually a JSON value or plain text).
122        ///
123        /// Accepts the legacy `output` field name (used by the Vercel AI SDK
124        /// and the 0.1.0 TypeScript bindings) as an alias during deserialization,
125        /// so multi-part tool messages constructed either way round-trip
126        /// correctly. Serialization always emits `result`.
127        #[serde(alias = "output")]
128        result: Value,
129        /// The name of the tool that produced this result (optional on the
130        /// user-input side; providers that need it can look it up from the
131        /// preceding tool call).
132        #[serde(default, skip_serializing_if = "Option::is_none")]
133        tool_name: Option<String>,
134        /// Whether the result is an error or error message.
135        #[serde(default, skip_serializing_if = "Option::is_none")]
136        is_error: Option<bool>,
137        /// Whether the result is preliminary (replaces prior, e.g. image previews).
138        #[serde(default, skip_serializing_if = "Option::is_none")]
139        preliminary: Option<bool>,
140        /// Whether the tool is dynamic (defined at runtime, e.g. MCP tools).
141        #[serde(default, skip_serializing_if = "Option::is_none")]
142        dynamic: Option<bool>,
143        /// Provider-specific options for this part (e.g.
144        /// `anthropic.cacheControl`).
145        #[serde(default, skip_serializing_if = "Option::is_none")]
146        provider_options: Option<Value>,
147    },
148}
149
150impl ContentPart {
151    /// Convenience constructor for a text part (no provider options).
152    pub fn text(text: impl Into<String>) -> Self {
153        ContentPart::Text {
154            text: text.into(),
155            provider_options: None,
156        }
157    }
158
159    /// Convenience constructor for a tool-call part (no provider options).
160    pub fn tool_call(
161        tool_call_id: impl Into<String>,
162        tool_name: impl Into<String>,
163        input: Value,
164    ) -> Self {
165        ContentPart::ToolCall {
166            tool_call_id: tool_call_id.into(),
167            tool_name: tool_name.into(),
168            input,
169            provider_executed: None,
170            thought_signature: None,
171            provider_options: None,
172        }
173    }
174
175    /// Convenience constructor for a tool-result part (no provider options).
176    pub fn tool_result(tool_call_id: impl Into<String>, result: Value) -> Self {
177        ContentPart::ToolResult {
178            tool_call_id: tool_call_id.into(),
179            result,
180            tool_name: None,
181            is_error: None,
182            preliminary: None,
183            dynamic: None,
184            provider_options: None,
185        }
186    }
187
188    /// Convenience constructor for a reasoning/thinking part (no provider
189    /// options, no signature).
190    pub fn reasoning(text: impl Into<String>) -> Self {
191        ContentPart::Reasoning {
192            text: text.into(),
193            signature: None,
194            provider_options: None,
195        }
196    }
197
198    /// Convenience constructor for an image part (no provider options).
199    pub fn image(image: Vec<u8>, media_type: impl Into<String>) -> Self {
200        ContentPart::Image {
201            image,
202            media_type: media_type.into(),
203            provider_options: None,
204        }
205    }
206
207    /// Convenience constructor for a file part (no provider options, no filename).
208    pub fn file(data: Vec<u8>, media_type: impl Into<String>) -> Self {
209        ContentPart::File {
210            data,
211            media_type: media_type.into(),
212            filename: None,
213            provider_options: None,
214        }
215    }
216
217    /// Convenience constructor for a base64-encoded file part (no provider
218    /// options, no filename).
219    pub fn file_base64(data: impl Into<String>, media_type: impl Into<String>) -> Self {
220        ContentPart::FileBase64 {
221            data: data.into(),
222            media_type: media_type.into(),
223            filename: None,
224            provider_options: None,
225        }
226    }
227
228    /// Convenience constructor for a URL-referenced file part (no provider options).
229    pub fn file_url(url: impl Into<String>, media_type: impl Into<String>) -> Self {
230        ContentPart::FileUrl {
231            url: url.into(),
232            media_type: media_type.into(),
233            provider_options: None,
234        }
235    }
236
237    /// Convenience constructor for a provider-referenced file part (no provider
238    /// options, no filename).
239    pub fn file_reference(media_type: impl Into<String>, reference: Value) -> Self {
240        ContentPart::FileReference {
241            media_type: media_type.into(),
242            reference,
243            filename: None,
244            provider_options: None,
245        }
246    }
247}