Skip to main content

lash_core/provider/
handle.rs

1use super::support::*;
2
3/// Component bundle returned by provider factories.
4#[derive(Debug)]
5pub struct ProviderComponents {
6    pub provider: Box<dyn Provider>,
7    pub failure_classifier: Arc<dyn ProviderFailureClassifier>,
8    pub rate_limiter: Arc<ProviderRateLimiter>,
9}
10
11impl ProviderComponents {
12    pub fn new(provider: Box<dyn Provider>) -> Self {
13        let options = provider.options();
14        Self {
15            provider,
16            failure_classifier: Arc::new(DefaultProviderFailureClassifier),
17            rate_limiter: Arc::new(ProviderRateLimiter::new(options.reliability.rate_limits)),
18        }
19    }
20
21    /// Install a transport-level decorator that wraps the provider.
22    pub fn map_provider(
23        mut self,
24        map: impl FnOnce(Box<dyn Provider>) -> Box<dyn Provider>,
25    ) -> Self {
26        self.provider = map(self.provider);
27        self
28    }
29
30    pub fn with_failure_classifier(
31        mut self,
32        classifier: Arc<dyn ProviderFailureClassifier>,
33    ) -> Self {
34        self.failure_classifier = classifier;
35        self
36    }
37
38    pub fn with_clock(mut self, clock: Arc<dyn crate::Clock>) -> Self {
39        let options = self.provider.options();
40        self.rate_limiter = Arc::new(ProviderRateLimiter::with_clock(
41            options.reliability.rate_limits,
42            clock,
43        ));
44        self
45    }
46}
47
48impl Clone for ProviderComponents {
49    fn clone(&self) -> Self {
50        Self {
51            provider: self.provider.clone_boxed(),
52            failure_classifier: Arc::clone(&self.failure_classifier),
53            rate_limiter: Arc::clone(&self.rate_limiter),
54        }
55    }
56}
57
58/// Owning handle to provider components. This is an executable transport
59/// handle supplied by the host, not a persistence format.
60pub struct ProviderHandle {
61    components: ProviderComponents,
62}
63
64impl ProviderHandle {
65    pub fn new(components: ProviderComponents) -> Self {
66        Self { components }
67    }
68
69    pub fn unconfigured() -> Self {
70        Self::new(UnconfiguredProvider::default().into_components())
71    }
72
73    pub fn components(&self) -> &ProviderComponents {
74        &self.components
75    }
76
77    pub fn components_mut(&mut self) -> &mut ProviderComponents {
78        &mut self.components
79    }
80
81    pub fn with_clock(mut self, clock: Arc<dyn crate::Clock>) -> Self {
82        self.components = self.components.with_clock(clock);
83        self
84    }
85
86    pub fn kind(&self) -> &'static str {
87        self.components.provider.kind()
88    }
89
90    pub fn options(&self) -> ProviderOptions {
91        self.components.provider.options()
92    }
93
94    pub fn set_options(&mut self, options: ProviderOptions) {
95        self.components
96            .rate_limiter
97            .configure(options.reliability.rate_limits.clone());
98        self.components.provider.set_options(options)
99    }
100
101    pub fn requires_streaming(&self) -> bool {
102        self.components.provider.requires_streaming()
103    }
104
105    pub async fn complete(
106        &mut self,
107        request: LlmRequest,
108    ) -> Result<LlmResponse, LlmTransportError> {
109        let reliability = self.options().reliability;
110        let attempts = reliability.retry.attempts();
111        let mut attempt = 0;
112        // Cumulative time already spent deferring to provider throttles
113        // without consuming attempts, bounded by the policy's budget.
114        let throttle_budget = Duration::from_millis(reliability.retry.throttle_wait_budget_ms);
115        let mut throttle_waited = Duration::ZERO;
116        loop {
117            let _permit = self.components.rate_limiter.admit(&request).await;
118            let result = self.components.provider.complete(request.clone()).await;
119            match result {
120                Ok(response) => return Ok(response),
121                Err(failure) => {
122                    let failure = self.components.failure_classifier.classify(failure);
123                    // Throttle deference: when the provider signals a throttle
124                    // (retryable `Quota`) AND states how long to back off
125                    // (`Retry-After`), honor the wait without consuming a
126                    // retry attempt — the provider is asking us to come back,
127                    // not failing. The courtesy is bounded: each deferred wait
128                    // charges at least `MIN_THROTTLE_BUDGET_CHARGE` against
129                    // the cumulative `throttle_wait_budget_ms`, and once the
130                    // budget is spent a throttle counts as an ordinary
131                    // retryable failure. A throttle WITHOUT `Retry-After`
132                    // never defers: there is no server-stated wait to honor,
133                    // so the normal backoff-and-count ladder applies.
134                    if failure.retryable
135                        && failure.kind == ProviderFailureKind::Quota
136                        && let Some(retry_after) = failure.retry_after
137                    {
138                        let wait = reliability.retry.cap_retry_after(retry_after);
139                        let charge = wait.max(MIN_THROTTLE_BUDGET_CHARGE);
140                        // Saturating: an absurd uncapped `Retry-After` must
141                        // overflow the budget check, not panic the ladder.
142                        if throttle_waited.saturating_add(charge) <= throttle_budget {
143                            throttle_waited += charge;
144                            tracing::debug!(
145                                target: "lash_core::provider::reliability",
146                                provider = self.kind(),
147                                attempt = attempt + 1,
148                                max_attempts = attempts,
149                                wait_ms = wait.as_millis() as u64,
150                                throttle_waited_ms = throttle_waited.as_millis() as u64,
151                                err = %failure.message,
152                                "provider throttled with retry-after; waiting without consuming a retry attempt"
153                            );
154                            if let Some(events) = request.stream_events.as_ref() {
155                                events.send(crate::llm::types::LlmStreamEvent::RetryStatus {
156                                    wait_seconds: wait.as_secs(),
157                                    attempt: (attempt + 1) as usize,
158                                    max_attempts: attempts as usize,
159                                    reason: failure.message.clone(),
160                                });
161                            }
162                            self.components.rate_limiter.clock().sleep(wait).await;
163                            continue;
164                        }
165                    }
166                    if attempt + 1 >= attempts || !failure.retryable {
167                        return Err(failure);
168                    }
169                    let delay = reliability
170                        .retry
171                        .delay_for_attempt(attempt, failure.retry_after);
172                    tracing::debug!(
173                        target: "lash_core::provider::reliability",
174                        provider = self.kind(),
175                        attempt = attempt + 1,
176                        max_attempts = attempts,
177                        delay_ms = delay.as_millis() as u64,
178                        err = %failure.message,
179                        "provider call failed with retryable failure; sleeping before retry"
180                    );
181                    if let Some(events) = request.stream_events.as_ref() {
182                        events.send(crate::llm::types::LlmStreamEvent::RetryStatus {
183                            wait_seconds: delay.as_secs(),
184                            attempt: (attempt + 1) as usize,
185                            max_attempts: attempts as usize,
186                            reason: failure.message.clone(),
187                        });
188                    }
189                    self.components.rate_limiter.clock().sleep(delay).await;
190                    attempt += 1;
191                }
192            }
193        }
194    }
195
196    /// Release the underlying provider's host-visible transport resources.
197    ///
198    /// This forwards to [`Provider::close`]. Hosts that want a graceful
199    /// transport shutdown (for example, sending WebSocket Close frames on
200    /// cached Codex sessions) retain a clone of the handle they hand to the
201    /// core and call this before process exit. Providers with no reusable
202    /// transport state close as a no-op.
203    pub async fn close(&self) -> Result<(), LlmTransportError> {
204        self.components.provider.close().await
205    }
206
207    pub fn to_spec(&self) -> ProviderSpec {
208        ProviderSpec {
209            kind: self.kind().to_string(),
210            config: self.components.provider.serialize_config(),
211        }
212    }
213
214    /// Validate model syntax only.
215    pub fn validate_model_name(&self, model: &str) -> Result<(), String> {
216        let m = model.trim();
217        if m.is_empty() {
218            return Err("model cannot be empty".to_string());
219        }
220        if m.contains(char::is_whitespace) {
221            return Err("model cannot contain whitespace".to_string());
222        }
223        Ok(())
224    }
225}
226
227impl std::fmt::Debug for ProviderHandle {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        self.components.fmt(f)
230    }
231}
232
233impl Clone for ProviderHandle {
234    fn clone(&self) -> Self {
235        Self {
236            components: self.components.clone(),
237        }
238    }
239}
240
241impl PartialEq for ProviderHandle {
242    fn eq(&self, other: &Self) -> bool {
243        self.kind() == other.kind() && self.to_spec().config == other.to_spec().config
244    }
245}
246
247impl Eq for ProviderHandle {}
248
249/// Placeholder provider used by runtime policy defaults before a host resolver
250/// installs the executable provider. Every transport-level method errors;
251/// calling code MUST replace this before executing a turn.
252#[derive(Clone, Debug, Default)]
253pub struct UnconfiguredProvider {
254    options: ProviderOptions,
255}
256
257impl UnconfiguredProvider {
258    fn into_components(self) -> ProviderComponents {
259        ProviderComponents::new(Box::new(self))
260    }
261}
262
263#[async_trait]
264impl Provider for UnconfiguredProvider {
265    fn kind(&self) -> &'static str {
266        "unconfigured"
267    }
268
269    fn options(&self) -> ProviderOptions {
270        self.options.clone()
271    }
272
273    fn set_options(&mut self, options: ProviderOptions) {
274        self.options = options;
275    }
276
277    fn serialize_config(&self) -> serde_json::Value {
278        serde_json::Value::Object(Default::default())
279    }
280
281    async fn complete(&mut self, _request: LlmRequest) -> Result<LlmResponse, LlmTransportError> {
282        Err(LlmTransportError::new(
283            "no provider configured: host must set SessionPolicy.provider before running a turn",
284        ))
285    }
286
287    fn clone_boxed(&self) -> Box<dyn Provider> {
288        Box::new(self.clone())
289    }
290}