Skip to main content

codineer_api/providers/
mod.rs

1use std::time::Duration;
2
3pub mod codineer_provider;
4pub mod openai_compat;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct RetryPolicy {
8    pub max_retries: u32,
9    pub initial_backoff: Duration,
10    pub max_backoff: Duration,
11}
12
13impl Default for RetryPolicy {
14    fn default() -> Self {
15        Self {
16            max_retries: 2,
17            initial_backoff: Duration::from_millis(200),
18            max_backoff: Duration::from_secs(2),
19        }
20    }
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ProviderKind {
25    CodineerApi,
26    Xai,
27    OpenAi,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct ProviderMetadata {
32    pub provider: ProviderKind,
33    pub auth_env: &'static str,
34    pub base_url_env: &'static str,
35    pub default_base_url: &'static str,
36}
37
38const MODEL_REGISTRY: &[(&str, ProviderMetadata)] = &[
39    (
40        "opus",
41        ProviderMetadata {
42            provider: ProviderKind::CodineerApi,
43            auth_env: "ANTHROPIC_API_KEY",
44            base_url_env: "ANTHROPIC_BASE_URL",
45            default_base_url: codineer_provider::DEFAULT_BASE_URL,
46        },
47    ),
48    (
49        "sonnet",
50        ProviderMetadata {
51            provider: ProviderKind::CodineerApi,
52            auth_env: "ANTHROPIC_API_KEY",
53            base_url_env: "ANTHROPIC_BASE_URL",
54            default_base_url: codineer_provider::DEFAULT_BASE_URL,
55        },
56    ),
57    (
58        "haiku",
59        ProviderMetadata {
60            provider: ProviderKind::CodineerApi,
61            auth_env: "ANTHROPIC_API_KEY",
62            base_url_env: "ANTHROPIC_BASE_URL",
63            default_base_url: codineer_provider::DEFAULT_BASE_URL,
64        },
65    ),
66    (
67        "claude-opus-4-6",
68        ProviderMetadata {
69            provider: ProviderKind::CodineerApi,
70            auth_env: "ANTHROPIC_API_KEY",
71            base_url_env: "ANTHROPIC_BASE_URL",
72            default_base_url: codineer_provider::DEFAULT_BASE_URL,
73        },
74    ),
75    (
76        "claude-sonnet-4-6",
77        ProviderMetadata {
78            provider: ProviderKind::CodineerApi,
79            auth_env: "ANTHROPIC_API_KEY",
80            base_url_env: "ANTHROPIC_BASE_URL",
81            default_base_url: codineer_provider::DEFAULT_BASE_URL,
82        },
83    ),
84    (
85        "claude-haiku-4-5-20251213",
86        ProviderMetadata {
87            provider: ProviderKind::CodineerApi,
88            auth_env: "ANTHROPIC_API_KEY",
89            base_url_env: "ANTHROPIC_BASE_URL",
90            default_base_url: codineer_provider::DEFAULT_BASE_URL,
91        },
92    ),
93    (
94        "grok",
95        ProviderMetadata {
96            provider: ProviderKind::Xai,
97            auth_env: "XAI_API_KEY",
98            base_url_env: "XAI_BASE_URL",
99            default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
100        },
101    ),
102    (
103        "grok-3",
104        ProviderMetadata {
105            provider: ProviderKind::Xai,
106            auth_env: "XAI_API_KEY",
107            base_url_env: "XAI_BASE_URL",
108            default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
109        },
110    ),
111    (
112        "grok-mini",
113        ProviderMetadata {
114            provider: ProviderKind::Xai,
115            auth_env: "XAI_API_KEY",
116            base_url_env: "XAI_BASE_URL",
117            default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
118        },
119    ),
120    (
121        "grok-3-mini",
122        ProviderMetadata {
123            provider: ProviderKind::Xai,
124            auth_env: "XAI_API_KEY",
125            base_url_env: "XAI_BASE_URL",
126            default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
127        },
128    ),
129    (
130        "grok-2",
131        ProviderMetadata {
132            provider: ProviderKind::Xai,
133            auth_env: "XAI_API_KEY",
134            base_url_env: "XAI_BASE_URL",
135            default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
136        },
137    ),
138    (
139        "gpt-4o",
140        ProviderMetadata {
141            provider: ProviderKind::OpenAi,
142            auth_env: "OPENAI_API_KEY",
143            base_url_env: "OPENAI_BASE_URL",
144            default_base_url: openai_compat::DEFAULT_OPENAI_BASE_URL,
145        },
146    ),
147    (
148        "gpt-4o-mini",
149        ProviderMetadata {
150            provider: ProviderKind::OpenAi,
151            auth_env: "OPENAI_API_KEY",
152            base_url_env: "OPENAI_BASE_URL",
153            default_base_url: openai_compat::DEFAULT_OPENAI_BASE_URL,
154        },
155    ),
156    (
157        "o3",
158        ProviderMetadata {
159            provider: ProviderKind::OpenAi,
160            auth_env: "OPENAI_API_KEY",
161            base_url_env: "OPENAI_BASE_URL",
162            default_base_url: openai_compat::DEFAULT_OPENAI_BASE_URL,
163        },
164    ),
165    (
166        "o3-mini",
167        ProviderMetadata {
168            provider: ProviderKind::OpenAi,
169            auth_env: "OPENAI_API_KEY",
170            base_url_env: "OPENAI_BASE_URL",
171            default_base_url: openai_compat::DEFAULT_OPENAI_BASE_URL,
172        },
173    ),
174];
175
176#[must_use]
177pub fn resolve_model_alias(model: &str) -> String {
178    let trimmed = model.trim();
179    let lower = trimmed.to_ascii_lowercase();
180    MODEL_REGISTRY
181        .iter()
182        .find_map(|(alias, metadata)| {
183            (*alias == lower).then_some(match metadata.provider {
184                ProviderKind::CodineerApi => match *alias {
185                    "opus" => "claude-opus-4-6",
186                    "sonnet" => "claude-sonnet-4-6",
187                    "haiku" => "claude-haiku-4-5-20251213",
188                    _ => trimmed,
189                },
190                ProviderKind::Xai => match *alias {
191                    "grok" | "grok-3" => "grok-3",
192                    "grok-mini" | "grok-3-mini" => "grok-3-mini",
193                    "grok-2" => "grok-2",
194                    _ => trimmed,
195                },
196                ProviderKind::OpenAi => trimmed,
197            })
198        })
199        .map_or_else(|| trimmed.to_string(), ToOwned::to_owned)
200}
201
202#[must_use]
203pub fn metadata_for_model(model: &str) -> Option<ProviderMetadata> {
204    let canonical = resolve_model_alias(model);
205    let lower = canonical.to_ascii_lowercase();
206    if let Some((_, metadata)) = MODEL_REGISTRY.iter().find(|(alias, _)| *alias == lower) {
207        return Some(*metadata);
208    }
209    if lower.starts_with("grok") {
210        return Some(ProviderMetadata {
211            provider: ProviderKind::Xai,
212            auth_env: "XAI_API_KEY",
213            base_url_env: "XAI_BASE_URL",
214            default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
215        });
216    }
217    None
218}
219
220#[must_use]
221pub fn detect_provider_kind(model: &str) -> ProviderKind {
222    if let Some(metadata) = metadata_for_model(model) {
223        return metadata.provider;
224    }
225    let fallback = detect_available_provider().unwrap_or(ProviderKind::CodineerApi);
226    eprintln!("[warn] unknown model \"{model}\", falling back to {fallback:?} provider");
227    fallback
228}
229
230fn detect_available_provider() -> Option<ProviderKind> {
231    if codineer_provider::has_auth_from_env_or_saved().unwrap_or(false) {
232        return Some(ProviderKind::CodineerApi);
233    }
234    if openai_compat::has_api_key("OPENAI_API_KEY") {
235        return Some(ProviderKind::OpenAi);
236    }
237    if openai_compat::has_api_key("XAI_API_KEY") {
238        return Some(ProviderKind::Xai);
239    }
240    None
241}
242
243/// Detect which provider has available credentials and return its default model.
244/// Returns `None` if no credentials are found for any provider.
245#[must_use]
246pub fn auto_detect_default_model() -> Option<&'static str> {
247    match detect_available_provider()? {
248        ProviderKind::CodineerApi => Some("claude-sonnet-4-6"),
249        ProviderKind::Xai => Some("grok-3"),
250        ProviderKind::OpenAi => Some("gpt-4o"),
251    }
252}
253
254#[must_use]
255pub fn max_tokens_for_model(model: &str) -> u32 {
256    let canonical = resolve_model_alias(model);
257    if canonical.starts_with("claude-opus") || canonical == "opus" {
258        32_000
259    } else {
260        64_000
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::{detect_provider_kind, max_tokens_for_model, resolve_model_alias, ProviderKind};
267
268    #[test]
269    fn resolves_grok_aliases() {
270        assert_eq!(resolve_model_alias("grok"), "grok-3");
271        assert_eq!(resolve_model_alias("grok-mini"), "grok-3-mini");
272        assert_eq!(resolve_model_alias("grok-2"), "grok-2");
273    }
274
275    #[test]
276    fn detects_provider_from_model_name_first() {
277        assert_eq!(detect_provider_kind("grok"), ProviderKind::Xai);
278        assert_eq!(
279            detect_provider_kind("claude-sonnet-4-6"),
280            ProviderKind::CodineerApi
281        );
282    }
283
284    #[test]
285    fn keeps_existing_max_token_heuristic() {
286        assert_eq!(max_tokens_for_model("opus"), 32_000);
287        assert_eq!(max_tokens_for_model("grok-3"), 64_000);
288    }
289}