Skip to main content

shift_preflight/payload/
mod.rs

1pub mod anthropic;
2pub mod openai;
3
4use serde_json::Value;
5
6/// An extracted image from a message payload.
7#[derive(Debug, Clone)]
8pub struct ExtractedImage {
9    /// Index of the message containing this image
10    pub message_index: usize,
11    /// Index of the content part within the message
12    pub content_index: usize,
13    /// The raw image data (decoded from base64 or fetched from URL)
14    pub data: Vec<u8>,
15    /// The original base64 string or URL (for reconstruction)
16    pub original_ref: ImageRef,
17    /// Sequential image index across the whole payload
18    pub global_index: usize,
19}
20
21/// Reference to how the image was originally specified.
22#[derive(Debug, Clone)]
23pub enum ImageRef {
24    /// Base64 data URI: data:image/png;base64,...
25    DataUri { mime_type: String, base64: String },
26    /// Plain base64 string (Anthropic style)
27    Base64 { media_type: String, base64: String },
28    /// URL reference
29    Url(String),
30}
31
32/// Detect which provider format a payload uses.
33pub fn detect_provider(payload: &Value) -> Option<&'static str> {
34    // Anthropic uses top-level "messages" with content blocks having "type": "image"
35    // OpenAI uses "messages" with content parts having "type": "image_url"
36    if let Some(messages) = payload.get("messages").and_then(|m| m.as_array()) {
37        for msg in messages {
38            if let Some(content) = msg.get("content").and_then(|c| c.as_array()) {
39                for part in content {
40                    if let Some(t) = part.get("type").and_then(|t| t.as_str()) {
41                        if t == "image_url" {
42                            return Some("openai");
43                        }
44                        if t == "image" {
45                            return Some("anthropic");
46                        }
47                    }
48                }
49            }
50        }
51    }
52    None
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use serde_json::json;
59
60    #[test]
61    fn test_detect_openai() {
62        let payload = json!({
63            "model": "gpt-4o",
64            "messages": [{
65                "role": "user",
66                "content": [{
67                    "type": "image_url",
68                    "image_url": {"url": "data:image/png;base64,abc"}
69                }]
70            }]
71        });
72        assert_eq!(detect_provider(&payload), Some("openai"));
73    }
74
75    #[test]
76    fn test_detect_anthropic() {
77        let payload = json!({
78            "model": "claude-sonnet-4-20250514",
79            "messages": [{
80                "role": "user",
81                "content": [{
82                    "type": "image",
83                    "source": {"type": "base64", "media_type": "image/png", "data": "abc"}
84                }]
85            }]
86        });
87        assert_eq!(detect_provider(&payload), Some("anthropic"));
88    }
89
90    #[test]
91    fn test_detect_text_only() {
92        let payload = json!({
93            "model": "gpt-4o",
94            "messages": [{
95                "role": "user",
96                "content": "Hello world"
97            }]
98        });
99        assert_eq!(detect_provider(&payload), None);
100    }
101}