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