Skip to main content

rig_core/providers/gemini/interactions_api/
streaming.rs

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