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, 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#[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(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 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}