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}