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