Skip to main content

rig_core/
driver.rs

1//! Calls a model. A [`Model`] pairs a [`Wire`] (what to send and how to read
2//! the reply) with a [`Transport`] (how the payload travels). Its `stream`
3//! yields any operation's events as a [`Streamed`], and its `call` is that
4//! stream, finished: one decoder, one fold and one response constructor for
5//! both. The `_observed` twins take the observation context a recorder
6//! records under.
7//!
8//! ```no_run
9//! use rig_core::completion::CompletionRequest;
10//! use rig_core::driver::Model;
11//! use rig_core::providers::openai::{self, OpenAIConfig};
12//!
13//! # async fn example(http: rig_core::http_client::DynHttpClient) -> Result<(), Box<dyn std::error::Error>> {
14//! let model = OpenAIConfig::from_env()?.connect(http).responses(openai::GPT_5_2);
15//! let response = model.call(CompletionRequest::new("Hello")).await?;
16//! # let _ = response;
17//! # Ok(())
18//! # }
19//! ```
20
21use std::future::Future;
22use std::pin::Pin;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::sync::{Arc, Mutex};
25use std::task::{Context, Poll};
26use std::time::Duration;
27
28use futures::StreamExt;
29use tracing::Instrument;
30
31use crate::error::ProviderError;
32use crate::observe::{AdapterContext, AdapterSlot};
33use crate::streaming::{Item, Streamed};
34use crate::wasm_compat::{WasmBoxedFuture, WasmBoxedStream, WasmCompatSend, WasmCompatSync};
35use crate::wire::document::Reassemble;
36use crate::wire::{
37    Call, Capabilities, Decoder, Descriptor, Flow, Mode, Operation, Out, Request, Response, Shared,
38    Wire, WireEvent,
39};
40
41mod dyn_model;
42pub(crate) mod http_transport;
43mod local;
44
45pub use dyn_model::DynModel;
46pub use local::{Local, Step};
47
48/// An endpoint of one provider: a wire bound to a transport.
49///
50/// The pair holds no invariant, so both halves are public. To share one
51/// transport across models, clone it; to send a model's requests another
52/// way, replace its transport. The transport defaults to the erased HTTP
53/// client, so a model on the default transport is `Model<W>`.
54#[derive(Clone, Debug, PartialEq)]
55pub struct Model<W, T = crate::http_client::DynHttpClient> {
56    /// What to send and how to read the reply.
57    pub wire: W,
58    /// How the payload travels.
59    pub transport: T,
60    /// What preparing the last request leaves for the next.
61    prepared: crate::wire::Prepared,
62    /// How long a streamed reply may send nothing before it fails.
63    idle_timeout: Option<Duration>,
64}
65
66/// How long a streamed reply may send nothing, before its first frame or
67/// between two, before it fails as [`ProviderError::Stalled`]: five minutes.
68/// Every frame restarts it, and so do bytes that are no frame, such as an
69/// event stream's comments, so a model that thinks at length while its
70/// provider pings is not cut off.
71pub const STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
72
73impl<W: Default, T: Default> Default for Model<W, T> {
74    fn default() -> Self {
75        Self::new(W::default(), T::default())
76    }
77}
78
79impl<W, T> Model<W, T> {
80    /// Pair `wire` with the `transport` that sends it. A streamed reply
81    /// that sends nothing for [`STREAM_IDLE_TIMEOUT`] fails.
82    pub fn new(wire: W, transport: T) -> Self {
83        Self {
84            wire,
85            transport,
86            prepared: crate::wire::Prepared::default(),
87            idle_timeout: Some(STREAM_IDLE_TIMEOUT),
88        }
89    }
90
91    /// This model with a streamed reply failing as
92    /// [`ProviderError::Stalled`] once it sends nothing for `timeout`, before
93    /// its first frame or between two; `None`, or a timeout longer than a
94    /// browser's timer takes (about 24.8 days), waits indefinitely. A unary reply is never
95    /// timed: it sends nothing until it is whole.
96    pub fn with_idle_timeout(mut self, timeout: Option<Duration>) -> Self {
97        self.idle_timeout = timeout;
98        self
99    }
100}
101
102impl<W, T> Model<W, T>
103where
104    W: Wire<Op = crate::operation::Completion>,
105    T: Transport<W>,
106{
107    /// Every option of `request` this model would refuse, found by preparing
108    /// and encoding it as [`Self::call`] does, with nothing sent. It lists
109    /// the refusals of the model's catalog entry
110    /// ([`ModelSpec::refusals`](crate::catalog::ModelSpec::refusals)) and
111    /// those of the wire and its route, all of them rather than the first.
112    /// The request's [`OnUnsupported`](crate::completion::OnUnsupported)
113    /// policy does not change what is listed, but a request whose
114    /// [`GenerationOptions`](crate::completion::GenerationOptions) are
115    /// default is not checked against the catalog, as when it is sent.
116    ///
117    /// The request's messages are not read: its system prompt and a
118    /// stand-in prompt take their place, so a check costs the same for any
119    /// history. A refusal that depends on what the messages hold, such as
120    /// Bedrock's of a cache checkpoint after a reasoning turn, is reported
121    /// when the request is sent.
122    ///
123    /// # Errors
124    ///
125    /// [`CheckError::Unsupported`](crate::completion::CheckError::Unsupported)
126    /// with every refusal, or
127    /// [`CheckError::Invalid`](crate::completion::CheckError::Invalid) when the request cannot be built for another
128    /// reason.
129    pub fn check(
130        &self,
131        request: &crate::completion::CompletionRequest,
132    ) -> Result<(), crate::completion::CheckError> {
133        check_with(request, |request| self.dry_run(request))
134    }
135}
136
137/// The answer of `run`, a dry run of `request` without its messages, for
138/// [`Model::check`]. The run goes on past each refusal as under
139/// [`OnUnsupported::Ignore`](crate::completion::OnUnsupported::Ignore), so
140/// it meets every one; a request whose options are default stays default,
141/// so it is not checked against the catalog, as when it is sent.
142pub(crate) fn check_with(
143    request: &crate::completion::CompletionRequest,
144    run: impl FnOnce(crate::completion::CompletionRequest) -> Result<(), ProviderError>,
145) -> Result<(), crate::completion::CheckError> {
146    use crate::completion::{CheckError, OnUnsupported};
147    let mut request = without_messages(request);
148    if !request.options.is_default() {
149        request.options.on_unsupported = Some(OnUnsupported::Ignore);
150    }
151    match crate::completion::options::collect_refusals(|| run(request)) {
152        (Err(error), _) => Err(CheckError::Invalid(error)),
153        (Ok(()), refusals) if refusals.is_empty() => Ok(()),
154        (Ok(()), refusals) => Err(CheckError::Unsupported(refusals)),
155    }
156}
157
158/// `request` with its leading system prompt and a stand-in prompt as its
159/// messages, everything else as it is: what [`check_with`] dry-runs.
160pub(crate) fn without_messages(
161    request: &crate::completion::CompletionRequest,
162) -> crate::completion::CompletionRequest {
163    use crate::completion::{CompletionRequest, Message};
164    let CompletionRequest {
165        model,
166        chat_history,
167        documents,
168        tools,
169        temperature,
170        max_tokens,
171        tool_choice,
172        additional_params,
173        output_schema,
174        record_telemetry_content,
175        accept_unknown_finish_reasons,
176        foreign_reasoning,
177        options,
178        provider_options,
179        cache_points,
180    } = request;
181    let system = chat_history
182        .first()
183        .filter(|message| matches!(&***message, Message::System { .. }));
184    CompletionRequest {
185        model: model.clone(),
186        chat_history: system
187            .cloned()
188            .into_iter()
189            .chain([std::sync::Arc::new(Message::user("check"))])
190            .collect(),
191        documents: documents.clone(),
192        tools: tools.clone(),
193        temperature: *temperature,
194        max_tokens: *max_tokens,
195        tool_choice: tool_choice.clone(),
196        additional_params: additional_params.clone(),
197        output_schema: output_schema.clone(),
198        record_telemetry_content: *record_telemetry_content,
199        accept_unknown_finish_reasons: *accept_unknown_finish_reasons,
200        foreign_reasoning: *foreign_reasoning,
201        options: options.clone(),
202        provider_options: provider_options.clone(),
203        cache_points: cache_points.clone(),
204    }
205}
206
207/// Sends a wire's payloads and delivers its replies' frames.
208///
209/// HTTP clients ([`HttpClientExt`](crate::http_client::HttpClientExt)) are
210/// transports for every wire that encodes [`Encoded`](crate::wire::Encoded)
211/// requests and reads [`WireFrame`](crate::wire::WireFrame)s. A local
212/// runtime, a proxy or a mock is a transport for the wires it serves.
213pub trait Transport<W: Wire>: Clone + WasmCompatSend + WasmCompatSync + 'static {
214    /// Open the reply to one payload. Nothing is sent until the opening is
215    /// first polled. A payload the transport cannot send in the exchange's
216    /// mode fails the opening; a failure after the reply opened is its last
217    /// frame.
218    fn send(&self, payload: W::Payload, exchange: Exchange) -> Opening<W::Frame>;
219}
220
221/// What the driver tells a transport about one send.
222#[derive(Clone)]
223pub struct Exchange {
224    /// How the reply is read.
225    pub mode: Mode,
226    /// The observation an observing transport records the send under.
227    pub(crate) observation: Option<AdapterContext>,
228    /// What the HTTP transport puts in the request's extensions, such as
229    /// the [`Serving`](crate::serve::Serving) of a dispatched effect.
230    pub(crate) extensions: http::Extensions,
231}
232
233/// What a caller adds to one send besides the request: the observation to
234/// record it under, and the HTTP request's extensions.
235#[derive(Default)]
236pub(crate) struct Sending {
237    pub(crate) observation: Option<AdapterContext>,
238    pub(crate) extensions: http::Extensions,
239}
240
241impl Sending {
242    /// A send observed under `observation`.
243    pub(crate) fn observed(observation: AdapterContext) -> Self {
244        Self {
245            observation: Some(observation),
246            extensions: http::Extensions::new(),
247        }
248    }
249}
250
251/// One reply, opened when first polled.
252pub struct Opening<F> {
253    open: WasmBoxedFuture<'static, Result<Opened<F>, ProviderError>>,
254    /// The attempt the transport records while opening, which a reply that
255    /// stalls before it opened ends.
256    slot: Option<AdapterSlot>,
257}
258
259impl<F: WasmCompatSend + 'static> Opening<F> {
260    /// The reply `open` opens.
261    pub fn new(
262        open: impl Future<Output = Result<Opened<F>, ProviderError>> + WasmCompatSend + 'static,
263    ) -> Self {
264        Self {
265            open: Box::pin(open),
266            slot: None,
267        }
268    }
269
270    /// This opening recording its attempt in `slot`.
271    pub(crate) fn with_slot(mut self, slot: Option<AdapterSlot>) -> Self {
272        self.slot = slot;
273        self
274    }
275
276    /// A reply that is already open.
277    pub fn ready(opened: Opened<F>) -> Self {
278        Self::new(std::future::ready(Ok(opened)))
279    }
280
281    /// A reply that failed to open with `error`.
282    pub fn failed(error: ProviderError) -> Self {
283        Self::new(std::future::ready(Err(error)))
284    }
285}
286
287impl<F> Future for Opening<F> {
288    type Output = Result<Opened<F>, ProviderError>;
289
290    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
291        self.open.as_mut().poll(cx)
292    }
293}
294
295/// One reply a transport opened: its frames, and the facts the transport
296/// owns.
297pub struct Opened<F> {
298    pub(crate) frames: WasmBoxedStream<'static, Result<F, ProviderError>>,
299    pub(crate) request_id: Option<String>,
300    pub(crate) status: Option<http::StatusCode>,
301    pub(crate) headers: Option<http::HeaderMap>,
302    pub(crate) route: Option<String>,
303    pub(crate) document: Option<serde_json::Value>,
304    pub(crate) slot: Option<AdapterSlot>,
305    pub(crate) analysis_only: Option<fn(&F) -> bool>,
306    /// When the transport last received bytes, frames or not, such as an
307    /// event stream's comments, which keep a reply from stalling.
308    pub(crate) received: Option<Arc<Received>>,
309}
310
311impl<F: WasmCompatSend + 'static> Opened<F> {
312    /// A reply of `frames`, in order. A transport failure is the last item.
313    pub fn new(
314        frames: impl futures::Stream<Item = Result<F, ProviderError>> + WasmCompatSend + 'static,
315    ) -> Self {
316        Self {
317            frames: Box::pin(frames),
318            request_id: None,
319            status: None,
320            headers: None,
321            route: None,
322            document: None,
323            slot: None,
324            analysis_only: None,
325            received: None,
326        }
327    }
328
329    /// A reply that failed with `error` before any frame.
330    pub fn failed(error: ProviderError) -> Self {
331        Self::new(futures::stream::once(async move { Err(error) }))
332    }
333
334    /// The provider's transport request id, when the reply carried one. An
335    /// empty id is no id.
336    pub fn with_request_id(mut self, request_id: Option<String>) -> Self {
337        self.request_id = crate::provider_response::reported(request_id);
338        self
339    }
340
341    /// The reply's HTTP status and headers, which enrich its unary failure.
342    pub fn with_http(mut self, status: http::StatusCode, headers: http::HeaderMap) -> Self {
343        self.status = Some(status);
344        self.headers = Some(headers);
345        self
346    }
347
348    /// The concrete request path, which a listing's failure names.
349    pub fn with_route(mut self, route: impl Into<String>) -> Self {
350        self.route = Some(route.into());
351        self
352    }
353
354    /// The whole reply as one document: the response's `raw`. A reply
355    /// with one is not fed to the wire's reassembler.
356    pub fn with_document(mut self, document: serde_json::Value) -> Self {
357        self.document = Some(document);
358        self
359    }
360
361    /// The same reply, its frames passed through `frames`: what a transport
362    /// that wraps another uses to watch or rewrite a reply in flight.
363    pub fn map_frames<S>(
364        mut self,
365        frames: impl FnOnce(WasmBoxedStream<'static, Result<F, ProviderError>>) -> S,
366    ) -> Self
367    where
368        S: futures::Stream<Item = Result<F, ProviderError>> + WasmCompatSend + 'static,
369    {
370        self.frames = Box::pin(frames(self.frames));
371        self
372    }
373}
374
375impl<W, T> Model<W, T>
376where
377    W: Wire,
378    T: Transport<W>,
379{
380    /// The wire's provider descriptor name (`"anthropic"`).
381    pub fn name(&self) -> &str {
382        self.wire.describe().name
383    }
384
385    /// The model id the wire addresses, when the operation addresses one.
386    pub fn id(&self) -> Option<&str> {
387        self.wire.describe().model
388    }
389
390    /// What a runtime accounts for about this model, such as an embedding
391    /// model's width.
392    pub fn capabilities(&self) -> Capabilities {
393        self.wire.describe().capabilities
394    }
395
396    /// Send `request` and fold the whole reply into the operation's
397    /// response: [`Self::stream`] of a unary reply, finished. A completion
398    /// takes a prompt, a conversation or a
399    /// [`CompletionRequest`](crate::completion::CompletionRequest).
400    pub fn call(
401        &self,
402        request: impl Into<Request<W>>,
403    ) -> impl Future<Output = Result<Response<W>, ProviderError>> + WasmCompatSend + 'static {
404        self.finished(request.into(), Sending::default())
405    }
406
407    /// [`Self::call`], with the attempt observed under `observation`.
408    pub fn call_observed(
409        &self,
410        request: impl Into<Request<W>>,
411        observation: AdapterContext,
412    ) -> impl Future<Output = Result<Response<W>, ProviderError>> + WasmCompatSend + 'static {
413        self.finished(request.into(), Sending::observed(observation))
414    }
415
416    /// The call opens when first polled, so its span is created under the
417    /// caller's instrumented context.
418    fn finished(
419        &self,
420        request: Request<W>,
421        sending: Sending,
422    ) -> impl Future<Output = Result<Response<W>, ProviderError>> + WasmCompatSend + 'static {
423        let model = self.clone();
424        async move { model.open(request, Mode::Unary, sending)?.finish().await }
425    }
426
427    /// Open a streamed reply. Encoding errors return here; every later
428    /// failure arrives in-band, as the stream's last item. Nothing is sent
429    /// until the stream is first polled.
430    pub fn stream(&self, request: impl Into<Request<W>>) -> Result<Streamed<W::Op>, ProviderError> {
431        self.open(request.into(), Mode::Streaming, Sending::default())
432    }
433
434    /// [`Self::stream`], with the attempt observed under `observation`.
435    pub fn stream_observed(
436        &self,
437        request: impl Into<Request<W>>,
438        observation: AdapterContext,
439    ) -> Result<Streamed<W::Op>, ProviderError> {
440        self.open(
441            request.into(),
442            Mode::Streaming,
443            Sending::observed(observation),
444        )
445    }
446
447    /// [`Self::call`], with a failure paired with the request path of the
448    /// reply that failed, for an operation whose errors name their route.
449    pub(crate) async fn call_routed(
450        &self,
451        request: Request<W>,
452    ) -> Result<Response<W>, (ProviderError, String)> {
453        self.open(request, Mode::Unary, Sending::default())
454            .map_err(|error| (error, String::new()))?
455            .finish_routed()
456            .await
457    }
458
459    /// `request` prepared and encoded as [`Self::call`] would, with nothing
460    /// sent.
461    pub(crate) fn dry_run(&self, request: Request<W>) -> Result<(), ProviderError> {
462        let describe = Descriptor {
463            prepared: Some(&self.prepared),
464            ..self.wire.describe()
465        };
466        let request = <W::Op as Operation>::prepare(request, &describe)?;
467        self.wire.encode(request, Mode::Unary)?;
468        Ok(())
469    }
470
471    /// The one entry to the driver: the operation's fold for the reply,
472    /// the encoded payload, and the transport's reply, in `mode`. A request
473    /// the operation rejects fails here, before it is encoded.
474    pub(crate) fn open(
475        &self,
476        request: Request<W>,
477        mode: Mode,
478        sending: Sending,
479    ) -> Result<Streamed<W::Op>, ProviderError> {
480        let describe = Descriptor {
481            prepared: Some(&self.prepared),
482            ..self.wire.describe()
483        };
484        let request = <W::Op as Operation>::prepare(request, &describe)?;
485        let provider = describe.name.to_owned();
486        let mut call = Call::new(&describe, mode);
487        let fold = <W::Op as Operation>::fold(&request, &mut call);
488        let span = call.span;
489        let payload = self.wire.encode(request, mode)?;
490        let Sending {
491            observation,
492            extensions,
493        } = sending;
494        let exchange = Exchange {
495            mode,
496            observation,
497            extensions,
498        };
499        let opening = self.transport.send(payload, exchange);
500        let shared = Arc::new(Mutex::new(Shared::new(fold)));
501        // Only a streamed reply sends as it goes, so only it can stall.
502        let idle = match mode {
503            Mode::Streaming => self.idle_timeout,
504            Mode::Unary => None,
505        };
506        let reading = read(
507            self.wire.clone(),
508            opening,
509            Arc::clone(&shared),
510            span.clone(),
511            (mode, idle),
512        );
513        Ok(Streamed::new(reading, shared, span, provider))
514    }
515}
516
517/// Read one reply: open it, classify each frame, decode it into the reply's
518/// writer, and stop at the provider's end. The stream yields once per frame
519/// read, so its items reach the consumer frame by frame. When the transport
520/// reported no whole document, the wire's reassembler sees every known
521/// event before the decoder decodes it, and what it rebuilds is the reply's
522/// `raw` however the reply ends.
523///
524/// The reply's `'id` brand is the borrow of its shared state inside this
525/// stream: the decoder and every part handle it holds are tied to it, and
526/// none can leave.
527fn read<W: Wire>(
528    wire: W,
529    opening: Opening<W::Frame>,
530    shared: Arc<Mutex<Shared<W::Op>>>,
531    span: tracing::Span,
532    (mode, idle): (Mode, Option<Duration>),
533) -> WasmBoxedStream<'static, ()> {
534    let decoding = span.clone();
535    let reading = async_stream::stream! {
536        let reply: &Mutex<Shared<W::Op>> = &shared;
537        let opening_slot = opening.slot.clone();
538        // A unary call sends under the call's span; a stream reads under it.
539        let opened = match mode {
540            Mode::Unary => opening.instrument(span.clone()).await,
541            Mode::Streaming => idle_within(idle, None, opening)
542                .await
543                .and_then(|opened| opened),
544        };
545        let Opened {
546            mut frames,
547            request_id,
548            status,
549            headers,
550            route,
551            document,
552            slot,
553            analysis_only,
554            received,
555        } = match opened {
556            Ok(opened) => opened,
557            Err(error) => {
558                fail(reply, opening_slot.as_ref(), error);
559                return;
560            }
561        };
562        record_request_id(&span, request_id.as_deref());
563        let mut reassembler = document.is_none().then(|| wire.reassembler());
564        {
565            let mut state = lock(reply);
566            state.request_id.clone_from(&request_id);
567            state.document = document;
568            state.route = route.unwrap_or_default();
569        }
570        let enrich = |error: ProviderError| match mode {
571            // A unary reply's failure carries what the transport reported.
572            Mode::Unary => error
573                .with_provider_status(status)
574                .with_provider_request_id(request_id.clone())
575                .with_response_headers(headers.clone()),
576            Mode::Streaming => error,
577        };
578        let mut decoder = wire.decoder();
579        let mut tally = slot.as_ref().map(|slot| Tally {
580            slot,
581            analysis_only,
582            counted: 0,
583        });
584        loop {
585            let next = match idle_within(idle, received.as_deref(), frames.next()).await {
586                Ok(next) => next,
587                Err(stalled) => {
588                    record(reply, reassembler.map(|document| document.finish()));
589                    fail(reply, slot.as_ref(), stalled);
590                    return;
591                }
592            };
593            let flow = match next {
594                Some(Ok(frame)) => step(
595                    &mut decoder,
596                    reassembler.as_mut(),
597                    reply,
598                    frame,
599                    tally.as_mut(),
600                ),
601                Some(Err(error)) => {
602                    record(reply, reassembler.map(|document| document.finish()));
603                    fail(reply, slot.as_ref(), error);
604                    return;
605                }
606                None => eof(&mut decoder, reply, tally.as_ref()),
607            };
608            match flow {
609                Ok(Flow::More) => yield (),
610                Ok(Flow::Ended(_)) => break,
611                Err(error) => {
612                    record(reply, reassembler.map(|document| document.finish()));
613                    fail(reply, slot.as_ref(), enrich(error));
614                    return;
615                }
616            }
617        }
618        record(reply, reassembler.map(|document| document.finish()));
619        if let Some(slot) = &slot {
620            let usage = lock(reply)
621                .end
622                .as_ref()
623                .and_then(W::Op::observed_usage)
624                .cloned();
625            slot.terminal(usage.as_ref());
626        }
627        {
628            let state = lock(reply);
629            if let Some(document) = state.raw.as_ref().or(state.document.as_ref()) {
630                crate::providers::internal::trace_json(
631                    crate::providers::internal::LogTarget::Completions,
632                    "reply",
633                    document,
634                );
635            }
636        }
637        yield ();
638    };
639    let mut reading: WasmBoxedStream<'static, ()> = Box::pin(reading);
640    match mode {
641        // A stream decodes under the call's span.
642        Mode::Streaming => Box::pin(futures::stream::poll_fn(move |cx| {
643            let _decoding = decoding.enter();
644            reading.as_mut().poll_next(cx)
645        })),
646        Mode::Unary => reading,
647    }
648}
649
650/// The longest wait a browser's timer takes, about 24.8 days; past it, the
651/// timer fires at once. A longer idle timeout waits indefinitely.
652const LONGEST_TIMER: Duration = Duration::from_millis(2_147_483_647);
653
654/// When a reply's transport last received bytes.
655pub(crate) struct Received {
656    opened: web_time::Instant,
657    /// Milliseconds from `opened` to the last bytes.
658    last: AtomicU64,
659}
660
661impl Received {
662    pub(crate) fn new() -> Self {
663        Self {
664            opened: web_time::Instant::now(),
665            last: AtomicU64::new(0),
666        }
667    }
668
669    /// Notes that bytes arrived now.
670    pub(crate) fn mark(&self) {
671        let millis = u64::try_from(self.opened.elapsed().as_millis()).unwrap_or(u64::MAX);
672        self.last.store(millis, Ordering::Relaxed);
673    }
674
675    /// How long ago the last bytes arrived, or the reply opened.
676    fn since(&self) -> Duration {
677        let last = Duration::from_millis(self.last.load(Ordering::Relaxed));
678        self.opened.elapsed().saturating_sub(last)
679    }
680}
681
682/// `next` awaited until `idle` passes with nothing from it and no bytes
683/// `received`, or for as long as it takes without `idle`: past it, the
684/// reply stalled.
685async fn idle_within<F: Future + Unpin>(
686    idle: Option<Duration>,
687    received: Option<&Received>,
688    mut next: F,
689) -> Result<F::Output, ProviderError> {
690    let Some(idle) = idle.filter(|idle| *idle <= LONGEST_TIMER) else {
691        return Ok(next.await);
692    };
693    // Timed from the last bytes, not from this wait: frames read earlier
694    // may have waited on the reader. A frame already in hand still comes.
695    let mut wait = idle.saturating_sub(received.map_or(Duration::ZERO, Received::since));
696    loop {
697        if let Ok(output) = crate::wasm_compat::timeout(wait, &mut next).await {
698            return Ok(output);
699        }
700        let since = received.map_or(idle, Received::since);
701        if since >= idle {
702            return Err(ProviderError::Stalled(idle));
703        }
704        wait = idle.saturating_sub(since);
705    }
706}
707
708/// A reply decoded from frames already in hand: its items, then its
709/// response or the error that ended it.
710pub(crate) struct Decoded<Op: Operation> {
711    /// Read only by relays and tests; a websocket session needs the outcome.
712    #[cfg(any(test, feature = "test-utils"))]
713    pub(crate) items: Vec<Result<crate::streaming::Item<Op::Event>, ProviderError>>,
714    pub(crate) outcome: Result<Op::Response, ProviderError>,
715}
716
717/// Observation's count of one observed reply's frames, for its EOF and
718/// corruption positions.
719pub(crate) struct Tally<'a, F> {
720    slot: &'a AdapterSlot,
721    /// Frames observation does not count, unless they are corrupt.
722    analysis_only: Option<fn(&F) -> bool>,
723    counted: usize,
724}
725
726/// What the driver does with a classified frame: a known event is decoded,
727/// an unmodeled payload is warned about and delivered (aggregation never
728/// folds it into the answer), and a corrupt frame is the error that ends
729/// the reply.
730pub(crate) fn triage<E>(event: WireEvent<E>) -> Result<Item<E>, ProviderError> {
731    match event {
732        WireEvent::Known(event) => Ok(Item::Event(event)),
733        WireEvent::Unknown { event_type, value } => {
734            warn_unmodeled(&event_type, &value);
735            Ok(Item::Unknown(value))
736        }
737        WireEvent::Corrupt(error) => Err(ProviderError::from(error)),
738    }
739}
740
741/// Classify one frame and decode it into `reply`, handing a known event to
742/// `reassembler` first, and count the frame in `tally` when the reply is
743/// observed.
744pub(crate) fn step<'id, Op, F, D, R>(
745    decoder: &mut D,
746    reassembler: Option<&mut R>,
747    reply: &'id Mutex<Shared<Op>>,
748    frame: F,
749    tally: Option<&mut Tally<'_, F>>,
750) -> Result<Flow, ProviderError>
751where
752    Op: Operation,
753    D: Decoder<'id, Op, F>,
754    R: Reassemble<D::Event>,
755{
756    let exempt = tally
757        .as_ref()
758        .and_then(|tally| tally.analysis_only)
759        .is_some_and(|analysis_only| analysis_only(&frame));
760    let classified = decoder.classify(frame);
761    if let Some(tally) = tally {
762        // A corrupt frame is never exempt, whatever its shape.
763        let corrupt = matches!(classified, WireEvent::Corrupt(_));
764        if corrupt || !exempt {
765            tally.counted += 1;
766        }
767        if corrupt {
768            tally.slot.corrupt(tally.counted);
769        }
770    }
771    match triage(classified)? {
772        Item::Event(event) => {
773            if let Some(reassembler) = reassembler {
774                reassembler.absorb(&event);
775            }
776            decoder.decode(event, Out::new(reply))
777        }
778        Item::Unknown(value) => {
779            Out::new(reply).unknown(value);
780            Ok(Flow::More)
781        }
782    }
783}
784
785/// The frames ran out: a decoder that saw the provider's end ends the reply
786/// here; otherwise it is [`ProviderError::Truncated`].
787fn eof<'id, Op, F, D>(
788    decoder: &mut D,
789    reply: &'id Mutex<Shared<Op>>,
790    tally: Option<&Tally<'_, F>>,
791) -> Result<Flow, ProviderError>
792where
793    Op: Operation,
794    D: Decoder<'id, Op, F>,
795{
796    if let Some(tally) = tally {
797        tally.slot.transport_eof(tally.counted);
798    }
799    let step = decoder.eof(Out::new(reply));
800    if !matches!(step, Ok(Flow::Ended(_)))
801        && let Some(tally) = tally
802    {
803        tally.slot.eof(tally.counted);
804    }
805    match step {
806        Ok(Flow::More) => Err(ProviderError::Truncated),
807        step => step,
808    }
809}
810
811/// Feed frames already in hand through `reassembler` and `decoder` into
812/// `reply`, as [`read`] does: the reply ends at the provider's end, or the
813/// decoder decides at EOF, and what the reassembler rebuilt is recorded.
814pub(crate) fn feed<'id, Op, F, D, R>(
815    decoder: &mut D,
816    mut reassembler: Option<R>,
817    reply: &'id Mutex<Shared<Op>>,
818    frames: impl IntoIterator<Item = F>,
819) -> Result<(), ProviderError>
820where
821    Op: Operation,
822    D: Decoder<'id, Op, F>,
823    R: Reassemble<D::Event>,
824{
825    let fed = feed_until_end(decoder, reassembler.as_mut(), reply, frames);
826    record(reply, reassembler.map(|document| document.finish()));
827    fed
828}
829
830fn feed_until_end<'id, Op, F, D, R>(
831    decoder: &mut D,
832    mut reassembler: Option<&mut R>,
833    reply: &'id Mutex<Shared<Op>>,
834    frames: impl IntoIterator<Item = F>,
835) -> Result<(), ProviderError>
836where
837    Op: Operation,
838    D: Decoder<'id, Op, F>,
839    R: Reassemble<D::Event>,
840{
841    for frame in frames {
842        if let Flow::Ended(_) = step(decoder, reassembler.as_deref_mut(), reply, frame, None)? {
843            return Ok(());
844        }
845    }
846    eof(decoder, reply, None).map(drop)
847}
848
849/// Record `document`, what a reassembler rebuilt, as the reply's `raw`. A
850/// `Null` document records nothing.
851pub(crate) fn record<Op: Operation>(
852    reply: &Mutex<Shared<Op>>,
853    document: Option<serde_json::Value>,
854) {
855    if let Some(document) = document.filter(|document| !document.is_null()) {
856        lock(reply).raw = Some(document);
857    }
858}
859
860/// Fold a fed reply as a stream does: its items, then its response, or the
861/// error that ended it. `reply.raw` stands unless it is null, when the
862/// decoder's record does.
863pub(crate) fn settle<Op: Operation>(
864    shared: Mutex<Shared<Op>>,
865    fed: Result<(), ProviderError>,
866    reply: crate::wire::Reply,
867) -> Decoded<Op> {
868    let mut shared = shared
869        .into_inner()
870        .unwrap_or_else(std::sync::PoisonError::into_inner);
871    if let Err(error) = fed {
872        shared.items.push_back(Err(error));
873    }
874    shared.document = Some(reply.raw).filter(|raw| !raw.is_null());
875    shared.request_id = reply.provider_request_id;
876    let mut items = Vec::new();
877    while let Some(item) = shared.take() {
878        let failed = item.is_err();
879        items.push(item);
880        if failed {
881            break;
882        }
883    }
884    let outcome = match items.last() {
885        Some(Err(error)) => Err(error.clone()),
886        _ => shared.conclude(&reply.provider),
887    };
888    #[cfg(any(test, feature = "test-utils"))]
889    if let Err(error) = &outcome
890        && !matches!(items.last(), Some(Err(_)))
891    {
892        items.push(Err(error.clone()));
893    }
894    Decoded {
895        #[cfg(any(test, feature = "test-utils"))]
896        items,
897        outcome,
898    }
899}
900
901/// [`decode_reply`](crate::wire::decode_reply), with `raw` the reply's
902/// whole document as a transport reports it (`Null` for none, so the
903/// reassembler's stands), for tests that state what the transport read.
904pub(crate) fn decode_reply<W: Wire>(
905    wire: &W,
906    request: &Request<W>,
907    mode: Mode,
908    frames: impl IntoIterator<Item = W::Frame>,
909    raw: serde_json::Value,
910) -> Result<Response<W>, ProviderError> {
911    let describe = wire.describe();
912    let fold = <W::Op as Operation>::fold(request, &mut Call::new(&describe, mode));
913    let shared = Mutex::new(Shared::new(fold));
914    let fed = feed(
915        &mut wire.decoder(),
916        Some(wire.reassembler()),
917        &shared,
918        frames,
919    );
920    let reply = crate::wire::Reply {
921        provider: describe.name.to_owned(),
922        raw,
923        provider_request_id: None,
924    };
925    settle(shared, fed, reply).outcome
926}
927
928/// A completion reply decoded from frames already in hand, as a handler
929/// relays it: its items, then the response, or the error that ended it.
930#[cfg(any(test, feature = "test-utils"))]
931pub(crate) fn relay_frames<W>(
932    wire: &W,
933    frames: impl IntoIterator<Item = W::Frame>,
934) -> crate::streaming::StreamEvents
935where
936    W: Wire<Op = crate::operation::Completion>,
937{
938    use crate::error::ErrorReport;
939    use crate::streaming::Relayed;
940
941    let provider = wire.describe().name.to_owned();
942    let shared = Mutex::new(Shared::new(crate::operation::ReplyFold::relayed(
943        provider.clone(),
944    )));
945    let fed = feed(
946        &mut wire.decoder(),
947        Some(wire.reassembler()),
948        &shared,
949        frames,
950    );
951    let decoded = settle(
952        shared,
953        fed,
954        crate::wire::Reply {
955            provider,
956            raw: serde_json::Value::Null,
957            provider_request_id: None,
958        },
959    );
960    let origin = decoded
961        .outcome
962        .as_ref()
963        .map(|response| response.origin.clone())
964        .ok();
965    let relayed: Vec<Result<Relayed, ErrorReport>> = origin
966        .map(|origin| Ok(Relayed::Origin(origin)))
967        .into_iter()
968        .chain(decoded.items.into_iter().map(|item| match item {
969            Ok(item) => Ok(Relayed::Item(item)),
970            Err(error) => Err(ErrorReport::from(&error)),
971        }))
972        // An error that ended the reply is already its last item.
973        .chain(
974            decoded
975                .outcome
976                .ok()
977                .map(|response| Ok(Relayed::Done(Arc::new(response)))),
978        )
979        .collect();
980    Box::pin(futures::stream::iter(relayed))
981}
982
983#[cfg(test)]
984impl Decoded<crate::operation::Completion> {
985    /// The events the reply yielded, without unmodeled payloads.
986    pub(crate) fn events(&self) -> Vec<&crate::streaming::StreamEvent> {
987        self.items
988            .iter()
989            .filter_map(|item| match item {
990                Ok(crate::streaming::Item::Event(event)) => Some(event),
991                _ => None,
992            })
993            .collect()
994    }
995
996    /// What each part ended with, in the order the parts ended.
997    pub(crate) fn ended(&self) -> Vec<crate::message::AssistantContent> {
998        self.events()
999            .into_iter()
1000            .filter_map(|event| match event {
1001                crate::streaming::StreamEvent::End { content, .. } => Some(content.clone()),
1002                _ => None,
1003            })
1004            .collect()
1005    }
1006}
1007
1008/// Decode typed `events` through `decoder` as one completion reply from
1009/// `provider`, then EOF: a decoder's test harness, without frames.
1010#[cfg(test)]
1011macro_rules! decode_events {
1012    ($decoder:expr, $provider:expr, $events:expr) => {
1013        $crate::driver::decode_with(
1014            $crate::operation::ReplyFold::relayed($provider),
1015            $provider,
1016            |reply| {
1017                let mut decoder = $decoder;
1018                for event in $events {
1019                    if let $crate::wire::Flow::Ended(_) =
1020                        $crate::wire::Decoder::decode(&mut decoder, event, reply.out())?
1021                    {
1022                        return Ok(());
1023                    }
1024                }
1025                match $crate::wire::Decoder::eof(&mut decoder, reply.out())? {
1026                    $crate::wire::Flow::Ended(_) => Ok(()),
1027                    $crate::wire::Flow::More => Err($crate::error::ProviderError::Truncated),
1028                }
1029            },
1030        )
1031    };
1032}
1033#[cfg(test)]
1034pub(crate) use decode_events;
1035
1036/// Decode `frames` through `decoder`, classifier included, as one
1037/// completion reply from `provider`: what the driver does with frames it
1038/// read. A reassembler, when given, sees each known event first and its
1039/// document is the reply's `raw`.
1040#[cfg(test)]
1041macro_rules! feed_frames {
1042    ($decoder:expr, $provider:expr, $frames:expr) => {
1043        $crate::driver::decode_with(
1044            $crate::operation::ReplyFold::relayed($provider),
1045            $provider,
1046            |reply| {
1047                let mut decoder = $decoder;
1048                reply.feed(
1049                    &mut decoder,
1050                    None::<$crate::wire::document::Unreassembled>,
1051                    $frames,
1052                )
1053            },
1054        )
1055    };
1056    ($decoder:expr, $reassembler:expr, $provider:expr, $frames:expr) => {
1057        $crate::driver::decode_with(
1058            $crate::operation::ReplyFold::relayed($provider),
1059            $provider,
1060            |reply| {
1061                let mut decoder = $decoder;
1062                reply.feed(&mut decoder, Some($reassembler), $frames)
1063            },
1064        )
1065    };
1066}
1067#[cfg(test)]
1068pub(crate) use feed_frames;
1069
1070/// One reply's writer, for a test that drives a decoder by hand.
1071#[cfg(test)]
1072pub(crate) struct Replying<'id, Op: Operation>(&'id Mutex<Shared<Op>>);
1073
1074#[cfg(test)]
1075impl<'id, Op: Operation> Replying<'id, Op> {
1076    /// A writer for the next event.
1077    pub(crate) fn out(&self) -> Out<'id, Op> {
1078        Out::new(self.0)
1079    }
1080
1081    /// Feed frames through `reassembler` and `decoder`, as the driver does.
1082    pub(crate) fn feed<F, D: Decoder<'id, Op, F>, R: Reassemble<D::Event>>(
1083        &self,
1084        decoder: &mut D,
1085        reassembler: Option<R>,
1086        frames: impl IntoIterator<Item = F>,
1087    ) -> Result<(), ProviderError> {
1088        feed(decoder, reassembler, self.0, frames)
1089    }
1090}
1091
1092/// Drive one reply by hand: `run` decodes into it until the reply ends, or
1093/// fails with what ended it, and the reply folds into `fold` as a stream
1094/// would.
1095#[cfg(test)]
1096pub(crate) fn decode_with<Op: Operation>(
1097    fold: Op::Fold,
1098    provider: &str,
1099    run: impl for<'id> FnOnce(Replying<'id, Op>) -> Result<(), ProviderError>,
1100) -> Decoded<Op> {
1101    let shared = Mutex::new(Shared::new(fold));
1102    let fed = run(Replying(&shared));
1103    settle(
1104        shared,
1105        fed,
1106        crate::wire::Reply {
1107            provider: provider.to_owned(),
1108            raw: serde_json::Value::Null,
1109            provider_request_id: None,
1110        },
1111    )
1112}
1113
1114/// The reply failed: the error is its last item.
1115fn fail<Op: Operation>(
1116    reply: &Mutex<Shared<Op>>,
1117    slot: Option<&AdapterSlot>,
1118    error: ProviderError,
1119) {
1120    if let Some(slot) = slot {
1121        slot.fail(&error);
1122    }
1123    lock(reply).items.push_back(Err(error));
1124}
1125
1126pub(crate) fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
1127    mutex
1128        .lock()
1129        .unwrap_or_else(std::sync::PoisonError::into_inner)
1130}
1131
1132/// Read pages until one names no cursor, names the cursor it was asked
1133/// with, or [`MAX_CONTINUATION_PAGES`] were read. `page` reads the page at a
1134/// cursor (`None` for the first) and returns it with the next cursor.
1135pub(crate) async fn follow_cursors<P, Fut>(
1136    provider: &str,
1137    operation: &str,
1138    mut page: impl FnMut(Option<String>) -> Fut,
1139) -> Result<Vec<P>, ProviderError>
1140where
1141    Fut: Future<Output = Result<(P, Option<String>), ProviderError>>,
1142{
1143    let mut pages = Vec::new();
1144    let mut cursor = None;
1145    loop {
1146        let (read, next) = page(cursor.clone()).await?;
1147        pages.push(read);
1148        // Warn only when an offered page is refused: normal exhaustion is
1149        // not truncation, while repeated or cycling cursors need a bound.
1150        let Some(next) = next else { break };
1151        if cursor.as_deref() == Some(next.as_str()) {
1152            // The next request would be identical to the one just answered,
1153            // so the page would repeat forever.
1154            tracing::warn!(
1155                provider,
1156                operation,
1157                pages = pages.len(),
1158                "listing repeated its pagination cursor; returning the pages fetched so far"
1159            );
1160            break;
1161        }
1162        if pages.len() >= MAX_CONTINUATION_PAGES {
1163            tracing::warn!(
1164                provider,
1165                operation,
1166                pages = pages.len(),
1167                "listing hit its page ceiling with a cursor still advancing; returning the pages \
1168                 fetched so far"
1169            );
1170            break;
1171        }
1172        cursor = Some(next);
1173    }
1174    Ok(pages)
1175}
1176
1177/// Logs an unmodeled payload's kind and serialized size, never its content.
1178/// Callers must supply a structural kind label without sensitive data.
1179pub fn warn_unmodeled(kind: &str, payload: &impl serde::Serialize) {
1180    tracing::warn!(
1181        kind,
1182        payload_bytes = unknown_payload_bytes(payload),
1183        "skipping unmodeled wire payload"
1184    );
1185}
1186
1187/// Serialized byte size of an unknown frame's payload, for the structural
1188/// warn log (the log never carries the payload itself).
1189fn unknown_payload_bytes(value: &impl serde::Serialize) -> u64 {
1190    /// Counter sink: measures how many bytes serialization would write
1191    /// without buffering them.
1192    struct CountingWriter(u64);
1193
1194    impl std::io::Write for CountingWriter {
1195        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1196            self.0 += buf.len() as u64;
1197            Ok(buf.len())
1198        }
1199
1200        fn flush(&mut self) -> std::io::Result<()> {
1201            Ok(())
1202        }
1203    }
1204
1205    let mut counter = CountingWriter(0);
1206    // A `Value` cannot fail to serialize; degrade to 0 rather than panic.
1207    let _ = serde_json::to_writer(&mut counter, value);
1208    counter.0
1209}
1210
1211/// Page count after which a listing's cursors are ignored, preventing
1212/// infinite cursor cycles.
1213const MAX_CONTINUATION_PAGES: usize = 1000;
1214
1215/// Record the transport request id on the call's span, success or failure.
1216pub(crate) fn record_request_id(span: &tracing::Span, request_id: Option<&str>) {
1217    if let Some(request_id) = request_id
1218        && !span.is_disabled()
1219    {
1220        span.record(crate::telemetry::PROVIDER_REQUEST_ID_FIELD, request_id);
1221    }
1222}
1223
1224#[cfg(test)]
1225pub(crate) mod tests;