Skip to main content

lc_providers/
error.rs

1// lc-providers/src/error.rs
2//! Unified error type for the lc-providers crate.
3//!
4//! Aggregates all provider-specific error types so the `?` operator works
5//! seamlessly across provider boundaries.
6
7pub use crate::ollama::OllamaError;
8pub use crate::openai::responses::types::ResponsesError;
9pub use crate::openai::AssistantError;
10pub use crate::openai::OpenAIError;
11pub use crate::providers::anthropic::error::AnthropicError;
12pub use crate::providers::azure::AzureOpenAIError;
13pub use crate::providers::cohere::CohereError;
14pub use crate::providers::gemini::GeminiError;
15
16/// Unified error type that aggregates all LLM provider errors.
17///
18/// This allows using `?` across provider boundaries without manually
19/// mapping error types. Each variant wraps the original provider
20/// error, preserving full context.
21#[derive(Debug)]
22#[non_exhaustive]
23pub enum ProviderError {
24    /// OpenAI API error.
25    OpenAI(OpenAIError),
26    /// Anthropic API error.
27    Anthropic(AnthropicError),
28    /// Gemini API error.
29    Gemini(GeminiError),
30    /// Azure OpenAI API error.
31    Azure(AzureOpenAIError),
32    /// Cohere API error.
33    Cohere(CohereError),
34    /// Ollama API error.
35    Ollama(OllamaError),
36    /// OpenAI Assistants API error.
37    Assistant(AssistantError),
38    /// OpenAI Responses API error.
39    Responses(ResponsesError),
40    /// DeepSeek API error (OpenAI-compatible endpoint).
41    DeepSeek(OpenAIError),
42    /// Qwen (Alibaba) API error (OpenAI-compatible endpoint).
43    Qwen(OpenAIError),
44    /// Moonshot (Kimi) API error (OpenAI-compatible endpoint).
45    Moonshot(OpenAIError),
46    /// Zhipu (ChatGLM) API error (OpenAI-compatible endpoint).
47    Zhipu(OpenAIError),
48    /// Mistral API error (OpenAI-compatible endpoint).
49    Mistral(OpenAIError),
50    /// B5: any OpenAI-compatible endpoint reached through the generic client
51    /// (Groq, OpenRouter, xAI, vLLM, LM Studio, private gateways, …). The
52    /// `provider` label identifies which preset or custom endpoint failed.
53    OpenAICompatible {
54        /// Endpoint label (`"groq"`, `"openrouter"`, `"xai"`, `"openai-compatible"`).
55        provider: String,
56        /// Underlying OpenAI-protocol error.
57        source: OpenAIError,
58    },
59    /// Configuration error (missing/malformed environment variables, etc.).
60    Config(String),
61    /// Testkit harness error (recording/replay failures from `lc-testkit`).
62    Testkit(String),
63}
64
65impl std::fmt::Display for ProviderError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            ProviderError::OpenAI(e) => write!(f, "OpenAI error: {e}"),
69            ProviderError::Anthropic(e) => write!(f, "Anthropic error: {e}"),
70            ProviderError::Gemini(e) => write!(f, "Gemini error: {e}"),
71            ProviderError::Azure(e) => write!(f, "Azure OpenAI error: {e}"),
72            ProviderError::Cohere(e) => write!(f, "Cohere error: {e}"),
73            ProviderError::Ollama(e) => write!(f, "Ollama error: {e}"),
74            ProviderError::Assistant(e) => write!(f, "Assistant error: {e}"),
75            ProviderError::Responses(e) => write!(f, "Responses error: {e}"),
76            ProviderError::DeepSeek(e) => write!(f, "DeepSeek error: {e}"),
77            ProviderError::Qwen(e) => write!(f, "Qwen error: {e}"),
78            ProviderError::Moonshot(e) => write!(f, "Moonshot error: {e}"),
79            ProviderError::Zhipu(e) => write!(f, "Zhipu error: {e}"),
80            ProviderError::Mistral(e) => write!(f, "Mistral error: {e}"),
81            ProviderError::OpenAICompatible { provider, source } => {
82                write!(f, "{provider} (OpenAI-compatible) error: {source}")
83            }
84            ProviderError::Config(msg) => write!(f, "Configuration error: {msg}"),
85            ProviderError::Testkit(msg) => write!(f, "Testkit error: {msg}"),
86        }
87    }
88}
89
90impl std::error::Error for ProviderError {
91    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
92        match self {
93            ProviderError::OpenAI(e) => Some(e),
94            ProviderError::Anthropic(e) => Some(e),
95            ProviderError::Gemini(e) => Some(e),
96            ProviderError::Azure(e) => Some(e),
97            ProviderError::Cohere(e) => Some(e),
98            ProviderError::Ollama(e) => Some(e),
99            ProviderError::Assistant(e) => Some(e),
100            ProviderError::Responses(e) => Some(e),
101            ProviderError::DeepSeek(e) => Some(e),
102            ProviderError::Qwen(e) => Some(e),
103            ProviderError::Moonshot(e) => Some(e),
104            ProviderError::Zhipu(e) => Some(e),
105            ProviderError::Mistral(e) => Some(e),
106            ProviderError::OpenAICompatible { source, .. } => Some(source),
107            ProviderError::Config(_) => None,
108            ProviderError::Testkit(_) => None,
109        }
110    }
111}
112
113// ---- From impls for all provider error types ----
114
115impl From<OpenAIError> for ProviderError {
116    fn from(e: OpenAIError) -> Self {
117        ProviderError::OpenAI(e)
118    }
119}
120impl From<AnthropicError> for ProviderError {
121    fn from(e: AnthropicError) -> Self {
122        ProviderError::Anthropic(e)
123    }
124}
125impl From<GeminiError> for ProviderError {
126    fn from(e: GeminiError) -> Self {
127        ProviderError::Gemini(e)
128    }
129}
130impl From<OllamaError> for ProviderError {
131    fn from(e: OllamaError) -> Self {
132        ProviderError::Ollama(e)
133    }
134}
135impl From<AssistantError> for ProviderError {
136    fn from(e: AssistantError) -> Self {
137        ProviderError::Assistant(e)
138    }
139}
140impl From<ResponsesError> for ProviderError {
141    fn from(e: ResponsesError) -> Self {
142        ProviderError::Responses(e)
143    }
144}
145impl From<AzureOpenAIError> for ProviderError {
146    fn from(e: AzureOpenAIError) -> Self {
147        ProviderError::Azure(e)
148    }
149}
150impl From<CohereError> for ProviderError {
151    fn from(e: CohereError) -> Self {
152        ProviderError::Cohere(e)
153    }
154}
155
156/// Allow testkit harness errors (recording/replay) to surface through the
157/// provider error chain. `lc-testkit` is an external crate and cannot
158/// construct `#[non_exhaustive]` variants, so this is the sole entry point.
159impl From<String> for ProviderError {
160    fn from(msg: String) -> Self {
161        ProviderError::Testkit(msg)
162    }
163}
164
165// ---- LCEL Error conversion ----
166
167/// Allow `ProviderError` to convert into `LcelError` for LCEL pipeline compatibility.
168/// This enables LLM providers to participate in `pipe()` chains.
169impl From<ProviderError> for lc_core::LcelError {
170    fn from(err: ProviderError) -> Self {
171        lc_core::LcelError::Provider(err.to_string())
172    }
173}
174
175/// Allow `OpenAIError` to convert into `LcelError` for LCEL pipeline compatibility.
176///
177/// `OpenAIChat`'s `Runnable` uses `OpenAIError` directly as its `Error` type; without the
178/// bridge, `OpenAIChat` cannot enter a `pipe()` chain (which needs `R2::Error: Into<LcelError>`).
179/// Qwen / DeepSeek go through OpenAI-compatible endpoints, but they wrap `OpenAIError` into
180/// `ProviderError` and bridge from there, already covered by the impl above.
181impl From<OpenAIError> for lc_core::LcelError {
182    fn from(err: OpenAIError) -> Self {
183        lc_core::LcelError::Provider(err.to_string())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    /// Native OpenAIChat errors convert cleanly into LcelError, so `openai.pipe(...)` compiles as the second pipe step.
192    #[test]
193    fn openai_error_into_lcel_error() {
194        let e = OpenAIError::Api("rate limited".to_string());
195        let lcel: lc_core::LcelError = e.into();
196        assert!(matches!(
197            lcel,
198            lc_core::LcelError::Provider(ref msg) if msg.contains("API error: rate limited")
199        ));
200    }
201
202    /// Qwen/DeepSeek reuse OpenAIError, but bridge through ProviderError, equally into LcelError.
203    #[test]
204    fn qwen_provider_error_into_lcel_error() {
205        let e = ProviderError::Qwen(OpenAIError::Http("timeout".to_string()));
206        let lcel: lc_core::LcelError = e.into();
207        assert!(matches!(
208            lcel,
209            lc_core::LcelError::Provider(ref msg) if msg.contains("Qwen error") && msg.contains("timeout")
210        ));
211    }
212
213    #[test]
214    fn deepseek_provider_error_into_lcel_error() {
215        let e = ProviderError::DeepSeek(OpenAIError::Parse("bad json".to_string()));
216        let lcel: lc_core::LcelError = e.into();
217        assert!(matches!(
218            lcel,
219            lc_core::LcelError::Provider(ref msg) if msg.contains("DeepSeek error")
220        ));
221    }
222
223    /// B5: generic OpenAI-compatible errors carry the preset label and source.
224    #[test]
225    fn openai_compatible_error_label_and_source() {
226        let e = ProviderError::OpenAICompatible {
227            provider: "groq".to_string(),
228            source: OpenAIError::Api("HTTP 429: slow down".to_string()),
229        };
230        let text = e.to_string();
231        assert!(text.contains("groq"), "{text}");
232        assert!(text.contains("OpenAI-compatible"), "{text}");
233        assert!(text.contains("429"), "{text}");
234        assert!(std::error::Error::source(&e).is_some());
235
236        let lcel: lc_core::LcelError = e.into();
237        assert!(matches!(
238            lcel,
239            lc_core::LcelError::Provider(ref msg) if msg.contains("groq")
240        ));
241    }
242}