1use std::collections::BTreeMap;
12
13use serde_json::{Map, Value, json};
14
15use crate::completion::{FinishReason, Usage};
16use crate::error::ProviderError;
17use crate::json_utils::Lenient;
18use crate::message::{
19 DocumentRange, DocumentSourceKind, Image, ImageMediaType, MimeType, Source, SourceLocation,
20};
21use crate::operation::completion::merge;
22use crate::operation::{Block, CallFragment, Completion, Finish};
23use crate::providers::internal::wire;
24use crate::wire::{Decoder, Flow, Out, SpanUnit, WireCitation, WireEvent, WireFrame, WireSpan};
25
26const KNOWN_EVENT_TYPES: &[&str] = &[
28 "interaction.created",
29 "interaction.completed",
30 "interaction.status_update",
31 "step.start",
32 "step.delta",
33 "step.stop",
34 "error",
35];
36
37const INTERACTION_MARKER_KEYS: &[&str] = &["steps", "status", "usage", "object", "id"];
40
41pub enum InteractionsEvent {
43 Sse(SseEvent),
45 Whole(Map<String, Value>),
47}
48
49#[derive(Debug, serde::Deserialize)]
51pub struct SseEvent {
52 pub event_type: String,
54 #[serde(flatten)]
56 pub fields: Map<String, Value>,
57}
58
59#[derive(Default)]
69pub struct InteractionsDecoder {
70 steps: BTreeMap<usize, Open>,
72 model: Option<String>,
75 status: Option<String>,
77}
78
79enum Open {
81 Thought,
82 Output(Value, Vec<(usize, Value)>),
85 Call(String),
87 Opaque,
88}
89
90pub fn usage_of(usage: &Value) -> Usage {
95 let tool_use = usage.u64("total_tool_use_tokens");
96 let thoughts = usage.u64("total_thought_tokens");
97 let add = |key: &str, more: Option<u64>| usage.u64(key).map(|count| count + more.unwrap_or(0));
98 let (input_tokens, output_tokens) = (
99 add("total_input_tokens", tool_use),
100 add("total_output_tokens", thoughts),
101 );
102 Usage {
103 input_tokens,
104 output_tokens,
105 cached_input_tokens: usage.u64("total_cached_tokens"),
106 reasoning_tokens: thoughts,
107 tool_use_prompt_tokens: tool_use,
108 total_tokens: input_tokens
109 .zip(output_tokens)
110 .map(|(input, output)| input + output),
111 cache_creation_input_tokens: None,
112 cost: None,
113 }
114}
115
116fn image_of(content: &Value) -> Option<Image> {
118 let data = match (content.str("data"), content.str("uri")) {
119 (Some(data), _) => DocumentSourceKind::Base64(data.to_owned()),
120 (None, Some(uri)) => DocumentSourceKind::Url(uri.to_owned()),
121 (None, None) => return None,
122 };
123 Some(Image {
124 data,
125 media_type: content
126 .str("mime_type")
127 .and_then(ImageMediaType::from_mime_type),
128 detail: None,
129 native: None,
130 })
131}
132
133fn open_content(
135 content: Value,
136 out: &mut Out<'_, Completion>,
137) -> Result<(usize, Value), ProviderError> {
138 let index = out.fresh_index();
139 let block = match content.str("type") {
140 Some("text") => Block::Text,
141 Some("image") => image_of(&content).map_or(Block::Opaque { replay: true }, Block::Image),
142 _ => Block::Opaque { replay: true },
143 };
144 let text = matches!(block, Block::Text);
145 out.open(index, block, Value::Null)?;
146 if let Some(fragment) = content.str("text").filter(|_| text) {
147 out.push(index, fragment)?;
148 }
149 Ok((index, content))
150}
151
152impl InteractionsDecoder {
153 fn start(
156 &mut self,
157 index: usize,
158 step: Value,
159 out: &mut Out<'_, Completion>,
160 ) -> Result<(), ProviderError> {
161 let open = match step.str("type") {
162 Some("thought") => {
163 let summary: Vec<String> = step
164 .arr("summary")
165 .iter()
166 .filter(|summary| summary.str("type") == Some("text"))
167 .filter_map(|summary| summary.str("text").map(str::to_owned))
168 .collect();
169 out.open(index, Block::Reasoning { redacted: false }, step)?;
170 for text in summary {
171 out.push(index, &text)?;
172 }
173 Open::Thought
174 }
175 Some("model_output") => {
176 let items = step.arr("content").iter();
177 let items = items.map(|content| open_content(content.clone(), out));
178 let items = items.collect::<Result<_, _>>()?;
179 let mut step = step;
180 if let Some(step) = step.as_object_mut() {
181 step.insert("content".to_owned(), json!([]));
182 }
183 Open::Output(step, items)
184 }
185 Some("function_call") => {
186 call_fields(index, &step, out)?;
187 out.edit(index, |slot| *slot = step)?;
188 Open::Call(String::new())
189 }
190 kind => {
193 let replay = !matches!(kind, Some("user_input" | "function_result"));
194 out.open(index, Block::Opaque { replay }, step)?;
195 Open::Opaque
196 }
197 };
198 self.steps.insert(index, open);
199 Ok(())
200 }
201
202 fn delta(
206 &mut self,
207 index: usize,
208 delta: Map<String, Value>,
209 out: &mut Out<'_, Completion>,
210 ) -> Result<(), ProviderError> {
211 let kind = delta
212 .get("type")
213 .and_then(Value::as_str)
214 .unwrap_or_default()
215 .to_owned();
216 if !self.steps.contains_key(&index) {
217 let step = match kind.as_str() {
218 "text" | "text_annotation_delta" | "image" | "audio" | "document" | "video" => {
219 "model_output"
220 }
221 "thought_summary" | "thought_signature" => "thought",
222 "arguments_delta" => "function_call",
223 other => other,
224 };
225 self.start(index, json!({ "type": step }), out)?;
226 }
227 let Some(open) = self.steps.get_mut(&index) else {
228 return Ok(());
229 };
230 match (open, kind.as_str()) {
231 (Open::Output(_, items), _) => match items.last_mut() {
234 Some((block, item))
235 if matches!(kind.as_str(), "text" | "text_annotation_delta")
236 && item.str("type") == Some("text") =>
237 {
238 if let Some(fragment) = delta.get("text").and_then(Value::as_str)
239 && kind == "text"
240 {
241 out.push(*block, fragment)?;
242 }
243 let mut delta = delta;
244 delta.shift_remove("type");
245 merge(item, &delta);
246 Ok(())
247 }
248 _ => {
249 items.push(open_content(Value::Object(delta), out)?);
250 Ok(())
251 }
252 },
253 (Open::Thought, "thought_summary") => {
254 let content = delta.get("content").cloned().unwrap_or_default();
255 if let Some(text) = content
256 .str("text")
257 .filter(|_| content.str("type") == Some("text"))
258 {
259 out.push(index, text)?;
260 }
261 out.edit(index, |item| {
262 merge(
263 item,
264 &Map::from_iter([("summary".to_owned(), json!([content]))]),
265 )
266 })
267 }
268 (Open::Call(arguments), "arguments_delta") => {
269 let fragment = match delta.get("arguments") {
270 Some(Value::String(fragment)) => fragment.clone(),
271 Some(Value::Null) | None => String::new(),
272 Some(other) => other.to_string(),
273 };
274 arguments.push_str(&fragment);
275 let fragment = CallFragment {
276 arguments: Some(&fragment),
277 ..CallFragment::default()
278 };
279 out.fragment(Some(index), fragment)
280 }
281 (open, _) => {
282 if matches!(open, Open::Call(_)) && kind == "function_call" {
283 call_fields(index, &Value::Object(delta.clone()), out)?;
284 }
285 out.edit(index, |item| {
288 if item.get("type") != delta.get("type") {
289 merge(item, &delta);
290 } else if let Some(item) = item.as_object_mut() {
291 item.extend(delta);
292 }
293 })
294 }
295 }
296 }
297
298 fn stop(&mut self, index: usize, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
300 match self.steps.remove(&index) {
301 None => Ok(()),
302 Some(Open::Call(arguments)) => {
303 if !arguments.is_empty()
306 && let Ok(arguments) = crate::json_utils::parse_tool_arguments(&arguments)
307 {
308 out.edit(index, |item| {
309 if let Some(item) = item.as_object_mut() {
310 item.insert("arguments".to_owned(), arguments);
311 }
312 })?;
313 }
314 out.finish(index)
315 }
316 Some(Open::Output(step, items)) if items.is_empty() => {
318 let block = out.fresh_index();
319 out.open(block, Block::Text, step)?;
320 out.finish(block)
321 }
322 Some(Open::Output(step, items)) => {
323 for (block, item) in items {
324 for annotation in item.arr("annotations") {
325 if let Some(citation) = annotation_citation(annotation) {
326 out.cite(block, citation);
327 }
328 }
329 let mut step = step.clone();
330 if let Some(step) = step.as_object_mut() {
331 step.insert("content".to_owned(), json!([item]));
332 }
333 out.edit(block, |slot| *slot = step)?;
334 out.finish(block)?;
335 }
336 Ok(())
337 }
338 Some(Open::Thought | Open::Opaque) => out.finish(index),
339 }
340 }
341
342 fn complete(&mut self, interaction: Map<String, Value>, out: Out<'_, Completion>) -> Flow {
344 let interaction = Value::Object(interaction);
345 let field = |key: &str| interaction.str(key).map(str::to_owned);
346 let model = field("model")
348 .or_else(|| field("agent"))
349 .or_else(|| self.model.take());
350 let status = field("status").or_else(|| self.status.take());
351 let (reason, error) = ending(status.as_deref(), interaction.arr("errors"));
352 let usage = interaction.get("usage").cloned().unwrap_or(Value::Null);
353 let finish = Finish {
354 usage: usage_of(&usage),
355 reason,
356 response_id: field("id"),
357 model,
358 error,
359 };
360 out.end(finish)
361 }
362}
363
364fn ending(status: Option<&str>, errors: &[Value]) -> (Option<FinishReason>, Option<String>) {
372 let messages: Vec<&str> = errors
373 .iter()
374 .filter_map(|error| error.str("message"))
375 .collect();
376 let detail = (!messages.is_empty()).then(|| format!(": {}", messages.join("; ")));
377 let detail = detail.unwrap_or_default();
378 let other = |status: &str| Some(FinishReason::Other(status.to_owned()));
379 match status {
380 Some("completed") => (Some(FinishReason::Stop), None),
381 Some("requires_action") => (Some(FinishReason::ToolCalls), None),
382 Some("incomplete" | "budget_exceeded") => (Some(FinishReason::Length), None),
383 Some(status @ ("failed" | "cancelled")) => (
384 other(status),
385 Some(format!("The interaction {status}{detail}")),
386 ),
387 Some(status @ ("in_progress" | "queued")) => (
388 other(status),
389 Some(format!("The interaction was read while {status}")),
390 ),
391 Some(status) => (other(status), None),
392 None => (None, Some("The interaction states no status".to_owned())),
393 }
394}
395
396fn annotation_citation(annotation: &Value) -> Option<WireCitation> {
401 let field = |key: &str| annotation.str(key).map(str::to_owned);
402 let (location, title) = match annotation.str("type")? {
403 "url_citation" => (SourceLocation::Url { url: field("url")? }, field("title")),
404 "file_citation" => {
405 let page = annotation
406 .u64("page_number")
407 .and_then(|page| u32::try_from(page).ok());
408 let within = page.map(|page| DocumentRange::Pages(page..page.saturating_add(1)));
409 let id = field("document_uri");
410 (
411 SourceLocation::Document {
412 index: None,
413 id,
414 within,
415 },
416 field("file_name"),
417 )
418 }
419 "place_citation" => {
420 let location = match (field("url"), field("place_id")) {
421 (Some(url), _) => SourceLocation::Url { url },
422 (None, Some(id)) => SourceLocation::Document {
423 index: None,
424 id: Some(id),
425 within: None,
426 },
427 (None, None) => return None,
428 };
429 (location, field("name"))
430 }
431 _ => return None,
432 };
433 let mut source = Source::new(location);
434 if let Some(title) = title {
435 source = source.title(title);
436 }
437 let span = annotation.u64("end_index").map(|end| {
439 WireSpan::new(
440 annotation.u64("start_index").unwrap_or(0),
441 end,
442 SpanUnit::Bytes,
443 )
444 });
445 Some(WireCitation::new(span, vec![source]))
446}
447
448fn call_fields(
450 index: usize,
451 step: &Value,
452 out: &mut Out<'_, Completion>,
453) -> Result<(), ProviderError> {
454 let (id, name) = (step.str("id"), step.str("name"));
455 out.fragment(
456 Some(index),
457 CallFragment {
458 id,
459 name,
460 arguments: None,
461 },
462 )?;
463 step.get("arguments")
464 .map_or(Ok(()), |arguments| out.announce(index, arguments.clone()))
465}
466
467impl<'id> Decoder<'id, Completion> for InteractionsDecoder {
470 type Event = InteractionsEvent;
471
472 fn classify(&self, frame: WireFrame) -> WireEvent<InteractionsEvent> {
476 wire::classify_or_untagged(
477 &frame.as_str(),
478 "event_type",
479 |data| {
480 wire::classify_tagged_frame(data, "event_type", |tag| {
481 KNOWN_EVENT_TYPES.contains(&tag)
482 })
483 .map(InteractionsEvent::Sse)
484 },
485 |data| {
486 wire::classify_marker_keyed_frame(data, INTERACTION_MARKER_KEYS)
487 .map(InteractionsEvent::Whole)
488 },
489 )
490 }
491
492 fn decode(
493 &mut self,
494 event: InteractionsEvent,
495 mut out: Out<'id, Completion>,
496 ) -> Result<Flow, ProviderError> {
497 let SseEvent {
498 event_type,
499 fields: event,
500 } = match event {
501 InteractionsEvent::Sse(event) => event,
502 InteractionsEvent::Whole(interaction) => {
503 let steps = Value::Object(interaction.clone());
504 for (index, step) in steps.arr("steps").iter().enumerate() {
505 self.start(index, step.clone(), &mut out)?;
506 self.stop(index, &mut out)?;
507 }
508 return Ok(self.complete(interaction, out));
509 }
510 };
511 let event = Value::Object(event);
512 let index = || {
513 event
514 .u64("index")
515 .and_then(|index| usize::try_from(index).ok())
516 .ok_or_else(|| {
517 ProviderError::Response(format!(
518 "an Interactions `{event_type}` event names no step index"
519 ))
520 })
521 };
522 let malformed = |field: &str| {
523 ProviderError::Response(format!(
524 "an Interactions `{event_type}` event carries no `{field}` object"
525 ))
526 };
527 match event_type.as_str() {
528 "step.start" => match event.get("step") {
529 None => {}
531 Some(step @ Value::Object(_)) => self.start(index()?, step.clone(), &mut out)?,
532 Some(_) => return Err(malformed("step")),
533 },
534 "step.delta" => match event.get("delta") {
535 Some(Value::Object(delta)) => self.delta(index()?, delta.clone(), &mut out)?,
536 _ => return Err(malformed("delta")),
537 },
538 "step.stop" => self.stop(index()?, &mut out)?,
539 "interaction.created" => {
540 let field = |key: &str| {
541 event
542 .at(&format!("/interaction/{key}"))
543 .and_then(Value::as_str)
544 .map(str::to_owned)
545 };
546 self.model = field("model").or_else(|| field("agent"));
547 self.status = field("status");
548 }
549 "interaction.status_update" => {
550 if let Some(status) = event.str("status") {
551 self.status = Some(status.to_owned());
552 }
553 }
554 "interaction.completed" => {
555 let interaction = event.obj("interaction").cloned().unwrap_or_default();
556 return Ok(self.complete(interaction, out));
557 }
558 "error" => {
559 let Value::Object(mut body) = event else {
560 return Err(ProviderError::from_provider_body(event_type));
561 };
562 body.insert("event_type".to_owned(), Value::from(event_type));
563 return Err(ProviderError::from_provider_body(
564 Value::Object(body).to_string(),
565 ));
566 }
567 _ => {}
568 }
569 Ok(Flow::More)
570 }
571}
572
573pub mod document;
574
575#[cfg(test)]
576mod tests;