Skip to main content

lash_core/provider/
traits.rs

1use super::support::*;
2use crate::LlmTerminalReason;
3
4/// A configured LLM backend: its identity, host-config serialization, its
5/// generation options, and the request transport.
6#[async_trait]
7pub trait Provider: Send + Sync + std::fmt::Debug {
8    fn kind(&self) -> &'static str;
9
10    fn options(&self) -> ProviderOptions;
11    fn set_options(&mut self, options: ProviderOptions);
12
13    /// Emit the provider-specific JSON body used by [`ProviderSpec`]. The
14    /// object must NOT contain a `type` field — [`ProviderSpec::Serialize`]
15    /// layers that on top.
16    fn serialize_config(&self) -> serde_json::Value;
17
18    async fn complete(&mut self, request: LlmRequest) -> Result<LlmResponse, LlmTransportError>;
19
20    fn requires_streaming(&self) -> bool {
21        false
22    }
23
24    fn clone_boxed(&self) -> Box<dyn Provider>;
25}
26
27pub trait ProviderModelPolicy: Send + Sync + std::fmt::Debug {
28    fn supported_variants(&self, model: &str) -> &'static [&'static str];
29}
30
31pub trait ProviderFailureClassifier: Send + Sync + std::fmt::Debug {
32    fn classify(&self, failure: ProviderFailure) -> ProviderFailure;
33}
34
35#[derive(Clone, Debug, Default)]
36pub struct DefaultProviderFailureClassifier;
37
38impl ProviderFailureClassifier for DefaultProviderFailureClassifier {
39    fn classify(&self, mut failure: ProviderFailure) -> ProviderFailure {
40        if let Some(status) = failure.status.or_else(|| {
41            failure
42                .code
43                .as_deref()
44                .and_then(|code| code.parse::<u16>().ok())
45        }) {
46            failure.status = Some(status);
47            if failure.kind == ProviderFailureKind::Unknown {
48                failure.kind = ProviderFailureKind::Http;
49            }
50            failure.retryable = matches!(status, 408 | 409 | 425 | 429 | 500 | 502 | 503 | 504);
51            if matches!(status, 401 | 403) {
52                failure.kind = ProviderFailureKind::Auth;
53            } else if matches!(status, 400 | 413 | 422) {
54                failure.kind = ProviderFailureKind::Validation;
55            }
56        } else if matches!(
57            failure.kind,
58            ProviderFailureKind::Transport | ProviderFailureKind::Timeout
59        ) {
60            failure.retryable = true;
61        }
62
63        let haystack = format!(
64            "{}\n{}\n{}",
65            failure.code.as_deref().unwrap_or_default(),
66            failure.message,
67            failure.raw.as_deref().unwrap_or_default()
68        )
69        .to_ascii_lowercase();
70        if is_context_overflow_text(&haystack) {
71            failure.kind = ProviderFailureKind::Validation;
72            failure.retryable = false;
73            failure.terminal_reason = LlmTerminalReason::ContextOverflow;
74        }
75        if haystack.contains("insufficient_quota")
76            || haystack.contains("usage_limit_reached")
77            || haystack.contains("usage_not_included")
78            || haystack.contains("quota")
79        {
80            failure.kind = ProviderFailureKind::Quota;
81            failure.retryable = false;
82        }
83        if haystack.contains("content_filter")
84            || haystack.contains("prohibited_content")
85            || haystack.contains("safety")
86            || haystack.contains("sensitive")
87        {
88            failure.terminal_reason = LlmTerminalReason::ContentFilter;
89        }
90        if haystack.contains("model_not_found")
91            || haystack.contains("unsupported model")
92            || haystack.contains("does not exist")
93        {
94            failure.kind = ProviderFailureKind::Unsupported;
95            failure.retryable = false;
96        }
97        failure
98    }
99}
100
101pub fn is_context_overflow_text(haystack: &str) -> bool {
102    let lower = haystack.to_ascii_lowercase();
103    if lower.contains("rate limit")
104        || lower.contains("rate_limit")
105        || lower.contains("ratelimit")
106        || lower.contains("throttle")
107        || lower.contains("throttling")
108        || lower.contains("too many requests")
109        || lower.contains("tokens per minute")
110        || lower.contains("tpm")
111        || lower.contains("quota")
112    {
113        return false;
114    }
115
116    lower.contains("context_length_exceeded")
117        || lower.contains("context_length")
118        || lower.contains("context length")
119        || lower.contains("maximum context")
120        || lower.contains("max context")
121        || lower.contains("context window")
122        || lower.contains("context window exceeds limit")
123        || lower.contains("exceeds the context window")
124        || lower.contains("prompt is too long")
125        || lower.contains("prompt too long")
126        || lower.contains("request_too_large")
127        || lower.contains("input token count") && lower.contains("exceeds the maximum")
128        || lower.contains("maximum prompt length is")
129        || lower.contains("reduce the length of the messages")
130        || lower.contains("maximum context length is")
131        || lower.contains("model's context length")
132        || lower.contains("models context length")
133        || lower.contains("exceeds the available context size")
134        || lower.contains("greater than the context length")
135        || lower.contains("exceeded model token limit")
136        || lower.contains("too large for model with")
137        || lower.contains("model_context_window_exceeded")
138        || lower.contains("too many tokens")
139        || lower.contains("exceeds the maximum number of tokens")
140        || lower.contains("exceeds maximum number of tokens")
141        || lower.contains("request too large")
142        || lower.contains("input is too long")
143        || lower.contains("token limit exceeded")
144        || lower.contains("reduce the length of the messages")
145        || lower.contains("reduce the length of your prompt")
146}