aimux_core/shared.rs
1//! Shared V4 value types used across all non-chat model traits.
2//!
3//! Aligned with Vercel AI SDK `SharedV4*` types
4//! (`reference/ai/packages/provider/src/shared/v4/`).
5//!
6//! These types are the common currency passed between the user-facing API and
7//! the provider-facing traits (`EmbeddingModel`, `ImageModel`, …). They are
8//! deliberately opaque to the core: providers read/write provider-specific
9//! keys inside them.
10
11use std::collections::HashMap;
12
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use tokio_util::sync::CancellationToken;
16use ts_rs::TS;
17
18// Re-export the existing `Warning` so providers can import everything they
19// need from `crate::shared` in one place.
20pub use crate::types::Warning;
21
22/// Additional HTTP headers sent with a provider request.
23///
24/// Aligned with V4 `SharedV4Headers` (`Record<string, string>`).
25pub type SharedHeaders = HashMap<String, String>;
26
27/// Additional provider-specific options (inputs), keyed by provider name.
28///
29/// The inner value is a JSON object whose keys are provider-specific option
30/// names. Aligned with V4 `SharedV4ProviderOptions`
31/// (`Record<string, JSONObject>`).
32///
33/// ```json
34/// { "anthropic": { "cacheControl": { "type": "ephemeral" } } }
35/// ```
36pub type SharedProviderOptions = HashMap<String, Value>;
37
38/// Additional provider-specific metadata (outputs), keyed by provider name.
39///
40/// Aligned with V4 `SharedV4ProviderMetadata` (`Record<string, JSONObject>`).
41pub type SharedProviderMetadata = HashMap<String, Value>;
42
43/// A mapping of provider names to provider-specific file identifiers.
44///
45/// Allows a logical file to be referenced across providers without
46/// re-uploading. Aligned with V4 `SharedV4ProviderReference`.
47///
48/// ```json
49/// { "openai": "file-abc123", "anthropic": "file-xyz789" }
50/// ```
51pub type SharedProviderReference = HashMap<String, String>;
52
53/// Either raw bytes or a base64-encoded string.
54///
55/// Providers should pass file data through without unnecessary conversion: if
56/// the upstream API returns base64, return [`FileBytes::Base64`]; if it
57/// returns binary, return [`FileBytes::Binary`].
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
59#[ts(export)]
60pub enum FileBytes {
61 /// Raw binary bytes.
62 Binary(Vec<u8>),
63 /// A base64-encoded string.
64 Base64(String),
65}
66
67/// File data as a tagged discriminated union.
68///
69/// Aligned with V4 `SharedV4FileData`.
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
71#[ts(export)]
72pub enum FileData {
73 /// Raw bytes (`Uint8Array`) or a base64-encoded string.
74 Data { data: FileBytes },
75 /// A URL that points to the file.
76 Url { url: String },
77 /// A provider reference (`{ [provider]: id }`).
78 Reference { reference: SharedProviderReference },
79 /// Inline text content (e.g. an inline text document).
80 Text { text: String },
81}
82
83/// A cancellation signal analogous to the Web `AbortSignal`.
84///
85/// Event-driven: backed by `tokio_util::sync::CancellationToken` (itself a
86/// `tokio::sync::Notify`). Consumers can poll [`is_aborted`](Self::is_aborted)
87/// synchronously, or await [`cancelled`](Self::cancelled) for prompt,
88/// notification-based wakeup — no polling loop needed.
89///
90/// This type is `Send + Sync` and cheap to clone.
91#[derive(Debug, Clone)]
92pub struct AbortSignal {
93 token: CancellationToken,
94}
95
96impl Default for AbortSignal {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102impl AbortSignal {
103 /// Create a fresh, un-aborted signal.
104 #[must_use]
105 pub fn new() -> Self {
106 Self {
107 token: CancellationToken::new(),
108 }
109 }
110
111 /// Request cancellation. All clones observe the aborted state and any
112 /// pending [`cancelled`](Self::cancelled) futures resolve.
113 pub fn abort(&self) {
114 self.token.cancel();
115 }
116
117 /// Returns `true` once [`abort`](Self::abort) has been called.
118 #[must_use]
119 pub fn is_aborted(&self) -> bool {
120 self.token.is_cancelled()
121 }
122
123 /// A future that resolves as soon as the signal is aborted.
124 ///
125 /// If the signal is already aborted, the future resolves immediately on
126 /// the first poll. Suitable for `tokio::select!` arms.
127 pub fn cancelled(&self) -> impl std::future::Future<Output = ()> + Send + 'static {
128 let token = self.token.clone();
129 async move { token.cancelled_owned().await }
130 }
131}
132
133/// Image/video size in `{width}x{height}` format (e.g. `"1024x1024"`).
134///
135/// Newtype that enforces the `WxH` format used by V4 image `size` and video
136/// `resolution` options. Aligned with the TS template-literal type
137/// `` `${number}x${number}` ``.
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
139#[ts(export)]
140pub struct Size {
141 width: u32,
142 height: u32,
143}
144
145impl Size {
146 /// Create a size from explicit pixel dimensions.
147 #[must_use]
148 pub const fn new(width: u32, height: u32) -> Self {
149 Self { width, height }
150 }
151
152 /// Parse a `{width}x{height}` string (e.g. `"1280x720"`).
153 ///
154 /// Returns an error if the string is not in the expected format or either
155 /// dimension is zero.
156 ///
157 /// # Errors
158 ///
159 /// Returns a `String` describing the malformed input when it is not
160 /// `{width}x{height}` or either dimension is zero.
161 pub fn parse(s: &str) -> Result<Self, String> {
162 let (w, h) = parse_pair(s, 'x').ok_or_else(|| {
163 format!("invalid size `{s}`: expected format `{{width}}x{{height}}` (e.g. `1024x1024`)")
164 })?;
165 if w == 0 || h == 0 {
166 return Err(format!("invalid size `{s}`: width and height must be > 0"));
167 }
168 Ok(Self::new(w, h))
169 }
170
171 /// Width in pixels.
172 #[must_use]
173 pub const fn width(&self) -> u32 {
174 self.width
175 }
176
177 /// Height in pixels.
178 #[must_use]
179 pub const fn height(&self) -> u32 {
180 self.height
181 }
182}
183
184impl std::fmt::Display for Size {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 write!(f, "{}x{}", self.width, self.height)
187 }
188}
189
190impl std::str::FromStr for Size {
191 type Err = String;
192 fn from_str(s: &str) -> Result<Self, Self::Err> {
193 Self::parse(s)
194 }
195}
196
197/// Aspect ratio in `{width}:{height}` format (e.g. `"16:9"`).
198///
199/// Newtype that enforces the `W:H` format used by V4 image and video
200/// `aspectRatio` options. Aligned with the TS template-literal type
201/// `` `${number}:${number}` ``.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
203#[ts(export)]
204pub struct AspectRatio {
205 width: u32,
206 height: u32,
207}
208
209impl AspectRatio {
210 /// Create an aspect ratio from explicit components.
211 #[must_use]
212 pub const fn new(width: u32, height: u32) -> Self {
213 Self { width, height }
214 }
215
216 /// Parse a `{width}:{height}` string (e.g. `"16:9"`).
217 ///
218 /// # Errors
219 ///
220 /// Returns a `String` describing the malformed input when it is not
221 /// `{width}:{height}` or either value is zero.
222 pub fn parse(s: &str) -> Result<Self, String> {
223 let (w, h) = parse_pair(s, ':').ok_or_else(|| {
224 format!(
225 "invalid aspect ratio `{s}`: expected format `{{width}}:{{height}}` (e.g. `16:9`)"
226 )
227 })?;
228 if w == 0 || h == 0 {
229 return Err(format!(
230 "invalid aspect ratio `{s}`: width and height must be > 0"
231 ));
232 }
233 Ok(Self::new(w, h))
234 }
235
236 /// Width component.
237 #[must_use]
238 pub const fn width(&self) -> u32 {
239 self.width
240 }
241
242 /// Height component.
243 #[must_use]
244 pub const fn height(&self) -> u32 {
245 self.height
246 }
247}
248
249impl std::fmt::Display for AspectRatio {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 write!(f, "{}:{}", self.width, self.height)
252 }
253}
254
255impl std::str::FromStr for AspectRatio {
256 type Err = String;
257 fn from_str(s: &str) -> Result<Self, Self::Err> {
258 Self::parse(s)
259 }
260}
261
262/// Parse a `{a}{sep}{b}` pair of unsigned integers.
263fn parse_pair(s: &str, sep: char) -> Option<(u32, u32)> {
264 let (a, b) = s.split_once(sep)?;
265 let a: u32 = a.trim().parse().ok()?;
266 let b: u32 = b.trim().parse().ok()?;
267 Some((a, b))
268}
269
270/// Response information for telemetry and debugging.
271///
272/// Aligned with the `response` object that appears on most V4 model results.
273/// All fields are optional because different providers populate different
274/// subsets; traits whose TS spec marks `response` as required use
275/// `ResponseInfo` directly, while those that mark it optional use
276/// `Option<ResponseInfo>`.
277#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
278#[ts(export)]
279pub struct ResponseInfo {
280 /// Timestamp for the start of the generated response (ISO 8601 string).
281 pub timestamp: Option<String>,
282 /// The ID of the model that was used to generate the response.
283 pub model_id: Option<String>,
284 /// Response headers.
285 pub headers: Option<SharedHeaders>,
286 /// The response body (opaque JSON).
287 pub body: Option<Value>,
288}
289
290/// Optional request information for telemetry and debugging.
291///
292/// Aligned with the `request?` object that appears on some V4 model results.
293#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
294#[ts(export)]
295pub struct RequestInfo {
296 /// The request body that was sent (opaque JSON).
297 pub body: Option<Value>,
298}