rig_core/providers/gemini/interactions_api/
streaming.rs1use async_stream::stream;
2use futures::{Stream, StreamExt};
3use serde::{Deserialize, Serialize};
4use std::pin::Pin;
5use tracing::{Level, enabled};
6use tracing_futures::Instrument;
7
8use super::InteractionsCompletionModel;
9use super::create_request_body;
10use super::interactions_api_types::{
11 Content, ContentDelta, FunctionCallContent, FunctionCallDelta, Interaction,
12 InteractionSseEvent, InteractionUsage, Step, TextDelta, ThoughtSummaryContent,
13 ThoughtSummaryDelta,
14};
15use crate::completion::{CompletionError, CompletionRequest, GetTokenUsage};
16use crate::http_client::HttpClientExt;
17use crate::http_client::Request;
18use crate::http_client::sse::{Event, GenericEventSource};
19use crate::streaming;
20use crate::telemetry::{CompletionOperation, CompletionSpanBuilder, SpanCombinator};
21use serde_json::{Map, Value};
22
23#[derive(Debug, Serialize, Deserialize, Default, Clone)]
25pub struct StreamingCompletionResponse {
26 pub usage: Option<InteractionUsage>,
27 pub interaction: Option<Interaction>,
28 #[serde(skip_serializing_if = "Option::is_none")]
32 pub model_version: Option<String>,
33}
34
35#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
36pub type InteractionEventStream =
37 Pin<Box<dyn Stream<Item = Result<InteractionSseEvent, CompletionError>> + Send>>;
38
39#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
40pub type InteractionEventStream =
41 Pin<Box<dyn Stream<Item = Result<InteractionSseEvent, CompletionError>>>>;
42
43impl GetTokenUsage for StreamingCompletionResponse {
44 fn token_usage(&self) -> crate::completion::Usage {
45 self.usage
46 .as_ref()
47 .map(|usage| usage.token_usage())
48 .unwrap_or_default()
49 }
50}
51
52impl<T> InteractionsCompletionModel<T>
53where
54 T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
55{
56 pub(crate) async fn stream(
57 &self,
58 completion_request: CompletionRequest,
59 ) -> Result<streaming::StreamingCompletionResponse<StreamingCompletionResponse>, CompletionError>
60 {
61 let span = CompletionSpanBuilder::new(
62 "gcp.gemini",
63 &self.model,
64 CompletionOperation::InteractionsStreaming,
65 )
66 .system_instructions(
67 completion_request.preamble.as_deref(),
68 completion_request.record_telemetry_content,
69 )
70 .build();
71
72 let request = create_request_body(self.model.clone(), completion_request, Some(true))?;
73
74 if enabled!(Level::TRACE) {
75 tracing::trace!(
76 target: "rig::streaming",
77 "Gemini interactions streaming request: {}",
78 serde_json::to_string_pretty(&request)?
79 );
80 }
81
82 let body = serde_json::to_vec(&request)?;
83 let req = self
84 .client
85 .post_sse("/v1beta/interactions")?
86 .header("Content-Type", "application/json")
87 .body(body)
88 .map_err(|e| CompletionError::HttpError(e.into()))?;
89
90 let mut event_source = GenericEventSource::new(self.client.clone(), req);
91
92 let stream = stream! {
93 let mut final_interaction: Option<Interaction> = None;
94 let mut final_usage: Option<InteractionUsage> = None;
95
96 while let Some(event_result) = event_source.next().await {
97 match event_result {
98 Ok(Event::Open) => {
99 tracing::debug!("SSE connection opened");
100 continue;
101 }
102 Ok(Event::Message(message)) => {
103 if message.data.trim().is_empty() {
104 continue;
105 }
106
107 let data = match serde_json::from_str::<InteractionSseEvent>(&message.data)
108 {
109 Ok(data) => data,
110 Err(err) => {
111 tracing::debug!(
112 "Failed to deserialize interactions SSE event: {err}"
113 );
114 continue;
115 }
116 };
117
118 match data {
119 InteractionSseEvent::StepDelta { delta, .. } => {
120 if let Some(choice) = content_delta_to_choice(delta) {
121 yield Ok(choice);
122 }
123 }
124 InteractionSseEvent::StepStart { step, .. } => {
125 if let Some(choice) = step_start_to_choice(step) {
126 yield Ok(choice);
127 }
128 }
129 InteractionSseEvent::InteractionCompleted { interaction, .. } => {
130 let span = tracing::Span::current();
131 span.record("gen_ai.response.id", &interaction.id);
132 if let Some(model) = interaction.model.clone() {
133 span.record("gen_ai.response.model", model);
134 }
135
136 if let Some(usage) = interaction.usage.clone() {
137 span.record_token_usage(&usage);
138 final_usage = Some(usage);
139 }
140 final_interaction = Some(interaction);
141 }
142 InteractionSseEvent::Error { .. } => {
143 yield Err(crate::provider_response::completion_error_from_body(
149 message.data,
150 ));
151 break;
152 }
153 _ => continue,
154 }
155 }
156 Err(crate::http_client::Error::StreamEnded) => {
157 break;
158 }
159 Err(error) => {
160 tracing::error!(?error, "SSE error");
161 yield Err(CompletionError::from_stream_transport(error));
162 break;
163 }
164 }
165 }
166
167 event_source.close();
168
169 let model_version = final_interaction.as_ref().and_then(|i| i.model.clone());
170 yield Ok(streaming::RawStreamingChoice::FinalResponse(StreamingCompletionResponse {
171 usage: final_usage.or_else(|| final_interaction.as_ref().and_then(|i| i.usage.clone())),
172 interaction: final_interaction,
173 model_version,
174 }));
175 }
176 .instrument(span);
177
178 Ok(streaming::StreamingCompletionResponse::stream(Box::pin(
179 stream,
180 )))
181 }
182}
183
184pub(crate) fn stream_interaction_events<T>(
185 client: super::InteractionsClient<T>,
186 request: Request<Vec<u8>>,
187) -> InteractionEventStream
188where
189 T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
190{
191 let mut event_source = GenericEventSource::new(client.clone(), request);
192
193 let stream = stream! {
194 while let Some(event_result) = event_source.next().await {
195 match event_result {
196 Ok(Event::Open) => continue,
197 Ok(Event::Message(message)) => {
198 if message.data.trim().is_empty() {
199 continue;
200 }
201
202 let data = serde_json::from_str::<InteractionSseEvent>(&message.data);
203 let Ok(data) = data else {
204 let Err(err) = data else {
205 continue;
206 };
207 tracing::debug!("Failed to deserialize interactions SSE event: {err}");
208 continue;
209 };
210
211 yield Ok(data);
212 }
213 Err(crate::http_client::Error::StreamEnded) => break,
214 Err(error) => {
215 tracing::error!(?error, "SSE error");
216 yield Err(CompletionError::from_stream_transport(error));
217 break;
218 }
219 }
220 }
221
222 event_source.close();
223 };
224
225 Box::pin(stream)
226}
227
228fn step_start_to_choice(
229 step: Step,
230) -> Option<streaming::RawStreamingChoice<StreamingCompletionResponse>> {
231 match step {
232 Step::ModelOutput { content } => content.into_iter().find_map(content_to_choice),
233 Step::FunctionCall(FunctionCallContent {
234 name,
235 arguments,
236 id,
237 }) => {
238 let name = name?;
239 let call_id = id.unwrap_or_else(|| name.clone());
240 Some(streaming::RawStreamingChoice::ToolCall(
241 streaming::RawStreamingToolCall::new(
242 name.clone(),
243 name,
244 arguments.unwrap_or(Value::Object(Map::new())),
245 )
246 .with_call_id(call_id),
247 ))
248 }
249 _ => None,
250 }
251}
252
253fn content_to_choice(
254 content: Content,
255) -> Option<streaming::RawStreamingChoice<StreamingCompletionResponse>> {
256 match content {
257 Content::Text(text) if !text.text.is_empty() => {
258 Some(streaming::RawStreamingChoice::Message(text.text))
259 }
260 Content::FunctionCall(content) => step_start_to_choice(Step::FunctionCall(content)),
261 _ => None,
262 }
263}
264
265fn content_delta_to_choice(
266 delta: ContentDelta,
267) -> Option<streaming::RawStreamingChoice<StreamingCompletionResponse>> {
268 match delta {
269 ContentDelta::Text(TextDelta {
270 text: Some(text), ..
271 }) => Some(streaming::RawStreamingChoice::Message(text)),
272 ContentDelta::FunctionCall(FunctionCallDelta {
273 name,
274 arguments,
275 id,
276 }) => {
277 let name = name?;
278 let call_id = id.unwrap_or_else(|| name.clone());
279 Some(streaming::RawStreamingChoice::ToolCall(
280 streaming::RawStreamingToolCall::new(
281 name.clone(),
282 name,
283 arguments.unwrap_or(Value::Object(Map::new())),
284 )
285 .with_call_id(call_id),
286 ))
287 }
288 ContentDelta::ThoughtSummary(ThoughtSummaryDelta { content }) => {
289 let text = match content {
290 ThoughtSummaryContent::Text(text) => text.text,
291 _ => return None,
292 };
293 Some(streaming::RawStreamingChoice::ReasoningDelta {
294 id: None,
295 reasoning: text,
296 })
297 }
298 _ => None,
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use serde_json::json;
306
307 #[test]
308 fn test_streaming_completion_response_has_model_version() {
309 let response = StreamingCompletionResponse {
310 usage: None,
311 interaction: None,
312 model_version: Some("gemini-2.5-pro-preview-05-06".to_string()),
313 };
314
315 assert_eq!(
316 response.model_version.as_deref(),
317 Some("gemini-2.5-pro-preview-05-06")
318 );
319
320 let json = serde_json::to_string(&response).unwrap();
321 let deserialized: StreamingCompletionResponse = serde_json::from_str(&json).unwrap();
322 assert_eq!(
323 deserialized.model_version.as_deref(),
324 Some("gemini-2.5-pro-preview-05-06")
325 );
326 }
327
328 #[test]
329 fn test_content_delta_text_event() {
330 let event_json = json!({
331 "event_type": "step.delta",
332 "index": 0,
333 "delta": {
334 "type": "text",
335 "text": "Hello"
336 }
337 });
338
339 let event: InteractionSseEvent = serde_json::from_value(event_json).unwrap();
340 let InteractionSseEvent::StepDelta { delta, .. } = event else {
341 panic!("expected step delta");
342 };
343
344 let choice = content_delta_to_choice(delta).expect("choice should exist");
345 match choice {
346 crate::streaming::RawStreamingChoice::Message(text) => {
347 assert_eq!(text, "Hello");
348 }
349 other => panic!("unexpected choice: {other:?}"),
350 }
351 }
352
353 #[test]
354 fn test_content_delta_function_call_event() {
355 let event_json = json!({
356 "event_type": "step.delta",
357 "index": 0,
358 "delta": {
359 "type": "function_call",
360 "name": "get_weather",
361 "arguments": {"location": "Paris"},
362 "id": "call-1"
363 }
364 });
365
366 let event: InteractionSseEvent = serde_json::from_value(event_json).unwrap();
367 let InteractionSseEvent::StepDelta { delta, .. } = event else {
368 panic!("expected step delta");
369 };
370
371 let choice = content_delta_to_choice(delta).expect("choice should exist");
372 match choice {
373 crate::streaming::RawStreamingChoice::ToolCall(call) => {
374 assert_eq!(call.name, "get_weather");
375 assert_eq!(call.call_id.as_deref(), Some("call-1"));
376 }
377 other => panic!("unexpected choice: {other:?}"),
378 }
379 }
380}