Skip to main content

rig_core/providers/gemini/interactions_api/
streaming.rs

1//! The decoder of Interactions replies: a whole interaction resource, or a
2//! stream of `event_type`-tagged step events, read as JSON.
3//!
4//! ```
5//! use rig_core::providers::gemini::interactions_api::streaming::InteractionsDecoder;
6//!
7//! let decoder = InteractionsDecoder::default();
8//! # let _ = decoder;
9//! ```
10
11use std::borrow::Cow;
12use std::collections::BTreeMap;
13
14use serde_json::{Map, Value, json};
15
16use crate::completion::{FinishReason, Usage};
17use crate::error::ProviderError;
18use crate::json_utils::Lenient;
19use crate::message::{
20    DocumentRange, DocumentSourceKind, Image, ImageMediaType, MimeType, Source, SourceLocation,
21};
22use crate::operation::completion::merge;
23use crate::operation::{Block, CallFragment, Completion, Finish};
24use crate::providers::internal::wire;
25use crate::wire::{Decoder, Flow, Out, SpanUnit, WireCitation, WireEvent, WireFrame, WireSpan};
26
27/// Recognized Interactions SSE tags; unlisted tags classify as unknown.
28const KNOWN_EVENT_TYPES: &[&str] = &[
29    "interaction.created",
30    "interaction.completed",
31    "interaction.status_update",
32    "step.start",
33    "step.delta",
34    "step.stop",
35    "error",
36];
37
38/// Top-level keys that mark an untagged frame as a whole interaction
39/// resource.
40const INTERACTION_MARKER_KEYS: &[&str] = &["steps", "status", "usage", "object", "id"];
41
42/// The tag of the unknown payload a [`StreamCursor`] travels in.
43const CURSOR_TYPE: &str = "gemini.stream_cursor";
44
45/// Where a resumable stream picks up: the interaction, and the last
46/// `event_id` the API marked as a resume point. A stream carries one after
47/// `interaction.created`, with no event yet, and one after each event with
48/// an `event_id`, as an [`Item::Unknown`](crate::streaming::Item::Unknown)
49/// that [`Self::of`] reads. A stream resumed with
50/// [`Gemini::interaction_resumed`](crate::providers::gemini::Gemini::interaction_resumed)
51/// continues after the event, or from the start without one, so a step
52/// that was open arrives without its start.
53#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
54pub struct StreamCursor {
55    /// The interaction's id, when the stream named it.
56    pub interaction_id: Option<String>,
57    /// The `event_id` to resume after; `None` resumes from the start.
58    pub event_id: Option<String>,
59}
60
61impl StreamCursor {
62    /// The cursor `item` carries, if it is one.
63    pub fn of<E>(item: &crate::streaming::Item<E>) -> Option<Self> {
64        let crate::streaming::Item::Unknown(payload) = item else {
65            return None;
66        };
67        let payload = payload.value();
68        let field = |key: &str| payload.str(key).map(str::to_owned);
69        (payload.str("type") == Some(CURSOR_TYPE)).then(|| Self {
70            interaction_id: field("interaction_id"),
71            event_id: field("event_id"),
72        })
73    }
74
75    /// The unknown payload that carries this cursor.
76    fn payload(&self) -> crate::streaming::UnknownPayload {
77        crate::streaming::UnknownPayload::new(json!({
78            "type": CURSOR_TYPE,
79            "interaction_id": self.interaction_id,
80            "event_id": self.event_id,
81        }))
82    }
83}
84
85/// A decoded frame, verbatim: the decoder reads the fields it needs.
86pub enum InteractionsEvent {
87    /// One `event_type`-tagged streaming event.
88    Sse(SseEvent),
89    /// The whole interaction resource, as the unary reply delivers it.
90    Whole(Map<String, Value>),
91}
92
93/// A streaming event: its tag, and its other fields verbatim.
94#[derive(Debug)]
95pub struct SseEvent {
96    /// The event's `event_type`.
97    pub event_type: String,
98    /// Every other field.
99    pub fields: Map<String, Value>,
100}
101
102/// Read as one object whose `event_type` is taken out, rather than through
103/// `#[serde(flatten)]`, which buffers every field before building the map.
104impl<'de> serde::Deserialize<'de> for SseEvent {
105    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
106        let mut fields = Map::deserialize(deserializer)?;
107        match fields.shift_remove("event_type") {
108            Some(Value::String(event_type)) => Ok(Self { event_type, fields }),
109            Some(_) => Err(serde::de::Error::custom("`event_type` is not a string")),
110            None => Err(serde::de::Error::missing_field("event_type")),
111        }
112    }
113}
114
115/// The Gemini Interactions wire's decoder. Steps decode in step order: a
116/// thought, a call and any other step is one block, and a model output is
117/// one block per content item, whose provider item is the model output step
118/// holding that item alone. A step opens on `step.start`, grows with its
119/// `step.delta`s, and becomes its blocks' provider item on `step.stop`, where
120/// the API states it complete. A whole interaction states each step
121/// complete. A step still open when the reply ends keeps no provider item.
122/// A text item's annotations, stated with it or streamed after it, cite its
123/// block when the step stops.
124#[derive(Default)]
125pub struct InteractionsDecoder {
126    /// The open steps, by wire index.
127    steps: BTreeMap<usize, Open>,
128    /// The model `interaction.created` named, for a completion that does
129    /// not.
130    model: Option<String>,
131    /// The last status a stream reported.
132    status: Option<String>,
133    /// The interaction the stream reads, for its cursors.
134    interaction_id: Option<String>,
135}
136
137/// What an open step decodes to.
138enum Open {
139    Thought,
140    /// A model output: the step's other fields, and each content item with
141    /// its block's writer index.
142    Output(Value, Vec<(usize, Value)>),
143    /// A function call, with the argument JSON streamed so far.
144    Call(String),
145    Opaque,
146}
147
148/// Rig's usage for an interaction's `usage`, read leniently: input is
149/// `total_input_tokens` plus the tool-use tokens, output
150/// `total_output_tokens` plus the thought tokens, and the total their sum;
151/// without a base count, that side and the total stay absent.
152pub fn usage_of(usage: &Value) -> Usage {
153    let tool_use = usage.u64("total_tool_use_tokens");
154    let thoughts = usage.u64("total_thought_tokens");
155    let add = |key: &str, more: Option<u64>| usage.u64(key).map(|count| count + more.unwrap_or(0));
156    let (input_tokens, output_tokens) = (
157        add("total_input_tokens", tool_use),
158        add("total_output_tokens", thoughts),
159    );
160    Usage {
161        input_tokens,
162        output_tokens,
163        cached_input_tokens: usage.u64("total_cached_tokens"),
164        reasoning_tokens: thoughts,
165        tool_use_prompt_tokens: tool_use,
166        total_tokens: input_tokens
167            .zip(output_tokens)
168            .map(|(input, output)| input + output),
169        cache_creation_input_tokens: None,
170        cost: None,
171    }
172}
173
174/// The image a content item states, when it has data or a URI.
175fn image_of(content: &Value) -> Option<Image> {
176    let data = match (content.str("data"), content.str("uri")) {
177        (Some(data), _) => DocumentSourceKind::Base64(data.to_owned()),
178        (None, Some(uri)) => DocumentSourceKind::Url(uri.to_owned()),
179        (None, None) => return None,
180    };
181    Some(Image {
182        data,
183        media_type: content
184            .str("mime_type")
185            .and_then(ImageMediaType::from_mime_type),
186        detail: None,
187        native: None,
188    })
189}
190
191/// Open the block for `content`, a model output's newest content item.
192fn open_content(
193    content: Value,
194    out: &mut Out<'_, Completion>,
195) -> Result<(usize, Value), ProviderError> {
196    let index = out.fresh_index();
197    let block = match content.str("type") {
198        Some("text") => Block::Text,
199        Some("image") => image_of(&content).map_or(Block::Opaque { replay: true }, Block::Image),
200        _ => Block::Opaque { replay: true },
201    };
202    let text = matches!(block, Block::Text);
203    out.open(index, block, Value::Null)?;
204    if let Some(fragment) = content.str("text").filter(|_| text) {
205        out.push(index, fragment)?;
206    }
207    Ok((index, content))
208}
209
210impl InteractionsDecoder {
211    /// The decoder of a stream that reads the interaction `interaction_id`.
212    pub fn resuming(interaction_id: impl Into<String>) -> Self {
213        Self {
214            interaction_id: Some(interaction_id.into()),
215            ..Self::default()
216        }
217    }
218
219    /// Open the step at `index` as it starts, writing the content it
220    /// already states.
221    fn start(
222        &mut self,
223        index: usize,
224        step: Value,
225        out: &mut Out<'_, Completion>,
226    ) -> Result<(), ProviderError> {
227        let open = match step.str("type") {
228            Some("thought") => {
229                let summary: Vec<String> = step
230                    .arr("summary")
231                    .iter()
232                    .filter(|summary| summary.str("type") == Some("text"))
233                    .filter_map(|summary| summary.str("text").map(str::to_owned))
234                    .collect();
235                out.open(index, Block::Reasoning { redacted: false }, step)?;
236                for text in summary {
237                    out.push(index, &text)?;
238                }
239                Open::Thought
240            }
241            Some("model_output") => {
242                let mut step = step;
243                let content = step
244                    .as_object_mut()
245                    .and_then(|step| step.insert("content".to_owned(), Value::Array(Vec::new())));
246                let items = match content {
247                    Some(Value::Array(items)) => items,
248                    _ => Vec::new(),
249                };
250                let items = items.into_iter().map(|content| open_content(content, out));
251                Open::Output(step, items.collect::<Result<_, _>>()?)
252            }
253            Some("function_call") => {
254                call_fields(index, &step, out)?;
255                out.edit(index, |slot| *slot = step)?;
256                Open::Call(String::new())
257            }
258            // Input a reply restates is kept and never sent back; a
259            // hosted-tool step, or one rig does not know, replays.
260            kind => {
261                let replay = !matches!(kind, Some("user_input" | "function_result"));
262                out.open(index, Block::Opaque { replay }, step)?;
263                Open::Opaque
264            }
265        };
266        self.steps.insert(index, open);
267        Ok(())
268    }
269
270    /// Apply one delta to the step at `index`. A delta for a step that
271    /// never started opens the step it implies: a resumed stream can join
272    /// a step after its start.
273    fn delta(
274        &mut self,
275        index: usize,
276        mut delta: Map<String, Value>,
277        out: &mut Out<'_, Completion>,
278    ) -> Result<(), ProviderError> {
279        let kind = delta
280            .get("type")
281            .and_then(Value::as_str)
282            .unwrap_or_default();
283        if !self.steps.contains_key(&index) {
284            let step = match kind {
285                "text" | "text_annotation_delta" | "image" | "audio" | "document" | "video" => {
286                    "model_output"
287                }
288                "thought_summary" | "thought_signature" => "thought",
289                "arguments_delta" => "function_call",
290                other => other,
291            };
292            self.start(index, json!({ "type": step }), out)?;
293        }
294        let Some(open) = self.steps.get_mut(&index) else {
295            return Ok(());
296        };
297        match (open, kind) {
298            // Text and its annotations extend the last text item and its
299            // block; any other delta is a content item of its own.
300            (Open::Output(_, items), _) => match items.last_mut() {
301                Some((block, item))
302                    if matches!(kind, "text" | "text_annotation_delta")
303                        && item.str("type") == Some("text") =>
304                {
305                    if let Some(fragment) = delta.get("text").and_then(Value::as_str)
306                        && kind == "text"
307                    {
308                        out.push(*block, fragment)?;
309                    }
310                    merge(item, &delta);
311                    Ok(())
312                }
313                _ => {
314                    items.push(open_content(Value::Object(delta), out)?);
315                    Ok(())
316                }
317            },
318            (Open::Thought, "thought_summary") => {
319                let content = delta.shift_remove("content").unwrap_or_default();
320                if let Some(text) = content
321                    .str("text")
322                    .filter(|_| content.str("type") == Some("text"))
323                {
324                    out.push(index, text)?;
325                }
326                let summary = Value::Array(vec![content]);
327                out.edit(index, |item| {
328                    merge(item, &Map::from_iter([("summary".to_owned(), summary)]))
329                })
330            }
331            (Open::Call(arguments), "arguments_delta") => {
332                let fragment = match delta.get("arguments") {
333                    Some(Value::String(fragment)) => Cow::Borrowed(fragment.as_str()),
334                    Some(Value::Null) | None => Cow::Borrowed(""),
335                    Some(other) => Cow::Owned(other.to_string()),
336                };
337                arguments.push_str(&fragment);
338                let fragment = CallFragment {
339                    arguments: Some(&fragment),
340                    ..CallFragment::default()
341                };
342                out.fragment(Some(index), fragment)
343            }
344            (open, _) => {
345                if matches!(open, Open::Call(_)) && kind == "function_call" {
346                    call_fields(index, &Value::Object(delta.clone()), out)?;
347                }
348                // A delta that restates its step (it carries the step's own
349                // `type`) replaces the fields it names; any other merges.
350                out.edit(index, |item| {
351                    if item.get("type") != delta.get("type") {
352                        merge(item, &delta);
353                    } else if let Some(item) = item.as_object_mut() {
354                        item.extend(delta);
355                    }
356                })
357            }
358        }
359    }
360
361    /// Finish the step at `index`: the API states it complete.
362    fn stop(&mut self, index: usize, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
363        match self.steps.remove(&index) {
364            None => Ok(()),
365            Some(Open::Call(arguments)) => {
366                // Streamed fragments supersede the arguments the start
367                // announced.
368                if !arguments.is_empty()
369                    && let Ok(arguments) = crate::json_utils::parse_tool_arguments(&arguments)
370                {
371                    out.edit(index, |item| {
372                        if let Some(item) = item.as_object_mut() {
373                            item.insert("arguments".to_owned(), arguments);
374                        }
375                    })?;
376                }
377                out.finish(index)
378            }
379            // An output with no content is one empty text block holding it.
380            Some(Open::Output(step, items)) if items.is_empty() => {
381                let block = out.fresh_index();
382                out.open(block, Block::Text, step)?;
383                out.finish(block)
384            }
385            Some(Open::Output(step, items)) => {
386                for (block, item) in items {
387                    for annotation in item.arr("annotations") {
388                        if let Some(citation) = annotation_citation(annotation) {
389                            out.cite(block, citation);
390                        }
391                    }
392                    let mut step = step.clone();
393                    if let Some(step) = step.as_object_mut() {
394                        step.insert("content".to_owned(), Value::Array(vec![item]));
395                    }
396                    out.edit(block, |slot| *slot = step)?;
397                    out.finish(block)?;
398                }
399                Ok(())
400            }
401            Some(Open::Thought | Open::Opaque) => out.finish(index),
402        }
403    }
404
405    /// End the reply with the interaction resource it completed with.
406    fn complete(&mut self, interaction: Map<String, Value>, out: Out<'_, Completion>) -> Flow {
407        let interaction = Value::Object(interaction);
408        let field = |key: &str| interaction.str(key).map(str::to_owned);
409        // An agent interaction names its agent in place of a model.
410        let model = field("model")
411            .or_else(|| field("agent"))
412            .or_else(|| self.model.take());
413        let status = field("status").or_else(|| self.status.take());
414        let (reason, error) = ending(status.as_deref(), interaction.arr("errors"));
415        let finish = Finish {
416            usage: usage_of(interaction.get("usage").unwrap_or(&Value::Null)),
417            reason,
418            response_id: field("id"),
419            model,
420            error,
421        };
422        out.end(finish)
423    }
424}
425
426/// The finish an interaction's `status` states, and the failure it reports.
427/// `completed` and `requires_action` (calls wait for results) are
428/// successes; `incomplete` is the token or execution budget running out,
429/// and `budget_exceeded` its deprecated spelling. `failed` and `cancelled`
430/// are failures carrying the interaction's `errors`; `in_progress` and
431/// `queued` are an interaction read before it ended. An unknown status, or
432/// none, is a failure.
433fn ending(status: Option<&str>, errors: &[Value]) -> (Option<FinishReason>, Option<String>) {
434    let messages: Vec<&str> = errors
435        .iter()
436        .filter_map(|error| error.str("message"))
437        .collect();
438    let detail = (!messages.is_empty()).then(|| format!(": {}", messages.join("; ")));
439    let detail = detail.unwrap_or_default();
440    let other = |status: &str| Some(FinishReason::Other(status.to_owned()));
441    match status {
442        Some("completed") => (Some(FinishReason::Stop), None),
443        Some("requires_action") => (Some(FinishReason::ToolCalls), None),
444        Some("incomplete" | "budget_exceeded") => (Some(FinishReason::Length), None),
445        Some(status @ ("failed" | "cancelled")) => (
446            other(status),
447            Some(format!("The interaction {status}{detail}")),
448        ),
449        Some(status @ ("in_progress" | "queued")) => (
450            other(status),
451            Some(format!("The interaction was read while {status}")),
452        ),
453        Some(status) => (other(status), None),
454        None => (None, Some("The interaction states no status".to_owned())),
455    }
456}
457
458/// The citation a text item's annotation states: a `url_citation` cites a
459/// web page, a `file_citation` a document and its page, a `place_citation`
460/// a place by its URL or id. Offsets count bytes of the item's text.
461/// Other annotations cite nothing.
462fn annotation_citation(annotation: &Value) -> Option<WireCitation> {
463    let field = |key: &str| annotation.str(key).map(str::to_owned);
464    let (location, title) = match annotation.str("type")? {
465        "url_citation" => (SourceLocation::Url { url: field("url")? }, field("title")),
466        "file_citation" => {
467            let page = annotation
468                .u64("page_number")
469                .and_then(|page| u32::try_from(page).ok());
470            let within = page.map(|page| DocumentRange::Pages(page..page.saturating_add(1)));
471            let id = field("document_uri");
472            (
473                SourceLocation::Document {
474                    index: None,
475                    id,
476                    within,
477                },
478                field("file_name"),
479            )
480        }
481        "place_citation" => {
482            let location = match (field("url"), field("place_id")) {
483                (Some(url), _) => SourceLocation::Url { url },
484                (None, Some(id)) => SourceLocation::Document {
485                    index: None,
486                    id: Some(id),
487                    within: None,
488                },
489                (None, None) => return None,
490            };
491            (location, field("name"))
492        }
493        _ => return None,
494    };
495    let mut source = Source::new(location);
496    if let Some(title) = title {
497        source = source.title(title);
498    }
499    // Proto3 JSON leaves out a zero start.
500    let span = annotation.u64("end_index").map(|end| {
501        WireSpan::new(
502            annotation.u64("start_index").unwrap_or(0),
503            end,
504            SpanUnit::Bytes,
505        )
506    });
507    Some(WireCitation::new(span, vec![source]))
508}
509
510/// Write the id, name and announced arguments a call step states.
511fn call_fields(
512    index: usize,
513    step: &Value,
514    out: &mut Out<'_, Completion>,
515) -> Result<(), ProviderError> {
516    let (id, name) = (step.str("id"), step.str("name"));
517    out.fragment(
518        Some(index),
519        CallFragment {
520            id,
521            name,
522            arguments: None,
523        },
524    )?;
525    step.get("arguments")
526        .map_or(Ok(()), |arguments| out.announce(index, arguments.clone()))
527}
528
529/// EOF without `interaction.completed` is truncation, not successful
530/// completion, so the decoder has nothing to add at the end of the reply.
531impl<'id> Decoder<'id, Completion> for InteractionsDecoder {
532    type Event = InteractionsEvent;
533
534    /// Classify a frame by its `event_type` tag, or as a whole resource by
535    /// its marker keys when it carries no tag. A tagged frame never passes
536    /// for a whole resource.
537    fn classify(&self, frame: WireFrame) -> WireEvent<InteractionsEvent> {
538        wire::classify_or_untagged(
539            &frame.as_str(),
540            "event_type",
541            |tag| KNOWN_EVENT_TYPES.contains(&tag),
542            InteractionsEvent::Sse,
543            |data| {
544                wire::classify_marker_keyed_frame(data, INTERACTION_MARKER_KEYS)
545                    .map(InteractionsEvent::Whole)
546            },
547        )
548    }
549
550    fn decode(
551        &mut self,
552        event: InteractionsEvent,
553        mut out: Out<'id, Completion>,
554    ) -> Result<Flow, ProviderError> {
555        let SseEvent {
556            event_type,
557            fields: event,
558        } = match event {
559            InteractionsEvent::Sse(event) => event,
560            InteractionsEvent::Whole(mut interaction) => {
561                let steps = match interaction.shift_remove("steps") {
562                    Some(Value::Array(steps)) => steps,
563                    _ => Vec::new(),
564                };
565                for (index, step) in steps.into_iter().enumerate() {
566                    self.start(index, step, &mut out)?;
567                    self.stop(index, &mut out)?;
568                }
569                return Ok(self.complete(interaction, out));
570            }
571        };
572        let mut event = Value::Object(event);
573        let index = |event: &Value| {
574            event
575                .u64("index")
576                .and_then(|index| usize::try_from(index).ok())
577                .ok_or_else(|| {
578                    ProviderError::Response(format!(
579                        "an Interactions `{event_type}` event names no step index"
580                    ))
581                })
582        };
583        let malformed = |field: &str| {
584            ProviderError::Response(format!(
585                "an Interactions `{event_type}` event carries no `{field}` object"
586            ))
587        };
588        let event_id = event.str("event_id").map(str::to_owned);
589        let resumable = event_id.is_some() || event_type == "interaction.created";
590        match event_type.as_str() {
591            "step.start" => match event.get_mut("step").map(Value::take) {
592                // A start that states no step leaves its deltas to open it.
593                None => {}
594                Some(step @ Value::Object(_)) => self.start(index(&event)?, step, &mut out)?,
595                Some(_) => return Err(malformed("step")),
596            },
597            "step.delta" => match event.get_mut("delta").map(Value::take) {
598                Some(Value::Object(delta)) => self.delta(index(&event)?, delta, &mut out)?,
599                _ => return Err(malformed("delta")),
600            },
601            "step.stop" => self.stop(index(&event)?, &mut out)?,
602            "interaction.created" => {
603                let field = |key: &str| {
604                    event
605                        .at(&format!("/interaction/{key}"))
606                        .and_then(Value::as_str)
607                        .map(str::to_owned)
608                };
609                self.model = field("model").or_else(|| field("agent"));
610                self.status = field("status");
611                if let Some(id) = field("id") {
612                    self.interaction_id = Some(id);
613                }
614            }
615            "interaction.status_update" => {
616                if let Some(status) = event.str("status") {
617                    self.status = Some(status.to_owned());
618                }
619            }
620            "interaction.completed" => {
621                let interaction = match event.get_mut("interaction").map(Value::take) {
622                    Some(Value::Object(interaction)) => interaction,
623                    _ => Map::new(),
624                };
625                return Ok(self.complete(interaction, out));
626            }
627            "error" => {
628                let Value::Object(mut body) = event else {
629                    return Err(ProviderError::from_provider_body(event_type));
630                };
631                body.insert("event_type".to_owned(), Value::from(event_type));
632                return Err(ProviderError::from_provider_body(
633                    Value::Object(body).to_string(),
634                ));
635            }
636            _ => {}
637        }
638        if resumable {
639            let cursor = StreamCursor {
640                interaction_id: self.interaction_id.clone(),
641                event_id,
642            };
643            out.unknown(cursor.payload());
644        }
645        Ok(Flow::More)
646    }
647}
648
649pub mod document;
650
651#[cfg(test)]
652mod tests;