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