Skip to main content

rig_core/streaming/
mod.rs

1//! This module provides functionality for working with streaming completion models.
2//! It provides traits and types for generating streaming completion requests and
3//! handling streaming completion responses.
4//!
5//! Provider implementations use these types to expose raw streamed completion
6//! events without depending on a runtime.
7
8mod identity;
9mod parts;
10
11use crate::completion::{CompletionError, CompletionResponse, Usage};
12use crate::message::{
13    AssistantContent, Reasoning, ReasoningContent, Text, ToolCall, ToolFunction, ToolResult,
14};
15use crate::wasm_compat::WasmCompatSend;
16use futures::stream::{AbortHandle, Abortable};
17use futures::{Stream, StreamExt};
18pub use identity::{MintKind, StreamPartId, SyntheticIds, WireId};
19use parts::PartsAccumulator;
20use serde::{Deserialize, Serialize};
21use std::future::Future;
22use std::pin::Pin;
23use std::sync::atomic::AtomicBool;
24use std::task::{Context, Poll};
25use tokio::sync::watch;
26
27/// Control for pausing and resuming a streaming response
28pub struct PauseControl {
29    pub(crate) paused_tx: watch::Sender<bool>,
30    pub(crate) paused_rx: watch::Receiver<bool>,
31}
32
33impl PauseControl {
34    /// Create a pause controller in the running state.
35    pub fn new() -> Self {
36        let (paused_tx, paused_rx) = watch::channel(false);
37        Self {
38            paused_tx,
39            paused_rx,
40        }
41    }
42
43    /// Pause polling of the public stream until [`PauseControl::resume`] is called.
44    pub fn pause(&self) {
45        let _ = self.paused_tx.send(true);
46    }
47
48    /// Resume polling after a pause.
49    pub fn resume(&self) {
50        let _ = self.paused_tx.send(false);
51    }
52
53    /// Returns whether the stream is currently paused.
54    pub fn is_paused(&self) -> bool {
55        *self.paused_rx.borrow()
56    }
57}
58
59impl Default for PauseControl {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65/// The content of a tool call delta - either the tool name or argument data
66#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
67pub enum ToolCallDeltaContent {
68    /// Tool/function name emitted by the provider.
69    Name(String),
70    /// Partial JSON argument data emitted by the provider.
71    Delta(String),
72}
73
74/// How the shared assembler treats an argument payload that does not parse as
75/// JSON when a streamed tool call's input ends.
76///
77/// This is genuine wire-family policy, declared by the adapter on the end
78/// event rather than hand-rolled per provider.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum UnparseableToolInput {
81    /// Drop the call silently: the input never fully arrived (the
82    /// OpenAI-compatible end-of-stream flush of pending calls).
83    Drop,
84    /// Deliver the call with `{}` arguments: the wire superseded the call
85    /// mid-assembly (the OpenAI-compatible same-slot eviction path).
86    EmptyObject,
87    /// Surface an in-band error item: the wire promised a complete block
88    /// (Anthropic `content_block_stop`, Bedrock `contentBlockStop`).
89    Error,
90    /// Leave the call open and emit nothing: the end was a completion
91    /// *probe* (the OpenAI-compatible single-chunk immediate-emission path),
92    /// and input that does not yet finalize may still be extended by later
93    /// fragments and closed by a genuine flush.
94    Keep,
95}
96
97/// End of a streamed tool call's input: the signal for the shared assembler
98/// ([`RawStreamingChoice::ToolInputEnd`]) to finalize the call.
99///
100/// Optional fields are authoritative wire values that supersede the assembled
101/// state — a wire whose completed item restates the call (OpenAI Responses
102/// `output_item.done`) carries them; delta-only wires leave them `None` and
103/// the assembled fragments are parsed instead.
104#[derive(Debug, Clone)]
105pub struct ToolInputEnd {
106    /// Assembly identity: the id the call's fragments were emitted under.
107    pub id: StreamPartId,
108    /// Authoritative provider-issued tool id, when one exists (e.g. an id
109    /// that arrived after the call opened id-less). The durable handle;
110    /// absence is `None`, never an empty string.
111    pub tool_id: Option<WireId>,
112    /// Authoritative tool name from the wire's completed item.
113    pub name: Option<String>,
114    /// Authoritative parsed arguments from the wire's completed item.
115    pub arguments: Option<serde_json::Value>,
116    /// Provider call-correlation id (e.g. OpenAI Responses `call_id`).
117    pub call_id: Option<String>,
118    /// Provider signature attached to the completed call.
119    pub signature: Option<String>,
120    /// Provider-specific metadata attached to the completed call.
121    pub additional_params: Option<serde_json::Value>,
122    /// Wire-family policy for assembled arguments that fail to parse.
123    pub on_unparseable: UnparseableToolInput,
124}
125
126/// Decoration a provider attaches to a streamed tool call that is still
127/// assembling, matched by its established provider id (e.g. OpenRouter
128/// encrypted reasoning details). Carried onto the completed call by the
129/// adapter's end event.
130#[derive(Debug, Clone)]
131pub struct ToolCallDecoration {
132    /// Established provider id of the call to decorate.
133    pub tool_id: String,
134    /// Provider signature to attach to the completed call.
135    pub signature: Option<String>,
136    /// Provider-specific metadata to attach to the completed call.
137    pub additional_params: Option<serde_json::Value>,
138}
139
140impl ToolInputEnd {
141    /// End the call identified by `id`, finalizing from assembled fragments
142    /// with the given unparseable-input policy.
143    pub fn new(id: impl Into<StreamPartId>, on_unparseable: UnparseableToolInput) -> Self {
144        Self {
145            id: id.into(),
146            tool_id: None,
147            name: None,
148            arguments: None,
149            call_id: None,
150            signature: None,
151            additional_params: None,
152            on_unparseable,
153        }
154    }
155}
156
157/// Discriminant for [`StreamFinal`].
158///
159/// [`StreamedAssistantContent`] is `#[serde(untagged)]` and its
160/// [`StreamedAssistantContent::Unknown`] variant matches any JSON value, so the
161/// terminal record needs a field that identifies it structurally.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "snake_case")]
164pub enum StreamFinalKind {
165    /// The provider's terminal stream event.
166    Final,
167}
168
169/// The provider's terminal stream record, normalized.
170///
171/// This replaces the provider-typed final payload that streams used to carry:
172/// usage is a plain field rather than a trait method, and the finish reason is
173/// normalized exactly as on the unary [`CompletionResponse`].
174///
175/// Providers that want their own terminal type keep it behind
176/// [`RawStreamingResult`] and map it once with [`normalize_stream`].
177///
178/// # Emission contract
179///
180/// A terminal record is emitted only when the provider signaled genuine
181/// completion — its own end-of-response event (an Anthropic `message_delta`
182/// with a stop reason, an OpenAI `[DONE]` / `response.completed`, a Gemini
183/// chunk carrying `finishReason`, and so on). Three failure shapes reach a
184/// consumer, and they are distinct:
185///
186/// | Shape | `Err` item | Stream continues | Terminal record |
187/// |---|---|---|---|
188/// | Transport error (connection lost, HTTP failure) | yes | no | never |
189/// | Malformed frame (recoverable parse error) | yes | yes | if a genuine terminal later arrives |
190/// | Truncation (EOF without the provider's end event) | no | — | never |
191///
192/// On a terminal error (a transport failure or the provider's own failure
193/// event), tool calls that were fully delivered before the failure are yielded
194/// *before* the terminal `Err`; nothing follows the error — the stream then
195/// ends without a terminal record.
196///
197/// Consequently an `Err` item is **not** by itself terminal: a malformed frame
198/// is surfaced and the stream keeps consuming, so a later genuine terminal
199/// still completes it. Consumers must drain the stream to `None` rather than
200/// stop at the first `Err`, and must treat the absence of a terminal record as
201/// truncation, never as a successful zero-usage completion.
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203#[serde(from = "StreamFinalRepr")]
204pub struct StreamFinal {
205    /// Discriminating field; always [`StreamFinalKind::Final`].
206    pub kind: StreamFinalKind,
207    /// Token usage reported by the provider for this streamed completion.
208    /// Zero-valued usage is the documented sentinel for missing metrics.
209    pub usage: Usage,
210    /// Why the model stopped generating, when the provider reported it.
211    ///
212    /// [`normalize_stream`] applies
213    /// [`FinishReason::reconcile_with_output`](crate::completion::FinishReason::reconcile_with_output)
214    /// to this value using the tool calls actually seen on the stream, so a
215    /// provider mapper does not need to (and cannot — it has no view of the
216    /// preceding events).
217    #[serde(default)]
218    pub finish_reason: Option<crate::completion::FinishReason>,
219    /// Provider-assigned *assistant message* ID, when available — only IDs the
220    /// provider would recognize on a replayed assistant message. Response-scoped
221    /// identifiers belong in [`StreamFinal::response_id`].
222    #[serde(default)]
223    pub message_id: Option<String>,
224    /// Provider-assigned response-scoped ID, when available — e.g. an OpenAI
225    /// chat `chatcmpl-` ID. Never replayed to a provider as a message ID.
226    #[serde(default)]
227    pub response_id: Option<String>,
228    /// The provider's transport-level request identifier, taken from the SSE
229    /// connection's HTTP response headers (Anthropic `request-id`, OpenAI/xAI
230    /// `x-request-id`). When the source reconnected, this is the connection
231    /// that delivered this terminal record. Never the body's message/response
232    /// id. `None` means the provider did not report one — a documented
233    /// outcome, never an error.
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub provider_request_id: Option<String>,
236    /// Stable descriptor name of the provider that produced this stream.
237    pub provider: String,
238    /// Provider-reported model identifier, when available.
239    #[serde(default)]
240    pub model: Option<String>,
241    /// The provider's own terminal record for this stream: the value the
242    /// model's inherent `raw_stream` would have yielded as its `FinalResponse`,
243    /// serialized. It is the terminal record as rig's wire type parsed it —
244    /// fields that type does not model are not here — and it is the terminal
245    /// record only, not the stream's frames; see the module docs for why
246    /// frames are a separate mechanism. [`normalize_stream`] populates it
247    /// unconditionally — the same parity the pre-normalization `Final(R)` had.
248    ///
249    /// An escape hatch for provider-specific data rig does not normalize — it
250    /// never replaces a normalized field, and every normalized field means the
251    /// same thing whatever this holds. `Value::Null` means the record was
252    /// built without a provider behind it — [`StreamFinal::new`] without
253    /// `with_raw` (a provider's mapper before [`normalize_stream`] attaches
254    /// the terminal, test doubles, hand-built records), or a record persisted
255    /// before the field existed — never that the provider sent nothing: no
256    /// stream that reached its terminal yields `Null` here.
257    ///
258    /// Typed access is recoverable: provider terminal types are
259    /// `Deserialize`, so `provider::StreamingCompletionResponse::deserialize(&raw)`
260    /// returns the provider's own type.
261    #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
262    pub raw: serde_json::Value,
263}
264
265impl StreamFinal {
266    /// Create a terminal record for `provider` with `usage`; optional metadata
267    /// starts unset and is filled in with the `with_*` helpers.
268    pub fn new(provider: impl Into<String>, usage: Usage) -> Self {
269        Self {
270            kind: StreamFinalKind::Final,
271            usage,
272            finish_reason: None,
273            message_id: None,
274            response_id: None,
275            provider_request_id: None,
276            provider: provider.into(),
277            model: None,
278            raw: serde_json::Value::Null,
279        }
280    }
281
282    /// Attach the normalized finish reason.
283    pub fn with_finish_reason(self, finish_reason: crate::completion::FinishReason) -> Self {
284        self.with_optional_finish_reason(Some(finish_reason))
285    }
286
287    /// Attach the normalized finish reason when the provider reported one.
288    pub fn with_optional_finish_reason(
289        mut self,
290        finish_reason: Option<crate::completion::FinishReason>,
291    ) -> Self {
292        self.finish_reason = finish_reason;
293        self
294    }
295
296    /// This terminal record's identity metadata as one
297    /// [`crate::completion::ResponseIdentity`] carrier.
298    pub fn identity(&self) -> crate::completion::ResponseIdentity {
299        crate::completion::ResponseIdentity {
300            message_id: self.message_id.clone(),
301            response_id: self.response_id.clone(),
302            provider_request_id: self.provider_request_id.clone(),
303        }
304    }
305}
306
307crate::provider_response::response_metadata_setters!(StreamFinal);
308
309/// Wire-shape mirror of [`StreamFinal`], used only for deserialization.
310///
311/// Serde must never construct an invariant-bearing value structurally: a plain
312/// derive would let `"message_id":""` skip the empty-string filtering the
313/// `with_*` setters apply. This mirror deserializes the exact wire shape —
314/// including the discriminating `kind` field — and [`From`] funnels it through
315/// [`StreamFinal::new`] and the setters, so every deserialized value satisfies
316/// the same invariants as a constructed one. Serialization stays derived on
317/// [`StreamFinal`] itself, so the wire format is unchanged.
318#[derive(Deserialize)]
319struct StreamFinalRepr {
320    kind: StreamFinalKind,
321    usage: Usage,
322    #[serde(default)]
323    finish_reason: Option<crate::completion::FinishReason>,
324    #[serde(default)]
325    message_id: Option<String>,
326    #[serde(default)]
327    response_id: Option<String>,
328    #[serde(default)]
329    provider_request_id: Option<String>,
330    provider: String,
331    #[serde(default)]
332    model: Option<String>,
333    // `default` because persisted terminal records predate the field; a
334    // missing key loads as `Null`, which is exactly what "no provider record
335    // behind this value" means.
336    #[serde(default)]
337    raw: serde_json::Value,
338}
339
340impl From<StreamFinalRepr> for StreamFinal {
341    fn from(repr: StreamFinalRepr) -> Self {
342        let StreamFinalRepr {
343            kind,
344            usage,
345            finish_reason,
346            message_id,
347            response_id,
348            provider_request_id,
349            provider,
350            model,
351            raw,
352        } = repr;
353        // `StreamFinal::new` sets the only possible discriminant; the
354        // irrefutable pattern consumes the mirrored field.
355        let StreamFinalKind::Final = kind;
356        Self::new(provider, usage)
357            .with_optional_finish_reason(finish_reason)
358            .with_optional_message_id(message_id)
359            .with_optional_response_id(response_id)
360            .with_optional_provider_request_id(provider_request_id)
361            .with_optional_model(model)
362            .with_raw(raw)
363    }
364}
365
366/// An unmodeled wire payload on the raw passthrough channel.
367///
368/// Wraps the raw JSON with a **redacted** `Debug` (structural metadata only):
369/// unmodeled frames can carry model output or other sensitive provider data,
370/// and `warn!(?value)`-style Debug captures in streaming modules were a
371/// recurring leak class a text scanner existed to police. With the payload
372/// unable to Debug-print its content, that class is structurally closed for
373/// the JSON channel — the redaction is a property of the type, not a
374/// convention. Consumers who want the content opt in explicitly via
375/// [`UnknownPayload::value`]; serialization
376/// is `#[serde(transparent)]`, so wire round-trips are unchanged.
377#[derive(Clone, PartialEq, Serialize, Deserialize)]
378#[serde(transparent)]
379pub struct UnknownPayload(serde_json::Value);
380
381impl UnknownPayload {
382    /// Wrap a raw unmodeled payload.
383    pub fn new(value: serde_json::Value) -> Self {
384        Self(value)
385    }
386
387    /// The raw payload, for consumers who opt in to the content.
388    pub fn value(&self) -> &serde_json::Value {
389        &self.0
390    }
391}
392
393impl std::fmt::Debug for UnknownPayload {
394    /// Structural metadata only — never the payload.
395    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396        let bytes = serde_json::to_vec(&self.0)
397            .map(|json| json.len())
398            .unwrap_or(0);
399        write!(f, "UnknownPayload({bytes} bytes redacted)")
400    }
401}
402
403impl From<serde_json::Value> for UnknownPayload {
404    fn from(value: serde_json::Value) -> Self {
405        Self(value)
406    }
407}
408
409#[cfg(test)]
410mod unknown_payload_tests {
411    use super::UnknownPayload;
412
413    /// The redaction is a property of the type: no Debug rendering — direct,
414    /// via a containing derive, or through a `warn!(?value)` capture — can
415    /// reproduce payload content.
416    #[test]
417    fn debug_output_never_contains_payload_content() {
418        let payload = UnknownPayload::new(serde_json::json!({
419            "secret_field": "SENSITIVE-CONTENT",
420        }));
421        let rendered = format!("{payload:?}");
422        assert!(!rendered.contains("SENSITIVE-CONTENT"));
423        assert!(!rendered.contains("secret_field"));
424        assert!(rendered.contains("redacted"));
425    }
426
427    /// Serialization stays transparent, so wire round-trips are unchanged.
428    #[test]
429    fn serde_round_trip_is_transparent() {
430        let value = serde_json::json!({"type": "future_event", "n": 1});
431        let payload = UnknownPayload::new(value.clone());
432        let encoded = serde_json::to_string(&payload).expect("serializes");
433        assert_eq!(encoded, serde_json::to_string(&value).expect("serializes"));
434        let decoded: UnknownPayload = serde_json::from_str(&encoded).expect("deserializes");
435        assert_eq!(decoded, payload);
436    }
437}
438
439/// Enum representing a streaming chunk from the model.
440///
441/// `R` is the terminal record type. Ordinary streams use the normalized
442/// [`StreamFinal`] default; a provider's inherent `raw_stream` method
443/// substitutes its own native terminal type over the same event vocabulary,
444/// which is what keeps [`crate::completion::CompletionModel`] free of response
445/// associated types.
446#[derive(Debug, Clone)]
447pub enum RawStreamingChoice<R = StreamFinal> {
448    /// A text chunk from a message response
449    Message(String),
450
451    /// Start a new text content block in the accumulated final choice.
452    ///
453    /// This is an internal provider-normalization event. It is not yielded to
454    /// public stream consumers, but lets providers preserve block boundaries
455    /// and metadata for final aggregated assistant text blocks.
456    TextStart {
457        /// Identity of the text block being opened.
458        ///
459        /// The same mandatory-identity contract as
460        /// [`RawStreamingChoice::Reasoning::id`]: distinct wire output items
461        /// must aggregate as distinct text parts (two OpenAI Responses
462        /// `message` items must not concatenate), so the accumulator keys
463        /// text blocks by identity. Providers propagate the wire's item
464        /// identity (`StreamPartId::Wire`: the Responses `item_id`, Anthropic's
465        /// block index) when it exists, or mint one at the boundary
466        /// (`StreamPartId::Minted`, via [`SyntheticIds`]). A wire that never
467        /// announces text boundaries may skip `TextStart` entirely: a bare
468        /// [`RawStreamingChoice::Message`] with no open block opens a
469        /// boundary-minted block.
470        id: StreamPartId,
471        /// Provider-specific metadata attached to this text block.
472        additional_params: Option<crate::message::AdditionalParams>,
473    },
474
475    /// Provider-specific metadata for the current text content block.
476    ///
477    /// This is not yielded to public stream consumers. The metadata is merged
478    /// into the current aggregated [`Text`] block.
479    /// [`crate::message::AdditionalParams`] is non-empty by construction, so
480    /// a provider with nothing to attach skips the variant instead of
481    /// emitting an empty carrier.
482    TextAdditionalParams(crate::message::AdditionalParams),
483
484    /// A tool call response (in its entirety) — wires that never fragment
485    /// tool input emit this directly; fragmenting wires emit
486    /// [`RawStreamingChoice::ToolCallDelta`] fragments closed by
487    /// [`RawStreamingChoice::ToolInputEnd`], and the shared accumulator
488    /// assembles the completed call.
489    ToolCall(RawStreamingToolCall),
490    /// A tool call partial/delta.
491    ///
492    /// All fragments of one call carry one `id`; the shared accumulator keys
493    /// assembly by it and mints the internal correlation id when the call
494    /// opens, so adapters never track per-call state.
495    ToolCallDelta {
496        /// Identity of the tool call this fragment extends, stable across the
497        /// call's fragments.
498        ///
499        /// The same mandatory-identity contract as
500        /// [`RawStreamingChoice::Reasoning::id`]: parallel calls interleave
501        /// their fragments on real wires, so the accumulator must key
502        /// assembly by identity. Providers propagate the wire's tool-call id
503        /// (`StreamPartId::Wire`), or mint one at the boundary from the wire's
504        /// own index (`StreamPartId::Minted`, via [`SyntheticIds`]) when the wire
505        /// omits it — a shared identity would collapse parallel calls into
506        /// one corrupted assembly. A minted identity keys assembly only; it
507        /// never becomes the completed call's durable
508        /// [`ToolCall::id`](crate::message::ToolCall::id).
509        id: StreamPartId,
510        content: ToolCallDeltaContent,
511    },
512    /// End of a streamed tool call's input: the shared accumulator finalizes
513    /// the assembled fragments (or the event's authoritative payload) into a
514    /// completed tool call.
515    ToolInputEnd(ToolInputEnd),
516    /// A reasoning (in its entirety)
517    Reasoning {
518        /// Identity of the reasoning item this block belongs to.
519        ///
520        /// Required: reasoning interleaves with other output on real wires
521        /// (OpenAI Responses emits the completed item after tool calls), so
522        /// the accumulator must key by identity rather than guess by
523        /// adjacency. Providers propagate the wire's item id
524        /// (`StreamPartId::Wire`: `item_id` on Responses events) or mint a
525        /// stream-scoped id at the boundary (`StreamPartId::Minted`, via
526        /// [`SyntheticIds`]) when the wire has none. Deltas and the full
527        /// block for the same item MUST carry the same key.
528        id: StreamPartId,
529        /// The provider-issued reasoning item id, when one exists — the
530        /// durable handle that becomes
531        /// [`Reasoning::id`](crate::message::Reasoning::id) and round-trips
532        /// upstream. Carried separately from the accumulation key: the key
533        /// is opaque and can never leak; the handle is data.
534        provider_id: Option<WireId>,
535        /// Complete reasoning content block.
536        content: ReasoningContent,
537    },
538    /// Open the reasoning part identified by `id`.
539    ///
540    /// Optional — a bare [`RawStreamingChoice::ReasoningDelta`] opens its
541    /// part leniently — but a wire that announces block starts should emit
542    /// it so arrival order is fixed at the wire's own boundary. A start for
543    /// an already-open key is a no-op; a start for a finished key opens a
544    /// new part (key reuse). Not yielded to public stream consumers.
545    ReasoningStart {
546        /// Accumulation key of the reasoning part being opened.
547        id: StreamPartId,
548        /// The provider-issued reasoning item id, when one exists.
549        provider_id: Option<WireId>,
550    },
551
552    /// Close the reasoning part identified by `id` — the lifecycle
553    /// primitive every wire has (or has synthesized by its adapter at the
554    /// boundaries it already detects), so "is this part still open?" is
555    /// never re-derived per wire.
556    ///
557    /// `reasoning` is the wire's authoritative whole-block restatement; it
558    /// supersedes the delta accumulation. `signature` is a provider
559    /// signature closing the block; it attaches to the part's text — and
560    /// because an end for an already-finished key with only a signature
561    /// attaches to THAT part, a trailing signature signs the block that
562    /// holds the chain-of-thought instead of fabricating an empty sibling.
563    /// A repeated end with no payload is a no-op: idempotence belongs to
564    /// the entity, not to a guard each route must remember.
565    ///
566    /// The completed part is yielded to consumers as
567    /// [`StreamedAssistantContent::Reasoning`] — the uniform
568    /// block-completed signal across every wire — when the wire itself
569    /// said something at the boundary: an end carrying a restatement or
570    /// signature, or a bare end frame the wire actually sent
571    /// (`wire_sent`). A bare end an adapter *synthesized* at an
572    /// interleaving boundary stays silent: the consumer already received
573    /// every delta, and fabricating a completion event the wire never
574    /// sent would change what downstream history builders observe.
575    ReasoningEnd {
576        /// Accumulation key of the reasoning part being closed.
577        id: StreamPartId,
578        /// The wire's authoritative completed block, when it restates one.
579        reasoning: Option<Reasoning>,
580        /// A provider signature closing the block.
581        signature: Option<String>,
582        /// Whether the wire itself sent this end frame (anthropic's
583        /// `content_block_stop`), as opposed to the adapter synthesizing
584        /// it at a boundary the wire never announces. Wire-sent ends
585        /// yield the completed block even when bare.
586        wire_sent: bool,
587    },
588
589    /// Close the text block identified by `id`: later bare text deltas open
590    /// a fresh block instead of extending it. (A later
591    /// [`RawStreamingChoice::TextStart`] with the same key still
592    /// reactivates the block — the keyed collapse is explicit.) Not yielded
593    /// to public stream consumers.
594    TextEnd {
595        /// Accumulation key of the text block being closed.
596        id: StreamPartId,
597    },
598
599    /// A reasoning partial/delta
600    ReasoningDelta {
601        /// Accumulation key of the reasoning item this delta extends. Same
602        /// contract as [`RawStreamingChoice::Reasoning::id`]; all deltas of
603        /// one block share one key.
604        id: StreamPartId,
605        /// The provider-issued reasoning item id, when one exists (see
606        /// [`RawStreamingChoice::Reasoning::provider_id`]) — what a
607        /// delta-built part records as its durable id.
608        provider_id: Option<WireId>,
609        /// Partial reasoning text.
610        reasoning: String,
611    },
612
613    /// The final response object, must be yielded if you want the
614    /// `response` field to be populated on the `StreamingCompletionResponse`
615    FinalResponse(R),
616
617    /// Provider-assigned message ID (e.g. OpenAI Responses API `msg_` ID).
618    /// Captured silently into `StreamingCompletionResponse::message_id`.
619    MessageId(String),
620
621    /// A provider-native output item this version does not model — e.g. an
622    /// OpenAI Responses hosted-tool result (`web_search_call`, `file_search_call`,
623    /// `computer_call`, `code_interpreter_call`). Carries the raw item object
624    /// verbatim. Forwarded to the stream consumer as
625    /// [`StreamedAssistantContent::Unknown`] but not folded into the accumulated
626    /// assistant message (there is no `AssistantContent::Unknown` history slot).
627    Unknown(UnknownPayload),
628}
629
630impl<R> RawStreamingChoice<R> {
631    /// Convert only the terminal record, preserving every incremental content
632    /// event unchanged.
633    pub fn try_map_final<S>(
634        self,
635        map: impl FnOnce(R) -> Result<S, CompletionError>,
636    ) -> Result<RawStreamingChoice<S>, CompletionError> {
637        Ok(match self {
638            Self::Message(text) => RawStreamingChoice::Message(text),
639            Self::TextStart {
640                id,
641                additional_params,
642            } => RawStreamingChoice::TextStart {
643                id,
644                additional_params,
645            },
646            Self::TextAdditionalParams(params) => RawStreamingChoice::TextAdditionalParams(params),
647            Self::ToolCall(call) => RawStreamingChoice::ToolCall(call),
648            Self::ToolCallDelta { id, content } => {
649                RawStreamingChoice::ToolCallDelta { id, content }
650            }
651            Self::ToolInputEnd(end) => RawStreamingChoice::ToolInputEnd(end),
652            Self::Reasoning {
653                id,
654                provider_id,
655                content,
656            } => RawStreamingChoice::Reasoning {
657                id,
658                provider_id,
659                content,
660            },
661            Self::ReasoningDelta {
662                id,
663                provider_id,
664                reasoning,
665            } => RawStreamingChoice::ReasoningDelta {
666                id,
667                provider_id,
668                reasoning,
669            },
670            Self::ReasoningStart { id, provider_id } => {
671                RawStreamingChoice::ReasoningStart { id, provider_id }
672            }
673            Self::ReasoningEnd {
674                id,
675                reasoning,
676                signature,
677                wire_sent,
678            } => RawStreamingChoice::ReasoningEnd {
679                id,
680                reasoning,
681                signature,
682                wire_sent,
683            },
684            Self::TextEnd { id } => RawStreamingChoice::TextEnd { id },
685            Self::FinalResponse(response) => RawStreamingChoice::FinalResponse(map(response)?),
686            Self::MessageId(id) => RawStreamingChoice::MessageId(id),
687            Self::Unknown(value) => RawStreamingChoice::Unknown(value),
688        })
689    }
690}
691
692/// Describes a streaming tool call response (in its entirety)
693#[derive(Debug, Clone)]
694pub struct RawStreamingToolCall {
695    /// Accumulation/reconciliation key of the tool call —
696    /// `StreamPartId::Wire`-derived when the provider supplied an id,
697    /// minted when the wire omitted one. A key only; the durable id is
698    /// [`RawStreamingToolCall::tool_id`].
699    pub id: StreamPartId,
700    /// The provider-issued tool id, when one exists — the durable handle
701    /// that becomes [`ToolCall::id`](crate::message::ToolCall::id). Absent
702    /// means absent: serializers omit the field, and nothing fabricated can
703    /// take its place.
704    pub tool_id: Option<WireId>,
705    /// Rig-generated unique identifier for this tool call.
706    pub internal_call_id: String,
707    /// Provider-specific call ID used by some APIs for tool result correlation.
708    pub call_id: Option<String>,
709    /// Tool/function name.
710    pub name: String,
711    /// Parsed tool arguments.
712    pub arguments: serde_json::Value,
713    /// Optional provider signature associated with the tool call.
714    pub signature: Option<String>,
715    /// Additional provider-specific tool call metadata.
716    pub additional_params: Option<serde_json::Value>,
717}
718
719impl RawStreamingToolCall {
720    /// Create an empty tool call accumulator for provider streaming parsers.
721    pub fn empty() -> Self {
722        Self {
723            // A parser-accumulator placeholder key; providers overwrite it
724            // with the wire's key before emitting. Deliberately minted: an
725            // unset key must never read as wire-derived.
726            id: StreamPartId::minted(MintKind::Tool, u64::MAX),
727            tool_id: None,
728            internal_call_id: crate::id::generate(),
729            call_id: None,
730            name: String::new(),
731            arguments: serde_json::Value::Null,
732            signature: None,
733            additional_params: None,
734        }
735    }
736
737    /// Create a complete tool call with a generated internal call ID.
738    pub fn new(id: impl Into<StreamPartId>, name: String, arguments: serde_json::Value) -> Self {
739        let id = id.into();
740        // A wire-derived key doubles as the durable id (the common case:
741        // providers key by the id the wire issued); minted keys carry none.
742        let tool_id = id.wire_str().and_then(WireId::new);
743        Self {
744            id,
745            tool_id,
746            internal_call_id: crate::id::generate(),
747            call_id: None,
748            name,
749            arguments,
750            signature: None,
751            additional_params: None,
752        }
753    }
754
755    /// Attach a provider-specific call ID.
756    pub fn with_call_id(mut self, call_id: String) -> Self {
757        self.call_id = Some(call_id);
758        self
759    }
760
761    /// Attach or clear a provider signature.
762    pub fn with_signature(mut self, signature: Option<String>) -> Self {
763        self.signature = signature;
764        self
765    }
766
767    /// Attach provider-specific metadata.
768    pub fn with_additional_params(mut self, additional_params: Option<serde_json::Value>) -> Self {
769        self.additional_params = additional_params;
770        self
771    }
772}
773
774impl From<RawStreamingToolCall> for ToolCall {
775    fn from(tool_call: RawStreamingToolCall) -> Self {
776        // Only provider-issued handles populate `provider`: a dual wire
777        // carries (call_id, item id), a single wire carries its id in
778        // `call_id`. With none, the correlation handle is minted and
779        // `provider` records the absence — never an empty sentinel.
780        let provider = crate::message::ProviderCallId::from_optional_wire(
781            tool_call.call_id,
782            tool_call.tool_id.map(WireId::into_string),
783        );
784        let id = crate::message::ToolCallId::for_provider(provider.as_ref());
785        ToolCall {
786            id,
787            provider,
788            function: ToolFunction {
789                name: tool_call.name,
790                arguments: tool_call.arguments,
791            },
792            signature: tool_call.signature,
793            additional_params: tool_call.additional_params,
794        }
795    }
796}
797
798#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
799/// Provider stream whose terminal record is the provider-native `R`, on native
800/// targets.
801///
802/// This is the raw channel of rig's two-channel contract: every provider's
803/// inherent `raw_stream`/`raw_completion` returns provider-native types
804/// directly from the wire decode, never routed through the normalized
805/// accumulation ([`normalize_stream`] / the parts accumulator) — the
806/// semantic channel maps this stream's terminal record exactly once. There
807/// is deliberately no provider-*typed* payload on the normalized types; the
808/// typed channels are the contract. What the normalized types also carry is
809/// that same terminal record *serialized* ([`StreamFinal::raw`]) — for
810/// callers who no longer hold the concrete model, an agent having erased it,
811/// and so cannot reach the typed channel at all. The frames of the stream are
812/// a different axis: they were never exposed on any rig surface, and exposing
813/// them is a per-frame mechanism (a raw stream part), not a field on the
814/// terminal record — so [`StreamFinal::raw`] captures the terminal only.
815///
816/// Precedent, read carefully: openai-agents also splits raw from semantic,
817/// but the load-bearing part of its design is elsewhere — its semantic
818/// layer never reads a delta at all (it acts only on whole done items and
819/// the completed response), and delta aggregation is confined to the
820/// per-provider adapter, which *synthesizes* a canonical terminal event so
821/// the shared layer sees one grammar. rig cannot fully adopt that shape
822/// (openai-agents' canonical grammar is one vendor's schema; rig normalizes
823/// 14 wire families through one accumulator), and centralizing the
824/// accumulator is what forces cross-provider identity — hence the
825/// provenance-typed [`StreamPartId`]. What rig does copy from that precedent is
826/// the raw channel itself and provenance-as-data rather than naming
827/// convention.
828pub type RawStreamingResult<R> =
829    Pin<Box<dyn Stream<Item = Result<RawStreamingChoice<R>, CompletionError>> + Send>>;
830
831#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
832/// Provider stream whose terminal record is the provider-native `R`, on wasm
833/// targets.
834pub type RawStreamingResult<R> =
835    Pin<Box<dyn Stream<Item = Result<RawStreamingChoice<R>, CompletionError>>>>;
836
837/// Normalized provider stream, as consumed by [`StreamingCompletionResponse`].
838pub type StreamingResult = RawStreamingResult<StreamFinal>;
839
840/// Normalize the terminal record of a provider-native stream.
841///
842/// Every incremental event passes through untouched; only
843/// [`RawStreamingChoice::FinalResponse`] is converted, by `map`. On the way
844/// through, the stream remembers whether it emitted any tool call and applies
845/// [`FinishReason::reconcile_with_output`](crate::completion::FinishReason::reconcile_with_output)
846/// to the mapped record — the streaming counterpart of what
847/// [`CompletionResponse::with_finish_reason`] does on the unary path, so both
848/// paths agree about a `stop` that was really a tool call.
849///
850/// The provider-native terminal `R` is also serialized onto
851/// [`StreamFinal::raw`] *before* `map` consumes it — this is the one
852/// streaming seam every provider routes through, so it is the streaming
853/// counterpart of the capture each provider's unary `completion` performs
854/// before `normalize`. That is why `R` is bounded `Serialize`: every in-tree
855/// terminal type already is, and a terminal that could not be serialized
856/// could not be surfaced to callers who no longer hold the typed model.
857pub fn normalize_stream<R, F>(stream: RawStreamingResult<R>, mut map: F) -> StreamingResult
858where
859    R: Serialize + 'static,
860    F: FnMut(R) -> Result<StreamFinal, CompletionError> + WasmCompatSend + 'static,
861{
862    let mut emitted_tool_call = false;
863    Box::pin(stream.map(move |item| {
864        item.and_then(|choice| {
865            // Only a completed `ToolCall` counts, because only that becomes an
866            // `AssistantContent::ToolCall` in the aggregated choice — which is
867            // exactly what the unary path reconciles against. Counting deltas
868            // here would make a stream whose tool call never assembled report
869            // `ToolCalls` while the same data converted to a unary response
870            // reported `Stop`.
871            if matches!(&choice, RawStreamingChoice::ToolCall(_)) {
872                emitted_tool_call = true;
873            }
874            choice.try_map_final(|response| {
875                // Capture before `map` consumes the terminal. A serialization
876                // failure propagates: a silent `None` would contradict the
877                // field's meaning (a provider record stands behind every
878                // normalized terminal). In practice `to_value` on a value
879                // that just deserialized cannot fail.
880                let raw = serde_json::to_value(&response)?;
881                let mut response = map(response)?.with_raw(raw);
882                response.finish_reason = response
883                    .finish_reason
884                    .map(|reason| reason.reconcile_with_output(emitted_tool_call));
885                Ok(response)
886            })
887        })
888    }))
889}
890
891#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
892/// Future a paused [`StreamingCompletionResponse`] parks on until resumed, on
893/// native targets.
894type ResumeWait = Pin<Box<dyn Future<Output = ()> + Send>>;
895
896#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
897/// Future a paused [`StreamingCompletionResponse`] parks on until resumed, on
898/// wasm targets.
899type ResumeWait = Pin<Box<dyn Future<Output = ()>>>;
900
901/// The response from a streaming completion request;
902/// message and response are populated at the end of the
903/// `inner` stream.
904pub struct StreamingCompletionResponse {
905    pub(crate) inner: Abortable<StreamingResult>,
906    pub(crate) abort_handle: AbortHandle,
907    pub(crate) pause_control: PauseControl,
908    /// Accumulates the streamed parts of the final aggregated choice.
909    parts: PartsAccumulator,
910    /// Stable descriptor name of the provider producing this stream.
911    ///
912    /// Known when the stream is opened rather than when it terminates, so a
913    /// stream that errors or is cancelled before its terminal record still
914    /// names its provider.
915    provider: String,
916    /// The final aggregated message from the stream
917    /// contains all text and tool calls generated
918    pub choice: Vec<AssistantContent>,
919    /// Whether the stream already reached its end and aggregated `choice`.
920    ///
921    /// [`PartsAccumulator::finish`] is destructive (it takes the accumulated
922    /// parts and falls back to one empty text part), so re-polling a drained
923    /// stream — which `Stream` permits and combinators do — would otherwise
924    /// replace a fully aggregated `choice` with empty text (#2258 H6).
925    finished: bool,
926    /// Parked wait on the pause channel while [`PauseControl`] holds the
927    /// stream paused; `None` whenever the stream is running (#2258 H7).
928    resume_wait: Option<ResumeWait>,
929    /// Rig-generated public correlators for reasoning parts, one per
930    /// accumulation key: stable across a part's deltas, unique per run, and
931    /// carrying nothing an accumulation key could leak.
932    reasoning_correlators: std::collections::HashMap<StreamPartId, String>,
933    /// Correlators of finished reasoning parts, kept for the stream's
934    /// lifetime (mirroring the accumulator's `finished_reasoning`): a
935    /// trailing signature-only end — Gemini's `thoughtSignature` after a
936    /// synthesized silent boundary — must restate the identity its part's
937    /// deltas carried, not mint a fresh one the assembler cannot match.
938    finished_reasoning_correlators: std::collections::HashMap<StreamPartId, String>,
939    /// The provider's normalized terminal record, may be `None`
940    /// if the provider didn't yield it during the stream
941    pub response: Option<StreamFinal>,
942    pub final_response_yielded: AtomicBool,
943    /// Provider-assigned message ID (e.g. OpenAI Responses API `msg_` ID).
944    pub message_id: Option<String>,
945}
946
947impl StreamingCompletionResponse {
948    /// Wrap a provider stream and initialize aggregation state.
949    ///
950    /// `provider` is the stable descriptor name of the provider producing the
951    /// stream; it is recorded up front so it is available even when the stream
952    /// never reaches its terminal record.
953    pub fn stream(provider: impl Into<String>, inner: StreamingResult) -> Self {
954        let (abort_handle, abort_registration) = AbortHandle::new_pair();
955        let abortable_stream = Abortable::new(inner, abort_registration);
956        let pause_control = PauseControl::new();
957        Self {
958            inner: abortable_stream,
959            abort_handle,
960            pause_control,
961            parts: PartsAccumulator::new(),
962            provider: provider.into(),
963            // A stream that has not produced anything yet has produced nothing.
964            // This used to hold a fabricated empty-text part because the field
965            // could not be empty; that part was indistinguishable from a real
966            // empty text block the model had emitted.
967            choice: Vec::new(),
968            finished: false,
969            resume_wait: None,
970            reasoning_correlators: std::collections::HashMap::new(),
971            finished_reasoning_correlators: std::collections::HashMap::new(),
972            response: None,
973            final_response_yielded: AtomicBool::new(false),
974            message_id: None,
975        }
976    }
977
978    /// Stable descriptor name of the provider producing this stream.
979    pub fn provider(&self) -> &str {
980        &self.provider
981    }
982
983    /// Resolve the public correlator for a reasoning part that just ended,
984    /// keeping the identity available for the part's afterlife.
985    ///
986    /// An end always clears the live delta map — a reused accumulation key
987    /// opens a NEW part whose deltas must mint fresh — but the taken
988    /// correlator moves to the finished map rather than dying, so trailing
989    /// metadata (a late signature after a synthesized silent end) restates
990    /// the identity the part's deltas carried. A restatement under a spent
991    /// key is a new sibling part: it mints fresh and overwrites the entry,
992    /// exactly as the accumulator overwrites its finished index. Entries
993    /// live until the stream is dropped, matching `finished_reasoning`.
994    fn reasoning_end_correlator(&mut self, id: StreamPartId, restated: bool) -> String {
995        match self.reasoning_correlators.remove(&id) {
996            Some(taken) => {
997                self.finished_reasoning_correlators
998                    .insert(id, taken.clone());
999                taken
1000            }
1001            None if restated => {
1002                let minted = crate::id::generate();
1003                self.finished_reasoning_correlators
1004                    .insert(id, minted.clone());
1005                minted
1006            }
1007            None => self
1008                .finished_reasoning_correlators
1009                .entry(id)
1010                .or_insert_with(crate::id::generate)
1011                .clone(),
1012        }
1013    }
1014
1015    /// Cancel the stream and immediately drop the provider's inner stream.
1016    /// Cancellation is surfaced as normal stream termination.
1017    ///
1018    /// Cancelling also resumes a paused stream: a consumer parked on the
1019    /// pause channel must observe the termination instead of waiting forever
1020    /// for a resume that will never affect a stream that no longer exists.
1021    pub fn cancel(&mut self) {
1022        self.abort_handle.abort();
1023        let (abort_handle, abort_registration) = AbortHandle::new_pair();
1024        let empty: StreamingResult = Box::pin(futures::stream::poll_fn(|_| Poll::Ready(None)));
1025        self.inner = Abortable::new(empty, abort_registration);
1026        self.abort_handle = abort_handle;
1027        self.pause_control.resume();
1028    }
1029
1030    /// Pause stream polling.
1031    pub fn pause(&self) {
1032        self.pause_control.pause();
1033    }
1034
1035    /// Resume stream polling after a pause.
1036    pub fn resume(&self) {
1037        self.pause_control.resume();
1038    }
1039
1040    /// Returns whether the stream is currently paused.
1041    pub fn is_paused(&self) -> bool {
1042        self.pause_control.is_paused()
1043    }
1044
1045    /// Token usage reported by the provider for this response.
1046    ///
1047    /// Returns the usage carried by the final response once the stream has
1048    /// produced it. Until then — or when the provider does not report streamed
1049    /// usage — this returns [`Usage::new`], the zero-valued sentinel for missing
1050    /// usage metrics.
1051    pub fn usage(&self) -> Usage {
1052        self.response
1053            .as_ref()
1054            .map(|response| response.usage)
1055            .unwrap_or_default()
1056    }
1057
1058    /// This stream's identity metadata as one
1059    /// [`crate::completion::ResponseIdentity`] carrier.
1060    ///
1061    /// The message id is read from the stream rather than the terminal record:
1062    /// an explicit `MessageId` event outranks the terminal's id, and the
1063    /// terminal record backfills the field when the stream never saw one. The
1064    /// response-scoped and transport ids exist only on the terminal record, so
1065    /// they stay `None` for a stream that ended without one.
1066    pub fn identity(&self) -> crate::completion::ResponseIdentity {
1067        crate::completion::ResponseIdentity {
1068            message_id: self.message_id.clone(),
1069            ..self
1070                .response
1071                .as_ref()
1072                .map(StreamFinal::identity)
1073                .unwrap_or_default()
1074        }
1075    }
1076}
1077
1078impl From<StreamingCompletionResponse> for CompletionResponse {
1079    fn from(value: StreamingCompletionResponse) -> CompletionResponse {
1080        // Usage is the zero sentinel (`Usage::new`) when the stream produced no
1081        // terminal record. `provider` comes from the stream itself rather than
1082        // the terminal record, so it is populated even then.
1083        let terminal = value.response.as_ref();
1084        CompletionResponse::new(
1085            value.choice,
1086            terminal.map(|response| response.usage).unwrap_or_default(),
1087            value.provider,
1088        )
1089        // An explicit `MessageId` event outranks the terminal record's ID.
1090        .with_optional_message_id(
1091            value
1092                .message_id
1093                .or_else(|| terminal.and_then(|response| response.message_id.clone())),
1094        )
1095        .with_optional_response_id(terminal.and_then(|response| response.response_id.clone()))
1096        .with_optional_provider_request_id(
1097            terminal.and_then(|response| response.provider_request_id.clone()),
1098        )
1099        .with_optional_finish_reason(terminal.and_then(|response| response.finish_reason.clone()))
1100        .with_optional_model(terminal.and_then(|response| response.model.clone()))
1101    }
1102}
1103
1104impl Stream for StreamingCompletionResponse {
1105    type Item = Result<StreamedAssistantContent, CompletionError>;
1106
1107    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1108        let stream = self.get_mut();
1109
1110        // A drained stream stays drained: `finish()` consumes the accumulated
1111        // parts, so re-polling must not run it again and clobber `choice`
1112        // with the empty-text fallback (#2258 H6).
1113        if stream.finished {
1114            return Poll::Ready(None);
1115        }
1116
1117        if stream.is_paused() {
1118            // Park on the pause channel rather than re-waking immediately: a
1119            // self-wake turns a pause into a busy poll loop that burns the
1120            // executor for as long as the consumer stays paused (#2258 H7).
1121            // `wait_for` evaluates the *current* value when it is first
1122            // polled, so a resume racing this branch resolves it at once
1123            // instead of parking forever on a notification already sent.
1124            let wait = match stream.resume_wait.as_mut() {
1125                Some(wait) => wait,
1126                None => {
1127                    let mut paused_rx = stream.pause_control.paused_rx.clone();
1128                    stream.resume_wait.insert(Box::pin(async move {
1129                        let _ = paused_rx.wait_for(|paused| !*paused).await;
1130                    }))
1131                }
1132            };
1133            if wait.as_mut().poll(cx).is_pending() {
1134                return Poll::Pending;
1135            }
1136            stream.resume_wait = None;
1137        }
1138
1139        // Non-yielding events (`continue` arms: block bookkeeping, dropped
1140        // ends, duplicate terminals) loop rather than recurse — a long run of
1141        // them must not grow the stack (#2258 review P3).
1142        loop {
1143            return match Pin::new(&mut stream.inner).poll_next(cx) {
1144                Poll::Pending => Poll::Pending,
1145                Poll::Ready(None) => {
1146                    // Run at the end of the inner stream to collect all tokens
1147                    // into a single unified `Message`. `finish` can now be
1148                    // empty — a turn that streamed nothing is no longer padded
1149                    // with a fabricated empty-text part — and an empty result
1150                    // leaves the already-empty `choice` alone.
1151                    let finished = stream.parts.finish();
1152                    if !finished.is_empty() {
1153                        stream.choice = finished;
1154                    }
1155                    stream.finished = true;
1156
1157                    Poll::Ready(None)
1158                }
1159                // Every error reaches the consumer. Cancellation is *not* an
1160                // error here: `cancel()` aborts through `Abortable`, which
1161                // terminates the inner stream with `Ready(None)` above, so
1162                // the aggregated choice is finished normally. (Until #2258 H8
1163                // this arm swallowed any `ProviderError` whose text merely
1164                // contained "aborted", reporting clean EOF while silently
1165                // discarding both the error and the streamed content.)
1166                Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))),
1167                Poll::Ready(Some(Ok(choice))) => match choice {
1168                    RawStreamingChoice::Message(text) => {
1169                        stream.parts.text_delta(&text);
1170                        Poll::Ready(Some(Ok(StreamedAssistantContent::text(&text))))
1171                    }
1172                    RawStreamingChoice::TextStart {
1173                        id,
1174                        additional_params,
1175                    } => {
1176                        stream.parts.text_start(&id, additional_params);
1177                        continue;
1178                    }
1179                    RawStreamingChoice::TextAdditionalParams(additional_params) => {
1180                        stream.parts.text_additional_params(additional_params);
1181                        continue;
1182                    }
1183                    RawStreamingChoice::ToolCallDelta { id, content } => {
1184                        // The accumulator owns assembly; it mints the internal
1185                        // correlation id when the call opens and returns it for
1186                        // every fragment, so the public delta stays correlated
1187                        // with the eventual completed call.
1188                        let internal_call_id = match &content {
1189                            ToolCallDeltaContent::Name(name) => {
1190                                stream.parts.tool_name_delta(&id, name)
1191                            }
1192                            ToolCallDeltaContent::Delta(fragment) => {
1193                                stream.parts.tool_args_delta(&id, fragment)
1194                            }
1195                        };
1196                        Poll::Ready(Some(Ok(StreamedAssistantContent::ToolCallDelta {
1197                            internal_call_id,
1198                            content,
1199                        })))
1200                    }
1201                    RawStreamingChoice::ToolInputEnd(end) => match stream.parts.tool_input_end(end)
1202                    {
1203                        Ok(Some((tool_call, internal_call_id))) => {
1204                            Poll::Ready(Some(Ok(StreamedAssistantContent::ToolCall {
1205                                tool_call,
1206                                internal_call_id,
1207                            })))
1208                        }
1209                        // Dropped (nameless or partial input): not content.
1210                        Ok(None) => continue,
1211                        // Malformed complete input surfaces in-band; the stream
1212                        // keeps consuming, matching the malformed-frame contract.
1213                        Err(err) => Poll::Ready(Some(Err(err))),
1214                    },
1215                    RawStreamingChoice::Reasoning {
1216                        id,
1217                        provider_id,
1218                        content,
1219                    } => {
1220                        // A whole block is open + authoritative restatement
1221                        // + close in one event. The durable `Reasoning::id`
1222                        // comes only from the provider-issued handle; the
1223                        // accumulation key is opaque and cannot reach the
1224                        // replayable message.
1225                        let restatement = Reasoning {
1226                            id: provider_id.map(WireId::into_string),
1227                            content: vec![content],
1228                        };
1229                        let completed = stream.parts.reasoning_end(&id, Some(restatement), None);
1230                        // The part is finished: its delta correlator (fresh-
1231                        // minted for a block with no prior deltas) is restated
1232                        // on the completed event and retained for trailing
1233                        // metadata under the same key.
1234                        let correlator = stream.reasoning_end_correlator(id, true);
1235                        match completed {
1236                            Some(completed) => {
1237                                Poll::Ready(Some(Ok(StreamedAssistantContent::Reasoning {
1238                                    reasoning: completed,
1239                                    id: correlator,
1240                                })))
1241                            }
1242                            None => continue,
1243                        }
1244                    }
1245                    RawStreamingChoice::ReasoningStart { id, provider_id } => {
1246                        // A start that genuinely opened a part installs a
1247                        // fresh live correlator: without it, a part that
1248                        // closes with no deltas (a signature-only end under
1249                        // a reused key) would fall back to the finished map
1250                        // and inherit the PREVIOUS part's public identity.
1251                        if stream.parts.reasoning_start(&id, provider_id.as_ref()) {
1252                            stream
1253                                .reasoning_correlators
1254                                .insert(id, crate::id::generate());
1255                        }
1256                        continue;
1257                    }
1258                    RawStreamingChoice::ReasoningEnd {
1259                        id,
1260                        reasoning,
1261                        signature,
1262                        wire_sent,
1263                    } => {
1264                        // The completed block is yielded when the wire said
1265                        // something at the boundary: an end payload (a
1266                        // restatement or a signature) or a bare end frame
1267                        // the wire actually sent (anthropic's
1268                        // `content_block_stop` on an unsigned block). Only a
1269                        // bare end an adapter *synthesized* stays silent —
1270                        // the consumer already received every delta, and
1271                        // fabricating a "completed block" event the wire
1272                        // never sent would change what downstream history
1273                        // builders observe.
1274                        let authoritative = reasoning.is_some() || signature.is_some() || wire_sent;
1275                        let restated = reasoning.is_some();
1276                        let completed = stream.parts.reasoning_end(&id, reasoning, signature);
1277                        // The part is finished: the live delta map is cleared
1278                        // unconditionally — a suppressed synthesized end must
1279                        // still make a reused key mint fresh — but the
1280                        // correlator survives in the finished map, so a
1281                        // trailing signature-bearing end for this key restates
1282                        // the identity its deltas carried instead of minting
1283                        // one the assembler cannot match.
1284                        let correlator = stream.reasoning_end_correlator(id, restated);
1285                        match completed {
1286                            Some(completed) if authoritative => {
1287                                Poll::Ready(Some(Ok(StreamedAssistantContent::Reasoning {
1288                                    reasoning: completed,
1289                                    id: correlator,
1290                                })))
1291                            }
1292                            _ => continue,
1293                        }
1294                    }
1295                    RawStreamingChoice::TextEnd { id } => {
1296                        stream.parts.text_end(&id);
1297                        continue;
1298                    }
1299                    RawStreamingChoice::ReasoningDelta {
1300                        id,
1301                        provider_id,
1302                        reasoning,
1303                    } => {
1304                        stream
1305                            .parts
1306                            .reasoning_delta(&id, provider_id.as_ref(), &reasoning);
1307                        // The public delta carries a rig-generated correlator
1308                        // (stable per part, unique per run) plus the durable
1309                        // provider id when one exists. The opaque
1310                        // accumulation key is never observable.
1311                        let correlator = stream
1312                            .reasoning_correlators
1313                            .entry(id)
1314                            .or_insert_with(crate::id::generate)
1315                            .clone();
1316                        Poll::Ready(Some(Ok(StreamedAssistantContent::ReasoningDelta {
1317                            id: correlator,
1318                            provider_id: provider_id.map(WireId::into_string),
1319                            reasoning,
1320                        })))
1321                    }
1322                    RawStreamingChoice::ToolCall(raw_tool_call) => {
1323                        let minted_internal_call_id = raw_tool_call.internal_call_id.clone();
1324                        let part_id = raw_tool_call.id.clone();
1325                        let tool_call: ToolCall = raw_tool_call.into();
1326                        // A wire that fragmented this call's input already
1327                        // published an internal id on its deltas; the
1328                        // accumulator adopts it so the completed call stays
1329                        // correlated with them (the contract on
1330                        // `StreamedAssistantContent::ToolCall`). With no open
1331                        // assembly the emitter's minted id is kept.
1332                        let internal_call_id = stream.parts.tool_call(
1333                            &part_id,
1334                            tool_call.clone(),
1335                            minted_internal_call_id,
1336                        );
1337                        Poll::Ready(Some(Ok(StreamedAssistantContent::ToolCall {
1338                            tool_call,
1339                            internal_call_id,
1340                        })))
1341                    }
1342                    RawStreamingChoice::FinalResponse(mut response) => {
1343                        // Assembled tool calls never pass `normalize_stream` as
1344                        // `RawStreamingChoice::ToolCall`, so the finish-reason
1345                        // reconciliation runs here too, against the accumulator's
1346                        // authoritative view of completed calls. Idempotent over
1347                        // the reconciliation `normalize_stream` already applied.
1348                        response.finish_reason = response.finish_reason.map(|reason| {
1349                            reason.reconcile_with_output(stream.parts.saw_tool_call())
1350                        });
1351                        if stream
1352                            .final_response_yielded
1353                            .load(std::sync::atomic::Ordering::SeqCst)
1354                        {
1355                            continue;
1356                        } else {
1357                            // Set the final response field and return the next item in the stream.
1358                            // An explicit `MessageId` event keeps precedence; the
1359                            // terminal record only fills a gap.
1360                            if stream.message_id.is_none() {
1361                                stream.message_id = response.message_id.clone();
1362                            }
1363                            stream.response = Some(response.clone());
1364                            stream
1365                                .final_response_yielded
1366                                .store(true, std::sync::atomic::Ordering::SeqCst);
1367                            let final_response = StreamedAssistantContent::final_response(response);
1368                            Poll::Ready(Some(Ok(final_response)))
1369                        }
1370                    }
1371                    RawStreamingChoice::MessageId(id) => {
1372                        stream.message_id = Some(id);
1373                        continue;
1374                    }
1375                    RawStreamingChoice::Unknown(value) => {
1376                        // Pass an unmodeled provider item straight through to the
1377                        // consumer; it is intentionally not pushed into
1378                        // `assistant_items` (no `AssistantContent::Unknown` exists).
1379                        // No exclusion warning here: everything reaching this arm
1380                        // is a live wire frame a provider adapter chose not to
1381                        // model (adapters warn on those themselves) — a persisted
1382                        // item that failed the strict `Text` decode is created by
1383                        // consumer-side serde and never re-enters this stream.
1384                        // The agent assembler, which does ingest such items,
1385                        // carries that warning.
1386                        Poll::Ready(Some(Ok(StreamedAssistantContent::Unknown(value))))
1387                    }
1388                },
1389            };
1390        }
1391    }
1392}
1393
1394// Test module
1395#[cfg(test)]
1396mod tests {
1397    use std::time::Duration;
1398
1399    use super::*;
1400    use crate::completion::FinishReason;
1401    use async_stream::stream;
1402    use tokio::time::sleep;
1403
1404    /// Provider descriptor used by the mock streams in this module.
1405    const TEST_PROVIDER: &str = "test-provider";
1406
1407    /// Fixture params: the JSON literal is always a non-empty object.
1408    fn fixture_params(value: serde_json::Value) -> crate::message::AdditionalParams {
1409        crate::message::AdditionalParams::try_from_value(value)
1410            .expect("fixture params must be a JSON object")
1411            .expect("fixture params must carry data")
1412    }
1413
1414    /// Terminal record with a known total-token count.
1415    fn mock_final_with_total_tokens(total_tokens: u64) -> StreamFinal {
1416        let mut usage = Usage::new();
1417        usage.total_tokens = total_tokens;
1418        StreamFinal::new(TEST_PROVIDER, usage)
1419    }
1420
1421    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1422    fn to_stream_result(
1423        stream: impl futures::Stream<Item = Result<RawStreamingChoice, CompletionError>>
1424        + Send
1425        + 'static,
1426    ) -> StreamingResult {
1427        Box::pin(stream)
1428    }
1429
1430    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1431    fn to_stream_result(
1432        stream: impl futures::Stream<Item = Result<RawStreamingChoice, CompletionError>> + 'static,
1433    ) -> StreamingResult {
1434        Box::pin(stream)
1435    }
1436
1437    fn create_mock_stream() -> StreamingCompletionResponse {
1438        let stream = stream! {
1439            yield Ok(RawStreamingChoice::Message("hello 1".to_string()));
1440            sleep(Duration::from_millis(100)).await;
1441            yield Ok(RawStreamingChoice::Message("hello 2".to_string()));
1442            sleep(Duration::from_millis(100)).await;
1443            yield Ok(RawStreamingChoice::Message("hello 3".to_string()));
1444            sleep(Duration::from_millis(100)).await;
1445            yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(15)));
1446        };
1447
1448        StreamingCompletionResponse::stream(TEST_PROVIDER, to_stream_result(stream))
1449    }
1450
1451    /// #2258 review P3: non-yielding events (`MessageId` here) drive the
1452    /// `poll_next` loop instead of synchronous self-recursion, so a long run
1453    /// of them cannot grow the stack. Pre-fix, each of these frames was one
1454    /// recursive `poll_next` stack frame and a run this long overflowed in
1455    /// debug builds.
1456    #[tokio::test]
1457    async fn a_long_run_of_non_yielding_events_does_not_grow_the_stack() {
1458        let raw = stream! {
1459            for n in 0..50_000u32 {
1460                yield Ok(RawStreamingChoice::MessageId(format!("msg_{n}")));
1461            }
1462            yield Ok(RawStreamingChoice::Message("done".to_string()));
1463            yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(1)));
1464        };
1465        let mut stream = StreamingCompletionResponse::stream(TEST_PROVIDER, to_stream_result(raw));
1466
1467        let mut texts = Vec::new();
1468        while let Some(item) = stream.next().await {
1469            if let Ok(StreamedAssistantContent::Text(text)) = item {
1470                texts.push(text.text);
1471            }
1472        }
1473        assert_eq!(texts, vec!["done".to_string()]);
1474        // The last id recorded wins.
1475        assert_eq!(stream.message_id.as_deref(), Some("msg_49999"));
1476    }
1477
1478    /// A stream that never saw a `MessageId` event takes all three identity
1479    /// axes from the terminal record.
1480    #[tokio::test]
1481    async fn stream_identity_falls_back_to_the_terminal_records_ids() {
1482        let raw = stream! {
1483            yield Ok(RawStreamingChoice::Message("done".to_string()));
1484            yield Ok(RawStreamingChoice::FinalResponse(
1485                mock_final_with_total_tokens(1)
1486                    .with_message_id("msg_terminal")
1487                    .with_response_id("resp_1")
1488                    .with_provider_request_id("req_1"),
1489            ));
1490        };
1491        let mut stream = StreamingCompletionResponse::stream(TEST_PROVIDER, to_stream_result(raw));
1492        while stream.next().await.is_some() {}
1493
1494        assert_eq!(
1495            stream.identity(),
1496            crate::completion::ResponseIdentity {
1497                message_id: Some("msg_terminal".to_string()),
1498                response_id: Some("resp_1".to_string()),
1499                provider_request_id: Some("req_1".to_string()),
1500            }
1501        );
1502    }
1503
1504    /// An explicit `MessageId` event outranks the terminal record's message id;
1505    /// the response-scoped and transport ids still come from the terminal.
1506    #[tokio::test]
1507    async fn stream_identity_prefers_an_explicit_message_id_event() {
1508        let raw = stream! {
1509            yield Ok(RawStreamingChoice::MessageId("msg_event".to_string()));
1510            yield Ok(RawStreamingChoice::Message("done".to_string()));
1511            yield Ok(RawStreamingChoice::FinalResponse(
1512                mock_final_with_total_tokens(1)
1513                    .with_message_id("msg_terminal")
1514                    .with_response_id("resp_1"),
1515            ));
1516        };
1517        let mut stream = StreamingCompletionResponse::stream(TEST_PROVIDER, to_stream_result(raw));
1518        while stream.next().await.is_some() {}
1519
1520        assert_eq!(
1521            stream.identity(),
1522            crate::completion::ResponseIdentity {
1523                message_id: Some("msg_event".to_string()),
1524                response_id: Some("resp_1".to_string()),
1525                provider_request_id: None,
1526            }
1527        );
1528    }
1529
1530    fn create_reasoning_stream() -> StreamingCompletionResponse {
1531        let stream = stream! {
1532            yield Ok(RawStreamingChoice::Reasoning {                id: StreamPartId::wire("rs_1"),
1533                provider_id: WireId::new("rs_1"),
1534                content: ReasoningContent::Text {
1535                    text: "step one".to_string(),
1536                    signature: Some("sig_1".to_string()),
1537                },
1538            });
1539            yield Ok(RawStreamingChoice::Message("final answer".to_string()));
1540            yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(5)));
1541        };
1542
1543        StreamingCompletionResponse::stream(TEST_PROVIDER, to_stream_result(stream))
1544    }
1545
1546    fn create_reasoning_only_stream() -> StreamingCompletionResponse {
1547        let stream = stream! {
1548            yield Ok(RawStreamingChoice::Reasoning {                id: StreamPartId::wire("rs_only"),
1549                provider_id: WireId::new("rs_only"),
1550                content: ReasoningContent::Summary("hidden summary".to_string()),
1551            });
1552            yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
1553        };
1554
1555        StreamingCompletionResponse::stream(TEST_PROVIDER, to_stream_result(stream))
1556    }
1557
1558    fn create_interleaved_stream() -> StreamingCompletionResponse {
1559        let stream = stream! {
1560            yield Ok(RawStreamingChoice::Reasoning {                id: StreamPartId::wire("rs_interleaved"),
1561                provider_id: WireId::new("rs_interleaved"),
1562                content: ReasoningContent::Text {
1563                    text: "chain-of-thought".to_string(),
1564                    signature: None,
1565                },
1566            });
1567            yield Ok(RawStreamingChoice::Message("final-text".to_string()));
1568            yield Ok(RawStreamingChoice::ToolCall(
1569                RawStreamingToolCall::new(
1570                    "tool_1".to_string(),
1571                    "mock_tool".to_string(),
1572                    serde_json::json!({"arg": 1}),
1573                ),
1574            ));
1575            yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(3)));
1576        };
1577
1578        StreamingCompletionResponse::stream(TEST_PROVIDER, to_stream_result(stream))
1579    }
1580
1581    fn create_text_tool_text_stream() -> StreamingCompletionResponse {
1582        let stream = stream! {
1583            yield Ok(RawStreamingChoice::Message("first".to_string()));
1584            yield Ok(RawStreamingChoice::ToolCall(
1585                RawStreamingToolCall::new(
1586                    "tool_split".to_string(),
1587                    "mock_tool".to_string(),
1588                    serde_json::json!({"arg": "x"}),
1589                ),
1590            ));
1591            yield Ok(RawStreamingChoice::Message("second".to_string()));
1592            yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(3)));
1593        };
1594
1595        StreamingCompletionResponse::stream(TEST_PROVIDER, to_stream_result(stream))
1596    }
1597
1598    fn create_text_metadata_stream() -> StreamingCompletionResponse {
1599        let stream = stream! {
1600            yield Ok(RawStreamingChoice::TextStart {
1601                id: StreamPartId::wire("block-0"),
1602                additional_params: None,
1603            });
1604            yield Ok(RawStreamingChoice::Message("first".to_string()));
1605            yield Ok(RawStreamingChoice::TextAdditionalParams(fixture_params(serde_json::json!({
1606                "citations": [{
1607                    "type": "char_location",
1608                    "cited_text": "First citation.",
1609                    "document_index": 0,
1610                    "start_char_index": 0,
1611                    "end_char_index": 15
1612                }]
1613            }))));
1614            yield Ok(RawStreamingChoice::TextAdditionalParams(fixture_params(serde_json::json!({
1615                "citations": [{
1616                    "type": "char_location",
1617                    "cited_text": "Second citation.",
1618                    "document_index": 0,
1619                    "start_char_index": 16,
1620                    "end_char_index": 32
1621                }]
1622            }))));
1623            yield Ok(RawStreamingChoice::TextStart {
1624                id: StreamPartId::wire("block-1"),
1625                additional_params: crate::message::AdditionalParams::try_from_value(serde_json::json!({
1626                    "block": 2
1627                })).expect("object params"),
1628            });
1629            yield Ok(RawStreamingChoice::Message("second".to_string()));
1630            yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(3)));
1631        };
1632
1633        StreamingCompletionResponse::stream(TEST_PROVIDER, to_stream_result(stream))
1634    }
1635
1636    #[tokio::test]
1637    async fn into_completion_response_derives_usage_from_final_response() {
1638        let mut stream = create_mock_stream();
1639
1640        // Drain the stream so the final response (and its usage) is captured.
1641        while stream.next().await.is_some() {}
1642
1643        // usage() surfaces the final response's token usage...
1644        assert_eq!(stream.usage().total_tokens, 15);
1645
1646        // ...and the From conversion carries it instead of a zero sentinel.
1647        let response: CompletionResponse = stream.into();
1648        assert_eq!(response.usage.total_tokens, 15);
1649        assert_eq!(response.provider, TEST_PROVIDER);
1650    }
1651
1652    /// Regression (rig#2265): the transport request id captured on the
1653    /// terminal record must survive stream→`CompletionResponse` conversion,
1654    /// exactly like the response id, usage, finish reason, and model do.
1655    #[tokio::test]
1656    async fn into_completion_response_carries_the_terminal_request_id() {
1657        let mut stream = StreamingCompletionResponse::stream(
1658            TEST_PROVIDER,
1659            to_stream_result(stream! {
1660                yield Ok(RawStreamingChoice::Message("hi".to_string()));
1661                yield Ok(RawStreamingChoice::FinalResponse(
1662                    StreamFinal::new(TEST_PROVIDER, Usage::new())
1663                        .with_response_id("resp_1")
1664                        .with_provider_request_id("req_transport_1"),
1665                ));
1666            }),
1667        );
1668        while stream.next().await.is_some() {}
1669
1670        let response: CompletionResponse = stream.into();
1671        assert_eq!(response.response_id.as_deref(), Some("resp_1"));
1672        assert_eq!(
1673            response.provider_request_id.as_deref(),
1674            Some("req_transport_1")
1675        );
1676    }
1677
1678    #[tokio::test]
1679    async fn a_stream_without_a_terminal_record_still_names_its_provider() {
1680        // The provider is known when the stream is opened, so a stream that
1681        // errors or is truncated before its terminal record must not degrade
1682        // `provider` to an empty string — every other missing value has a
1683        // documented sentinel (`Usage::new`, `None`) and this one should too.
1684        let mut stream = StreamingCompletionResponse::stream(
1685            TEST_PROVIDER,
1686            to_stream_result(stream! {
1687                yield Ok(RawStreamingChoice::Message("truncated".to_string()));
1688            }),
1689        );
1690        while stream.next().await.is_some() {}
1691
1692        // No terminal record was ever yielded, so none may be synthesized.
1693        assert!(stream.response.is_none());
1694
1695        let response: CompletionResponse = stream.into();
1696        assert_eq!(response.provider, TEST_PROVIDER);
1697        assert_eq!(response.usage, Usage::new());
1698        assert_eq!(response.finish_reason(), None);
1699        assert_eq!(response.model, None);
1700    }
1701
1702    #[tokio::test]
1703    async fn a_stream_that_errors_mid_stream_keeps_content_and_omits_the_terminal() {
1704        // A transport error after some content must forward the error, keep
1705        // the content already aggregated, and never fabricate a terminal
1706        // record the provider did not send.
1707        let mut stream = StreamingCompletionResponse::stream(
1708            TEST_PROVIDER,
1709            to_stream_result(stream! {
1710                yield Ok(RawStreamingChoice::Message("partial".to_string()));
1711                yield Err(CompletionError::ProviderError(
1712                    "connection reset".to_string(),
1713                ));
1714            }),
1715        );
1716
1717        let mut saw_error = false;
1718        while let Some(item) = stream.next().await {
1719            if item.is_err() {
1720                saw_error = true;
1721            }
1722        }
1723        assert!(saw_error, "the mid-stream error must be forwarded");
1724
1725        // No StreamFinal may be synthesized for the aborted stream...
1726        assert!(stream.response.is_none());
1727
1728        // ...but the content delivered before the error is preserved.
1729        assert_eq!(
1730            stream.choice.first(),
1731            Some(&AssistantContent::text("partial".to_string())),
1732        );
1733    }
1734
1735    #[tokio::test]
1736    async fn normalize_stream_upgrades_a_stop_that_carried_a_tool_call() {
1737        // Several gateways report a plain `stop` on a tool-calling turn. The
1738        // streaming path must reconcile it exactly as the unary path does.
1739        let raw: RawStreamingResult<Usage> = Box::pin(stream! {
1740            yield Ok(RawStreamingChoice::ToolCall(RawStreamingToolCall {
1741                tool_id: WireId::new("call_1"),
1742                id: StreamPartId::wire("call_1"),
1743                call_id: None,
1744                internal_call_id: "internal_1".to_string(),
1745                name: "lookup".to_string(),
1746                arguments: serde_json::json!({}),
1747                signature: None,
1748                additional_params: None,
1749            }));
1750            yield Ok(RawStreamingChoice::FinalResponse(Usage::new()));
1751        });
1752
1753        let normalized = normalize_stream(raw, |usage| {
1754            Ok(StreamFinal::new(TEST_PROVIDER, usage).with_finish_reason(FinishReason::Stop))
1755        });
1756
1757        let mut stream = StreamingCompletionResponse::stream(TEST_PROVIDER, normalized);
1758        while stream.next().await.is_some() {}
1759
1760        assert_eq!(
1761            stream
1762                .response
1763                .as_ref()
1764                .and_then(|final_record| final_record.finish_reason.clone()),
1765            Some(FinishReason::ToolCalls),
1766        );
1767    }
1768
1769    #[tokio::test]
1770    async fn normalize_stream_leaves_a_stop_without_tool_calls_alone() {
1771        let raw: RawStreamingResult<Usage> = Box::pin(stream! {
1772            yield Ok(RawStreamingChoice::Message("done".to_string()));
1773            yield Ok(RawStreamingChoice::FinalResponse(Usage::new()));
1774        });
1775
1776        let normalized = normalize_stream(raw, |usage| {
1777            Ok(StreamFinal::new(TEST_PROVIDER, usage).with_finish_reason(FinishReason::Stop))
1778        });
1779
1780        let mut stream = StreamingCompletionResponse::stream(TEST_PROVIDER, normalized);
1781        while stream.next().await.is_some() {}
1782
1783        assert_eq!(
1784            stream
1785                .response
1786                .as_ref()
1787                .and_then(|final_record| final_record.finish_reason.clone()),
1788            Some(FinishReason::Stop),
1789        );
1790    }
1791
1792    #[test]
1793    fn stream_final_round_trips_and_is_distinguishable_from_unknown_content() {
1794        let final_record = StreamFinal::new(
1795            "example",
1796            Usage {
1797                input_tokens: 4,
1798                output_tokens: 6,
1799                total_tokens: 10,
1800                cached_input_tokens: 1,
1801                cache_creation_input_tokens: 2,
1802                tool_use_prompt_tokens: 3,
1803                reasoning_tokens: 4,
1804            },
1805        )
1806        .with_finish_reason(FinishReason::Other("future_reason".to_owned()))
1807        .with_message_id("msg_123")
1808        .with_model("provider-model-v2");
1809
1810        let encoded = serde_json::to_value(StreamedAssistantContent::Final(final_record.clone()))
1811            .expect("serialize final item");
1812        assert_eq!(encoded["kind"], serde_json::json!("final"));
1813
1814        let decoded = serde_json::from_value::<StreamedAssistantContent>(encoded)
1815            .expect("deserialize final item");
1816        assert_eq!(decoded, StreamedAssistantContent::Final(final_record));
1817
1818        // An unmodeled provider item must still land in `Unknown` rather than
1819        // being mistaken for a terminal record.
1820        let provider_item = serde_json::json!({
1821            "provider_native_event": "future_terminal",
1822            "usage": {"total_tokens": 10}
1823        });
1824        let decoded = serde_json::from_value::<StreamedAssistantContent>(provider_item.clone())
1825            .expect("deserialize unknown item");
1826        assert_eq!(
1827            decoded,
1828            StreamedAssistantContent::Unknown(provider_item.into())
1829        );
1830    }
1831
1832    /// Deserialization funnels through `new` + the setters, so the invariants
1833    /// hold on persisted values too: a `""` identifier comes back as `None`.
1834    #[test]
1835    fn deserializing_stream_final_filters_empty_identifiers() {
1836        let decoded = serde_json::from_value::<StreamFinal>(serde_json::json!({
1837            "kind": "final",
1838            "usage": Usage::new(),
1839            "message_id": "",
1840            "response_id": "",
1841            "model": "",
1842            "provider": "example",
1843        }))
1844        .expect("deserialize terminal record");
1845
1846        assert_eq!(decoded.message_id, None);
1847        assert_eq!(decoded.response_id, None);
1848        assert_eq!(decoded.model, None);
1849    }
1850
1851    /// A provider-native terminal type standing in for the real ones: it
1852    /// carries a field the normalized record does not model, so the test can
1853    /// tell "the raw payload is the terminal record" from "some value was
1854    /// attached".
1855    #[derive(Debug, Serialize, Deserialize, PartialEq)]
1856    struct ProviderTerminal {
1857        usage: Usage,
1858        provider_only: String,
1859    }
1860
1861    fn provider_terminal_stream() -> RawStreamingResult<ProviderTerminal> {
1862        Box::pin(stream! {
1863            yield Ok(RawStreamingChoice::Message("done".to_string()));
1864            yield Ok(RawStreamingChoice::FinalResponse(ProviderTerminal {
1865                usage: Usage {
1866                    input_tokens: 3,
1867                    output_tokens: 5,
1868                    total_tokens: 8,
1869                    ..Usage::new()
1870                },
1871                provider_only: "kept".to_string(),
1872            }));
1873        })
1874    }
1875
1876    async fn drain(normalized: StreamingResult) -> StreamFinal {
1877        let mut stream = StreamingCompletionResponse::stream(TEST_PROVIDER, normalized);
1878        while stream.next().await.is_some() {}
1879        stream
1880            .response
1881            .expect("stream should end with a terminal record")
1882    }
1883
1884    /// The load-bearing streaming test: `raw` is the provider's terminal
1885    /// record serialized — it deserializes back into the provider's own type
1886    /// and re-serializes equal — and the normalized fields are what the
1887    /// mapper produced.
1888    #[tokio::test]
1889    async fn normalize_stream_captures_the_terminal_record() {
1890        let normalized = normalize_stream(provider_terminal_stream(), |terminal| {
1891            Ok(StreamFinal::new(TEST_PROVIDER, terminal.usage))
1892        });
1893        let final_record = drain(normalized).await;
1894        let raw = &final_record.raw;
1895
1896        let typed = ProviderTerminal::deserialize(raw).expect("raw is the provider's terminal");
1897        assert_eq!(typed.provider_only, "kept");
1898        assert_eq!(&serde_json::to_value(&typed).expect("re-serialize"), raw);
1899
1900        assert_eq!(final_record.usage.total_tokens, 8);
1901        assert_eq!(final_record.provider, TEST_PROVIDER);
1902        assert_eq!(final_record.finish_reason, None);
1903    }
1904
1905    /// Finish-reason reconciliation is unchanged by capture: a `stop` that
1906    /// carried a tool call is still upgraded, with `raw` attached.
1907    #[tokio::test]
1908    async fn normalize_stream_reconciles_finish_reason_with_raw_attached() {
1909        let raw: RawStreamingResult<Usage> = Box::pin(stream! {
1910            yield Ok(RawStreamingChoice::ToolCall(RawStreamingToolCall {
1911                tool_id: WireId::new("call_1"),
1912                id: StreamPartId::wire("call_1"),
1913                call_id: None,
1914                internal_call_id: "internal_1".to_string(),
1915                name: "lookup".to_string(),
1916                arguments: serde_json::json!({}),
1917                signature: None,
1918                additional_params: None,
1919            }));
1920            yield Ok(RawStreamingChoice::FinalResponse(Usage::new()));
1921        });
1922        let normalized = normalize_stream(raw, |usage| {
1923            Ok(StreamFinal::new(TEST_PROVIDER, usage).with_finish_reason(FinishReason::Stop))
1924        });
1925        let final_record = drain(normalized).await;
1926        assert_eq!(final_record.finish_reason, Some(FinishReason::ToolCalls));
1927        assert!(!final_record.raw.is_null());
1928    }
1929
1930    /// The deserialization mirror carries `raw`: a terminal record with a
1931    /// captured payload survives serialize → deserialize with the payload
1932    /// intact, both bare and wrapped in `StreamedAssistantContent::Final`
1933    /// (the shape the agent forwards). A record serialized before the field
1934    /// existed still loads, with `raw` unset.
1935    #[test]
1936    fn stream_final_raw_round_trips_through_serde_mirror() {
1937        let payload = serde_json::json!({
1938            "usage": {"total_tokens": 8},
1939            "provider_only": "kept"
1940        });
1941        let final_record = StreamFinal::new("example", Usage::new())
1942            .with_message_id("msg_123")
1943            .with_raw(payload.clone());
1944
1945        let encoded = serde_json::to_value(&final_record).expect("serialize");
1946        assert_eq!(encoded["raw"], payload);
1947        let decoded = serde_json::from_value::<StreamFinal>(encoded.clone()).expect("deserialize");
1948        assert_eq!(decoded.raw, payload);
1949        assert_eq!(decoded, final_record);
1950        assert_eq!(
1951            serde_json::to_value(&decoded).expect("re-serialize"),
1952            encoded
1953        );
1954
1955        let wrapped = StreamedAssistantContent::Final(final_record.clone());
1956        let encoded = serde_json::to_value(&wrapped).expect("serialize wrapped");
1957        let decoded = serde_json::from_value::<StreamedAssistantContent>(encoded)
1958            .expect("deserialize wrapped");
1959        assert_eq!(decoded, wrapped);
1960
1961        // Pre-field JSON: no `raw` key.
1962        let legacy = serde_json::json!({
1963            "kind": "final",
1964            "usage": serde_json::to_value(Usage::new()).unwrap(),
1965            "provider": "example"
1966        });
1967        let decoded = serde_json::from_value::<StreamFinal>(legacy).expect("legacy loads");
1968        assert!(decoded.raw.is_null());
1969
1970        // Unset `raw` is not written, so a record without capture serializes
1971        // exactly as it did before the field existed.
1972        let bare = serde_json::to_value(StreamFinal::new("example", Usage::new())).unwrap();
1973        assert!(bare.get("raw").is_none());
1974    }
1975
1976    /// The deserialization mirror must not change the wire format: a fully
1977    /// populated terminal record round-trips to byte-identical JSON.
1978    #[test]
1979    fn stream_final_serde_round_trip_is_identity() {
1980        let final_record = StreamFinal::new(
1981            "example",
1982            Usage {
1983                input_tokens: 4,
1984                output_tokens: 6,
1985                total_tokens: 10,
1986                cached_input_tokens: 1,
1987                cache_creation_input_tokens: 2,
1988                tool_use_prompt_tokens: 3,
1989                reasoning_tokens: 4,
1990            },
1991        )
1992        .with_finish_reason(FinishReason::Stop)
1993        .with_message_id("msg_123")
1994        .with_response_id("resp_456")
1995        .with_model("provider-model-v2");
1996
1997        let encoded = serde_json::to_value(&final_record).expect("serialize terminal record");
1998        assert_eq!(encoded["kind"], serde_json::json!("final"));
1999
2000        let decoded = serde_json::from_value::<StreamFinal>(encoded.clone()).expect("deserialize");
2001        assert_eq!(decoded, final_record);
2002        assert_eq!(
2003            serde_json::to_value(&decoded).expect("re-serialize"),
2004            encoded
2005        );
2006    }
2007
2008    #[tokio::test]
2009    async fn usage_is_zero_sentinel_before_final_response() {
2010        // A stream that never yields a FinalResponse reports the zero sentinel.
2011        let stream = StreamingCompletionResponse::stream(
2012            TEST_PROVIDER,
2013            to_stream_result(stream! {
2014                yield Ok(RawStreamingChoice::Message("no final response".to_string()));
2015            }),
2016        );
2017        assert_eq!(stream.usage().total_tokens, 0);
2018    }
2019
2020    #[tokio::test]
2021    async fn test_stream_cancellation() {
2022        let mut stream = create_mock_stream();
2023
2024        println!("Response: ");
2025        let mut chunk_count = 0;
2026        while let Some(chunk) = stream.next().await {
2027            match chunk {
2028                Ok(StreamedAssistantContent::Text(text)) => {
2029                    print!("{}", text.text);
2030                    std::io::Write::flush(&mut std::io::stdout()).unwrap();
2031                    chunk_count += 1;
2032                }
2033                Ok(StreamedAssistantContent::ToolCall {
2034                    tool_call,
2035                    internal_call_id,
2036                }) => {
2037                    println!("\nTool Call: {tool_call:?}, internal_call_id={internal_call_id:?}");
2038                    chunk_count += 1;
2039                }
2040                Ok(StreamedAssistantContent::ToolCallDelta {
2041                    internal_call_id,
2042                    content,
2043                }) => {
2044                    println!(
2045                        "\nTool Call delta: internal_call_id={internal_call_id:?}, content={content:?}"
2046                    );
2047                    chunk_count += 1;
2048                }
2049                Ok(StreamedAssistantContent::Final(res)) => {
2050                    println!("\nFinal response: {res:?}");
2051                }
2052                Ok(StreamedAssistantContent::Reasoning { reasoning, .. }) => {
2053                    let reasoning = reasoning.display_text();
2054                    print!("{reasoning}");
2055                    std::io::Write::flush(&mut std::io::stdout()).unwrap();
2056                }
2057                Ok(StreamedAssistantContent::ReasoningDelta { reasoning, .. }) => {
2058                    println!("Reasoning delta: {reasoning}");
2059                    chunk_count += 1;
2060                }
2061                Ok(StreamedAssistantContent::Unknown(value)) => {
2062                    println!("\nUnknown item: {value:?}");
2063                    chunk_count += 1;
2064                }
2065                Err(e) => {
2066                    eprintln!("Error: {e:?}");
2067                    break;
2068                }
2069            }
2070
2071            if chunk_count >= 2 {
2072                println!("\nCancelling stream...");
2073                stream.cancel();
2074                println!("Stream cancelled.");
2075                break;
2076            }
2077        }
2078
2079        let next_chunk = stream.next().await;
2080        assert!(
2081            next_chunk.is_none(),
2082            "Expected no further chunks after cancellation, got {next_chunk:?}"
2083        );
2084    }
2085
2086    #[tokio::test]
2087    async fn test_stream_pause_resume() {
2088        let stream = create_mock_stream();
2089
2090        // Test pause
2091        stream.pause();
2092        assert!(stream.is_paused());
2093
2094        // Test resume
2095        stream.resume();
2096        assert!(!stream.is_paused());
2097    }
2098
2099    /// #2258 H7: a paused stream parks on the pause channel instead of
2100    /// re-waking itself, which turned a pause into a busy poll loop. The
2101    /// `is_woken` assertion is the pin: pre-fix the paused poll woke the task
2102    /// immediately, so it failed.
2103    ///
2104    /// Not inducible from a recorded provider turn — pause/resume is
2105    /// consumer-side control flow with no wire representation.
2106    #[tokio::test]
2107    async fn a_paused_stream_parks_until_resume_instead_of_busy_waking() {
2108        let stream = StreamingCompletionResponse::stream(
2109            TEST_PROVIDER,
2110            to_stream_result(stream! {
2111                yield Ok(RawStreamingChoice::Message("hello".to_string()));
2112            }),
2113        );
2114        let resume = stream.pause_control.paused_tx.clone();
2115        stream.pause();
2116
2117        let mut task = tokio_test::task::spawn(stream);
2118        assert!(
2119            task.poll_next().is_pending(),
2120            "a paused stream yields nothing"
2121        );
2122        assert!(
2123            !task.is_woken(),
2124            "a paused stream must idle, not re-wake itself"
2125        );
2126
2127        resume.send(false).expect("resume");
2128        assert!(task.is_woken(), "resuming must wake the parked stream");
2129        assert!(matches!(
2130            task.poll_next(),
2131            Poll::Ready(Some(Ok(StreamedAssistantContent::Text(text)))) if text.text == "hello"
2132        ));
2133    }
2134
2135    /// #2258 B7: cancelling a paused stream must not deadlock — the consumer
2136    /// parked on the pause channel observes the termination because
2137    /// `cancel()` also resumes.
2138    #[tokio::test]
2139    async fn cancelling_a_paused_stream_terminates_instead_of_deadlocking() {
2140        let mut stream = create_mock_stream();
2141        stream.pause();
2142        stream.cancel();
2143        assert!(
2144            !stream.is_paused(),
2145            "cancel must lift the pause so the termination is observable"
2146        );
2147        assert!(
2148            stream.next().await.is_none(),
2149            "a cancelled stream terminates"
2150        );
2151    }
2152
2153    /// #2258 H6: `finish()` is destructive, so a second poll of a drained
2154    /// stream must not run it again — pre-fix the re-poll replaced a fully
2155    /// aggregated `choice` with the empty-text fallback.
2156    ///
2157    /// Not inducible from a recorded provider turn: re-polling a terminated
2158    /// stream is consumer behavior (`Stream` permits it, and combinators do
2159    /// it), independent of any wire.
2160    #[tokio::test]
2161    async fn re_polling_a_drained_stream_preserves_the_aggregated_choice() {
2162        let mut stream = create_mock_stream();
2163        while stream.next().await.is_some() {}
2164
2165        let drained: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2166        assert_eq!(
2167            drained,
2168            vec![AssistantContent::text("hello 1hello 2hello 3")]
2169        );
2170
2171        for _ in 0..3 {
2172            assert!(
2173                stream.next().await.is_none(),
2174                "a drained stream stays drained"
2175            );
2176        }
2177        assert_eq!(
2178            stream.choice.clone().into_iter().collect::<Vec<_>>(),
2179            drained,
2180            "re-polling must not re-run the destructive finish()"
2181        );
2182
2183        // The conversion into a unary response still carries the content.
2184        let response: CompletionResponse = stream.into();
2185        assert_eq!(response.choice.into_iter().collect::<Vec<_>>(), drained);
2186    }
2187
2188    /// #2258 H8: a `ProviderError` whose text happens to contain "aborted"
2189    /// is an error like any other. It used to be swallowed as clean EOF,
2190    /// discarding both the failure and the content streamed before it.
2191    ///
2192    /// Not inducible from a recorded provider turn: no in-tree provider emits
2193    /// this sentinel, and real cancellation arrives as `Ready(None)` through
2194    /// `Abortable` rather than as an error item.
2195    #[tokio::test]
2196    async fn a_provider_error_mentioning_aborted_reaches_the_consumer() {
2197        let mut stream = StreamingCompletionResponse::stream(
2198            TEST_PROVIDER,
2199            to_stream_result(stream! {
2200                yield Ok(RawStreamingChoice::Message("partial".to_string()));
2201                yield Err(CompletionError::ProviderError(
2202                    "upstream aborted the request".to_string(),
2203                ));
2204            }),
2205        );
2206
2207        let mut errors = Vec::new();
2208        while let Some(item) = stream.next().await {
2209            if let Err(err) = item {
2210                errors.push(err.to_string());
2211            }
2212        }
2213        assert_eq!(errors.len(), 1, "the error must not be swallowed");
2214        assert!(errors[0].contains("upstream aborted the request"));
2215
2216        // The content streamed before the failure is still aggregated.
2217        assert_eq!(
2218            stream.choice.first(),
2219            Some(&AssistantContent::text("partial".to_string()))
2220        );
2221        assert!(stream.response.is_none());
2222    }
2223
2224    /// #2258 F1, at the stream boundary: a wire that fragments a call's input
2225    /// and then restates it as one complete block must publish the completed
2226    /// call under the id its deltas already published — the correlation
2227    /// contract on [`StreamedAssistantContent::ToolCall`]. Pre-fix the
2228    /// completed call carried a fresh id no delta ever mentioned.
2229    ///
2230    /// Not inducible from a recorded provider turn: no in-tree wire mixes the
2231    /// two shapes for one call, though out-of-tree adapters can.
2232    #[tokio::test]
2233    async fn a_full_tool_call_correlates_with_the_deltas_of_the_same_id() {
2234        let mut stream = StreamingCompletionResponse::stream(
2235            TEST_PROVIDER,
2236            to_stream_result(stream! {
2237                yield Ok(RawStreamingChoice::ToolCallDelta {
2238                    id: StreamPartId::wire("tc1"),
2239                    content: ToolCallDeltaContent::Name("add".to_string()),
2240                });
2241                yield Ok(RawStreamingChoice::ToolCallDelta {
2242                    id: StreamPartId::wire("tc1"),
2243                    content: ToolCallDeltaContent::Delta("{\"x\":1}".to_string()),
2244                });
2245                yield Ok(RawStreamingChoice::ToolCall(RawStreamingToolCall::new(
2246                    "tc1".to_string(),
2247                    "add".to_string(),
2248                    serde_json::json!({"x": 1}),
2249                )));
2250                yield Ok(RawStreamingChoice::ToolInputEnd(ToolInputEnd::new(
2251                    "tc1",
2252                    UnparseableToolInput::Drop,
2253                )));
2254                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(1)));
2255            }),
2256        );
2257
2258        let mut delta_ids = Vec::new();
2259        let mut completed_ids = Vec::new();
2260        while let Some(item) = stream.next().await {
2261            match item.expect("stream item should be Ok") {
2262                StreamedAssistantContent::ToolCallDelta {
2263                    internal_call_id, ..
2264                } => delta_ids.push(internal_call_id),
2265                StreamedAssistantContent::ToolCall {
2266                    internal_call_id, ..
2267                } => completed_ids.push(internal_call_id),
2268                _ => {}
2269            }
2270        }
2271
2272        assert_eq!(delta_ids.len(), 2);
2273        assert_eq!(delta_ids[0], delta_ids[1], "one call, one internal id");
2274        assert_eq!(
2275            completed_ids,
2276            vec![delta_ids[0].clone()],
2277            "the completed call must carry the id its deltas published"
2278        );
2279
2280        // The trailing end event for a call a full block already delivered
2281        // finalizes nothing: exactly one tool call reaches the choice.
2282        let tool_calls: Vec<&ToolCall> = stream
2283            .choice
2284            .iter()
2285            .filter_map(|item| match item {
2286                AssistantContent::ToolCall(tool_call) => Some(tool_call),
2287                _ => None,
2288            })
2289            .collect();
2290        assert_eq!(tool_calls.len(), 1, "got {:?}", stream.choice);
2291    }
2292
2293    #[tokio::test]
2294    async fn test_stream_aggregates_reasoning_content() {
2295        let mut stream = create_reasoning_stream();
2296        while stream.next().await.is_some() {}
2297
2298        let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2299
2300        assert!(choice_items.iter().any(|item| matches!(
2301            item,
2302            AssistantContent::Reasoning(Reasoning {
2303                id: Some(id),
2304                content
2305            }) if id == "rs_1"
2306                && matches!(
2307                    content.first(),
2308                    Some(ReasoningContent::Text {
2309                        text,
2310                        signature: Some(signature)
2311                    }) if text == "step one" && signature == "sig_1"
2312                )
2313        )));
2314    }
2315
2316    /// A full reasoning block replaces its own delta accumulation, so the
2317    /// aggregated choice matches unary normalization of the same turn: one
2318    /// reasoning item carrying the completed block, not delta-plus-duplicate.
2319    #[tokio::test]
2320    async fn full_reasoning_block_supersedes_its_accumulated_deltas() {
2321        let mut stream = StreamingCompletionResponse::stream(
2322            TEST_PROVIDER,
2323            to_stream_result(stream! {
2324                yield Ok(RawStreamingChoice::ReasoningDelta {
2325                    id: StreamPartId::wire("rs_1"),
2326                provider_id: WireId::new("rs_1"),
2327                    reasoning: "partial ".to_string(),
2328                });
2329                yield Ok(RawStreamingChoice::Reasoning {                    id: StreamPartId::wire("rs_1"),
2330                provider_id: WireId::new("rs_1"),
2331                    content: ReasoningContent::Text {
2332                        text: "the complete chain".to_string(),
2333                        signature: Some("sig_1".to_string()),
2334                    },
2335                });
2336                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2337            }),
2338        );
2339        while stream.next().await.is_some() {}
2340
2341        let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2342        let reasoning_items: Vec<&Reasoning> = choice_items
2343            .iter()
2344            .filter_map(|item| match item {
2345                AssistantContent::Reasoning(reasoning) => Some(reasoning),
2346                _ => None,
2347            })
2348            .collect();
2349
2350        assert_eq!(reasoning_items.len(), 1, "got {choice_items:?}");
2351        let reasoning = reasoning_items.first().expect("one reasoning item");
2352        assert_eq!(reasoning.id.as_deref(), Some("rs_1"));
2353        assert!(matches!(
2354            reasoning.content.first(),
2355            Some(ReasoningContent::Text { text, signature: Some(signature) })
2356                if text == "the complete chain" && signature == "sig_1"
2357        ));
2358    }
2359
2360    /// A full block whose ID differs from the accumulating item's ID is a
2361    /// distinct reasoning item and is appended, not a replacement.
2362    #[tokio::test]
2363    async fn full_reasoning_block_with_a_different_id_appends() {
2364        let mut stream = StreamingCompletionResponse::stream(
2365            TEST_PROVIDER,
2366            to_stream_result(stream! {
2367                yield Ok(RawStreamingChoice::ReasoningDelta {
2368                    id: StreamPartId::wire("rs_1"),
2369                provider_id: WireId::new("rs_1"),
2370                    reasoning: "first item deltas".to_string(),
2371                });
2372                yield Ok(RawStreamingChoice::Reasoning {                    id: StreamPartId::wire("rs_2"),
2373                provider_id: WireId::new("rs_2"),
2374                    content: ReasoningContent::Text {
2375                        text: "a different item".to_string(),
2376                        signature: None,
2377                    },
2378                });
2379                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2380            }),
2381        );
2382        while stream.next().await.is_some() {}
2383
2384        let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2385        let reasoning_ids: Vec<Option<&str>> = choice_items
2386            .iter()
2387            .filter_map(|item| match item {
2388                AssistantContent::Reasoning(reasoning) => Some(reasoning.id.as_deref()),
2389                _ => None,
2390            })
2391            .collect();
2392
2393        assert_eq!(reasoning_ids, vec![Some("rs_1"), Some("rs_2")]);
2394    }
2395
2396    /// A bare end the wire actually sent yields the completed block (the
2397    /// wire announced the boundary and the consumer must see it — e.g.
2398    /// anthropic's `content_block_stop` on an unsigned thinking block); a
2399    /// bare end an adapter synthesized stays silent.
2400    #[tokio::test]
2401    async fn wire_sent_bare_end_yields_the_completed_block_synthesized_stays_silent() {
2402        let run = |wire_sent: bool| async move {
2403            let mut stream = StreamingCompletionResponse::stream(
2404                TEST_PROVIDER,
2405                to_stream_result(stream! {
2406                    yield Ok(RawStreamingChoice::ReasoningDelta {
2407                        id: StreamPartId::minted(MintKind::Block, 0),
2408                        provider_id: None,
2409                        reasoning: "unsigned thoughts".to_string(),
2410                    });
2411                    yield Ok(RawStreamingChoice::ReasoningEnd {
2412                        id: StreamPartId::minted(MintKind::Block, 0),
2413                        reasoning: None,
2414                        signature: None,
2415                        wire_sent,
2416                    });
2417                    yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2418                }),
2419            );
2420            let mut completed = Vec::new();
2421            while let Some(item) = stream.next().await {
2422                if let Ok(StreamedAssistantContent::Reasoning { reasoning, .. }) = item {
2423                    completed.push(reasoning);
2424                }
2425            }
2426            completed
2427        };
2428
2429        let wire = run(true).await;
2430        assert_eq!(wire.len(), 1, "a wire-sent end announces the boundary");
2431        assert!(matches!(
2432            wire[0].content.first(),
2433            Some(ReasoningContent::Text { text, signature: None }) if text == "unsigned thoughts"
2434        ));
2435
2436        let synthesized = run(false).await;
2437        assert!(
2438            synthesized.is_empty(),
2439            "a synthesized bare end fabricates nothing: {synthesized:?}"
2440        );
2441    }
2442
2443    /// The public delta correlator is unique per *part*, not per key: when a
2444    /// constant minted key (boundary-less wires) is reused for a new block
2445    /// after the previous one ended, the new block's deltas carry a fresh
2446    /// correlator.
2447    #[tokio::test]
2448    async fn reused_key_after_end_mints_a_fresh_delta_correlator() {
2449        let key = || StreamPartId::minted(MintKind::Reasoning, 0);
2450        let mut stream = StreamingCompletionResponse::stream(
2451            TEST_PROVIDER,
2452            to_stream_result(stream! {
2453                yield Ok(RawStreamingChoice::ReasoningDelta {
2454                    id: key(),
2455                    provider_id: None,
2456                    reasoning: "block A".to_string(),
2457                });
2458                yield Ok(RawStreamingChoice::ReasoningEnd {
2459                    id: key(),
2460                    reasoning: None,
2461                    signature: None,
2462                    wire_sent: false,
2463                });
2464                yield Ok(RawStreamingChoice::Message("interleaved".to_string()));
2465                yield Ok(RawStreamingChoice::ReasoningDelta {
2466                    id: key(),
2467                    provider_id: None,
2468                    reasoning: "block B".to_string(),
2469                });
2470                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2471            }),
2472        );
2473
2474        let mut delta_ids = Vec::new();
2475        while let Some(item) = stream.next().await {
2476            if let Ok(StreamedAssistantContent::ReasoningDelta { id, .. }) = item {
2477                delta_ids.push(id);
2478            }
2479        }
2480
2481        assert_eq!(delta_ids.len(), 2, "one delta per block");
2482        assert_ne!(
2483            delta_ids[0], delta_ids[1],
2484            "distinct parts must not share a correlator"
2485        );
2486    }
2487
2488    /// The completed reasoning event restates the correlator its deltas
2489    /// carried (the anthropic shape: id-less deltas, wire-sent bare stop),
2490    /// keeping it distinct from the durable provider handle, which stays
2491    /// absent.
2492    #[tokio::test]
2493    async fn completed_reasoning_restates_the_delta_correlator() {
2494        let key = || StreamPartId::minted(MintKind::Block, 0);
2495        let mut stream = StreamingCompletionResponse::stream(
2496            TEST_PROVIDER,
2497            to_stream_result(stream! {
2498                yield Ok(RawStreamingChoice::ReasoningDelta {
2499                    id: key(),
2500                    provider_id: None,
2501                    reasoning: "unsigned thoughts".to_string(),
2502                });
2503                yield Ok(RawStreamingChoice::ReasoningEnd {
2504                    id: key(),
2505                    reasoning: None,
2506                    signature: None,
2507                    wire_sent: true,
2508                });
2509                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2510            }),
2511        );
2512
2513        let mut delta_ids = Vec::new();
2514        let mut completed = Vec::new();
2515        while let Some(item) = stream.next().await {
2516            match item {
2517                Ok(StreamedAssistantContent::ReasoningDelta { id, .. }) => delta_ids.push(id),
2518                Ok(StreamedAssistantContent::Reasoning { reasoning, id }) => {
2519                    completed.push((reasoning, id));
2520                }
2521                _ => {}
2522            }
2523        }
2524
2525        let (reasoning, correlator) = completed.first().expect("one completed block");
2526        assert_eq!(
2527            Some(correlator),
2528            delta_ids.first(),
2529            "the completed block restates its deltas' correlator"
2530        );
2531        assert_eq!(
2532            reasoning.id, None,
2533            "no provider handle exists on this wire; the correlator must not leak into it"
2534        );
2535    }
2536
2537    /// On a signed end (the gemini shape) the completed event carries BOTH
2538    /// identities as distinct values: the rig correlator matching the
2539    /// deltas, and the durable provider handle in `reasoning.id`.
2540    #[tokio::test]
2541    async fn completed_reasoning_keeps_correlator_and_provider_handle_distinct() {
2542        let mut stream = StreamingCompletionResponse::stream(
2543            TEST_PROVIDER,
2544            to_stream_result(stream! {
2545                yield Ok(RawStreamingChoice::ReasoningDelta {
2546                    id: StreamPartId::wire("rs_1"),
2547                    provider_id: WireId::new("rs_1"),
2548                    reasoning: "signed thoughts".to_string(),
2549                });
2550                yield Ok(RawStreamingChoice::ReasoningEnd {
2551                    id: StreamPartId::wire("rs_1"),
2552                    reasoning: None,
2553                    signature: Some("sig_1".to_string()),
2554                    wire_sent: true,
2555                });
2556                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2557            }),
2558        );
2559
2560        let mut delta_ids = Vec::new();
2561        let mut completed = Vec::new();
2562        while let Some(item) = stream.next().await {
2563            match item {
2564                Ok(StreamedAssistantContent::ReasoningDelta { id, .. }) => delta_ids.push(id),
2565                Ok(StreamedAssistantContent::Reasoning { reasoning, id }) => {
2566                    completed.push((reasoning, id));
2567                }
2568                _ => {}
2569            }
2570        }
2571
2572        let (reasoning, correlator) = completed.first().expect("one completed block");
2573        assert_eq!(Some(correlator), delta_ids.first());
2574        assert_eq!(reasoning.id.as_deref(), Some("rs_1"));
2575        assert_ne!(
2576            correlator.as_str(),
2577            "rs_1",
2578            "the rig correlator and the provider handle are separate values"
2579        );
2580    }
2581
2582    /// A trailing signature after a synthesized silent end restates the
2583    /// deltas' correlator (the gemini shape: thought deltas, visible text
2584    /// forcing a synthesized boundary, then a bare `thoughtSignature`
2585    /// frame). The suppressed end must not discard the part's identity —
2586    /// a fresh mint here strands the signed completion where the
2587    /// streamed-turn assembler cannot match it, duplicating the part.
2588    #[tokio::test]
2589    async fn late_signature_after_synthesized_end_restates_the_delta_correlator() {
2590        let key = || StreamPartId::minted(MintKind::Reasoning, 0);
2591        let mut stream = StreamingCompletionResponse::stream(
2592            TEST_PROVIDER,
2593            to_stream_result(stream! {
2594                yield Ok(RawStreamingChoice::ReasoningDelta {
2595                    id: key(),
2596                    provider_id: None,
2597                    reasoning: "hidden thoughts".to_string(),
2598                });
2599                // The adapter saw visible text begin and synthesized a
2600                // silent boundary the wire never sent.
2601                yield Ok(RawStreamingChoice::ReasoningEnd {
2602                    id: key(),
2603                    reasoning: None,
2604                    signature: None,
2605                    wire_sent: false,
2606                });
2607                yield Ok(RawStreamingChoice::Message("visible".to_string()));
2608                // The trailing signature frame closes the same part.
2609                yield Ok(RawStreamingChoice::ReasoningEnd {
2610                    id: key(),
2611                    reasoning: None,
2612                    signature: Some("sig_late".to_string()),
2613                    wire_sent: true,
2614                });
2615                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2616            }),
2617        );
2618
2619        let mut delta_ids = Vec::new();
2620        let mut completed = Vec::new();
2621        while let Some(item) = stream.next().await {
2622            match item {
2623                Ok(StreamedAssistantContent::ReasoningDelta { id, .. }) => delta_ids.push(id),
2624                Ok(StreamedAssistantContent::Reasoning { reasoning, id }) => {
2625                    completed.push((reasoning, id));
2626                }
2627                _ => {}
2628            }
2629        }
2630
2631        assert_eq!(completed.len(), 1, "one signed completion, no duplicate");
2632        let (reasoning, correlator) = completed.first().expect("one completed block");
2633        assert_eq!(
2634            Some(correlator),
2635            delta_ids.first(),
2636            "the signed completion restates the correlator its deltas carried"
2637        );
2638        assert!(
2639            reasoning.content.iter().any(|content| matches!(
2640                content,
2641                ReasoningContent::Text { signature: Some(sig), .. } if sig == "sig_late"
2642            )),
2643            "the trailing signature landed on the completed part"
2644        );
2645    }
2646
2647    /// A delta-less `ReasoningStart` under a reused key opens a NEW part
2648    /// with a fresh public correlator — even when that part closes with a
2649    /// signature-only end and no delta ever minted one (sequence O9: the
2650    /// finished map must never leak the previous part's identity onto a
2651    /// distinct part).
2652    #[tokio::test]
2653    async fn a_delta_less_start_under_a_reused_key_mints_a_fresh_correlator() {
2654        let key = || StreamPartId::minted(MintKind::Reasoning, 0);
2655        let mut stream = StreamingCompletionResponse::stream(
2656            TEST_PROVIDER,
2657            to_stream_result(stream! {
2658                yield Ok(RawStreamingChoice::ReasoningDelta {
2659                    id: key(),
2660                    provider_id: None,
2661                    reasoning: "part one".to_string(),
2662                });
2663                yield Ok(RawStreamingChoice::ReasoningEnd {
2664                    id: key(),
2665                    reasoning: None,
2666                    signature: None,
2667                    wire_sent: true,
2668                });
2669                yield Ok(RawStreamingChoice::ReasoningStart {
2670                    id: key(),
2671                    provider_id: None,
2672                });
2673                yield Ok(RawStreamingChoice::ReasoningEnd {
2674                    id: key(),
2675                    reasoning: None,
2676                    signature: Some("sig2".to_string()),
2677                    wire_sent: true,
2678                });
2679                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2680            }),
2681        );
2682
2683        let mut completed_ids = Vec::new();
2684        while let Some(item) = stream.next().await {
2685            if let Ok(StreamedAssistantContent::Reasoning { id, .. }) = item {
2686                completed_ids.push(id);
2687            }
2688        }
2689
2690        assert_eq!(completed_ids.len(), 2, "two distinct parts complete");
2691        assert_ne!(
2692            completed_ids.first(),
2693            completed_ids.get(1),
2694            "distinct parts must not share a public correlator"
2695        );
2696    }
2697
2698    /// Ending a part and streaming new deltas under the same accumulation
2699    /// key opens a NEW part: the second part's correlator is fresh, never
2700    /// the finished part's retained identity.
2701    #[tokio::test]
2702    async fn reused_accumulation_key_mints_a_fresh_correlator_after_an_end() {
2703        let key = || StreamPartId::minted(MintKind::Reasoning, 0);
2704        let mut stream = StreamingCompletionResponse::stream(
2705            TEST_PROVIDER,
2706            to_stream_result(stream! {
2707                yield Ok(RawStreamingChoice::ReasoningDelta {
2708                    id: key(),
2709                    provider_id: None,
2710                    reasoning: "first part".to_string(),
2711                });
2712                yield Ok(RawStreamingChoice::ReasoningEnd {
2713                    id: key(),
2714                    reasoning: None,
2715                    signature: None,
2716                    wire_sent: true,
2717                });
2718                yield Ok(RawStreamingChoice::ReasoningDelta {
2719                    id: key(),
2720                    provider_id: None,
2721                    reasoning: "second part".to_string(),
2722                });
2723                yield Ok(RawStreamingChoice::ReasoningEnd {
2724                    id: key(),
2725                    reasoning: None,
2726                    signature: None,
2727                    wire_sent: true,
2728                });
2729                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2730            }),
2731        );
2732
2733        let mut completed_ids = Vec::new();
2734        while let Some(item) = stream.next().await {
2735            if let Ok(StreamedAssistantContent::Reasoning { id, .. }) = item {
2736                completed_ids.push(id);
2737            }
2738        }
2739
2740        assert_eq!(completed_ids.len(), 2, "two parts under the reused key");
2741        assert_ne!(
2742            completed_ids.first(),
2743            completed_ids.get(1),
2744            "a reused key opens a new part with a fresh correlator"
2745        );
2746    }
2747
2748    /// A whole-block reasoning event with no prior deltas still carries a
2749    /// non-empty correlator, and two such parts never share one.
2750    #[tokio::test]
2751    async fn whole_block_reasoning_mints_a_unique_correlator() {
2752        let mut stream = StreamingCompletionResponse::stream(
2753            TEST_PROVIDER,
2754            to_stream_result(stream! {
2755                yield Ok(RawStreamingChoice::Reasoning {
2756                    id: StreamPartId::wire("rs_1"),
2757                    provider_id: WireId::new("rs_1"),
2758                    content: ReasoningContent::Text {
2759                        text: "first".to_string(),
2760                        signature: None,
2761                    },
2762                });
2763                yield Ok(RawStreamingChoice::Reasoning {
2764                    id: StreamPartId::wire("rs_2"),
2765                    provider_id: WireId::new("rs_2"),
2766                    content: ReasoningContent::Text {
2767                        text: "second".to_string(),
2768                        signature: None,
2769                    },
2770                });
2771                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2772            }),
2773        );
2774
2775        let mut correlators = Vec::new();
2776        while let Some(item) = stream.next().await {
2777            if let Ok(StreamedAssistantContent::Reasoning { id, .. }) = item {
2778                correlators.push(id);
2779            }
2780        }
2781
2782        assert_eq!(correlators.len(), 2);
2783        assert!(correlators.iter().all(|id| !id.is_empty()));
2784        assert_ne!(
2785            correlators[0], correlators[1],
2786            "distinct parts must not share a correlator"
2787        );
2788    }
2789
2790    #[tokio::test]
2791    async fn full_reasoning_block_supersedes_deltas_across_interleaved_output() {
2792        // Providers may emit the completed reasoning item after other output
2793        // (reasoning -> tool call -> completed block). The tool call clears
2794        // the active reasoning index, so replacement must fall back to the
2795        // by-ID scan rather than appending a duplicate.
2796        let mut stream = StreamingCompletionResponse::stream(
2797            TEST_PROVIDER,
2798            to_stream_result(stream! {
2799                yield Ok(RawStreamingChoice::ReasoningDelta {
2800                    id: StreamPartId::wire("rs_1"),
2801                provider_id: WireId::new("rs_1"),
2802                    reasoning: "partial ".to_string(),
2803                });
2804                yield Ok(RawStreamingChoice::ToolCall(RawStreamingToolCall::new(
2805                    "call_1".to_string(),
2806                    "probe".to_string(),
2807                    serde_json::json!({}),
2808                )));
2809                yield Ok(RawStreamingChoice::Reasoning {                    id: StreamPartId::wire("rs_1"),
2810                provider_id: WireId::new("rs_1"),
2811                    content: ReasoningContent::Text {
2812                        text: "the full block".to_string(),
2813                        signature: None,
2814                    },
2815                });
2816                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2817            }),
2818        );
2819        while stream.next().await.is_some() {}
2820
2821        let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2822        let reasoning_items: Vec<&Reasoning> = choice_items
2823            .iter()
2824            .filter_map(|item| match item {
2825                AssistantContent::Reasoning(reasoning) => Some(reasoning),
2826                _ => None,
2827            })
2828            .collect();
2829
2830        assert_eq!(
2831            reasoning_items.len(),
2832            1,
2833            "the full block must replace the delta-built item, not join it"
2834        );
2835        let only = reasoning_items.first().expect("one reasoning item");
2836        assert_eq!(only.id.as_deref(), Some("rs_1"));
2837        assert!(
2838            only.content.iter().any(|content| matches!(
2839                content,
2840                ReasoningContent::Text { text, .. } if text == "the full block"
2841            )),
2842            "the surviving item must carry the full block's content"
2843        );
2844    }
2845
2846    #[tokio::test]
2847    async fn minted_id_full_reasoning_block_does_not_clobber_a_wire_id_item() {
2848        // Ids are mandatory on the grammar; a provider-minted id (the
2849        // "reasoning-0"-style boundary fallback) is a distinct identity from
2850        // a wire-supplied one, so the block appends rather than overwriting
2851        // an unrelated item's deltas.
2852        let mut stream = StreamingCompletionResponse::stream(
2853            TEST_PROVIDER,
2854            to_stream_result(stream! {
2855                yield Ok(RawStreamingChoice::ReasoningDelta {
2856                    id: StreamPartId::wire("rs_1"),
2857                provider_id: WireId::new("rs_1"),
2858                    reasoning: "identified deltas".to_string(),
2859                });
2860                yield Ok(RawStreamingChoice::Reasoning {
2861                    id: StreamPartId::wire("reasoning-0"),
2862                provider_id: WireId::new("reasoning-0"),
2863                    content: ReasoningContent::Text {
2864                        text: "anonymous block".to_string(),
2865                        signature: None,
2866                    },
2867                });
2868                yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(2)));
2869            }),
2870        );
2871        while stream.next().await.is_some() {}
2872
2873        let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2874        let reasoning_ids: Vec<Option<&str>> = choice_items
2875            .iter()
2876            .filter_map(|item| match item {
2877                AssistantContent::Reasoning(reasoning) => Some(reasoning.id.as_deref()),
2878                _ => None,
2879            })
2880            .collect();
2881
2882        assert_eq!(reasoning_ids, vec![Some("rs_1"), Some("reasoning-0")]);
2883    }
2884
2885    #[tokio::test]
2886    async fn test_stream_reasoning_only_does_not_inject_empty_text() {
2887        let mut stream = create_reasoning_only_stream();
2888        while stream.next().await.is_some() {}
2889
2890        let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2891        assert_eq!(choice_items.len(), 1);
2892        assert!(matches!(
2893            choice_items.first(),
2894            Some(AssistantContent::Reasoning(Reasoning { id: Some(id), .. })) if id == "rs_only"
2895        ));
2896    }
2897
2898    #[tokio::test]
2899    async fn test_stream_aggregates_assistant_items_in_arrival_order() {
2900        let mut stream = create_interleaved_stream();
2901        while stream.next().await.is_some() {}
2902
2903        let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2904        assert_eq!(choice_items.len(), 3);
2905        assert!(matches!(
2906            choice_items.first(),
2907            Some(AssistantContent::Reasoning(Reasoning { id: Some(id), .. })) if id == "rs_interleaved"
2908        ));
2909        assert!(matches!(
2910            choice_items.get(1),
2911            Some(AssistantContent::Text(Text { text, .. })) if text == "final-text"
2912        ));
2913        assert!(matches!(
2914            choice_items.get(2),
2915            Some(AssistantContent::ToolCall(ToolCall { id, .. })) if id == "tool_1"
2916        ));
2917    }
2918
2919    #[tokio::test]
2920    async fn unknown_choice_reaches_consumer_but_not_aggregated_choice() {
2921        let unknown = serde_json::json!({
2922            "type": "web_search_call",
2923            "id": "ws_1",
2924            "status": "completed",
2925        });
2926        let yielded = unknown.clone();
2927        let stream = stream! {
2928            yield Ok(RawStreamingChoice::Unknown(yielded.into()));
2929            yield Ok(RawStreamingChoice::Message("done".to_string()));
2930            yield Ok(RawStreamingChoice::FinalResponse(mock_final_with_total_tokens(1)));
2931        };
2932        let mut stream =
2933            StreamingCompletionResponse::stream(TEST_PROVIDER, to_stream_result(stream));
2934
2935        let mut consumer_unknown = None;
2936        let mut consumer_text = String::new();
2937        while let Some(item) = stream.next().await {
2938            match item.expect("stream item should be Ok") {
2939                StreamedAssistantContent::Unknown(value) => consumer_unknown = Some(value),
2940                StreamedAssistantContent::Text(text) => consumer_text.push_str(&text.text),
2941                _ => {}
2942            }
2943        }
2944
2945        // The consumer receives the unmodeled item verbatim ...
2946        assert_eq!(consumer_unknown.as_ref(), Some(&unknown.into()));
2947        assert_eq!(consumer_text, "done");
2948
2949        // ... but it is structurally absent from the aggregated assistant choice
2950        // (the sole source of persisted history): only the text item remains.
2951        let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2952        assert_eq!(choice_items.len(), 1);
2953        assert!(matches!(
2954            choice_items.first(),
2955            Some(AssistantContent::Text(Text { text, .. })) if text == "done"
2956        ));
2957    }
2958
2959    #[tokio::test]
2960    async fn test_stream_keeps_non_contiguous_text_chunks_split_by_tool_call() {
2961        let mut stream = create_text_tool_text_stream();
2962        while stream.next().await.is_some() {}
2963
2964        let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2965        assert_eq!(choice_items.len(), 3);
2966        assert!(matches!(
2967            choice_items.first(),
2968            Some(AssistantContent::Text(Text { text, .. })) if text == "first"
2969        ));
2970        assert!(matches!(
2971            choice_items.get(1),
2972            Some(AssistantContent::ToolCall(ToolCall { id, .. })) if id == "tool_split"
2973        ));
2974        assert!(matches!(
2975            choice_items.get(2),
2976            Some(AssistantContent::Text(Text { text, .. })) if text == "second"
2977        ));
2978    }
2979
2980    #[tokio::test]
2981    async fn test_stream_preserves_text_additional_params() {
2982        let mut stream = create_text_metadata_stream();
2983        while stream.next().await.is_some() {}
2984
2985        let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
2986        assert_eq!(choice_items.len(), 2);
2987
2988        let Some(AssistantContent::Text(Text {
2989            text,
2990            additional_params: Some(additional_params),
2991        })) = choice_items.first()
2992        else {
2993            panic!("expected first text item with metadata");
2994        };
2995        assert_eq!(text, "first");
2996        assert_eq!(
2997            additional_params["citations"]
2998                .as_array()
2999                .expect("citations should be an array")
3000                .len(),
3001            2
3002        );
3003
3004        let Some(AssistantContent::Text(Text {
3005            text,
3006            additional_params: Some(additional_params),
3007        })) = choice_items.get(1)
3008        else {
3009            panic!("expected second text item with metadata");
3010        };
3011        assert_eq!(text, "second");
3012        assert_eq!(additional_params["block"], 2);
3013    }
3014}
3015
3016/// Describes responses from a streamed provider response which is either text, a tool call or a final usage response.
3017#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
3018#[serde(untagged)]
3019pub enum StreamedAssistantContent {
3020    /// Text delta emitted by the assistant.
3021    Text(Text),
3022    /// Complete tool call emitted by the assistant.
3023    ToolCall {
3024        tool_call: ToolCall,
3025        /// Rig-generated unique identifier for this tool call.
3026        /// Use this to correlate with ToolCallDelta events.
3027        internal_call_id: String,
3028    },
3029    /// Partial tool call data emitted by the assistant.
3030    ToolCallDelta {
3031        /// Rig-generated correlator for this call: stable across the call's
3032        /// fragments, matches the eventual
3033        /// [`StreamedAssistantContent::ToolCall`], and unique per run.
3034        /// Provider-issued ids arrive on the completed [`ToolCall`]; no
3035        /// stream-internal key is ever rendered here.
3036        internal_call_id: String,
3037        content: ToolCallDeltaContent,
3038    },
3039    /// Complete reasoning block emitted by the assistant.
3040    ///
3041    /// Supersedes any prior [`StreamedAssistantContent::ReasoningDelta`]s
3042    /// carrying the same correlator `id`: render it as a *replacement* for
3043    /// the accumulated delta text, not an addition. The match key is this
3044    /// variant's `id`, not [`Reasoning::id`](crate::message::Reasoning::id).
3045    /// The aggregated [`StreamingCompletionResponse::choice`] already
3046    /// applies this replacement.
3047    Reasoning {
3048        reasoning: Reasoning,
3049        /// Rig-generated correlator: matches the `id` on this part's prior
3050        /// [`StreamedAssistantContent::ReasoningDelta`]s and is unique per
3051        /// run. The durable provider handle is `reasoning.id`; this value
3052        /// never enters replayable history.
3053        id: String,
3054    },
3055    /// Partial reasoning text emitted by the assistant.
3056    ReasoningDelta {
3057        /// Rig-generated correlator for the reasoning part this delta
3058        /// extends: stable across the part's deltas and unique per run.
3059        /// Never a stream-internal key and never a fabricated provider
3060        /// value.
3061        id: String,
3062        /// The provider-issued reasoning item id, when one exists — the
3063        /// durable handle the aggregated
3064        /// [`Reasoning::id`](crate::message::Reasoning::id) will carry
3065        /// (`None` on wires that issue no reasoning ids).
3066        #[serde(default, skip_serializing_if = "Option::is_none")]
3067        provider_id: Option<String>,
3068        /// Partial reasoning text.
3069        reasoning: String,
3070    },
3071    /// The provider's normalized terminal record, if yielded by the stream.
3072    Final(StreamFinal),
3073    /// A provider-native output item rig does not model, preserved verbatim —
3074    /// e.g. an OpenAI Responses hosted-tool result (`web_search_call`,
3075    /// `file_search_call`, `computer_call`, `code_interpreter_call`). It is
3076    /// yielded to the consumer for inspection/forwarding but is not added to the
3077    /// accumulated assistant message or persisted history. Kept last because the
3078    /// enum is `#[serde(untagged)]` and the transparent payload wrapper
3079    /// matches anything, so earlier (typed) variants must be tried first.
3080    Unknown(UnknownPayload),
3081}
3082
3083impl StreamedAssistantContent {
3084    /// Create a text stream item.
3085    pub fn text(text: &str) -> Self {
3086        Self::Text(Text::new(text.to_string()))
3087    }
3088
3089    /// Create a final response stream item.
3090    pub fn final_response(res: StreamFinal) -> Self {
3091        Self::Final(res)
3092    }
3093}
3094
3095/// Streamed user content. This content is primarily used to represent tool results from tool calls made during a multi-turn/step agent prompt.
3096#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
3097#[serde(untagged)]
3098pub enum StreamedUserContent {
3099    /// Tool result emitted during a multi-turn streaming agent loop.
3100    ToolResult {
3101        tool_result: ToolResult,
3102        /// Rig-generated unique identifier for the tool call this result
3103        /// belongs to. Use this to correlate with the originating
3104        /// [`StreamedAssistantContent::ToolCall::internal_call_id`].
3105        internal_call_id: String,
3106    },
3107}
3108
3109impl StreamedUserContent {
3110    /// Create a streamed tool result correlated to an internal tool call ID.
3111    pub fn tool_result(tool_result: ToolResult, internal_call_id: String) -> Self {
3112        Self::ToolResult {
3113            tool_result,
3114            internal_call_id,
3115        }
3116    }
3117}