Skip to main content

switchyard_libsy/core/
algorithm.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The [`Algorithm`] trait and its [`Driver`] — the orchestration contract every
5//! algorithm implements and the offload channel it uses for routing-time model calls.
6
7use std::{
8    collections::HashMap, future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc,
9    time::Instant,
10};
11
12use async_trait::async_trait;
13use futures::{FutureExt, Stream, StreamExt};
14use parking_lot::Mutex;
15use serde_json::Value;
16use tokio::sync::{mpsc, oneshot};
17use tokio_stream::wrappers::ReceiverStream;
18use tracing::Instrument;
19
20/// The request/response protocol types come from [`switchyard_protocol`].
21/// [`switchyard_protocol::LlmRequest`] is the normalized request;
22/// [`switchyard_protocol::AggLlmResponse`] is the buffered response;
23/// [`switchyard_protocol::LlmResponseChunk`] is normalized streaming content;
24/// [`switchyard_protocol::LlmResponseStreamEvent`] is its host/algorithm envelope; and
25/// [`switchyard_protocol::LlmResponse`] carries either a live
26/// [`switchyard_protocol::LlmResponseStream`] or the terminal aggregate.
27use switchyard_protocol::{Category, ModelId, Request, Response};
28
29use crate::{DriverError, LibsyError, Result, observability};
30
31/// A boxed, `Send` stream of [`Step`]s — the output of
32/// [`Algorithm::run_stream`]. Boxed so the trait method that produces it keeps
33/// `Arc<dyn Algorithm>` object-safe.
34pub type StepStream = Pin<Box<dyn Stream<Item = Result<Step>> + Send>>;
35
36/// The models one algorithm run may use, grouped by [`Category`]. Within a
37/// category they are ordered best-first.
38///
39/// Delegated sub-agent work gets its own groups, reachable only through
40/// [`Driver::for_subagent`]. Keeping them separate is what stops a sub-agent's
41/// `capable` from resolving to the parent's, and stops the parent falling back
42/// onto a model only its sub-agents were given.
43///
44/// One run's driver clones all read the same value, so it is passed as
45/// `Arc<RuntimeModels>` rather than cloned per driver.
46#[derive(Clone, Debug, Default)]
47pub struct RuntimeModels {
48    /// When using subagents this is the parent agent category.
49    by_category: HashMap<Category, Vec<ModelId>>,
50    subagent: Option<HashMap<Category, Vec<ModelId>>>,
51}
52
53impl RuntimeModels {
54    /// The models available to the algorithm itself.
55    pub fn new(by_category: HashMap<Category, Vec<ModelId>>) -> Self {
56        Self {
57            by_category,
58            subagent: None,
59        }
60    }
61
62    /// Adds the groups used for delegated sub-agent work.
63    pub fn with_subagent(mut self, models: HashMap<Category, Vec<ModelId>>) -> Self {
64        self.subagent = Some(models);
65        self
66    }
67
68    /// The models in `category`, ordered best-first.
69    pub fn models_for(&self, category: &Category) -> &[ModelId] {
70        self.by_category.get(category).map_or(&[], Vec::as_slice)
71    }
72
73    /// The models delegated sub-agent work uses for `category`, ordered best-first.
74    pub fn subagent_models_for(&self, category: &Category) -> &[ModelId] {
75        self.subagent
76            .as_ref()
77            .and_then(|models| models.get(category))
78            .map_or(&[], Vec::as_slice)
79    }
80}
81
82impl From<HashMap<Category, Vec<ModelId>>> for RuntimeModels {
83    fn from(by_category: HashMap<Category, Vec<ModelId>>) -> Self {
84        Self::new(by_category)
85    }
86}
87
88/// Which of a [`RuntimeModels`]' groups a driver reads.
89#[derive(Clone, Copy)]
90enum Scope {
91    Parent,
92    Subagent,
93}
94
95/// An offloaded model call, surfaced inside [`Step::CallModel`].
96///
97/// The host reads the public fields, performs (or delegates) the model call, and fulfills it
98/// with [`respond`](Self::respond) — unblocking the algorithm's [`Driver::call_model`] on the
99/// other side. `switchyard-llm-client`'s `run` is the ready-made consumer that does this for
100/// you.
101///
102/// [`Driver::call_model`] stamps the first candidate model onto the request before publishing
103/// the call. A consumer that falls through to a later candidate must re-stamp it.
104pub struct CallModel {
105    /// The name of the algorithm that produced this call, so a host instrumenting the
106    /// calls it serves can attribute its own spans to the algorithm behind them.
107    pub algorithm: String,
108    /// The request to serve; its `model` is stamped with the first candidate.
109    pub request: Request,
110    /// Candidate models, tried in order until one answers. Never empty.
111    pub models: Vec<ModelId>,
112    /// How to send the response back to the algorithm. `None` once the call is recorded.
113    reply: Option<oneshot::Sender<Result<Response>>>,
114    started: Instant,
115}
116
117impl CallModel {
118    /// Fulfill the promise with the caller's model-call result. Pass `Err(..)` to
119    /// propagate a failed model call back to the algorithm. Consumes the promise: it
120    /// can only be fulfilled once.
121    pub fn respond(mut self, result: Result<Response>) -> Result<()> {
122        self.record(result.is_ok());
123        self.reply
124            .take()
125            .ok_or(DriverError::ResponseDropped)?
126            .send(result)
127            .map_err(|_| DriverError::ResponseDropped.into())
128    }
129
130    /// Record a failed call and return its error to stop [`drive`].
131    /// Leaves the promise unfulfilled so the driver can cancel the algorithm.
132    pub fn fail(mut self, error: LibsyError) -> Result<()> {
133        self.reply = None;
134        self.record(false);
135        Err(error)
136    }
137
138    fn record(&self, is_ok: bool) {
139        observability::record_llm_call(
140            &self.algorithm,
141            self.models
142                .first()
143                .map(ModelId::as_str)
144                .unwrap_or("NoTargets"),
145            self.started.elapsed(),
146            is_ok,
147        );
148    }
149}
150
151impl Drop for CallModel {
152    fn drop(&mut self) {
153        if self.reply.is_some() {
154            self.record(false);
155        }
156    }
157}
158
159/// The terminal result of routing.
160pub struct RoutingOutcome {
161    /// Models selected by the algorithm, ordered best model first.
162    pub selected_model_ids: Vec<ModelId>,
163    /// The request after all routing-time rewrites, stamped with the selected model.
164    pub request: Request,
165    /// A response produced while routing, or `None` when the client must make the answer call.
166    pub response: Option<Response>,
167    /// Outcome identity and optional algorithm evidence.
168    ///
169    /// Constructors leave this empty; [`Algorithm::run_stream`] fills it before publishing a
170    /// successful outcome.
171    pub metadata: Option<crate::OutcomeMetadata>,
172}
173
174impl RoutingOutcome {
175    /// The model the algorithm recommends, the best model for this request.
176    /// `LibsyError::NoTargets` if the algorithm selected no models, which should be impossible.
177    pub fn selected_model_id(&self) -> Result<&ModelId> {
178        self.selected_model_ids.first().ok_or(LibsyError::NoTargets)
179    }
180
181    /// The decision is that client should send this `request`. The `selected_model_id`
182    /// will be written into it by this function.
183    /// If that fails client should try the `fallback_models` in order.
184    pub fn route_to(
185        selected_model_id: ModelId,
186        fallback_models: Vec<ModelId>,
187        mut request: Request,
188    ) -> Self {
189        request.llm_request.model = Some(selected_model_id.to_string());
190        let mut selected_model_ids = Vec::with_capacity(1 + fallback_models.len());
191        selected_model_ids.push(selected_model_id);
192        selected_model_ids.extend(fallback_models);
193        Self {
194            selected_model_ids,
195            request,
196            response: None,
197            metadata: None,
198        }
199    }
200
201    /// Algorithm generated the response as part of the routing decision. Here it is.
202    /// The `request` will have the `selected_model_id` written into it by this function.
203    pub fn answered(selected_model_id: ModelId, mut request: Request, response: Response) -> Self {
204        request.llm_request.model = Some(selected_model_id.to_string());
205        Self {
206            selected_model_ids: vec![selected_model_id],
207            request,
208            response: Some(response),
209            metadata: None,
210        }
211    }
212}
213
214/// How an algorithm's [`route`](Algorithm::route) makes model calls.
215#[derive(Clone)]
216pub struct Driver {
217    step_tx: mpsc::Sender<Result<Step>>,
218
219    /// The owning algorithm's telemetry label, stamped onto every call this driver publishes.
220    algorithm: String,
221
222    /// Run-scoped evidence shared by driver clones and attached only to a successful outcome.
223    evidence: Arc<Mutex<Option<Value>>>,
224
225    /// Every group this run may route over, shared by all driver clones.
226    models: Arc<RuntimeModels>,
227
228    /// Which of those groups this driver reads.
229    scope: Scope,
230}
231
232impl Driver {
233    /// Build an empty driver with its step channel ready. Created per call by
234    /// [`run_stream`](Algorithm::run_stream). Also returns the Step receiver.
235    pub(crate) fn new(
236        algorithm: &str,
237        models: Arc<RuntimeModels>,
238    ) -> (Self, mpsc::Receiver<Result<Step>>) {
239        // Capacity one keeps the algorithm paced by the stream consumer. It limits queued steps,
240        // not model calls already pulled from the stream, which can still run at the same time.
241        // A larger buffer would use more memory and let the algorithm run farther ahead with
242        // little benefit because reading a step is cheap compared with serving a model call.
243        let (step_tx, step_rx) = mpsc::channel(1);
244        (
245            Self {
246                step_tx,
247                algorithm: algorithm.to_string(),
248                evidence: Arc::new(Mutex::new(None)),
249                models,
250                scope: Scope::Parent,
251            },
252            step_rx,
253        )
254    }
255
256    /// Replace the current run's evidence when a component makes the final decision.
257    pub(crate) fn set_evidence(&self, evidence: Value) {
258        *self.evidence.lock() = Some(evidence);
259    }
260
261    /// Supply fallback evidence without replacing a decision made earlier in the cascade.
262    pub(crate) fn set_evidence_if_empty(&self, evidence: Value) {
263        let mut current = self.evidence.lock();
264        if current.is_none() {
265            *current = Some(evidence);
266        }
267    }
268
269    /// Publish a model call and await the consumer's response.
270    ///
271    /// Errors if the stream is closed or the call failed.
272    /// The await is wrapped in a `libsy.llm_call` span measuring *fulfillment* as
273    /// the algorithm observes it (host queueing/serving included; a streamed
274    /// response resolves when its stream handle arrives). The host records call metrics
275    /// through [`CallModel::respond`] or [`CallModel::fail`]; outcome and token usage
276    /// are recorded on the span when the promise resolves. The provider call itself is the
277    /// host's, and is instrumented by whoever makes it.
278    #[tracing::instrument(
279        target = "libsy",
280        name = "libsy.llm_call",
281        skip_all,
282        fields(
283            algorithm = self.algorithm,
284            selected_model = %models.first().map(ModelId::as_str).unwrap_or("NoTargets"),
285            openinference.span.kind = "CHAIN",
286            outcome = tracing::field::Empty,
287            input_tokens = tracing::field::Empty,
288            output_tokens = tracing::field::Empty,
289            total_tokens = tracing::field::Empty,
290            reasoning_tokens = tracing::field::Empty,
291        )
292    )]
293    pub async fn call_model(&self, mut request: Request, models: Vec<ModelId>) -> Result<Response> {
294        let Some(selected_model_id) = models.first() else {
295            return Err(LibsyError::NoTargets);
296        };
297        request.llm_request.model = Some(selected_model_id.to_string());
298        let started = Instant::now();
299        let (reply, response) = oneshot::channel::<Result<Response>>();
300        let call = CallModel {
301            algorithm: self.algorithm.clone(),
302            request,
303            models,
304            reply: Some(reply),
305            started,
306        };
307        let result = async {
308            self.step_tx
309                .send(Ok(Step::CallModel(Box::new(call))))
310                .await
311                .map_err(|_| DriverError::StreamClosed)?;
312            response
313                .await
314                .map_err(|_| LibsyError::from(DriverError::ResponseDropped))?
315        }
316        .await;
317        observability::record_llm_call_span(&result, &tracing::Span::current());
318        result
319    }
320
321    /// The available models for this category, typically ordered best-first.
322    pub fn models_for(&self, category: &Category) -> &[ModelId] {
323        match self.scope {
324            Scope::Parent => self.models.models_for(category),
325            Scope::Subagent => self.models.subagent_models_for(category),
326        }
327    }
328
329    /// The first available model for `category`.
330    pub fn first_model_for(&self, category: &Category) -> Result<&ModelId> {
331        self.models_for(category)
332            .first()
333            .ok_or_else(|| LibsyError::AlgorithmError {
334                message: format!("no models available for category {}", category.as_str()),
335            })
336    }
337
338    /// A driver scoped to delegated sub-agent work: its categories are the
339    /// sub-agent's own, and the parent's are no longer reachable through it.
340    pub fn for_subagent(&self) -> Result<Self> {
341        if self.models.subagent.is_none() {
342            return Err(LibsyError::AlgorithmError {
343                message: "delegated work has no sub-agent models".to_string(),
344            });
345        }
346        Ok(Self {
347            scope: Scope::Subagent,
348            ..self.clone()
349        })
350    }
351
352    /// Emit the terminal step: [`Step::Done`] on `Ok`, or an `Err` stream
353    /// item on failure. Internal: called once by [`run_stream`](Algorithm::run_stream)
354    /// when the algorithm finishes.
355    pub(crate) async fn finish(&self, result: Result<RoutingOutcome>) -> Result<()> {
356        let result = result.map(|mut outcome| {
357            let metadata = outcome.metadata.get_or_insert_with(|| {
358                crate::OutcomeMetadata::new(self.algorithm.clone(), self.evidence.lock().take())
359            });
360            observability::record_outcome(metadata, &outcome.selected_model_ids);
361            outcome
362        });
363        let selected_model = result
364            .as_ref()
365            .ok()
366            .and_then(|outcome| outcome.selected_model_id().ok().cloned());
367        let step = result.map(|outcome| Step::Done(Box::new(outcome)));
368        self.step_tx
369            .send(step)
370            .await
371            .map_err(|_| DriverError::StreamClosed)?;
372        if let Some(selected_model) = selected_model {
373            observability::record_decision(&self.algorithm, &selected_model);
374        }
375        Ok(())
376    }
377}
378
379/// One item in the stream returned by [`Algorithm::run_stream`].
380pub enum Step {
381    /// The algorithm needs this model call performed. The host serves it and fulfills
382    /// it with [`CallModel::respond`]. Boxed: it is by far the largest variant.
383    CallModel(Box<CallModel>),
384    /// The algorithm finished with its routing outcome — the last step of a run.
385    Done(Box<RoutingOutcome>),
386}
387
388/// Drive [`Algorithm::run_stream`] to completion, handing each offloaded call to `serve`.
389///
390/// Returns the final [`RoutingOutcome`].
391/// `serve` owns the call: it performs it however the host likes and must fulfill the promise
392/// with [`CallModel::respond`]. A failed *model* call belongs in `respond` — the
393/// algorithm may route around it. To stop routing on a model-call failure, return
394/// [`CallModel::fail`] instead. Returning `Err` from `serve` aborts the whole run.
395/// Calls are served concurrently, so an algorithm that offloads several at once (hedging, fan-out)
396/// gets real parallelism.
397///
398/// libsy performs no I/O; this is only the mechanics of consuming its own step stream, kept
399/// here so every host does not reimplement the same loop. `switchyard-llm-client`'s `run`
400/// is this function plus an HTTP client.
401pub async fn drive<F, Fut>(
402    algorithm: Arc<dyn Algorithm>,
403    request: Request,
404    models: Arc<RuntimeModels>,
405    serve: F,
406) -> Result<RoutingOutcome>
407where
408    F: Fn(CallModel) -> Fut,
409    Fut: Future<Output = Result<()>>,
410{
411    let stream = algorithm.run_stream(request, models);
412    tokio::pin!(stream);
413
414    let mut in_flight = futures::stream::FuturesUnordered::new();
415    let mut final_outcome: Option<RoutingOutcome> = None;
416
417    loop {
418        tokio::select! {
419            Some(result) = in_flight.next() => match result {
420                Ok(()) => {}, // CallModel completed successfully
421                Err(err) => return Err(err), // CallModel failed, propagate the error
422            },
423            step = stream.next() => {
424                match step {
425                    None => break, // stream has ended, no more steps
426                    Some(item) => match item? {
427                        Step::CallModel(call) => in_flight.push(serve(*call)),
428                        Step::Done(outcome) => {
429                            final_outcome = Some(*outcome);
430                            break;
431                        }
432                    }
433                }
434            },
435        }
436    }
437    final_outcome.ok_or(LibsyError::MissingFinalResponse)
438}
439
440/// Recover the message from an algorithm's panic.
441fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
442    payload
443        .downcast_ref::<&'static str>()
444        .map(|message| (*message).to_string())
445        .or_else(|| payload.downcast_ref::<String>().cloned())
446        .unwrap_or_else(|| "unknown panic payload".to_string())
447}
448
449/// Abort guard
450struct AbortOnDrop(tokio::task::AbortHandle);
451
452impl Drop for AbortOnDrop {
453    fn drop(&mut self) {
454        self.0.abort();
455    }
456}
457
458/// Errors unless `targets` contains `name`.
459///
460/// Config target names must be resolved before an algorithm is built. This list contains
461/// model IDs, not target names.
462pub(crate) fn ensure_model_is_target(targets: &[ModelId], name: &ModelId) -> Result<()> {
463    targets
464        .iter()
465        .any(|target| target == name)
466        .then_some(())
467        .ok_or_else(|| LibsyError::TargetNotFound {
468            target: name.clone(),
469        })
470}
471
472/// Key for routing affinity: a root request by its session, a child request by its session
473/// and agent.
474#[derive(Clone, Hash, PartialEq, Eq)]
475pub(crate) enum RoutingIdentity {
476    /// Root request, keyed by session ID.
477    Session(String),
478    /// Child request, keyed by session and agent IDs.
479    Subagent { session: String, agent: String },
480}
481
482impl RoutingIdentity {
483    /// Builds a root or child identity from non-empty request metadata.
484    ///
485    /// A child request missing either ID returns `None`, so it keeps no routing history
486    /// rather than sharing the parent's.
487    pub(crate) fn from_request(request: &Request) -> Option<Self> {
488        let metadata = request.metadata.as_ref()?;
489        let session = metadata.session_id.as_deref().filter(|id| !id.is_empty())?;
490        if metadata.is_subagent {
491            let agent = metadata.agent_id.as_deref().filter(|id| !id.is_empty())?;
492            Some(Self::Subagent {
493                session: session.to_string(),
494                agent: agent.to_string(),
495            })
496        } else {
497            Some(Self::Session(session.to_string()))
498        }
499    }
500}
501
502/// An optimization strategy. Implement [`route`](Self::route);
503/// callers drive it with [`run_stream`](Self::run_stream), serving each [`Step::CallModel`]
504/// it emits. `switchyard-llm-client`'s `run` is the ready-made consumer that does this
505/// over HTTP.
506///
507/// Methods take `self: Arc<Self>`: one algorithm (`Arc<dyn Algorithm>`) is shared across
508/// requests and run concurrently, so it owns its thread-safety and any shared state.
509///
510/// # Concurrency
511///
512/// A host may run the same algorithm concurrently for many requests. Implementations
513/// must synchronize their own mutable shared state. Each call to [`run_stream`](Self::run_stream)
514/// creates an independent [`Driver`], so model-call promises and emitted [`Step`]s cannot
515/// cross between runs.
516///
517/// # Observability
518///
519/// [`run_stream`](Self::run_stream) creates a `libsy.run` span, and each offloaded model
520/// call creates a nested `libsy.llm_call` span. Successful outcomes record their
521/// [`OutcomeMetadata::outcome_id`](crate::OutcomeMetadata::outcome_id) on `libsy.run`,
522/// alongside `selected_model_ids` (an ordered OpenTelemetry string array).
523/// `algorithm` and `switchyard.algorithm` retain the run's [`Algorithm::name`].
524/// Optional `evidence.source`, `evidence.verdict`, `evidence.trigger`, and
525/// `evidence.reason_code` are strings; `evidence.score`, `evidence.confidence`, and
526/// `evidence.threshold` are numbers. Unknown evidence fields are not exported.
527/// These fields are span attributes, never metric labels.
528///
529/// The run/call observability helpers retain `outcome` status and operational metrics,
530/// but omit error details and arbitrary request extra metadata. Algorithms and hosts
531/// may emit their own logs. Errors still reach the caller unchanged.
532/// The host controls the tracing subscriber and global OpenTelemetry
533/// meter provider; libsy installs no exporter and performs no telemetry network I/O.
534#[async_trait]
535pub trait Algorithm: Send + Sync + 'static {
536    /// Stable, low-cardinality name identifying this algorithm — the
537    /// `algorithm` attribute on every span, metric, and log line the crate
538    /// emits for its runs.
539    fn name(&self) -> &str;
540
541    /// Run one request to completion: make routing-time model calls with
542    /// [`Driver::call_model`] and return the terminal [`RoutingOutcome`].
543    /// The method an algorithm implements; [`run_stream`](Self::run_stream) drives it.
544    async fn route(self: Arc<Self>, driver: Driver, request: Request) -> Result<RoutingOutcome>;
545
546    /// Process a request to completion, returning a stream of [`Step`]s.
547    ///
548    /// The consumer must fulfill every [`Step::CallModel`] before the algorithm can
549    /// continue. Every run ends with exactly one terminal item — [`Step::Done`] on
550    /// success, an `Err` item on failure, including when the algorithm panics. Dropping
551    /// the stream aborts the spawned algorithm task.
552    ///
553    /// Every invocation owns a separate [`Driver`].
554    fn run_stream(self: Arc<Self>, request: Request, models: Arc<RuntimeModels>) -> StepStream {
555        let (driver, step_rx) = Driver::new(self.name(), models);
556        let span = observability::run_span(self.name(), &request);
557        let handle = tokio::spawn(
558            async move {
559                let algorithm = self.name().to_string();
560                // Catch a panicking algorithm so the run still publishes a terminal step.
561                let route = AssertUnwindSafe(self.route(driver.clone(), request)).catch_unwind();
562                let result = observability::observe_run(&algorithm, async move {
563                    route.await.unwrap_or_else(|payload| {
564                        Err(LibsyError::AlgorithmError {
565                            message: format!(
566                                "algorithm task panicked: {}",
567                                panic_message(payload.as_ref())
568                            ),
569                        })
570                    })
571                })
572                .await;
573
574                let _ = driver.finish(result).await;
575            }
576            .instrument(span),
577        );
578        // Dropping the stream aborts the algorithm task when its consumer goes away.
579        let abort_guard = AbortOnDrop(handle.abort_handle());
580        Box::pin(ReceiverStream::new(step_rx).map(move |step| {
581            // link abort guard to stream
582            let _keep_alive = &abort_guard;
583            step
584        }))
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    use std::collections::HashMap;
591
592    use super::*;
593    use crate::core::testing::{Serve, ServeResult, echo, reply, test_drive};
594    use futures::StreamExt;
595    use switchyard_protocol::{
596        LlmResponse, LlmResponseChunk, completion_text, text_request, text_response,
597    };
598
599    #[derive(Debug, thiserror::Error)]
600    #[error("{0}")]
601    struct TestError(&'static str);
602
603    fn test_error(message: &'static str) -> LibsyError {
604        LibsyError::external("test", TestError(message))
605    }
606
607    /// Trivial algo used only to exercise the orchestrator: calls the first target
608    /// and returns its response as the routing outcome.
609    struct TestAlgo {
610        target_set: Vec<ModelId>,
611    }
612
613    #[async_trait]
614    impl Algorithm for TestAlgo {
615        fn name(&self) -> &str {
616            "test"
617        }
618
619        async fn route(
620            self: Arc<Self>,
621            driver: Driver,
622            request: Request,
623        ) -> Result<RoutingOutcome> {
624            let target = self
625                .target_set
626                .first()
627                .ok_or(LibsyError::NoTargets)?
628                .clone();
629            let response = driver
630                .call_model(request.clone(), vec![target.clone()])
631                .await?;
632            driver.set_evidence(serde_json::json!({"source": "test"}));
633            driver.set_evidence_if_empty(serde_json::json!({"source": "ignored"}));
634            Ok(RoutingOutcome::answered(target, request, response))
635        }
636    }
637
638    /// Build a shared `TestAlgo` over the given target set.
639    fn orch(target_set: Vec<ModelId>) -> Arc<dyn Algorithm> {
640        Arc::new(TestAlgo { target_set })
641    }
642
643    fn request() -> Request {
644        Request {
645            llm_request: text_request(Some("auto".to_string()), "hi".to_string()),
646            raw_request: None,
647            metadata: None,
648        }
649    }
650
651    #[test]
652    fn routing_outcome_constructors_stamp_selection_and_preserve_payloads() {
653        let outcome = RoutingOutcome::route_to(
654            "selected".into(),
655            target_set(&["fallback-one", "fallback-two"]),
656            request(),
657        );
658
659        assert_eq!(
660            outcome.selected_model_ids,
661            target_set(&["selected", "fallback-one", "fallback-two"])
662        );
663        assert_eq!(outcome.request.model_id().as_deref(), Some("selected"));
664        assert!(outcome.response.is_none());
665        assert!(outcome.metadata.is_none());
666
667        let outcome = RoutingOutcome::route_to("only".into(), Vec::new(), request());
668        assert_eq!(outcome.selected_model_ids, target_set(&["only"]));
669
670        let outcome = RoutingOutcome::answered(
671            "answered".into(),
672            request(),
673            Response {
674                llm_response: LlmResponse::Agg(text_response(None, "existing")),
675                metadata: None,
676                upstream_headers: http::HeaderMap::new(),
677            },
678        );
679
680        assert_eq!(outcome.selected_model_ids, target_set(&["answered"]));
681        assert_eq!(outcome.request.model_id().as_deref(), Some("answered"));
682        assert_eq!(
683            outcome
684                .response
685                .as_ref()
686                .and_then(|response| response.llm_response.as_agg())
687                .map(completion_text),
688            Some("existing".to_string())
689        );
690    }
691
692    fn target_set(names: &[&str]) -> Vec<ModelId> {
693        names.iter().map(|name| ModelId::from(*name)).collect()
694    }
695
696    #[tokio::test]
697    async fn typed_driver_preserves_call_and_stream_boundaries() -> Result<()> {
698        tokio::time::timeout(std::time::Duration::from_secs(1), async {
699            // Distinct oneshots keep reverse-order replies paired with their producers, and a
700            // retained call remains pending until the host responds.
701            let (driver, mut step_rx) = Driver::new("test", Arc::new(RuntimeModels::default()));
702            let first_driver = driver.clone();
703            let mut first = tokio::spawn(async move {
704                first_driver
705                    .call_model(request(), vec![ModelId::from("first")])
706                    .await
707            });
708            let second = tokio::spawn(async move {
709                driver
710                    .call_model(request(), vec![ModelId::from("second")])
711                    .await
712            });
713
714            let mut calls = HashMap::new();
715            for _ in 0..2 {
716                let step = step_rx.recv().await.ok_or(DriverError::StreamClosed)??;
717                let Step::CallModel(call) = step else {
718                    return Err(test_error("expected a CallModel step"));
719                };
720                let selected_model = call
721                    .models
722                    .first()
723                    .ok_or_else(|| test_error("model call has no candidates"))?
724                    .to_string();
725                calls.insert(selected_model, call);
726            }
727            assert!(
728                tokio::time::timeout(std::time::Duration::from_millis(20), &mut first)
729                    .await
730                    .is_err(),
731                "call completed before the host responded"
732            );
733            calls
734                .remove("second")
735                .ok_or_else(|| test_error("missing second call"))?
736                .respond(Ok(reply("second response")))?;
737            calls
738                .remove("first")
739                .ok_or_else(|| test_error("missing first call"))?
740                .respond(Ok(reply("first response")))?;
741
742            let first_response = first
743                .await
744                .map_err(|source| LibsyError::external("joining a test task", source))??;
745            let second_response = second
746                .await
747                .map_err(|source| LibsyError::external("joining a test task", source))??;
748            assert_eq!(
749                first_response.llm_response.as_agg().map(completion_text),
750                Some("first response".to_string())
751            );
752            assert_eq!(
753                second_response.llm_response.as_agg().map(completion_text),
754                Some("second response".to_string())
755            );
756
757            // Dropping the host-facing promise closes only that call's reply channel.
758            let (driver, mut step_rx) = Driver::new("test", Arc::new(RuntimeModels::default()));
759            let producer = tokio::spawn(async move {
760                driver
761                    .call_model(request(), vec![ModelId::from("dropped")])
762                    .await
763            });
764            let step = step_rx.recv().await.ok_or(DriverError::StreamClosed)??;
765            let Step::CallModel(call) = step else {
766                return Err(test_error("expected a CallModel step"));
767            };
768            drop(call);
769            let result = producer
770                .await
771                .map_err(|source| LibsyError::external("joining a test task", source))?;
772            assert!(matches!(
773                result,
774                Err(LibsyError::Driver(DriverError::ResponseDropped))
775            ));
776
777            // A standalone driver reports the typed step receiver disappearing at its next call.
778            let (driver, step_rx) = Driver::new("test", Arc::new(RuntimeModels::default()));
779            drop(step_rx);
780            let result = driver
781                .call_model(request(), vec![ModelId::from("closed")])
782                .await;
783            assert!(matches!(
784                result,
785                Err(LibsyError::Driver(DriverError::StreamClosed))
786            ));
787            Ok(())
788        })
789        .await
790        .map_err(|error| LibsyError::external("waiting for typed driver boundaries", error))?
791    }
792
793    #[test]
794    fn target_lookup_returns_the_missing_target() {
795        let error = ensure_model_is_target(&target_set(&[]), &ModelId::from("missing")).err();
796        assert!(matches!(
797            error,
798            Some(LibsyError::TargetNotFound { target }) if target == "missing"
799        ));
800    }
801
802    /// Build a single-target algo, plus a `serve` that answers it as a token stream
803    /// replaying `chunks` in order (as `Ok` items).
804    fn streaming_orch(chunks: Vec<LlmResponseChunk>) -> (Arc<dyn Algorithm>, impl Serve) {
805        let algo = orch(target_set(&["stream/model"]));
806        let serve = move |_target: ModelId, _request: Request| {
807            let chunks = chunks.clone();
808            async move {
809                let stream =
810                    futures::stream::iter(chunks.into_iter().map(|chunk| Ok(chunk.into()))).boxed();
811                Ok(Response {
812                    llm_response: LlmResponse::Stream(stream),
813                    metadata: None,
814                    upstream_headers: http::HeaderMap::new(),
815                })
816            }
817        };
818        (algo, serve)
819    }
820
821    #[tokio::test]
822    async fn run_returns_a_streamed_response_the_caller_aggregates() -> Result<()> {
823        // A streaming client -> its chunks flow through the promise and `Done`,
824        // and `run` returns the live stream untouched for the caller to fold.
825        let (orch, serve) = streaming_orch(vec![
826            LlmResponseChunk::MessageStart {
827                id: Some("m1".to_string()),
828                model: Some("stream/model".to_string()),
829            },
830            LlmResponseChunk::TextDelta {
831                index: 0,
832                text: "hel".to_string(),
833            },
834            LlmResponseChunk::TextDelta {
835                index: 0,
836                text: "lo".to_string(),
837            },
838            LlmResponseChunk::MessageStop {
839                reason: Some("stop".to_string()),
840            },
841        ]);
842        let (selected_model, response) = test_drive(orch, request(), serve).await?;
843        // The run handed back the live stream; the caller folds it to a buffered aggregate.
844        let agg = response
845            .llm_response
846            .into_agg()
847            .await
848            .map_err(|error| LibsyError::external("aggregating response stream", error))?;
849        assert_eq!(completion_text(&agg), "hello");
850        assert_eq!(agg.model.as_deref(), Some("stream/model"));
851        assert_eq!(selected_model, "stream/model");
852        Ok(())
853    }
854
855    #[tokio::test]
856    async fn aggregating_a_streamed_response_propagates_a_mid_stream_error() -> Result<()> {
857        // The run succeeds and returns the stream; the in-band `Error` chunk surfaces only
858        // when the caller aggregates it.
859        let (orch, serve) = streaming_orch(vec![
860            LlmResponseChunk::TextDelta {
861                index: 0,
862                text: "partial".to_string(),
863            },
864            LlmResponseChunk::StreamError {
865                message: "upstream exploded".to_string(),
866            },
867        ]);
868        let (_, response) = test_drive(orch, request(), serve).await?;
869        match response.llm_response.into_agg().await {
870            Ok(_) => panic!("expected a mid-stream error, got an aggregate"),
871            Err(err) => {
872                assert!(err.to_string().contains("upstream exploded"));
873                Ok(())
874            }
875        }
876    }
877
878    #[tokio::test]
879    async fn run_offloads_via_promise_then_finishes() -> Result<()> {
880        // Every call is offloaded via a promise the orchestrator surfaces as a
881        // `CallModel` step for us to fulfill.
882        let stream = orch(target_set(&["offload/model"]))
883            .run_stream(request(), Arc::new(RuntimeModels::default()));
884        tokio::pin!(stream);
885
886        let mut saw_call = false;
887        let mut final_completion = None;
888        while let Some(step) = stream.next().await {
889            match step? {
890                Step::CallModel(call) => {
891                    saw_call = true;
892                    assert_eq!(call.models, vec![ModelId::from("offload/model")]);
893                    // Fulfilling the promise is the "real" model call the caller makes.
894                    call.respond(Ok(Response {
895                        llm_response: LlmResponse::Agg(text_response(
896                            None,
897                            "fulfilled".to_string(),
898                        )),
899                        metadata: None,
900                        upstream_headers: http::HeaderMap::new(),
901                    }))?;
902                }
903                Step::Done(outcome) => {
904                    let metadata = outcome
905                        .metadata
906                        .as_ref()
907                        .expect("run_stream should attach outcome metadata");
908                    assert_eq!(metadata.algorithm, "test");
909                    assert_eq!(
910                        uuid::Uuid::parse_str(metadata.outcome_id())
911                            .expect("outcome id should be a UUID")
912                            .get_version_num(),
913                        7
914                    );
915                    assert_eq!(
916                        metadata.evidence,
917                        Some(serde_json::json!({"source": "test"}))
918                    );
919                    let response = outcome
920                        .response
921                        .ok_or_else(|| test_error("expected an answered outcome"))?;
922                    final_completion = Some(
923                        response
924                            .llm_response
925                            .as_agg()
926                            .map(completion_text)
927                            .unwrap_or_default(),
928                    );
929                }
930            }
931        }
932
933        assert!(saw_call, "expected a CallModel step before Done");
934        assert_eq!(
935            final_completion.ok_or_else(|| test_error("no Done step"))?,
936            "fulfilled"
937        );
938        Ok(())
939    }
940
941    #[tokio::test(flavor = "multi_thread", worker_threads = 12)]
942    async fn requests_are_processed_in_parallel() -> Result<()> {
943        use std::time::Duration;
944        use tokio::sync::Barrier;
945
946        const N: usize = 12;
947
948        // Serving blocks until all N concurrent calls have arrived. If requests were
949        // serialized (one algorithm behind a `Mutex`), only one call could be in flight,
950        // the barrier would never reach N, and the test would time out. It passes only
951        // because the shared algorithm is driven concurrently across requests.
952        let barrier = Arc::new(Barrier::new(N));
953        // One shared algorithm driven by many concurrent requests.
954        let algo = orch(target_set(&["m"]));
955
956        let mut handles = Vec::new();
957        for _ in 0..N {
958            let algo = algo.clone();
959            let barrier = barrier.clone();
960            let serve = move |target: ModelId, _request: Request| {
961                let barrier = barrier.clone();
962                async move {
963                    barrier.wait().await;
964                    Ok(reply(target))
965                }
966            };
967            handles.push(tokio::spawn(async move {
968                test_drive(algo, request(), serve)
969                    .await
970                    .map(|(_, response)| {
971                        response
972                            .llm_response
973                            .as_agg()
974                            .map(completion_text)
975                            .unwrap_or_default()
976                    })
977            }));
978        }
979
980        for handle in handles {
981            // The timeout turns a serialization deadlock into a failure, not a hang.
982            let completion = tokio::time::timeout(Duration::from_secs(5), handle)
983                .await
984                .map_err(|error| LibsyError::external("waiting for test task", error))?
985                .map_err(|source| LibsyError::external("joining a test task", source))??;
986            assert_eq!(completion, "m");
987        }
988        Ok(())
989    }
990
991    #[tokio::test]
992    async fn offload_error_propagates_back_to_the_algorithm() -> Result<()> {
993        // A client-less target offloads its call; we fulfill the promise with an
994        // Err, which must flow back through `call_model_target` into the algorithm and
995        // out as an error step — not a response.
996        let stream = orch(target_set(&["offload/model"]))
997            .run_stream(request(), Arc::new(RuntimeModels::default()));
998        tokio::pin!(stream);
999
1000        let mut saw_error = false;
1001        while let Some(step) = stream.next().await {
1002            match step {
1003                Ok(Step::CallModel(call)) => {
1004                    call.respond(Err(test_error("upstream model call failed")))?;
1005                }
1006                Ok(Step::Done(..)) => {
1007                    return Err(test_error(
1008                        "expected the offload error to propagate, got a response",
1009                    ));
1010                }
1011                Err(err) => {
1012                    // The algorithm's `call_model_target` saw the error via the promise.
1013                    assert!(err.to_string().contains("upstream model call failed"));
1014                    saw_error = true;
1015                }
1016            }
1017        }
1018
1019        assert!(saw_error, "expected an error step");
1020        Ok(())
1021    }
1022
1023    #[tokio::test]
1024    async fn dropping_the_stream_cancels_the_algorithm_task() -> Result<()> {
1025        use std::sync::atomic::{AtomicBool, Ordering};
1026        use std::time::Duration;
1027        use tokio::sync::mpsc;
1028
1029        // Sets a flag when dropped, so we can observe whether the algorithm task was
1030        // cancelled/dropped.
1031        struct DropGuard(Arc<AtomicBool>);
1032        impl Drop for DropGuard {
1033            fn drop(&mut self) {
1034                self.0.store(true, Ordering::SeqCst);
1035            }
1036        }
1037
1038        struct StuckAlgo {
1039            started: mpsc::UnboundedSender<()>,
1040            dropped: Arc<AtomicBool>,
1041        }
1042
1043        #[async_trait]
1044        impl Algorithm for StuckAlgo {
1045            fn name(&self) -> &str {
1046                "stuck"
1047            }
1048
1049            async fn route(
1050                self: Arc<Self>,
1051                _driver: Driver,
1052                _request: Request,
1053            ) -> Result<RoutingOutcome> {
1054                let _guard = DropGuard(self.dropped.clone());
1055                let _ = self.started.send(());
1056                // Await forever without ever touching the driver.
1057                std::future::pending::<()>().await;
1058                unreachable!()
1059            }
1060        }
1061
1062        let (started_tx, mut started_rx) = mpsc::unbounded_channel();
1063        let dropped = Arc::new(AtomicBool::new(false));
1064        let algo: Arc<dyn Algorithm> = Arc::new(StuckAlgo {
1065            started: started_tx,
1066            dropped: dropped.clone(),
1067        });
1068
1069        let stream = algo.run_stream(request(), Arc::new(RuntimeModels::default()));
1070        started_rx
1071            .recv()
1072            .await
1073            .ok_or_else(|| test_error("task never started"))?;
1074        drop(stream);
1075        tokio::time::sleep(Duration::from_millis(100)).await;
1076
1077        assert!(
1078            dropped.load(Ordering::SeqCst),
1079            "algorithm task was NOT cancelled after dropping the stream"
1080        );
1081        Ok(())
1082    }
1083
1084    #[tokio::test]
1085    async fn route_panic_surfaces_as_a_stream_error() -> Result<()> {
1086        // An algorithm whose task panics must surface an `Err` step carrying the panic
1087        // message, not abort the process from an unobserved detached task.
1088        struct Panicky;
1089
1090        #[async_trait]
1091        impl Algorithm for Panicky {
1092            fn name(&self) -> &str {
1093                "panicky"
1094            }
1095
1096            async fn route(
1097                self: Arc<Self>,
1098                _driver: Driver,
1099                _request: Request,
1100            ) -> Result<RoutingOutcome> {
1101                panic!("boom");
1102            }
1103        }
1104
1105        let algo: Arc<dyn Algorithm> = Arc::new(Panicky);
1106        let stream = algo.run_stream(request(), Arc::new(RuntimeModels::default()));
1107        tokio::pin!(stream);
1108
1109        let mut saw_error = false;
1110        while let Some(step) = stream.next().await {
1111            match step {
1112                Err(err) => {
1113                    // The panic message is preserved, not flattened into an opaque failure.
1114                    assert!(err.to_string().contains("algorithm task panicked: boom"));
1115                    saw_error = true;
1116                }
1117                Ok(_) => return Err(test_error("expected the panic to surface as an error step")),
1118            }
1119        }
1120
1121        assert!(saw_error, "expected an error step from the panicked task");
1122        Ok(())
1123    }
1124
1125    /// A panicking algorithm must publish its terminal step even when it left a `Driver`
1126    /// clone alive in another task. That clone holds the step channel open, so a run that
1127    /// merely unwound would never terminate and the consumer would wait forever.
1128    #[tokio::test]
1129    async fn a_panic_with_a_leaked_driver_clone_still_terminates_the_run() -> Result<()> {
1130        struct LeakyPanic;
1131
1132        #[async_trait]
1133        impl Algorithm for LeakyPanic {
1134            fn name(&self) -> &str {
1135                "leaky_panic"
1136            }
1137
1138            async fn route(
1139                self: Arc<Self>,
1140                driver: Driver,
1141                _request: Request,
1142            ) -> Result<RoutingOutcome> {
1143                tokio::spawn(async move {
1144                    // Outlives the panic below, keeping a sender clone alive.
1145                    let _keep_alive = driver;
1146                    std::future::pending::<()>().await;
1147                });
1148                tokio::task::yield_now().await;
1149                panic!("boom");
1150            }
1151        }
1152
1153        let algo: Arc<dyn Algorithm> = Arc::new(LeakyPanic);
1154        // The timeout turns the hang this guards against into a failure rather than a hang.
1155        let result = tokio::time::timeout(
1156            std::time::Duration::from_secs(1),
1157            test_drive(algo, request(), echo()),
1158        )
1159        .await
1160        .map_err(|error| LibsyError::external("waiting for the panicked run to end", error))?;
1161
1162        match result {
1163            Ok(_) => Err(test_error(
1164                "expected the panic to end the run with an error",
1165            )),
1166            Err(err) => {
1167                assert!(err.to_string().contains("algorithm task panicked: boom"));
1168                Ok(())
1169            }
1170        }
1171    }
1172
1173    #[tokio::test]
1174    async fn cancelling_run_cancels_the_algorithm_task() -> Result<()> {
1175        use std::sync::atomic::{AtomicBool, Ordering};
1176        use std::time::Duration;
1177        use tokio::sync::mpsc;
1178
1179        // Sets a flag when dropped, so we can observe whether the algorithm task was
1180        // cancelled once the `run` future driving it is dropped.
1181        struct DropGuard(Arc<AtomicBool>);
1182        impl Drop for DropGuard {
1183            fn drop(&mut self) {
1184                self.0.store(true, Ordering::SeqCst);
1185            }
1186        }
1187
1188        struct StuckAlgo {
1189            started: mpsc::UnboundedSender<()>,
1190            dropped: Arc<AtomicBool>,
1191        }
1192
1193        #[async_trait]
1194        impl Algorithm for StuckAlgo {
1195            fn name(&self) -> &str {
1196                "stuck"
1197            }
1198
1199            async fn route(
1200                self: Arc<Self>,
1201                _driver: Driver,
1202                _request: Request,
1203            ) -> Result<RoutingOutcome> {
1204                let _guard = DropGuard(self.dropped.clone());
1205                let _ = self.started.send(());
1206                // Hang forever without ever touching the driver, so only cancellation
1207                // (not a dropped step channel) can stop this task.
1208                std::future::pending::<()>().await;
1209                unreachable!()
1210            }
1211        }
1212
1213        let (started_tx, mut started_rx) = mpsc::unbounded_channel();
1214        let dropped = Arc::new(AtomicBool::new(false));
1215        let algo: Arc<dyn Algorithm> = Arc::new(StuckAlgo {
1216            started: started_tx,
1217            dropped: dropped.clone(),
1218        });
1219
1220        // Drive the run on its own task, wait until the algorithm task is up, then cancel
1221        // it — dropping its future (and the `run_stream` stream it holds).
1222        let run_task = tokio::spawn(async move { test_drive(algo, request(), echo()).await });
1223        started_rx
1224            .recv()
1225            .await
1226            .ok_or_else(|| test_error("task never started"))?;
1227        run_task.abort();
1228        tokio::time::sleep(Duration::from_millis(100)).await;
1229
1230        assert!(
1231            dropped.load(Ordering::SeqCst),
1232            "algorithm task was NOT cancelled after cancelling run"
1233        );
1234        Ok(())
1235    }
1236
1237    // --- first-wins hedging: `run` must not wait on losing speculative calls -------------
1238
1239    /// Offloads two targets concurrently and returns the first to resolve, dropping the
1240    /// loser's call (first-wins hedging).
1241    struct Hedge {
1242        winner: String,
1243        loser: String,
1244    }
1245
1246    #[async_trait]
1247    impl Algorithm for Hedge {
1248        fn name(&self) -> &str {
1249            "hedge"
1250        }
1251
1252        async fn route(
1253            self: Arc<Self>,
1254            driver: Driver,
1255            request: Request,
1256        ) -> Result<RoutingOutcome> {
1257            let outcome_request = request.clone();
1258            let win = driver.call_model(request.clone(), vec![self.winner.clone().into()]);
1259            let lose = driver.call_model(request, vec![self.loser.clone().into()]);
1260            // First to resolve wins; `select!` drops the losing future (and its promise).
1261            tokio::select! {
1262                res = win => Ok(RoutingOutcome::answered(
1263                    self.winner.clone().into(),
1264                    outcome_request,
1265                    res?,
1266                )),
1267                res = lose => Ok(RoutingOutcome::answered(
1268                    self.loser.clone().into(),
1269                    outcome_request,
1270                    res?,
1271                )),
1272            }
1273        }
1274    }
1275
1276    /// Builds a hedging algo and the `serve` that drives it: the winner is gated behind the
1277    /// loser starting (so the loser's serve is guaranteed in flight when the winner wins),
1278    /// and the loser finishes after `loser_delay` — or never, when `None`.
1279    fn hedge(loser_delay: Option<std::time::Duration>) -> (Arc<dyn Algorithm>, impl Serve) {
1280        let started = Arc::new(tokio::sync::Notify::new());
1281        let algo = Arc::new(Hedge {
1282            winner: "winner".to_string(),
1283            loser: "loser".to_string(),
1284        });
1285        let serve = move |target: ModelId, _request: Request| {
1286            let started = started.clone();
1287            async move {
1288                if target == "loser" {
1289                    started.notify_one();
1290                    match loser_delay {
1291                        Some(delay) => tokio::time::sleep(delay).await,
1292                        None => std::future::pending::<()>().await,
1293                    }
1294                } else {
1295                    started.notified().await;
1296                }
1297                Ok(reply(target))
1298            }
1299        };
1300        (algo, serve)
1301    }
1302
1303    #[tokio::test]
1304    async fn run_returns_the_winner_without_a_late_loser_overwriting_it() -> Result<()> {
1305        // The loser responds 50ms after the winner has already won. `run` must return the
1306        // winner, not the loser's `respond`-to-a-dropped-receiver error.
1307        let (algo, serve) = hedge(Some(std::time::Duration::from_millis(50)));
1308        let (_, response) = test_drive(algo, request(), serve).await?;
1309        assert_eq!(
1310            response
1311                .llm_response
1312                .as_agg()
1313                .map(completion_text)
1314                .unwrap_or_default(),
1315            "winner"
1316        );
1317        Ok(())
1318    }
1319
1320    #[tokio::test]
1321    async fn run_returns_the_winner_without_hanging_on_a_pending_loser() -> Result<()> {
1322        // The loser never resolves. `run` must return the winner promptly, not hang
1323        // waiting for the in-flight loser.
1324        let (algo, serve) = hedge(None);
1325        let run = test_drive(algo, request(), serve);
1326        let (_, response) = tokio::time::timeout(std::time::Duration::from_secs(1), run)
1327            .await
1328            .map_err(|error| LibsyError::external("waiting for pending loser", error))??;
1329        assert_eq!(
1330            response
1331                .llm_response
1332                .as_agg()
1333                .map(completion_text)
1334                .unwrap_or_default(),
1335            "winner"
1336        );
1337        Ok(())
1338    }
1339
1340    #[tokio::test]
1341    async fn run_surfaces_a_terminal_error_with_many_calls_in_flight() -> Result<()> {
1342        use std::sync::atomic::{AtomicUsize, Ordering};
1343
1344        // A large fan-out (10 matched the old, now-removed concurrency cap). The terminal
1345        // error must still reach the caller with all of these calls pending.
1346        const N: usize = 10;
1347
1348        // Fans out N calls, then errors as soon as all N are in flight — exercising a
1349        // terminal failure emitted while the offloaded calls are still pending.
1350        struct FanOutThenError {
1351            all_started: Arc<tokio::sync::Notify>,
1352            n: usize,
1353        }
1354
1355        #[async_trait]
1356        impl Algorithm for FanOutThenError {
1357            fn name(&self) -> &str {
1358                "fan_out_then_error"
1359            }
1360
1361            async fn route(
1362                self: Arc<Self>,
1363                driver: Driver,
1364                request: Request,
1365            ) -> Result<RoutingOutcome> {
1366                let offloads = futures::future::join_all(
1367                    (0..self.n)
1368                        .map(|i| driver.call_model(request.clone(), vec![format!("m{i}").into()])),
1369                );
1370                tokio::select! {
1371                    _ = offloads => Err(test_error("offloads unexpectedly completed")),
1372                    _ = self.all_started.notified() => {
1373                        Err(test_error("terminal error while calls pending"))
1374                    }
1375                }
1376            }
1377        }
1378
1379        let all_started = Arc::new(tokio::sync::Notify::new());
1380        let algo: Arc<dyn Algorithm> = Arc::new(FanOutThenError {
1381            all_started: all_started.clone(),
1382            n: N,
1383        });
1384
1385        // Serving enters each call; once all N are in flight it signals, then pends forever.
1386        let started = Arc::new(AtomicUsize::new(0));
1387        let serve = move |_target: ModelId, _request: Request| {
1388            let started = started.clone();
1389            let all_started = all_started.clone();
1390            async move {
1391                if started.fetch_add(1, Ordering::SeqCst) + 1 == N {
1392                    all_started.notify_one();
1393                }
1394                std::future::pending::<ServeResult>().await
1395            }
1396        };
1397
1398        // With the cap gone, the driver keeps polling the stream even with N calls in
1399        // flight, so the terminal error surfaces promptly instead of hanging.
1400        let run = test_drive(algo, request(), serve);
1401        let result = tokio::time::timeout(std::time::Duration::from_millis(500), run)
1402            .await
1403            .map_err(|error| {
1404                LibsyError::external("waiting for terminal error with full call cap", error)
1405            })?;
1406        match result {
1407            Ok(_) => Err(test_error("expected the terminal error, got a response")),
1408            Err(err) => {
1409                assert!(
1410                    err.to_string()
1411                        .contains("terminal error while calls pending")
1412                );
1413                Ok(())
1414            }
1415        }
1416    }
1417}