Skip to main content

gproxy_protocol/adapt/generate/resources/
claude.rs

1use super::*;
2use crate::wire::{
3    DeclaredFields,
4    claude::{content as c, generate_content as cg},
5};
6
7impl<R: ResourceAccess> GenerationResources<'_, R> {
8    pub async fn claude(
9        &self,
10        input: cg::GenerateContentRequestBody,
11    ) -> Result<cg::GenerateContentRequestBody, TransformError> {
12        let mut input = input.into_declared();
13        let mut budget = self.budget(crate::Dialect::Claude);
14        for message in &mut input.messages {
15            if let c::MessageContent::Blocks(blocks) = &mut message.content {
16                for block in blocks {
17                    match block {
18                        c::ContentBlock::Image(image) => budget.image(image).await?,
19                        c::ContentBlock::Document(doc) => budget.document(doc).await?,
20                        c::ContentBlock::ToolResult(result) => {
21                            if let Some(c::ToolResultContent::Blocks(blocks)) = &mut result.content
22                            {
23                                for block in blocks {
24                                    match block {
25                                        c::ToolResultContentBlock::Image(image) => {
26                                            budget.image(image).await?
27                                        }
28                                        c::ToolResultContentBlock::Document(doc) => {
29                                            budget.document(doc).await?
30                                        }
31                                        _ => {}
32                                    }
33                                }
34                            }
35                        }
36                        _ => {}
37                    }
38                }
39            }
40        }
41        Ok(input)
42    }
43}
44
45impl<R: ResourceAccess> Budget<'_, '_, R> {
46    async fn image(&mut self, image: &mut c::ImageBlock) -> Result<(), TransformError> {
47        let reference = match &image.source {
48            c::ImageSource::Url(source) => ResourceReference::Url(source.url.clone()),
49            c::ImageSource::File(source) => ResourceReference::Id(source.file_id.clone()),
50            c::ImageSource::Base64(_) => return Ok(()),
51        };
52        let Some(media) = self.read(reference, true).await? else {
53            return Ok(());
54        };
55        let mime = match media.mime.as_str() {
56            "image/png" => c::ImageMediaType::Png,
57            "image/jpeg" => c::ImageMediaType::Jpeg,
58            "image/gif" => c::ImageMediaType::Gif,
59            "image/webp" => c::ImageMediaType::Webp,
60            _ => {
61                return Err(TransformError::unsupported(
62                    "image.mime",
63                    "Claude has no native image variant for this MIME",
64                ));
65            }
66        };
67        image.source = c::ImageSource::Base64(
68            c::Base64Source::builder(STANDARD.encode(media.bytes), mime).build(),
69        );
70        Ok(())
71    }
72    async fn document(&mut self, doc: &mut c::DocumentBlock) -> Result<(), TransformError> {
73        let reference = match &mut doc.source {
74            c::DocumentSource::Url(source) => ResourceReference::Url(source.url.clone()),
75            c::DocumentSource::File(source) => ResourceReference::Id(source.file_id.clone()),
76            c::DocumentSource::Content(source) => {
77                if let c::DocumentContent::Blocks(blocks) = &mut source.content {
78                    for block in blocks {
79                        if let c::DocumentContentBlock::Image(image) = block {
80                            self.image(image).await?;
81                        }
82                    }
83                }
84                return Ok(());
85            }
86            _ => return Ok(()),
87        };
88        let Some(media) = self.read(reference, false).await? else {
89            return Ok(());
90        };
91        doc.source = match media.mime.as_str() {
92            "application/pdf" => {
93                if !media.bytes.starts_with(b"%PDF-") {
94                    return Err(TransformError::invalid_result(
95                        "document.mime",
96                        "PDF metadata does not match bytes",
97                    ));
98                }
99                c::DocumentSource::Base64(
100                    c::PdfBase64Source::builder(STANDARD.encode(media.bytes), c::PdfMediaType::Pdf)
101                        .build(),
102                )
103            }
104            "text/plain" => c::DocumentSource::Text(
105                c::PlainTextSource::builder(
106                    String::from_utf8(media.bytes.to_vec()).map_err(|e| {
107                        TransformError::invalid_result("document.utf8", e.to_string())
108                    })?,
109                    c::TextMediaType::Plain,
110                )
111                .build(),
112            ),
113            _ => {
114                return Err(TransformError::unsupported(
115                    "document.mime",
116                    "Claude document requires PDF or plain text",
117                ));
118            }
119        };
120        Ok(())
121    }
122}