gproxy_protocol/transform/generate/claude_gemini/
media.rs1use crate::{
2 transform::{Report, TransformError},
3 wire::{claude::content as c, gemini as g},
4};
5use base64::{Engine, engine::general_purpose::STANDARD};
6use std::collections::BTreeMap;
7
8#[derive(Debug, Default)]
11pub struct MediaFacts {
12 pub resources: BTreeMap<String, g::FileData>,
13}
14
15pub(super) fn image(
16 block: c::ImageBlock,
17 facts: &MediaFacts,
18 report: &mut Report,
19) -> Result<g::Part, TransformError> {
20 if block.cache_control.is_some() {
21 report.omitted("image.cache_control", "Gemini has no per-part cache field");
22 }
23 match block.source {
24 c::ImageSource::Base64(source) => blob(source.data, mime(source.media_type).into()),
25 c::ImageSource::Url(source) => resource(&source.url, facts, true),
26 c::ImageSource::File(source) => resource(&source.file_id, facts, false),
27 }
28}
29
30fn resource(key: &str, facts: &MediaFacts, is_url: bool) -> Result<g::Part, TransformError> {
31 if let Some(value) = facts.resources.get(key) {
32 if value.mime_type.as_deref().is_none_or(str::is_empty)
33 || !(value.file_uri.starts_with("https://")
34 || value.file_uri.starts_with("http://")
35 || value.file_uri.starts_with("gs://"))
36 {
37 return Err(TransformError::missing_metadata(
38 "media resolved URI and MIME",
39 ));
40 }
41 return Ok(g::Part::builder()
42 .file_data(g::FileData {
43 file_uri: value.file_uri.clone(),
44 mime_type: value.mime_type.clone(),
45 display_name: value.display_name.clone(),
46 rest: Default::default(),
47 })
48 .build());
49 }
50 if is_url && key.starts_with("data:") {
51 let (header, data) = key
52 .split_once(',')
53 .ok_or_else(|| TransformError::shape("data_url", "missing separator"))?;
54 let mime = header
55 .strip_prefix("data:")
56 .and_then(|v| v.strip_suffix(";base64"))
57 .ok_or_else(|| TransformError::unsupported("data_url", "base64 data URL required"))?;
58 return blob(data.into(), mime.into());
59 }
60 Err(TransformError::missing_metadata(format!(
61 "media[{key}] resolved URI and MIME"
62 )))
63}
64
65fn blob(data: String, mime: String) -> Result<g::Part, TransformError> {
66 STANDARD
67 .decode(&data)
68 .map_err(|e| TransformError::shape("media.base64", e.to_string()))?;
69 if mime.is_empty() {
70 return Err(TransformError::missing_metadata("media.mime"));
71 }
72 Ok(g::Part::builder()
73 .inline_data(g::Blob::builder(mime, data).build())
74 .build())
75}
76
77fn mime(value: c::ImageMediaType) -> &'static str {
78 match value {
79 c::ImageMediaType::Jpeg => "image/jpeg",
80 c::ImageMediaType::Png => "image/png",
81 c::ImageMediaType::Gif => "image/gif",
82 c::ImageMediaType::Webp => "image/webp",
83 }
84}
85
86fn image_mime(value: &str) -> Option<c::ImageMediaType> {
87 match value {
88 "image/jpeg" => Some(c::ImageMediaType::Jpeg),
89 "image/png" => Some(c::ImageMediaType::Png),
90 "image/gif" => Some(c::ImageMediaType::Gif),
91 "image/webp" => Some(c::ImageMediaType::Webp),
92 _ => None,
93 }
94}
95
96pub(super) fn document(
97 block: c::DocumentBlock,
98 facts: &MediaFacts,
99 report: &mut Report,
100) -> Result<Vec<g::Part>, TransformError> {
101 if block.cache_control.is_some() {
102 report.omitted(
103 "document.cache_control",
104 "Gemini has no per-part cache field",
105 );
106 }
107 Ok(match block.source {
108 c::DocumentSource::Base64(source) => vec![blob(source.data, "application/pdf".into())?],
109 c::DocumentSource::Text(source) => vec![blob(
110 STANDARD.encode(source.data.as_bytes()),
111 "text/plain".into(),
112 )?],
113 c::DocumentSource::Url(source) => vec![resource(&source.url, facts, true)?],
114 c::DocumentSource::File(source) => vec![resource(&source.file_id, facts, false)?],
115 c::DocumentSource::Content(source) => match source.content {
116 c::DocumentContent::Text(text) => vec![g::Part::builder().text(text).build()],
117 c::DocumentContent::Blocks(blocks) => {
118 let mut out = Vec::new();
119 for block in blocks {
120 out.push(match block {
121 c::DocumentContentBlock::Text(text) => {
122 g::Part::builder().text(text.text).build()
123 }
124 c::DocumentContentBlock::Image(block) => image(block, facts, report)?,
125 });
126 }
127 out
128 }
129 },
130 })
131}
132
133pub(super) fn inline(source: g::Blob) -> Result<c::ContentBlock, TransformError> {
134 let bytes = STANDARD
135 .decode(&source.data)
136 .map_err(|e| TransformError::shape("inline_data", e.to_string()))?;
137 if let Some(media_type) = image_mime(&source.mime_type) {
138 return Ok(c::ContentBlock::Image(
139 c::ImageBlock::builder(
140 c::ImageBlockType::Tag,
141 c::ImageSource::Base64(c::Base64Source::builder(source.data, media_type).build()),
142 )
143 .build(),
144 ));
145 }
146 let source = match source.mime_type.as_str() {
147 "application/pdf" => c::DocumentSource::Base64(
148 c::PdfBase64Source::builder(source.data, c::PdfMediaType::Pdf).build(),
149 ),
150 "text/plain" => c::DocumentSource::Text(
151 c::PlainTextSource::builder(
152 String::from_utf8(bytes)
153 .map_err(|e| TransformError::shape("inline_data.text", e.to_string()))?,
154 c::TextMediaType::Plain,
155 )
156 .build(),
157 ),
158 _ => {
159 return Err(TransformError::unsupported(
160 "inline_data.mime_type",
161 "Claude supports only image, PDF or text document media in this contract",
162 ));
163 }
164 };
165 Ok(c::ContentBlock::Document(
166 c::DocumentBlock::builder(c::DocumentBlockType::Tag, source).build(),
167 ))
168}
169
170pub(super) fn file(source: g::FileData) -> Result<c::ContentBlock, TransformError> {
171 if !source.file_uri.starts_with("https://") && !source.file_uri.starts_with("http://") {
172 return Err(TransformError::missing_metadata(
173 "file_data public URL or transferred Claude file ID",
174 ));
175 }
176 let mime = source
177 .mime_type
178 .ok_or_else(|| TransformError::missing_metadata("file_data.mime_type"))?;
179 if image_mime(&mime).is_some() {
180 return Ok(c::ContentBlock::Image(
181 c::ImageBlock::builder(
182 c::ImageBlockType::Tag,
183 c::ImageSource::Url(c::UrlSource::builder(source.file_uri).build()),
184 )
185 .build(),
186 ));
187 }
188 if mime == "application/pdf" {
189 return Ok(c::ContentBlock::Document(
190 c::DocumentBlock::builder(
191 c::DocumentBlockType::Tag,
192 c::DocumentSource::Url(c::UrlSource::builder(source.file_uri).build()),
193 )
194 .build(),
195 ));
196 }
197 Err(TransformError::unsupported(
198 "file_data.mime_type",
199 "requested file MIME needs a resource conversion",
200 ))
201}