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