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