1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! Neutral contracts for image artifacts and model image resolution.
use crate::error::Result;
use crate::typed_id::ImageId;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use uuid::Uuid;
/// Metadata for a stored image artifact.
#[derive(Debug, Clone)]
pub struct StoredImageInfo {
pub id: ImageId,
pub filename: String,
pub content_type: String,
pub size_bytes: i64,
pub metadata: serde_json::Value,
pub created_at: DateTime<Utc>,
}
/// Stored image artifact with binary data.
#[derive(Debug, Clone)]
pub struct StoredImage {
pub info: StoredImageInfo,
pub data: Vec<u8>,
}
/// Input for creating a stored image artifact.
#[derive(Debug, Clone)]
pub struct CreateStoredImage {
pub filename: String,
pub content_type: String,
pub data: Vec<u8>,
pub metadata: serde_json::Value,
}
#[async_trait]
pub trait ImageArtifactStore: Send + Sync {
/// Persist an image artifact and return its durable metadata.
async fn create_image(&self, input: CreateStoredImage) -> Result<StoredImageInfo>;
/// Load a stored image artifact including bytes.
async fn get_image(&self, image_id: ImageId) -> Result<Option<StoredImage>>;
/// Load stored image metadata without binary data.
async fn get_image_info(&self, image_id: ImageId) -> Result<Option<StoredImageInfo>>;
}
/// Resolved image data for LLM consumption
///
/// This struct contains the actual image data in a format suitable for
/// sending to LLM providers. Both OpenAI and Anthropic accept base64-encoded
/// images with media type information.
#[derive(Debug, Clone)]
pub struct ResolvedImage {
/// Base64-encoded image data (without data URL prefix)
pub base64: String,
/// MIME type (e.g., "image/png", "image/jpeg")
pub media_type: String,
}
impl ResolvedImage {
/// Create a new resolved image
pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
Self {
base64: base64.into(),
media_type: media_type.into(),
}
}
/// Convert to a data URL suitable for OpenAI Vision API
///
/// Format: `data:{media_type};base64,{base64_data}`
pub fn to_data_url(&self) -> String {
format!("data:{};base64,{}", self.media_type, self.base64)
}
}
/// Trait for resolving image_file content parts to actual image data
///
/// When building LLM messages, `image_file` content parts contain only
/// a reference (UUID) to an uploaded image. This trait allows resolving
/// those references to actual image data.
///
/// # Provider-specific formatting
///
/// The resolved image data is then converted to provider-specific formats:
///
/// **OpenAI Vision:**
/// ```json
/// {
/// "type": "image_url",
/// "image_url": { "url": "data:image/png;base64,..." }
/// }
/// ```
///
/// **Anthropic Vision:**
/// ```json
/// {
/// "type": "image",
/// "source": { "type": "base64", "media_type": "image/png", "data": "..." }
/// }
/// ```
///
/// # Implementation notes
///
/// Implementations should:
/// - Fetch image data from storage (database, S3, etc.)
/// - Return base64-encoded data with media type
/// - Handle missing images gracefully (return None)
#[async_trait]
pub trait ImageResolver: Send + Sync {
/// Resolve an image_file reference to actual image data
///
/// Returns `None` if the image is not found.
async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
}
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolved_images_preserve_mime_and_base64_in_complete_data_urls() {
for (data, mime, expected) in [
("SGVsbG8=", "image/png", "data:image/png;base64,SGVsbG8="),
("+/8=", "image/jpeg", "data:image/jpeg;base64,+/8="),
(
"PHN2Zy8+",
"image/svg+xml",
"data:image/svg+xml;base64,PHN2Zy8+",
),
] {
assert_eq!(ResolvedImage::new(data, mime).to_data_url(), expected);
}
}
}