1use crate::ProviderError;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct FailoverTarget {
5 pub provider: String,
6 pub model: String,
7}
8
9impl FailoverTarget {
10 pub fn new(provider: impl Into<String>, model: impl Into<String>) -> Self {
11 Self {
12 provider: provider.into(),
13 model: model.into(),
14 }
15 }
16
17 pub fn qualified_model(&self) -> String {
18 format!("{}/{}", self.provider, self.model)
19 }
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum FailoverDecision {
24 TryNext,
25 Return,
26}
27
28#[derive(Debug, Clone, Copy, Default)]
29pub struct FailoverPolicy;
30
31impl FailoverPolicy {
32 pub fn decide(&self, error: &ProviderError, has_next: bool) -> FailoverDecision {
33 if has_next && error.is_retryable() {
34 FailoverDecision::TryNext
35 } else {
36 FailoverDecision::Return
37 }
38 }
39
40 pub fn ordered_targets(
41 &self,
42 targets: impl IntoIterator<Item = FailoverTarget>,
43 ) -> Result<Vec<FailoverTarget>, ProviderError> {
44 let targets: Vec<_> = targets.into_iter().collect();
45 if targets.is_empty() {
46 Err(ProviderError::InvalidRequest(
47 "at least one provider target is required".into(),
48 ))
49 } else {
50 Ok(targets)
51 }
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn retries_only_retryable_errors_with_remaining_target() {
61 let policy = FailoverPolicy;
62 assert_eq!(
63 policy.decide(&ProviderError::transport("openai", "timeout"), true),
64 FailoverDecision::TryNext
65 );
66 assert_eq!(
67 policy.decide(&ProviderError::InvalidRequest("bad".into()), true),
68 FailoverDecision::Return
69 );
70 assert_eq!(
71 policy.decide(&ProviderError::transport("openai", "timeout"), false),
72 FailoverDecision::Return
73 );
74 }
75}