Skip to main content

rig_core/providers/openai/responses_api/
streaming.rs

1//! Responses frame classification and event decoding: one block per output
2//! item, in the order the items arrive, each holding the item as the
3//! provider stated it complete.
4//!
5//! Frames are read as JSON and classified by their `type` alone, and every
6//! field is read leniently, so a gateway that omits or retypes a field no
7//! block or finish needs never fails a reply.
8//!
9//! ```
10//! use rig_core::providers::openai::responses_api::streaming::ResponsesDecoder;
11//! let decoder = ResponsesDecoder::new();
12//! # let _ = decoder;
13//! ```
14
15use std::borrow::Cow;
16use std::collections::{HashMap, HashSet};
17
18use serde_json::{Value, json};
19
20use crate::completion::{Cost, FinishReason, Usage};
21use crate::error::ProviderError;
22use crate::json_utils::Lenient;
23use crate::json_utils::remove_preserving_order;
24use crate::message::{Source, SourceLocation};
25use crate::operation::{Block, CallFragment, Completion, Finish};
26use crate::providers::internal::wire;
27use crate::wire::{Decoder, Flow, Out, WireCitation, WireEvent, WireFrame};
28
29/// Whether `kind` is a Responses event type this decoder reads. A frame of
30/// any other type passes through as unknown. `keepalive` is ChatGPT's idle
31/// heartbeat, read and skipped.
32fn is_known_responses_event_type(kind: &str) -> bool {
33    matches!(kind, "error" | "keepalive")
34        || is_lifecycle_event(kind)
35        || matches!(
36            kind.strip_prefix("response."),
37            Some(
38                "output_item.added"
39                    | "output_item.done"
40                    | "content_part.added"
41                    | "content_part.done"
42                    | "output_text.delta"
43                    | "output_text.done"
44                    | "refusal.delta"
45                    | "refusal.done"
46                    | "function_call_arguments.delta"
47                    | "function_call_arguments.done"
48                    | "custom_tool_call_input.delta"
49                    | "custom_tool_call_input.done"
50                    | "reasoning_summary_part.added"
51                    | "reasoning_summary_part.done"
52                    | "reasoning_summary_text.delta"
53                    | "reasoning_summary_text.done"
54                    | "reasoning_text.delta"
55                    | "reasoning_text.done"
56            )
57        )
58}
59
60/// Whether `kind` is a response lifecycle event, which carries the response
61/// object under `response`.
62pub(crate) fn is_lifecycle_event(kind: &str) -> bool {
63    matches!(
64        kind.strip_prefix("response."),
65        Some("created" | "queued" | "in_progress" | "completed" | "failed" | "incomplete")
66    )
67}
68
69/// A classified Responses payload.
70#[derive(Debug)]
71pub enum ResponsesEvent {
72    /// A stream event of a known `type`, as the provider sent it.
73    Frame {
74        /// The event's `type`.
75        kind: String,
76        /// The event.
77        frame: Value,
78        /// The payload of a `response.failed` event, verbatim, for the
79        /// error the failed reply reports; `None` for every other event.
80        raw: Option<String>,
81    },
82    /// The unary reply: the response object itself, which carries no `type`
83    /// because it is not an event.
84    Whole(Value),
85    /// The provider's error, as the stream's `error` event or a success
86    /// body holding an error envelope instead of a response.
87    Failure(String),
88    /// The `[DONE]` sentinel. The provider's end is `response.completed`.
89    Sentinel,
90}
91
92/// The keys a response object, or the error envelope a success body can
93/// hold instead, always has one of.
94const WHOLE_BODY_MARKERS: &[&str] = &["object", "output", "status", "id", "error"];
95
96/// A payload that names its `type`, read as the JSON it is.
97struct Tagged(Value);
98
99impl<'de> serde::Deserialize<'de> for Tagged {
100    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
101    where
102        D: serde::Deserializer<'de>,
103    {
104        let value = Value::deserialize(deserializer)?;
105        if value.str("type").is_some() {
106            Ok(Self(value))
107        } else {
108            Err(serde::de::Error::custom("the payload names no `type`"))
109        }
110    }
111}
112
113/// Classify one payload by its `type`; a payload without one is the unary
114/// response object or an error envelope.
115pub fn classify_responses_payload(data: &str) -> WireEvent<ResponsesEvent> {
116    if data.trim() == "[DONE]" {
117        return WireEvent::Known(ResponsesEvent::Sentinel);
118    }
119    wire::classify_or_untagged(
120        data,
121        "type",
122        is_known_responses_event_type,
123        |Tagged(frame)| match frame.str("type") {
124            Some("error") => ResponsesEvent::Failure(data.to_owned()),
125            kind => ResponsesEvent::Frame {
126                raw: (kind == Some("response.failed")).then(|| data.to_owned()),
127                kind: kind.unwrap_or_default().to_owned(),
128                frame,
129            },
130        },
131        |data| {
132            wire::classify_marker_keyed_frame::<Value>(data, WHOLE_BODY_MARKERS).map(|body| {
133                let error = body.get("error").is_some_and(|error| !error.is_null());
134                if error && body.get("output").is_none() && body.get("status").is_none() {
135                    ResponsesEvent::Failure(data.to_owned())
136                } else {
137                    ResponsesEvent::Whole(body)
138                }
139            })
140        },
141    )
142}
143
144/// What an output item becomes.
145#[derive(Clone, Copy, PartialEq, Eq, Debug)]
146enum Kind {
147    Message,
148    Reasoning,
149    Call,
150    Opaque,
151}
152
153impl Kind {
154    fn of(item: &Value) -> Self {
155        match item.str("type") {
156            Some("message") => Self::Message,
157            Some("reasoning") => Self::Reasoning,
158            Some("function_call" | "custom_tool_call") => Self::Call,
159            _ => Self::Opaque,
160        }
161    }
162}
163
164/// One output item of the reply, as the decoder saw it.
165struct Slot {
166    /// The writer index its block opened at.
167    at: usize,
168    kind: Kind,
169    /// The item id it states.
170    id: Option<String>,
171    open: bool,
172    /// The text its deltas wrote, and the field and part that last
173    /// extended it: a new part of reasoning starts a paragraph, and a second
174    /// field restating the same reasoning is not appended.
175    text: String,
176    field: Option<&'static str>,
177    part: u64,
178    /// A call's streamed argument text, or a custom call's input.
179    arguments: String,
180    custom: bool,
181    /// Whether the call's id was stated.
182    named: bool,
183    /// Whether the call's tool name was stated.
184    titled: bool,
185}
186
187/// The item types the client executes, which nothing in rig answers: they
188/// are kept in history and never sent back.
189const CLIENT_EXECUTED: &[&str] = &[
190    "computer_call",
191    "local_shell_call",
192    "shell_call",
193    "apply_patch_call",
194    "mcp_approval_request",
195];
196
197fn item_id(item: &Value) -> Option<&str> {
198    item.str("id").filter(|id| !id.is_empty())
199}
200
201/// The argument JSON a done call states: a function call's `arguments`, a
202/// custom call's `input` as `{"input": ...}`; `None` when it states none.
203fn arguments_of(item: &Value) -> Option<String> {
204    if item.str("type") == Some("custom_tool_call") {
205        return item
206            .str("input")
207            .map(|input| json!({ "input": input }).to_string());
208    }
209    match item.get("arguments")? {
210        Value::String(arguments) if arguments.is_empty() => None,
211        Value::String(arguments) => Some(arguments.clone()),
212        Value::Null => None,
213        arguments => Some(arguments.to_string()),
214    }
215}
216
217/// The text a done item states: a message's parts joined, refusals
218/// included; reasoning's summary, or its raw content when it has none.
219fn text_of(item: &Value) -> String {
220    let texts = |key: &str| -> Vec<&str> {
221        match item.get(key) {
222            Some(Value::String(text)) => vec![text.as_str()],
223            _ => item
224                .arr(key)
225                .iter()
226                .filter_map(|part| {
227                    part.as_str()
228                        .or_else(|| part.str("text"))
229                        .or_else(|| part.str("refusal"))
230                })
231                .collect(),
232        }
233    };
234    match Kind::of(item) {
235        Kind::Message => texts("content").concat(),
236        Kind::Reasoning => Some(texts("summary"))
237            .filter(|summary| !summary.is_empty())
238            .unwrap_or_else(|| texts("content"))
239            .join("\n\n"),
240        Kind::Call | Kind::Opaque => String::new(),
241    }
242}
243
244/// `item`, a done item that states no text, stating `text`, so the item
245/// replays as the block it is.
246fn stating(mut item: Value, kind: Kind, text: &str) -> Value {
247    let (key, part) = match kind {
248        Kind::Message => (
249            "content",
250            json!({"type": "output_text", "text": text, "annotations": []}),
251        ),
252        _ => ("summary", json!({"type": "summary_text", "text": text})),
253    };
254    if let Some(fields) = item.as_object_mut() {
255        fields.insert(key.to_owned(), json!([part]));
256    }
257    item
258}
259
260/// The provider's own words for a failed or cancelled response.
261fn provider_message(response: &Value, fallback: &str) -> String {
262    let parts: Vec<&str> = ["/error/code", "/error/message"]
263        .iter()
264        .filter_map(|pointer| response.at(pointer).and_then(Value::as_str))
265        .collect();
266    if parts.is_empty() {
267        fallback.to_owned()
268    } else {
269        parts.join(": ")
270    }
271}
272
273/// How the turn ended, for every status the API documents. A response
274/// without a status ended with a stop. A status that is
275/// not a documented end, including one still `queued` or `in_progress`, is
276/// a failed turn.
277pub(crate) fn finish_reason_of(response: &Value) -> (FinishReason, Option<String>) {
278    let reason = response
279        .at("/incomplete_details/reason")
280        .and_then(Value::as_str)
281        .filter(|reason| !reason.is_empty());
282    let other = |reason: &str, error: String| (FinishReason::Other(reason.to_owned()), Some(error));
283    match response.str("status") {
284        None | Some("completed") => (FinishReason::Stop, None),
285        Some("incomplete") => match reason {
286            Some("max_output_tokens") => (FinishReason::Length, None),
287            Some("content_filter") => (FinishReason::ContentFilter, None),
288            Some(reason) => other(
289                &format!("incomplete: {reason}"),
290                format!("Response incomplete: {reason}"),
291            ),
292            None => other(
293                "incomplete",
294                "Response incomplete without a provider reason".to_owned(),
295            ),
296        },
297        Some(status @ ("failed" | "cancelled")) => other(
298            status,
299            provider_message(response, &format!("Response {status}")),
300        ),
301        Some(status @ ("queued" | "in_progress")) => {
302            other(status, format!("Response ended while {status}"))
303        }
304        Some(status) => other(
305            status,
306            format!("Response ended with the unknown status `{status}`"),
307        ),
308    }
309}
310
311/// The usage a Responses `usage` value reports; a counter that is absent or
312/// not a count is unreported. Its cost is the one the provider reports.
313pub(crate) fn usage_of(usage: &Value) -> Usage {
314    let count = |pointer: &str| usage.at(pointer).and_then(Lenient::as_u64_lenient);
315    Usage {
316        input_tokens: count("/input_tokens"),
317        output_tokens: count("/output_tokens"),
318        total_tokens: count("/total_tokens"),
319        cached_input_tokens: count("/input_tokens_details/cached_tokens"),
320        cache_creation_input_tokens: count("/input_tokens_details/cache_write_tokens"),
321        reasoning_tokens: count("/output_tokens_details/reasoning_tokens"),
322        ..Usage::default()
323    }
324    .cost(reported_cost(usage))
325}
326
327/// xAI cost ticks per USD.
328const TICKS_PER_USD: f64 = 1e10;
329
330/// The total a `usage` value reports in USD: xAI's integer
331/// `cost_in_usd_ticks`, or OpenRouter's `cost` in credits of one USD.
332fn reported_cost(usage: &Value) -> Option<Cost> {
333    let total = match usage.u64("cost_in_usd_ticks") {
334        Some(ticks) => ticks as f64 / TICKS_PER_USD,
335        None => usage.f64("cost").filter(|cost| cost.is_finite())?,
336    };
337    Some(Cost::from_total(total))
338}
339
340/// The citations a message item's `output_text` parts state, in part order.
341/// The API does not document the unit of `start_index` and `end_index` and
342/// quotes no text, so each cites the whole block and its offsets stay in the
343/// item.
344fn citations_of(item: &Value) -> Vec<WireCitation> {
345    item.arr("content")
346        .iter()
347        .flat_map(|part| part.arr("annotations"))
348        .filter_map(source_of)
349        .map(|source| WireCitation::new(None, vec![source]))
350        .collect()
351}
352
353/// The source an annotation names; `None` for an annotation that names
354/// none or is of an unknown type.
355fn source_of(annotation: &Value) -> Option<Source> {
356    let text = |key: &str| {
357        annotation
358            .str(key)
359            .filter(|text| !text.is_empty())
360            .map(str::to_owned)
361    };
362    let location = match annotation.str("type")? {
363        "url_citation" => SourceLocation::Url { url: text("url")? },
364        "file_citation" | "file_path" | "container_file_citation" => SourceLocation::File {
365            file_id: text("file_id")?,
366            filename: text("filename"),
367            container_id: text("container_id"),
368        },
369        _ => return None,
370    };
371    let source = Source::new(location);
372    Some(match text("title") {
373        Some(title) => source.title(title),
374        None => source,
375    })
376}
377
378/// The OpenAI Responses wire's decoder: one state machine for the SSE
379/// stream, the unary body and the websocket session.
380///
381/// Each output item becomes one block. It opens when it is announced, or at
382/// its first delta, with no provider item, and its done item closes it,
383/// becoming the block's native: an item the provider never stated complete
384/// replays from its canonical fields. A done item merges into what streamed:
385/// a call keeps the id and name it was announced with and the arguments it
386/// streamed when its done item leaves them out, and text the done item
387/// leaves out stays, written into the item. The terminal response is the
388/// whole output: it finishes the items no done event carried, writes the
389/// ones the stream never stated, which is how a unary body decodes, and
390/// backfills the ciphertext of reasoning done without it.
391#[derive(Default)]
392pub struct ResponsesDecoder {
393    /// Every item, in the order it arrived.
394    slots: Vec<Slot>,
395    /// The latest slot at each output index the provider named.
396    indexed: HashMap<usize, usize>,
397    /// The slot the last item event addressed.
398    current: Option<usize>,
399    /// The writer indices of reasoning done and waiting for the item after
400    /// it to complete.
401    held: Vec<usize>,
402}
403
404impl ResponsesDecoder {
405    /// A decoder for one reply of a Responses endpoint.
406    pub fn new() -> Self {
407        Self::default()
408    }
409
410    /// The slot `frame` addresses, an event for an item of `kind`: the one
411    /// at its output index, else the one its item id names, else the open
412    /// one the stream is on, when the frame names no index or that item
413    /// was opened by a frame that named none (an envelope-less delta, then
414    /// its envelope-full done item). `None` for an item no slot holds yet.
415    fn addressed(&self, frame: &Value, kind: Kind) -> Result<Option<usize>, ProviderError> {
416        let index = output_index(frame)?;
417        if let Some(slot) = index.and_then(|index| self.indexed.get(&index)) {
418            return Ok(Some(*slot));
419        }
420        let id = frame
421            .at("/item/id")
422            .and_then(Value::as_str)
423            .or_else(|| frame.str("item_id"));
424        if let Some(slot) = id.filter(|id| !id.is_empty()).and_then(|id| {
425            self.slots
426                .iter()
427                .rposition(|slot| slot.id.as_deref() == Some(id))
428        }) {
429            return Ok(Some(slot));
430        }
431        Ok(self.current.filter(|current| {
432            (index.is_none() || !self.indexed.values().any(|slot| slot == current))
433                && self
434                    .slots
435                    .get(*current)
436                    .is_some_and(|slot| slot.open && slot.kind == kind)
437        }))
438    }
439
440    /// Open the block of `item` as a new slot at output `index`, closing what
441    /// is still open there.
442    fn added(
443        &mut self,
444        index: Option<usize>,
445        item: &Value,
446        out: &mut Out<'_, Completion>,
447    ) -> Result<usize, ProviderError> {
448        if let Some(previous) = index.and_then(|index| self.indexed.get(&index).copied()) {
449            self.vacate(previous, out)?;
450        }
451        let at = match index {
452            Some(index) if !self.slots.iter().any(|slot| slot.at == index) => index,
453            _ => out.fresh_index(),
454        };
455        let kind = Kind::of(item);
456        let custom = item.str("type") == Some("custom_tool_call");
457        match kind {
458            Kind::Message => out.open(at, Block::Text, Value::Null)?,
459            Kind::Reasoning => out.open(at, Block::Reasoning { redacted: false }, Value::Null)?,
460            Kind::Call => out.fragment(
461                Some(at),
462                CallFragment {
463                    id: item.str("call_id"),
464                    name: item.str("name"),
465                    arguments: None,
466                },
467            )?,
468            // An item that names no type is kept and never sent back.
469            Kind::Opaque => {
470                let replay = item
471                    .str("type")
472                    .is_some_and(|kind| !CLIENT_EXECUTED.contains(&kind));
473                out.open(at, Block::Opaque { replay }, item.clone())?;
474            }
475        }
476        self.slots.push(Slot {
477            at,
478            kind,
479            id: item_id(item).map(str::to_owned),
480            open: true,
481            text: String::new(),
482            field: None,
483            part: 0,
484            arguments: String::new(),
485            custom,
486            named: item.str("call_id").is_some_and(|id| !id.is_empty()),
487            titled: item.str("name").is_some_and(|name| !name.is_empty()),
488        });
489        let slot = self.slots.len() - 1;
490        if let Some(index) = index {
491            self.indexed.insert(index, slot);
492        }
493        self.current = Some(slot);
494        if let Some(call) = self
495            .slots
496            .get_mut(slot)
497            .filter(|slot| slot.kind == Kind::Call)
498        {
499            let arguments = item.str(if custom { "input" } else { "arguments" });
500            send(call, arguments.unwrap_or_default(), out)?;
501        }
502        Ok(slot)
503    }
504
505    /// Close the slot's block before another item takes its place: the
506    /// provider never stated it complete. A call keeps what streamed.
507    fn vacate(&mut self, slot: usize, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
508        let Some(open) = self.slots.get_mut(slot).filter(|slot| slot.open) else {
509            return Ok(());
510        };
511        open.open = false;
512        let at = open.at;
513        if open.kind == Kind::Call {
514            flush(open, out)?;
515        }
516        out.close(at)?;
517        self.release(false, out)
518    }
519
520    /// End the reasoning held for the item after it: complete when that
521    /// item completed, else as never stated complete.
522    fn release(
523        &mut self,
524        complete: bool,
525        out: &mut Out<'_, Completion>,
526    ) -> Result<(), ProviderError> {
527        std::mem::take(&mut self.held)
528            .into_iter()
529            .try_for_each(|at| {
530                if complete {
531                    out.finish(at)
532                } else {
533                    out.close(at)
534                }
535            })
536    }
537
538    /// Append a text delta of `field` to the item `frame` addresses,
539    /// opening a block for an item the stream never announced.
540    fn text(
541        &mut self,
542        frame: &Value,
543        field: &'static str,
544        part: &str,
545        out: &mut Out<'_, Completion>,
546    ) -> Result<(), ProviderError> {
547        let delta = frame.str("delta").unwrap_or_default();
548        if delta.is_empty() {
549            return Ok(());
550        }
551        let part = frame.u64(part).unwrap_or(0);
552        let kind = match field {
553            "message" => Kind::Message,
554            _ => Kind::Reasoning,
555        };
556        let known = self.addressed(frame, kind)?.and_then(|at| {
557            let slot = self.slots.get(at)?;
558            Some((at, slot.open, slot.kind == kind))
559        });
560        let slot = match known {
561            Some((slot, true, true)) => slot,
562            // A delta for an item that already closed is left to the item,
563            // which states its text.
564            Some((_, false, true)) => return Ok(()),
565            _ => self.added(
566                output_index(frame)?,
567                &json!({"type": if kind == Kind::Message { "message" } else { "reasoning" }}),
568                out,
569            )?,
570        };
571        let Some(streamed) = self.slots.get_mut(slot).filter(|slot| slot.open) else {
572            return Ok(());
573        };
574        if streamed.field.is_some_and(|known| known != field) {
575            return Ok(());
576        }
577        if streamed.part != part && kind != Kind::Message && !streamed.text.is_empty() {
578            streamed.text.push_str("\n\n");
579            out.push(streamed.at, "\n\n")?;
580        }
581        streamed.field = Some(field);
582        streamed.part = part;
583        streamed.text.push_str(delta);
584        out.push(streamed.at, delta)
585    }
586
587    /// Append an argument delta to the call `frame` addresses and write
588    /// it to the call, which streams it.
589    fn arguments(
590        &mut self,
591        frame: &Value,
592        out: &mut Out<'_, Completion>,
593    ) -> Result<(), ProviderError> {
594        let slot = self.addressed(frame, Kind::Call)?;
595        if let Some(call) = slot
596            .and_then(|slot| self.slots.get_mut(slot))
597            .filter(|slot| slot.open && slot.kind == Kind::Call)
598        {
599            send(call, frame.str("delta").unwrap_or_default(), out)?;
600        }
601        Ok(())
602    }
603
604    /// The item at `slot` is done: it merges into what streamed and becomes
605    /// the block's native.
606    fn done(
607        &mut self,
608        slot: usize,
609        item: Value,
610        out: &mut Out<'_, Completion>,
611    ) -> Result<(), ProviderError> {
612        let Some(done) = self.slots.get_mut(slot) else {
613            return Ok(());
614        };
615        done.open = false;
616        if done.id.is_none() {
617            done.id = item_id(&item).map(str::to_owned);
618        }
619        let at = done.at;
620        let mut item = item;
621        let mut nameless = false;
622        match done.kind {
623            Kind::Call => {
624                nameless = !done.titled && item.str("name").is_none_or(str::is_empty);
625                let arguments = arguments_of(&item).unwrap_or_else(|| streamed_arguments(done));
626                let rest = remainder(done, &arguments, out)?;
627                out.fragment(
628                    Some(at),
629                    CallFragment {
630                        id: item.str("call_id").filter(|_| !done.named),
631                        name: item.str("name"),
632                        arguments: Some(&rest),
633                    },
634                )?;
635            }
636            Kind::Message | Kind::Reasoning => {
637                let text = text_of(&item);
638                if text.is_empty() {
639                    // Raw reasoning text the done item leaves out stays out:
640                    // written in as a summary, it would replay one the
641                    // provider never produced.
642                    if !done.text.is_empty() && done.field != Some("reasoning") {
643                        item = stating(item, done.kind, &done.text);
644                    }
645                } else if let Some(rest) = text.strip_prefix(done.text.as_str()) {
646                    out.push(at, rest)?;
647                } else {
648                    // The done item states the whole text, as pi takes it.
649                    out.restate(at, &text)?;
650                }
651                let citations = citations_of(&item);
652                if done.kind == Kind::Message && !citations.is_empty() {
653                    // The item restates every annotation, so its list
654                    // replaces any an earlier snapshot gave.
655                    out.set_citations(at, citations);
656                }
657            }
658            Kind::Opaque => {}
659        }
660        let reasoning = done.kind == Kind::Reasoning;
661        out.edit(at, |native| *native = item)?;
662        if reasoning {
663            // Reasoning is sent only with the item it precedes, so it is
664            // complete only once that item is.
665            self.held.push(at);
666            return Ok(());
667        }
668        out.finish(at)?;
669        // The writer drops a call with no name, so the reasoning it held
670        // has no item to go with.
671        self.release(!nameless, out)
672    }
673
674    /// Write one output-item event into the reply.
675    fn item_event(
676        &mut self,
677        kind: &str,
678        mut frame: Value,
679        out: &mut Out<'_, Completion>,
680    ) -> Result<(), ProviderError> {
681        match kind {
682            "response.output_item.added" | "response.output_item.done" => {
683                let Some(item) = frame.get("item").filter(|item| item.is_object()) else {
684                    return Ok(());
685                };
686                let index = output_index(&frame)?;
687                if kind == "response.output_item.added" {
688                    return self.added(index, item, out).map(|_| ());
689                }
690                let known = self.addressed(&frame, Kind::of(item))?;
691                // At a named output index the item is the one there, as pi
692                // reads it, whatever id it states.
693                let restates = |slot: &Slot| {
694                    slot.kind == Kind::of(item)
695                        && (index.is_some()
696                            || item_id(item)
697                                .zip(slot.id.as_deref())
698                                .is_none_or(|(id, known)| id == known))
699                };
700                let known = known.and_then(|at| {
701                    let slot = self.slots.get(at)?;
702                    let same = item_id(item).is_some() && slot.id.as_deref() == item_id(item);
703                    Some((at, slot.open && restates(slot), same))
704                });
705                let slot = match known {
706                    Some((slot, true, _)) => slot,
707                    // The item this one restates is already done.
708                    Some((_, false, true)) => return Ok(()),
709                    _ => self.added(index, item, out)?,
710                };
711                let item = frame
712                    .as_object_mut()
713                    .and_then(|frame| remove_preserving_order(frame, "item"))
714                    .unwrap_or_default();
715                self.done(slot, item, out)
716            }
717            // A refusal is the assistant's message for that turn.
718            "response.output_text.delta" | "response.refusal.delta" => {
719                self.text(&frame, "message", "content_index", out)
720            }
721            "response.reasoning_summary_text.delta" => {
722                self.text(&frame, "summary", "summary_index", out)
723            }
724            "response.reasoning_text.delta" => self.text(&frame, "reasoning", "content_index", out),
725            "response.function_call_arguments.delta" | "response.custom_tool_call_input.delta" => {
726                self.arguments(&frame, out)
727            }
728            _ => Ok(()),
729        }
730    }
731
732    /// The slot the terminal's item at output `index` restates, among those
733    /// no earlier terminal item took: the one with its id; else the one the
734    /// stream gave that index when it is of the item's kind, whatever id it
735    /// states; else the first of its kind the stream
736    /// gave no index and no other id; else the first of its kind with no
737    /// other id that the stream put at an index the terminal holds no item
738    /// of that kind at, a stream whose indices are shifted against it.
739    fn restated(
740        &self,
741        index: usize,
742        item: &Value,
743        kinds: &[Kind],
744        taken: &HashSet<usize>,
745    ) -> Option<usize> {
746        let id = item_id(item);
747        let free = |at: &usize| {
748            !taken.contains(at)
749                && self
750                    .slots
751                    .get(*at)
752                    .is_some_and(|slot| slot.kind == Kind::of(item))
753        };
754        let find = |fits: &dyn Fn(usize, &Slot) -> bool| {
755            self.slots
756                .iter()
757                .enumerate()
758                .find(|(at, slot)| free(at) && fits(*at, slot))
759                .map(|(at, _)| at)
760        };
761        id.and_then(|id| find(&|_, slot| slot.id.as_deref() == Some(id)))
762            .or_else(|| self.indexed.get(&index).copied().filter(free))
763            .or_else(|| {
764                find(&|at, slot| {
765                    !self.indexed.values().any(|indexed| *indexed == at)
766                        && (id.is_none() || slot.id.is_none())
767                })
768            })
769            .or_else(|| {
770                find(&|at, slot| {
771                    (id.is_none() || slot.id.is_none())
772                        && self.indexed.iter().any(|(streamed, indexed)| {
773                            *indexed == at
774                                && kinds.get(*streamed).is_none_or(|other| *other != slot.kind)
775                        })
776                })
777            })
778    }
779
780    /// The terminal response: finish or write each item of its output,
781    /// backfill the ciphertext of reasoning done without it, then end with
782    /// how the turn ended. A body that reports its reasoning as one
783    /// top-level string and no reasoning item is written that reasoning
784    /// first.
785    fn finish(
786        &mut self,
787        mut response: Value,
788        mut out: Out<'_, Completion>,
789    ) -> Result<Flow, ProviderError> {
790        let output = match response.get_mut("output") {
791            Some(Value::Array(output)) => std::mem::take(output),
792            _ => Vec::new(),
793        };
794        if self.slots.is_empty()
795            && !output.iter().any(|item| Kind::of(item) == Kind::Reasoning)
796            && let Some(reasoning) = response.str("reasoning").filter(|text| !text.is_empty())
797        {
798            // Index 0 orders it before the output; it closes at once, so the
799            // output's first item may open there after it.
800            out.whole(
801                0,
802                Block::Reasoning { redacted: false },
803                Value::Null,
804                reasoning,
805            )?;
806        }
807        let mut taken = HashSet::new();
808        let kinds: Vec<Kind> = output.iter().map(Kind::of).collect();
809        for (index, item) in output
810            .into_iter()
811            .enumerate()
812            .filter(|(_, item)| item.is_object())
813        {
814            let known = self.restated(index, &item, &kinds, &taken).and_then(|at| {
815                let slot = self.slots.get(at)?;
816                Some((at, slot.open, slot.kind, slot.at))
817            });
818            let slot = match known {
819                Some((slot, false, kind, at)) => {
820                    taken.insert(slot);
821                    let ciphertext = item
822                        .get("encrypted_content")
823                        .filter(|cipher| cipher.as_str().is_some_and(|cipher| !cipher.is_empty()));
824                    if let (Kind::Reasoning, Some(ciphertext)) = (kind, ciphertext) {
825                        out.edit(at, |native| {
826                            let stated = native
827                                .str("encrypted_content")
828                                .is_some_and(|cipher| !cipher.is_empty());
829                            if let (false, Some(fields)) = (stated, native.as_object_mut()) {
830                                fields.insert("encrypted_content".to_owned(), ciphertext.clone());
831                            }
832                        })?;
833                    }
834                    continue;
835                }
836                Some((slot, ..)) => slot,
837                None => self.added(
838                    Some(index).filter(|index| !self.indexed.contains_key(index)),
839                    &item,
840                    &mut out,
841                )?,
842            };
843            taken.insert(slot);
844            self.done(slot, item, &mut out)?;
845        }
846        // Reasoning still waiting is complete when nothing after it is left
847        // open.
848        let open = self.slots.iter().any(|slot| slot.open);
849        self.release(!open, &mut out)?;
850        // A call never done shows what streamed, in a turn that fails.
851        for slot in self.slots.iter_mut().filter(|slot| slot.open) {
852            if slot.kind == Kind::Call {
853                flush(slot, &mut out)?;
854            }
855        }
856        let (reason, error) = finish_reason_of(&response);
857        let end = Finish {
858            usage: response.get("usage").map(usage_of).unwrap_or_default(),
859            reason: Some(reason),
860            response_id: item_id(&response).map(str::to_owned),
861            model: response
862                .str("model")
863                .filter(|model| !model.is_empty())
864                .map(str::to_owned),
865            error,
866        };
867        Ok(out.end(end))
868    }
869}
870
871/// Give `slot`'s call what it streamed, before it closes undone.
872fn flush(slot: &Slot, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
873    let arguments = streamed_arguments(slot);
874    let rest = remainder(slot, &arguments, out)?;
875    let fragment = CallFragment {
876        arguments: Some(&rest),
877        ..CallFragment::default()
878    };
879    out.fragment(Some(slot.at), fragment)
880}
881
882/// Append `delta` to `slot`'s call and write it to the call, which
883/// streams it. A custom call's input streams inside its `{"input": ...}`
884/// object, escaped as the object's string, so its fragments join to the
885/// object its end states: JSON escapes each character on its own, so the
886/// escaped delta extends what was written.
887fn send(slot: &mut Slot, delta: &str, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
888    if delta.is_empty() {
889        return Ok(());
890    }
891    let fragment = if slot.custom {
892        let quoted = serde_json::to_string(delta)?;
893        let open = quoted.strip_suffix('"').unwrap_or(&quoted);
894        Cow::Owned(match open.strip_prefix('"') {
895            Some(escaped) if !slot.arguments.is_empty() => escaped.to_owned(),
896            _ => format!("{{\"input\":{open}"),
897        })
898    } else {
899        Cow::Borrowed(delta)
900    };
901    slot.arguments.push_str(delta);
902    let fragment = CallFragment {
903        arguments: Some(&fragment),
904        ..CallFragment::default()
905    };
906    out.fragment(Some(slot.at), fragment)
907}
908
909/// What of `arguments`, the whole JSON `slot`'s call states, the call has
910/// not been given yet. When `arguments` does not extend what streamed, it
911/// replaces the call's text and nothing more streams: the call's end states
912/// it.
913fn remainder(
914    slot: &Slot,
915    arguments: &str,
916    out: &mut Out<'_, Completion>,
917) -> Result<String, ProviderError> {
918    // What `send` wrote: the streamed JSON, a custom call's input still open.
919    let streamed = streamed_arguments(slot);
920    let sent = if slot.arguments.is_empty() {
921        ""
922    } else if slot.custom {
923        streamed.strip_suffix("\"}").unwrap_or(&streamed)
924    } else {
925        &streamed
926    };
927    Ok(match arguments.strip_prefix(sent) {
928        Some(rest) => rest.to_owned(),
929        None => {
930            out.restate(slot.at, arguments)?;
931            String::new()
932        }
933    })
934}
935
936/// The argument JSON of what `slot`'s call streamed.
937fn streamed_arguments(slot: &Slot) -> String {
938    if slot.custom {
939        json!({ "input": slot.arguments }).to_string()
940    } else {
941        slot.arguments.clone()
942    }
943}
944
945/// The output index `frame` names.
946fn output_index(frame: &Value) -> Result<Option<usize>, ProviderError> {
947    frame
948        .u64("output_index")
949        .map(|index| {
950            usize::try_from(index).map_err(|_| {
951                ProviderError::Response(format!("output_index {index} is out of range"))
952            })
953        })
954        .transpose()
955}
956
957impl<'id> Decoder<'id, Completion> for ResponsesDecoder {
958    type Event = ResponsesEvent;
959
960    fn classify(&self, frame: WireFrame) -> WireEvent<ResponsesEvent> {
961        classify_responses_payload(&frame.as_str())
962    }
963
964    fn decode(
965        &mut self,
966        event: ResponsesEvent,
967        mut out: Out<'id, Completion>,
968    ) -> Result<Flow, ProviderError> {
969        // A terminal may state items the stream never announced; they take
970        // their place by output index.
971        out.order_by_index();
972        match event {
973            ResponsesEvent::Frame {
974                kind,
975                mut frame,
976                raw,
977            } => match kind.as_str() {
978                // `response.incomplete` is a genuine terminal that keeps the
979                // partial output and usage. It is incomplete whatever its
980                // `status` says, where pi reads a missing status as a stop.
981                "response.completed" | "response.incomplete" => {
982                    let mut response = frame
983                        .as_object_mut()
984                        .and_then(|frame| remove_preserving_order(frame, "response"))
985                        .filter(Value::is_object)
986                        .unwrap_or_else(|| json!({}));
987                    if let (true, Some(fields)) =
988                        (kind == "response.incomplete", response.as_object_mut())
989                    {
990                        fields.insert("status".to_owned(), json!("incomplete"));
991                    }
992                    self.finish(response, out)
993                }
994                "response.failed" => {
995                    Err(ProviderError::from_provider_body(raw.unwrap_or_default()))
996                }
997                "keepalive" => Ok(Flow::More),
998                kind if is_lifecycle_event(kind) => Ok(Flow::More),
999                kind => {
1000                    self.item_event(kind, frame, &mut out)?;
1001                    Ok(Flow::More)
1002                }
1003            },
1004            // The unary reply is the terminal response with no stream before it.
1005            ResponsesEvent::Whole(body) => self.finish(body, out),
1006            ResponsesEvent::Failure(raw) => Err(ProviderError::from_provider_body(raw)),
1007            // Nothing to write: the provider's end is `response.completed`.
1008            ResponsesEvent::Sentinel => Ok(Flow::More),
1009        }
1010    }
1011}
1012
1013pub(crate) mod document;
1014
1015#[cfg(test)]
1016mod tests;