Skip to main content

everruns_core/
image_services.rs

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