gproxy_protocol/transform/generate/gemini_chat/stream/
gemini_to_chat.rs1use super::{
2 common::{Budget, StreamEnd, chat_finish, id},
3 *,
4};
5use crate::transform::generate::stream::{
6 chat,
7 gemini::{GeminiStreamCollector, GeminiStreamLimits},
8};
9
10#[derive(Debug, Clone)]
14pub struct GeminiToChatContext {
15 pub created: i64,
16 pub model: Option<String>,
17}
18
19pub struct GeminiToChatStream {
20 source: Option<GeminiStreamCollector>,
21 policy: TargetIdPolicy,
22 flow: IdentityFlow,
23 budget: Budget,
24 calls: super::super::content::Calls,
25 signatures: super::super::content::Signatures,
26 tool_indexes: BTreeMap<i64, i64>,
27 context: GeminiToChatContext,
28 response_id: Option<String>,
29 model: Option<String>,
30 pending: Vec<g::GenerateContentResponseBody>,
31 emitted: bool,
32 failed: bool,
33 report: Report,
34}
35
36impl GeminiToChatStream {
37 pub fn new(
38 context: GeminiToChatContext,
39 flow: IdentityFlow,
40 limits: StreamLimits,
41 ) -> Result<Self, TransformError> {
42 Self::new_with_policy(
43 context,
44 flow,
45 limits,
46 TargetIdPolicy::new(crate::Dialect::OpenAiChat),
47 )
48 }
49 pub fn new_with_policy(
50 context: GeminiToChatContext,
51 flow: IdentityFlow,
52 limits: StreamLimits,
53 policy: TargetIdPolicy,
54 ) -> Result<Self, TransformError> {
55 if policy.dialect != crate::Dialect::OpenAiChat {
56 return Err(TransformError::shape(
57 "stream.policy",
58 "target policy dialect mismatch",
59 ));
60 }
61
62 if context.created < 0 || context.model.as_ref().is_some_and(|v| v.is_empty()) {
63 return Err(invalid("invalid factual creation/model context"));
64 }
65 if let Some(model) = &context.model {
66 super::common::bounded(model, limits.max_bytes)?;
67 }
68 Ok(Self {
69 source: Some(GeminiStreamCollector::new(GeminiStreamLimits {
70 max_bytes: limits.max_bytes,
71 })),
72 policy,
73 flow,
74 budget: Budget::new(limits),
75 calls: Default::default(),
76 signatures: Default::default(),
77 tool_indexes: BTreeMap::new(),
78 model: context.model.clone(),
79 context,
80 response_id: None,
81 pending: Vec::new(),
82 emitted: false,
83 failed: false,
84 report: Default::default(),
85 })
86 }
87 pub fn identities(&self) -> &IdentityFlow {
88 &self.flow
89 }
90 pub fn push(
91 &mut self,
92 chunk: g::GenerateContentResponseBody,
93 ) -> Result<Converted<Vec<s::ChatCompletionChunk>>, TransformError> {
94 if self.failed {
95 return Err(invalid("stream already failed"));
96 }
97 let result = self.push_inner(chunk.into_declared());
98 if result.is_err() {
99 self.failed = true;
100 }
101 result
102 }
103 fn push_inner(
104 &mut self,
105 chunk: g::GenerateContentResponseBody,
106 ) -> Result<Converted<Vec<s::ChatCompletionChunk>>, TransformError> {
107 self.budget.input(&chunk)?;
108 self.source
109 .as_mut()
110 .ok_or_else(|| invalid("source consumed"))?
111 .push(chunk.clone())?;
112 if let Some(model) = &chunk.model_version {
113 if self.emitted && self.model.as_ref().is_some_and(|v| v != model) {
114 return Err(invalid(
115 "source model contradicts already emitted model fact",
116 ));
117 }
118 self.model = Some(model.clone());
119 }
120 if let Some(value) = &chunk.response_id {
121 self.response_id = Some(value.clone());
122 }
123 if self.model.is_none() {
124 self.pending.push(chunk);
125 return Ok(Converted {
126 value: Vec::new(),
127 report: Report::default(),
128 });
129 }
130 let mut output = Vec::new();
131 for pending in std::mem::take(&mut self.pending) {
132 self.content(pending, &mut output)?;
133 }
134 self.content(chunk, &mut output)?;
135 self.emitted |= !output.is_empty();
136 Ok(Converted {
137 value: output,
138 report: std::mem::take(&mut self.report),
139 })
140 }
141 fn content(
142 &mut self,
143 chunk: g::GenerateContentResponseBody,
144 out: &mut Vec<s::ChatCompletionChunk>,
145 ) -> Result<(), TransformError> {
146 let policy = self.policy.clone();
147 let response = id(
150 &mut self.flow,
151 &policy,
152 IdentityRole::Response,
153 crate::Dialect::Gemini,
154 self.response_id.clone(),
155 0,
156 )?;
157 for candidate in chunk.candidates.into_iter().flatten() {
158 let index = candidate.index.unwrap_or(0);
159 if let Some(reason) = candidate.finish_reason
160 && reason != g::FinishReason::Unspecified
161 {
162 chat_finish(
163 reason,
164 self.tool_indexes.get(&index).is_some_and(|n| *n > 0),
165 )?;
166 }
167 let Some(mut content) = candidate.content else {
168 continue;
169 };
170 if content.role.is_none() {
171 content.role = Some("model".into());
172 }
173 if content.role.as_deref() != Some("model") {
174 return Err(invalid("generated candidate must have model role"));
175 }
176
177 for part in content.parts.iter().flatten() {
178 if part
179 .function_call
180 .as_ref()
181 .is_some_and(|call| call.name.is_empty())
182 {
183 return Err(TransformError::missing_metadata("function_call.name"));
184 }
185 }
186 let messages = super::super::content::gemini_content_to_chat(
187 content,
188 &mut self.report,
189 &mut self.flow,
190 &policy,
191 &mut self.calls,
192 Some(&mut self.signatures),
193 )?;
194 for message in messages {
195 let c::ChatMessage::Assistant(message) = message else {
196 return Err(invalid(
197 "model candidate produced nonassistant Chat message",
198 ));
199 };
200 let mut delta = s::Delta::builder().role(s::DeltaRole::Assistant).build();
201 let mut has_content = false;
202 if let Some(details) = message.reasoning_details.clone().flatten() {
203 delta.reasoning_details = Some(Some(details));
204 has_content = true;
205 }
206 if let Some(text) = c::visible_reasoning(
207 &message.reasoning_content,
208 &message.reasoning,
209 &message.reasoning_details,
210 ) {
211 delta.reasoning_content = Some(Some(text));
212 has_content = true;
213 }
214 if let Some(content) = message.content.flatten() {
215 let mut text = String::new();
216 match content {
217 c::AssistantContent::Text(v) => text = v,
218 c::AssistantContent::Parts(parts) => {
219 for part in parts {
220 match part {
221 c::AssistantContentPart::Text(v) => text.push_str(&v.text),
222 c::AssistantContentPart::Refusal(_) => {
223 return Err(invalid(
224 "unexpected refusal in model text mapping",
225 ));
226 }
227 }
228 }
229 }
230 }
231 delta.content = Some(Some(text));
232 has_content = true;
233 }
234 let mut tools = Vec::new();
235 for tool in message.tool_calls.into_iter().flatten() {
236 let c::MessageToolCall::Function(tool) = tool else {
237 return Err(invalid("Gemini function produced nonfunction call"));
238 };
239 let next = self.tool_indexes.entry(index).or_default();
240 let mut call = s::DeltaToolCall::builder(*next).build();
241 *next = next.checked_add(1).ok_or_else(limit)?;
242 call.id = Some(Some(tool.id));
243 call.type_ = Some(Some(s::DeltaToolCallType::Function));
244 call.function = Some(Some(
245 s::DeltaFunctionCall::builder()
246 .name(tool.function.name)
247 .arguments(tool.function.arguments)
248 .build(),
249 ));
250 tools.push(call);
251 }
252 if !tools.is_empty() {
253 delta.tool_calls = Some(Some(tools));
254 has_content = true;
255 }
256 if has_content {
257 let chunk = self.chunk(
258 response.clone(),
259 vec![s::StreamChoice::builder(index, delta).build()],
260 );
261 self.emit(chunk, out)?;
262 }
263 }
264 }
265 Ok(())
266 }
267 fn emit(
268 &mut self,
269 chunk: s::ChatCompletionChunk,
270 out: &mut Vec<s::ChatCompletionChunk>,
271 ) -> Result<(), TransformError> {
272 self.budget.output(std::slice::from_ref(&chunk))?;
276 out.push(chunk);
277 Ok(())
278 }
279 fn chunk(&self, response: String, choices: Vec<s::StreamChoice>) -> s::ChatCompletionChunk {
280 s::ChatCompletionChunk::builder(
281 response,
282 choices,
283 self.context.created,
284 self.model.clone().expect("model checked before emission"),
285 s::ChunkObject::ChatCompletionChunk,
286 )
287 .build()
288 }
289 pub fn finish(mut self) -> Result<StreamEnd<s::ChatCompletionChunk>, TransformError> {
290 if self.failed {
291 return Err(invalid("stream failed before finish"));
292 }
293 let source = self
294 .source
295 .take()
296 .ok_or_else(|| invalid("source consumed"))?
297 .finish()?
298 .value;
299 if self.model.is_none() {
300 return Err(TransformError::missing_metadata(
301 "response.model_version or factual context.model",
302 ));
303 }
304 let mut chunks = Vec::new();
305 for pending in std::mem::take(&mut self.pending) {
306 self.content(pending, &mut chunks)?;
307 }
308 let policy = self.policy.clone();
309 let response = id(
310 &mut self.flow,
311 &policy,
312 IdentityRole::Response,
313 crate::Dialect::Gemini,
314 source.response_id,
315 0,
316 )?;
317 let candidates = source
318 .candidates
319 .filter(|v| !v.is_empty())
320 .ok_or_else(|| invalid("prompt blocked without generated candidates"))?;
321 let mut choices = Vec::new();
322 for (expected, candidate) in candidates.into_iter().enumerate() {
323 let index = candidate.index.unwrap_or(0);
324 if index != expected as i64 {
325 return Err(invalid(
326 "final candidate indexes cannot form a complete native Chat choice set",
327 ));
328 }
329 let finish = chat_finish(
330 candidate
331 .finish_reason
332 .ok_or_else(|| TransformError::missing_metadata("candidate.finish_reason"))?,
333 self.tool_indexes.get(&index).is_some_and(|n| *n > 0),
334 )?;
335 let logs = candidate
336 .logprobs_result
337 .map(|v| super::super::logs::to_chat(v, &mut self.report))
338 .map(crate::transform::optional)
339 .transpose()?
340 .flatten()
341 .map(chat::logs::synthesize)
342 .map(crate::transform::optional)
343 .transpose()?
344 .flatten();
345 let mut choice = s::StreamChoice::builder(
346 index,
347 s::Delta::builder().role(s::DeltaRole::Assistant).build(),
348 )
349 .finish_reason(finish)
350 .build();
351 choice.logprobs = logs.map(Some);
352 choices.push(choice);
353 for (present, field) in [
354 (candidate.safety_ratings.is_some(), "safety_ratings"),
355 (candidate.citation_metadata.is_some(), "citation_metadata"),
356 (
357 candidate.grounding_attributions.is_some(),
358 "grounding_attributions",
359 ),
360 (candidate.grounding_metadata.is_some(), "grounding_metadata"),
361 (candidate.avg_logprobs.is_some(), "avg_logprobs"),
362 (
363 candidate.url_context_metadata.is_some(),
364 "url_context_metadata",
365 ),
366 (candidate.token_count.is_some(), "token_count"),
367 (candidate.finish_message.is_some(), "finish_message"),
368 ] {
369 if present {
370 self.report.omitted(
371 format!("candidate.{field}"),
372 "Chat stream has no corresponding declared metadata field",
373 );
374 }
375 }
376 }
377 let terminal = self.chunk(response.clone(), choices);
378 self.emit(terminal, &mut chunks)?;
379 if let Some(usage) = source.usage_metadata {
380 let usage = super::super::usage::to_chat(&usage, &mut self.report)?;
381 let mut tail = self.chunk(response, Vec::new());
382 tail.usage = Some(Some(chat::usage::synthesize(usage)?));
383 self.emit(tail, &mut chunks)?;
384 }
385 if source.prompt_feedback.is_some() {
386 self.report.omitted(
387 "prompt_feedback",
388 "Chat has no equivalent Gemini prompt feedback",
389 );
390 }
391 if source.model_status.is_some() {
392 self.report
393 .omitted("model_status", "Chat has no Gemini model-status field");
394 }
395 Ok(StreamEnd {
396 chunks,
397 identities: self.flow,
398 report: self.report,
399 })
400 }
401}