Skip to main content

smos_application/errors/
upstream_error.rs

1//! Upstream (LLM proxy) errors.
2//!
3//! Returned by the OpenAI-compatible HTTP upstream adapter. The shape mirrors
4//! a typical HTTP client error surface: connection, timeout, status code +
5//! body, stream error during SSE, and (de)serialisation issues.
6
7use std::time::Duration;
8use thiserror::Error;
9
10/// Errors returned by the LLM upstream adapter.
11#[derive(Debug, Error)]
12pub enum UpstreamError {
13    #[error("upstream connect failed: {0}")]
14    ConnectFailed(String),
15
16    #[error("upstream timeout after {0:?}")]
17    Timeout(Duration),
18
19    #[error("upstream returned {status}: {body}")]
20    StatusError { status: u16, body: String },
21
22    #[error("upstream stream error: {0}")]
23    StreamError(String),
24
25    /// Upstream returned a 2xx body that the adapter could not parse (non-JSON
26    /// or malformed). Distinct from `SerializationError` (which is an SMOS-side
27    /// request-encoding bug) so the HTTP layer can map it to 502 (upstream's
28    /// fault) rather than 500 (our fault).
29    #[error("upstream returned an unparseable body: {0}")]
30    BadResponse(String),
31
32    #[error("upstream serialization error: {0}")]
33    SerializationError(String),
34
35    /// Returned by `ReqwestUpstreamRouter::complete` when the named
36    /// provider is missing from the `[[providers]]` map. Should be
37    /// unreachable in practice (the routing layer validates the
38    /// person → provider reference at startup), but is retained for
39    /// defensive depth: a hot config edit could otherwise produce a
40    /// confusing 404-from-the-upstream instead of a clear 502.
41    ///
42    /// Historically this variant was constructed by the round-robin /
43    /// failover pool's failover path when every configured provider
44    /// failed; the new per-person router does not exercise that path,
45    /// so the variant is now produced only by the unknown-provider
46    /// branch above. Kept (rather than removed) because the HTTP error
47    /// mapper still classifies it as 502 and the type is part of the
48    /// public API surface (`pub enum`).
49    #[error("all upstream providers failed; last error: {0}")]
50    AllProvidersFailed(String),
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn status_error_display_shows_code_and_body() {
59        let e = UpstreamError::StatusError {
60            status: 503,
61            body: "service unavailable".into(),
62        };
63        let msg = e.to_string();
64        assert!(msg.contains("503"));
65        assert!(msg.contains("service unavailable"));
66    }
67
68    #[test]
69    fn timeout_display_uses_debug_format() {
70        let e = UpstreamError::Timeout(Duration::from_secs(5));
71        assert!(e.to_string().contains("5s"));
72    }
73}