gproxy_protocol/transform/generate/claude_gemini/stream/
claude_to_gemini.rs1use 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}