1use 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
28const 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
39const INTERACTION_MARKER_KEYS: &[&str] = &["steps", "status", "usage", "object", "id"];
42
43const CURSOR_TYPE: &str = "gemini.stream_cursor";
45
46#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
55pub struct StreamCursor {
56 pub interaction_id: Option<String>,
58 pub event_id: Option<String>,
60}
61
62impl StreamCursor {
63 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 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
86pub enum InteractionsEvent {
88 Sse(SseEvent),
90 Whole(Map<String, Value>),
92}
93
94#[derive(Debug)]
96pub struct SseEvent {
97 pub event_type: String,
99 pub fields: Map<String, Value>,
101}
102
103impl<'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#[derive(Default)]
126pub struct InteractionsDecoder {
127 steps: BTreeMap<usize, Open>,
129 model: Option<String>,
132 status: Option<String>,
134 interaction_id: Option<String>,
136}
137
138enum Open {
140 Thought,
141 Output(Value, Vec<(usize, Value)>),
144 Call(String),
146 Opaque,
147}
148
149pub 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
175fn 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
192fn 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 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 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 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 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 (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 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 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 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 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 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 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
427fn 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
459fn 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 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
511fn 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
530impl<'id> Decoder<'id, Completion> for InteractionsDecoder {
533 type Event = InteractionsEvent;
534
535 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 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;