Skip to main content

gproxy_protocol/transform/generate/claude_gemini/stream/
claude_to_gemini.rs

1use super::super::ClaudeGeminiUsageFacts;
2use super::{
3    claude_blocks::Blocks,
4    common::{Budget, StreamEnd, StreamLimits, bound, invalid, normalize_gemini, response_id},
5    usage::ClaudeUsageProgress,
6};
7use crate::transform::generate::stream::{
8    claude::{ClaudeStreamCollector, ClaudeStreamLimits},
9    gemini::{GeminiStreamCollector, GeminiStreamLimits},
10};
11use crate::{
12    transform::{
13        Converted, Report, TransformError,
14        identity::{IdentityFlow, TargetIdPolicy},
15    },
16    wire::{DeclaredFields, claude::stream as s, gemini as g},
17};
18
19#[derive(Debug, Default, Clone, Copy)]
20pub struct ClaudeToGeminiContext {
21    pub usage: ClaudeGeminiUsageFacts,
22}
23
24pub struct ClaudeToGeminiStream {
25    source: Option<ClaudeStreamCollector>,
26    target: Option<GeminiStreamCollector>,
27    flow: IdentityFlow,
28    policy: TargetIdPolicy,
29    budget: Budget,
30    limits: StreamLimits,
31    blocks: Blocks,
32    facts: ClaudeGeminiUsageFacts,
33    usage: ClaudeUsageProgress,
34    response_id: Option<String>,
35    model: Option<String>,
36    failed: bool,
37    stopped: bool,
38}
39
40impl ClaudeToGeminiStream {
41    pub fn new(
42        context: ClaudeToGeminiContext,
43        flow: IdentityFlow,
44        limits: StreamLimits,
45    ) -> Result<Self, TransformError> {
46        Self::new_with_policy(
47            context,
48            flow,
49            limits,
50            TargetIdPolicy::new(crate::Dialect::Gemini),
51        )
52    }
53    pub fn new_with_policy(
54        context: ClaudeToGeminiContext,
55        flow: IdentityFlow,
56        limits: StreamLimits,
57        policy: TargetIdPolicy,
58    ) -> Result<Self, TransformError> {
59        if policy.dialect != crate::Dialect::Gemini {
60            return Err(TransformError::shape(
61                "stream.policy",
62                "target policy dialect mismatch",
63            ));
64        }
65
66        Ok(Self {
67            source: Some(ClaudeStreamCollector::new(ClaudeStreamLimits {
68                max_text_bytes: limits.max_bytes,
69                max_json_bytes: limits.max_bytes,
70            })),
71            target: Some(GeminiStreamCollector::new(GeminiStreamLimits {
72                max_bytes: limits.max_bytes,
73            })),
74            flow,
75            policy,
76            budget: Budget::new(limits),
77            limits,
78            blocks: Blocks::new(limits),
79            facts: context.usage,
80            usage: Default::default(),
81            response_id: None,
82            model: None,
83            failed: false,
84            stopped: false,
85        })
86    }
87    pub(crate) fn reserve_external_ids(
88        &mut self,
89        role: crate::transform::identity::IdentityRole,
90        ids: &std::collections::BTreeSet<String>,
91    ) -> Result<(), TransformError> {
92        self.flow.reserve_external_ids(role, ids).map_err(|e| {
93            TransformError::new(
94                crate::transform::TransformErrorKind::Conflict,
95                "fanout.ids",
96                e.to_string(),
97            )
98        })
99    }
100    pub fn identities(&self) -> &IdentityFlow {
101        &self.flow
102    }
103    pub fn push(
104        &mut self,
105        event: s::StreamEvent,
106    ) -> Result<Converted<Vec<g::GenerateContentResponseBody>>, TransformError> {
107        if self.failed || self.stopped {
108            self.failed = true;
109            return Err(invalid("stream", "event after message_stop or failure"));
110        }
111        let result = self.inner(event.into_declared());
112        if result.is_err() {
113            self.failed = true;
114        }
115        result
116    }
117    fn inner(
118        &mut self,
119        event: s::StreamEvent,
120    ) -> Result<Converted<Vec<g::GenerateContentResponseBody>>, TransformError> {
121        self.budget.input(&event)?;
122        self.source
123            .as_mut()
124            .ok_or_else(|| invalid("stream", "source consumed"))?
125            .push(event.clone())?;
126        match event {
127            s::StreamEvent::MessageStart(v) => {
128                self.usage.start(&v.message.usage)?;
129                self.response_id = Some(response_id(
130                    &mut self.flow,
131                    &self.policy,
132                    crate::Dialect::Claude,
133                    Some(v.message.id),
134                )?);
135                self.model = Some(v.message.model);
136            }
137            s::StreamEvent::ContentBlockStart(v) => {
138                self.blocks.start(v, &mut self.flow, &self.policy)?
139            }
140            s::StreamEvent::ContentBlockDelta(v) => self.blocks.delta(v)?,
141            s::StreamEvent::ContentBlockStop(v) => self.blocks.stop(v.index)?,
142            s::StreamEvent::MessageDelta(v) => self.usage.delta(&v.usage)?,
143            s::StreamEvent::MessageStop(_) => self.stopped = true,
144            s::StreamEvent::Ping(_) => {}
145            s::StreamEvent::Error(_) => {
146                return Err(invalid("stream.error", "Claude upstream error"));
147            }
148        }
149        let mut out = Vec::new();
150        while let Some(part) = self.blocks.next()? {
151            let body = g::GenerateContentResponseBody::builder()
152                .candidates(vec![
153                    g::Candidate::builder()
154                        .index(0)
155                        .content(
156                            g::Content::builder()
157                                .role("model")
158                                .parts(vec![part])
159                                .build(),
160                        )
161                        .build(),
162                ])
163                .build();
164            self.emit(&mut out, body)?;
165        }
166        self.blocks.check_pending()?;
167        Ok(Converted {
168            value: out,
169            report: Report::default(),
170        })
171    }
172    fn emit(
173        &mut self,
174        out: &mut Vec<g::GenerateContentResponseBody>,
175        mut body: g::GenerateContentResponseBody,
176    ) -> Result<(), TransformError> {
177        body.response_id = self.response_id.clone();
178        body.model_version = self.model.clone();
179        self.budget.output(&body)?;
180        self.target
181            .as_mut()
182            .ok_or_else(|| invalid("stream", "target consumed"))?
183            .push(body.clone())?;
184        out.push(body);
185        Ok(())
186    }
187    pub fn finish(mut self) -> Result<StreamEnd<g::GenerateContentResponseBody>, TransformError> {
188        if self.failed || !self.stopped || !self.blocks.is_empty() {
189            return Err(invalid(
190                "stream",
191                "failed stream, premature EOF or pending blocks",
192            ));
193        }
194        let mut source = self
195            .source
196            .take()
197            .ok_or_else(|| invalid("stream", "source consumed"))?
198            .finish()?
199            .value;
200        self.usage.finalize(&mut source.usage, self.facts)?;
201        let converted = super::super::claude_to_gemini_response(
202            source,
203            self.facts,
204            &mut self.flow.clone(),
205            &self.policy,
206        )?;
207        let mut expected = converted.value;
208        bound(&expected, self.limits.max_bytes)?;
209        let mut tail = expected.clone();
210        for candidate in tail.candidates.iter_mut().flatten() {
211            if let Some(content) = &mut candidate.content {
212                content.parts = Some(Vec::new());
213            }
214        }
215        let mut out = Vec::new();
216        self.emit(&mut out, tail)?;
217        let mut actual = self
218            .target
219            .take()
220            .ok_or_else(|| invalid("stream", "target consumed"))?
221            .finish()?
222            .value;
223        normalize_gemini(&mut actual);
224        normalize_gemini(&mut expected);
225        if actual != expected {
226            return Err(invalid(
227                "stream.parity",
228                "native Gemini output differs from completed Claude conversion",
229            ));
230        }
231        Ok(StreamEnd {
232            chunks: out,
233            identities: self.flow,
234            report: converted.report,
235        })
236    }
237}