Skip to main content

gateway_core/
failover.rs

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}