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#[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}