Skip to main content

switchyard_libsy/
error.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Typed failures surfaced by libsy's orchestration APIs.
5
6use std::error::Error as StdError;
7
8use switchyard_protocol::{LlmClientError, ModelId};
9use thiserror::Error;
10
11/// Result type returned by libsy APIs.
12pub type Result<T> = std::result::Result<T, LibsyError>;
13
14/// Failures surfaced while selecting a route, driving an algorithm, or serving a model call.
15#[derive(Debug, Error)]
16pub enum LibsyError {
17    /// A named target was not present in the configured target set.
18    #[error("target {target:?} was not found")]
19    TargetNotFound {
20        /// Missing target model id.
21        target: ModelId,
22    },
23
24    /// Routing was attempted without any configured targets.
25    #[error("no routing targets are configured")]
26    NoTargets,
27
28    /// An algorithm could not complete for an algorithm-specific reason.
29    #[error("{message}")]
30    AlgorithmError {
31        /// Description of the algorithm failure.
32        message: String,
33    },
34
35    /// The step-stream driver could not complete an operation.
36    #[error(transparent)]
37    Driver(#[from] DriverError),
38
39    /// An algorithm's step stream ended without a terminal routing outcome.
40    #[error("algorithm run ended without a routing outcome")]
41    MissingFinalResponse,
42
43    /// A target's protocol client failed while serving a routed request.
44    #[error("client call to target {target:?} failed: {source}")]
45    ClientCall {
46        /// Target whose client failed.
47        target: ModelId,
48        /// Typed error supplied by the protocol-owned client trait.
49        #[source]
50        source: LlmClientError,
51    },
52
53    /// A target was skipped because its endpoint circuit is open.
54    #[error("target {target:?} is unavailable because its circuit breaker is open")]
55    CircuitOpen {
56        /// Target whose endpoint is being skipped.
57        target: ModelId,
58    },
59
60    /// Both VGR serving tiers were unavailable for the same request.
61    #[error("both VGR tiers are unavailable: local: {local}; cloud: {cloud}")]
62    VgrTiersUnavailable {
63        /// Failure from the local attempt.
64        local: Box<LibsyError>,
65        /// Failure from the terminal cloud completion.
66        cloud: Box<LibsyError>,
67    },
68
69    /// A user extension or other foreign operation failed.
70    #[error("{operation} failed: {source}")]
71    External {
72        /// Short description of the operation that failed.
73        operation: &'static str,
74        /// Original failure.
75        #[source]
76        source: Box<dyn StdError + Send + Sync>,
77    },
78}
79
80impl LibsyError {
81    /// Wrap an error returned by the protocol-owned client trait.
82    pub fn client_call(target: impl Into<ModelId>, source: LlmClientError) -> Self {
83        Self::ClientCall {
84            target: target.into(),
85            source,
86        }
87    }
88
89    /// Preserve a concrete foreign error with a description of the failed operation.
90    pub fn external(
91        operation: &'static str,
92        source: impl StdError + Send + Sync + 'static,
93    ) -> Self {
94        Self::External {
95            operation,
96            source: Box::new(source),
97        }
98    }
99}
100
101/// Failures in the step-stream driver.
102#[derive(Debug, Error)]
103pub enum DriverError {
104    /// The consumer side of the step channel was dropped.
105    #[error("driver stream is closed")]
106    StreamClosed,
107
108    /// One side of a response promise was dropped before delivery.
109    #[error("driver response promise was dropped")]
110    ResponseDropped,
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn client_call_preserves_target_and_source() {
119        let error = LibsyError::client_call(
120            "strong",
121            LlmClientError::General("upstream down".to_string()),
122        );
123        match &error {
124            LibsyError::ClientCall { target, source } => {
125                assert_eq!(target, "strong");
126                assert_eq!(source.to_string(), "upstream down");
127            }
128            other => panic!("expected ClientCall, got {other:?}"),
129        }
130        assert_eq!(
131            StdError::source(&error).map(ToString::to_string),
132            Some("upstream down".to_string())
133        );
134    }
135
136    #[test]
137    fn external_preserves_operation_and_source() {
138        let error = LibsyError::external(
139            "loading extension",
140            std::io::Error::other("bad configuration"),
141        );
142        assert!(matches!(
143            &error,
144            LibsyError::External { operation, .. } if *operation == "loading extension"
145        ));
146        assert_eq!(
147            StdError::source(&error).map(ToString::to_string),
148            Some("bad configuration".to_string())
149        );
150    }
151}