1use std::sync::Arc;
16
17use agent_sdk_foundation::llm::{ChatOutcome, ChatRequest};
18use anyhow::Result;
19use async_trait::async_trait;
20use futures::StreamExt;
21
22use crate::provider::LlmProvider;
23use crate::streaming::{StreamBox, StreamDelta};
24
25pub struct FallbackProvider {
31 primary: Arc<dyn LlmProvider>,
32 fallbacks: Vec<Arc<dyn LlmProvider>>,
33}
34
35impl FallbackProvider {
36 #[must_use]
39 pub fn new(primary: Arc<dyn LlmProvider>) -> Self {
40 Self {
41 primary,
42 fallbacks: Vec::new(),
43 }
44 }
45
46 #[must_use]
48 pub fn with_fallback(mut self, provider: Arc<dyn LlmProvider>) -> Self {
49 self.fallbacks.push(provider);
50 self
51 }
52
53 #[must_use]
55 pub fn from_providers(
56 primary: Arc<dyn LlmProvider>,
57 fallbacks: impl IntoIterator<Item = Arc<dyn LlmProvider>>,
58 ) -> Self {
59 Self {
60 primary,
61 fallbacks: fallbacks.into_iter().collect(),
62 }
63 }
64
65 #[must_use]
67 pub fn len(&self) -> usize {
68 1 + self.fallbacks.len()
69 }
70
71 #[must_use]
73 pub const fn is_empty(&self) -> bool {
74 false
75 }
76
77 fn ordered(&self) -> Vec<Arc<dyn LlmProvider>> {
79 let mut providers = Vec::with_capacity(self.len());
80 providers.push(Arc::clone(&self.primary));
81 providers.extend(self.fallbacks.iter().map(Arc::clone));
82 providers
83 }
84}
85
86const fn is_retryable(result: &Result<ChatOutcome>) -> bool {
89 matches!(
90 result,
91 Err(_) | Ok(ChatOutcome::RateLimited(_) | ChatOutcome::ServerError(_))
92 )
93}
94
95#[async_trait]
96impl LlmProvider for FallbackProvider {
97 async fn chat(&self, request: ChatRequest) -> Result<ChatOutcome> {
98 let providers = self.ordered();
99 let last = providers.len() - 1;
100 for (idx, provider) in providers.iter().enumerate() {
101 let result = provider.chat(request.clone()).await;
102 if idx == last || !is_retryable(&result) {
103 return result;
104 }
105 log::warn!(
106 "FallbackProvider: provider '{}' failed retryably, failing over to next",
107 provider.provider()
108 );
109 }
110 Ok(ChatOutcome::ServerError(
114 "FallbackProvider: no providers configured".to_owned(),
115 ))
116 }
117
118 fn chat_stream(&self, request: ChatRequest) -> StreamBox<'_> {
119 let providers = self.ordered();
120 Box::pin(async_stream::stream! {
121 let last = providers.len() - 1;
122 for (idx, provider) in providers.iter().enumerate() {
123 let is_last = idx == last;
124 let mut stream = provider.chat_stream(request.clone());
125 let mut committed = false;
129 let mut failed_over = false;
130
131 while let Some(item) = stream.next().await {
132 match item {
133 Ok(StreamDelta::Error { message, kind }) => {
134 if !committed && !is_last && kind.is_recoverable() {
135 log::warn!(
136 "FallbackProvider: provider '{}' recoverable stream error ({kind:?}), failing over",
137 provider.provider()
138 );
139 failed_over = true;
140 break;
141 }
142 yield Ok(StreamDelta::Error { message, kind });
143 }
144 Ok(delta) => {
145 committed = true;
146 yield Ok(delta);
147 }
148 Err(error) => {
149 if !committed && !is_last {
150 log::warn!(
151 "FallbackProvider: provider '{}' stream transport error, failing over: {error}",
152 provider.provider()
153 );
154 failed_over = true;
155 break;
156 }
157 yield Err(error);
158 }
159 }
160 }
161
162 if !failed_over {
163 return;
164 }
165 }
166 })
167 }
168
169 async fn list_models(&self) -> Result<Vec<crate::provider::ModelInfo>> {
172 self.primary.list_models().await
173 }
174
175 fn model(&self) -> &str {
176 self.primary.model()
177 }
178
179 fn provider(&self) -> &'static str {
180 self.primary.provider()
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 use std::sync::Mutex;
189 use std::sync::atomic::{AtomicUsize, Ordering};
190
191 use agent_sdk_foundation::llm::{ChatResponse, ContentBlock, StopReason, Usage};
192 use anyhow::anyhow;
193
194 use crate::streaming::StreamErrorKind;
195
196 struct ScriptedProvider {
198 name: &'static str,
199 results: Mutex<std::collections::VecDeque<Result<ChatOutcome>>>,
200 stream_deltas: Mutex<Vec<Result<StreamDelta>>>,
201 calls: AtomicUsize,
202 }
203
204 impl ScriptedProvider {
205 fn chat_only(name: &'static str, results: Vec<Result<ChatOutcome>>) -> Arc<Self> {
206 Arc::new(Self {
207 name,
208 results: Mutex::new(results.into()),
209 stream_deltas: Mutex::new(Vec::new()),
210 calls: AtomicUsize::new(0),
211 })
212 }
213
214 fn streaming(name: &'static str, deltas: Vec<Result<StreamDelta>>) -> Arc<Self> {
215 Arc::new(Self {
216 name,
217 results: Mutex::new(std::collections::VecDeque::new()),
218 stream_deltas: Mutex::new(deltas),
219 calls: AtomicUsize::new(0),
220 })
221 }
222
223 fn calls(&self) -> usize {
224 self.calls.load(Ordering::SeqCst)
225 }
226 }
227
228 #[async_trait]
229 impl LlmProvider for ScriptedProvider {
230 async fn chat(&self, _request: ChatRequest) -> Result<ChatOutcome> {
231 self.calls.fetch_add(1, Ordering::SeqCst);
232 self.results
233 .lock()
234 .map_err(|_| anyhow!("results lock poisoned"))?
235 .pop_front()
236 .unwrap_or_else(|| Ok(ChatOutcome::ServerError("exhausted".to_owned())))
237 }
238
239 async fn list_models(&self) -> Result<Vec<crate::provider::ModelInfo>> {
240 Ok(vec![crate::provider::ModelInfo {
241 id: format!("{}-model", self.name),
242 display_name: None,
243 context_window: None,
244 max_output_tokens: None,
245 }])
246 }
247
248 fn chat_stream(&self, _request: ChatRequest) -> StreamBox<'_> {
249 self.calls.fetch_add(1, Ordering::SeqCst);
250 let deltas: Vec<Result<StreamDelta>> = self
251 .stream_deltas
252 .lock()
253 .map(|d| {
254 d.iter()
255 .map(|r| match r {
256 Ok(delta) => Ok(delta.clone()),
257 Err(e) => Err(anyhow!("{e}")),
258 })
259 .collect()
260 })
261 .unwrap_or_default();
262 Box::pin(async_stream::stream! {
263 for delta in deltas {
264 yield delta;
265 }
266 })
267 }
268
269 fn model(&self) -> &str {
270 self.name
271 }
272
273 fn provider(&self) -> &'static str {
274 self.name
275 }
276 }
277
278 fn success(text: &str) -> ChatOutcome {
279 ChatOutcome::Success(ChatResponse {
280 id: "r".to_owned(),
281 content: vec![ContentBlock::Text {
282 text: text.to_owned(),
283 }],
284 model: "m".to_owned(),
285 stop_reason: Some(StopReason::EndTurn),
286 usage: Usage {
287 input_tokens: 1,
288 output_tokens: 1,
289 cached_input_tokens: 0,
290 cache_creation_input_tokens: 0,
291 },
292 })
293 }
294
295 fn request() -> ChatRequest {
296 ChatRequest::new("sys", vec![agent_sdk_foundation::llm::Message::user("hi")])
297 }
298
299 #[tokio::test]
300 async fn server_error_fails_over_to_secondary() -> Result<()> {
301 let primary = ScriptedProvider::chat_only(
302 "primary",
303 vec![Ok(ChatOutcome::ServerError("boom".to_owned()))],
304 );
305 let secondary = ScriptedProvider::chat_only("secondary", vec![Ok(success("ok"))]);
306 let fb = FallbackProvider::new(primary.clone()).with_fallback(secondary.clone());
307
308 let outcome = fb.chat(request()).await?;
309 assert!(matches!(outcome, ChatOutcome::Success(r) if r.first_text() == Some("ok")));
310 assert_eq!(primary.calls(), 1);
311 assert_eq!(secondary.calls(), 1);
312 Ok(())
313 }
314
315 #[tokio::test]
316 async fn rate_limit_fails_over() -> Result<()> {
317 let primary =
318 ScriptedProvider::chat_only("primary", vec![Ok(ChatOutcome::RateLimited(None))]);
319 let secondary = ScriptedProvider::chat_only("secondary", vec![Ok(success("ok"))]);
320 let fb = FallbackProvider::from_providers(primary.clone(), [secondary.clone() as Arc<_>]);
321
322 let outcome = fb.chat(request()).await?;
323 assert!(matches!(outcome, ChatOutcome::Success(_)));
324 assert_eq!(secondary.calls(), 1);
325 Ok(())
326 }
327
328 #[tokio::test]
329 async fn transport_error_fails_over() -> Result<()> {
330 let primary = ScriptedProvider::chat_only("primary", vec![Err(anyhow!("timeout"))]);
331 let secondary = ScriptedProvider::chat_only("secondary", vec![Ok(success("ok"))]);
332 let fb = FallbackProvider::new(primary.clone()).with_fallback(secondary.clone());
333
334 let outcome = fb.chat(request()).await?;
335 assert!(matches!(outcome, ChatOutcome::Success(_)));
336 Ok(())
337 }
338
339 #[tokio::test]
340 async fn invalid_request_does_not_fail_over() -> Result<()> {
341 let primary = ScriptedProvider::chat_only(
342 "primary",
343 vec![Ok(ChatOutcome::InvalidRequest("bad".to_owned()))],
344 );
345 let secondary = ScriptedProvider::chat_only("secondary", vec![Ok(success("ok"))]);
346 let fb = FallbackProvider::new(primary.clone()).with_fallback(secondary.clone());
347
348 let outcome = fb.chat(request()).await?;
349 assert!(matches!(outcome, ChatOutcome::InvalidRequest(_)));
350 assert_eq!(secondary.calls(), 0);
352 Ok(())
353 }
354
355 #[tokio::test]
356 async fn last_provider_outcome_is_returned_when_all_fail() -> Result<()> {
357 let primary = ScriptedProvider::chat_only(
358 "primary",
359 vec![Ok(ChatOutcome::ServerError("a".to_owned()))],
360 );
361 let secondary = ScriptedProvider::chat_only(
362 "secondary",
363 vec![Ok(ChatOutcome::ServerError("b".to_owned()))],
364 );
365 let fb = FallbackProvider::new(primary).with_fallback(secondary);
366
367 let outcome = fb.chat(request()).await?;
368 assert!(matches!(outcome, ChatOutcome::ServerError(msg) if msg == "b"));
369 Ok(())
370 }
371
372 #[tokio::test]
373 async fn list_models_delegates_to_primary() -> Result<()> {
374 let primary = ScriptedProvider::chat_only("primary", vec![]);
375 let secondary = ScriptedProvider::chat_only("secondary", vec![]);
376 let fb = FallbackProvider::new(primary).with_fallback(secondary);
377
378 let models = fb.list_models().await?;
379 assert_eq!(models.len(), 1);
381 assert_eq!(models[0].id, "primary-model");
382 Ok(())
383 }
384
385 #[tokio::test]
386 async fn stream_fails_over_on_recoverable_first_error() -> Result<()> {
387 let primary = ScriptedProvider::streaming(
388 "primary",
389 vec![Ok(StreamDelta::Error {
390 message: "rate limited".to_owned(),
391 kind: StreamErrorKind::RateLimited,
392 })],
393 );
394 let secondary = ScriptedProvider::streaming(
395 "secondary",
396 vec![
397 Ok(StreamDelta::TextDelta {
398 delta: "hello".to_owned(),
399 block_index: 0,
400 }),
401 Ok(StreamDelta::Done {
402 stop_reason: Some(StopReason::EndTurn),
403 }),
404 ],
405 );
406 let fb = FallbackProvider::new(primary.clone()).with_fallback(secondary.clone());
407
408 let mut stream = fb.chat_stream(request());
409 let mut text = String::new();
410 while let Some(item) = stream.next().await {
411 if let StreamDelta::TextDelta { delta, .. } = item? {
412 text.push_str(&delta);
413 }
414 }
415 assert_eq!(text, "hello");
416 assert_eq!(primary.calls(), 1);
417 assert_eq!(secondary.calls(), 1);
418 Ok(())
419 }
420
421 #[test]
422 fn reports_primary_identity() {
423 let primary = ScriptedProvider::chat_only("primary", vec![]);
424 let fb = FallbackProvider::new(primary);
425 assert_eq!(fb.provider(), "primary");
426 assert_eq!(fb.model(), "primary");
427 assert_eq!(fb.len(), 1);
428 assert!(!fb.is_empty());
429 }
430}