Skip to main content

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}