codetether_agent/provider/
models.rs1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8const MODELS_API_URL: &str = "https://api.codetether.run/static/models/api.json";
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ModelCost {
13 pub input: f64,
14 pub output: f64,
15 #[serde(default)]
16 pub cache_read: Option<f64>,
17 #[serde(default)]
18 pub cache_write: Option<f64>,
19 #[serde(default)]
20 pub reasoning: Option<f64>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ModelLimit {
26 #[serde(default)]
27 pub context: u64,
28 #[serde(default)]
29 pub output: u64,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ModelModalities {
35 #[serde(default)]
36 pub input: Vec<String>,
37 #[serde(default)]
38 pub output: Vec<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ApiModelInfo {
44 pub id: String,
45 pub name: String,
46 #[serde(default)]
47 pub family: Option<String>,
48 #[serde(default)]
49 pub attachment: bool,
50 #[serde(default)]
51 pub reasoning: bool,
52 #[serde(default)]
53 pub tool_call: bool,
54 #[serde(default)]
55 pub structured_output: Option<bool>,
56 #[serde(default)]
57 pub temperature: Option<bool>,
58 #[serde(default)]
59 pub knowledge: Option<String>,
60 #[serde(default)]
61 pub release_date: Option<String>,
62 #[serde(default)]
63 pub last_updated: Option<String>,
64 #[serde(default)]
65 pub modalities: Option<ModelModalities>,
66 #[serde(default)]
67 pub open_weights: bool,
68 #[serde(default)]
69 pub cost: Option<ModelCost>,
70 #[serde(default)]
71 pub limit: Option<ModelLimit>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ProviderInfo {
77 pub id: String,
78 #[serde(default)]
79 pub env: Vec<String>,
80 #[serde(default)]
81 pub npm: Option<String>,
82 #[serde(default)]
83 pub api: Option<String>,
84 pub name: String,
85 #[serde(default)]
86 pub doc: Option<String>,
87 #[serde(default)]
88 pub models: HashMap<String, ApiModelInfo>,
89}
90
91pub type ModelsApiResponse = HashMap<String, ProviderInfo>;
93
94#[derive(Debug, Clone, Default)]
96pub struct ModelCatalog {
97 providers: HashMap<String, ProviderInfo>,
98}
99
100#[allow(dead_code)]
101impl ModelCatalog {
102 pub fn new() -> Self {
104 Self::default()
105 }
106
107 pub async fn fetch() -> anyhow::Result<Self> {
109 tracing::info!("Fetching models from {}", MODELS_API_URL);
110 let response = reqwest::get(MODELS_API_URL).await?;
111 let providers: ModelsApiResponse = response.json().await?;
112 tracing::info!("Loaded {} providers", providers.len());
113 Ok(Self { providers })
114 }
115
116 #[allow(dead_code)]
118 pub async fn fetch_from(url: &str) -> anyhow::Result<Self> {
119 let response = reqwest::get(url).await?;
120 let providers: ModelsApiResponse = response.json().await?;
121 Ok(Self { providers })
122 }
123
124 pub fn provider_has_api_key(&self, provider_id: &str) -> bool {
129 if let Some(manager) = crate::secrets::secrets_manager() {
131 let cache = manager.cache.try_read();
133 if let Ok(cache) = cache {
134 return cache.contains_key(provider_id);
135 }
136 }
137 false
138 }
139
140 pub async fn check_provider_api_key_async(&self, provider_id: &str) -> bool {
142 crate::secrets::has_api_key(provider_id).await
143 }
144
145 pub async fn preload_available_providers(&self) -> Vec<String> {
147 let mut available = Vec::new();
148
149 if let Some(manager) = crate::secrets::secrets_manager() {
150 if let Ok(providers) = manager.list_configured_providers().await {
152 for provider_id in providers {
153 if manager.has_api_key(&provider_id).await {
155 available.push(provider_id);
156 }
157 }
158 }
159 }
160
161 available
162 }
163
164 pub fn available_providers(&self) -> Vec<&str> {
166 self.providers
167 .keys()
168 .filter(|id| self.provider_has_api_key(id))
169 .map(|s| s.as_str())
170 .collect()
171 }
172
173 #[allow(dead_code)]
175 pub async fn available_providers_async(&self) -> Vec<String> {
176 let mut available = Vec::new();
177 for provider_id in self.providers.keys() {
178 if self.check_provider_api_key_async(provider_id).await {
179 available.push(provider_id.clone());
180 }
181 }
182 available
183 }
184
185 pub fn get_provider(&self, provider_id: &str) -> Option<&ProviderInfo> {
187 self.providers.get(provider_id)
188 }
189
190 pub fn get_available_provider(&self, provider_id: &str) -> Option<&ProviderInfo> {
192 if self.provider_has_api_key(provider_id) {
193 self.providers.get(provider_id)
194 } else {
195 None
196 }
197 }
198
199 pub fn get_model(&self, provider_id: &str, model_id: &str) -> Option<&ApiModelInfo> {
201 self.providers
202 .get(provider_id)
203 .and_then(|p| p.models.get(model_id))
204 }
205
206 pub fn get_available_model(&self, provider_id: &str, model_id: &str) -> Option<&ApiModelInfo> {
208 if self.provider_has_api_key(provider_id) {
209 self.get_model(provider_id, model_id)
210 } else {
211 None
212 }
213 }
214
215 pub fn find_model(&self, model_id: &str) -> Option<(&str, &ApiModelInfo)> {
217 for (provider_id, provider) in &self.providers {
218 if !self.provider_has_api_key(provider_id) {
219 continue;
220 }
221 if let Some(model) = provider.models.get(model_id) {
222 return Some((provider_id, model));
223 }
224 }
225 None
226 }
227
228 pub fn find_model_any(&self, model_id: &str) -> Option<(&str, &ApiModelInfo)> {
230 for (provider_id, provider) in &self.providers {
231 if let Some(model) = provider.models.get(model_id) {
232 return Some((provider_id, model));
233 }
234 }
235 None
236 }
237
238 #[allow(dead_code)]
240 pub fn provider_ids(&self) -> Vec<&str> {
241 self.providers.keys().map(|s| s.as_str()).collect()
242 }
243
244 pub fn all_providers(&self) -> &HashMap<String, ProviderInfo> {
246 &self.providers
247 }
248
249 pub fn models_for_provider(&self, provider_id: &str) -> Vec<&ApiModelInfo> {
251 if !self.provider_has_api_key(provider_id) {
252 return Vec::new();
253 }
254 self.providers
255 .get(provider_id)
256 .map(|p| p.models.values().collect())
257 .unwrap_or_default()
258 }
259
260 pub fn tool_capable_models(&self) -> Vec<(&str, &ApiModelInfo)> {
262 let mut result = Vec::new();
263 for (provider_id, provider) in &self.providers {
264 if !self.provider_has_api_key(provider_id) {
265 continue;
266 }
267 for model in provider.models.values() {
268 if model.tool_call {
269 result.push((provider_id.as_str(), model));
270 }
271 }
272 }
273 result
274 }
275
276 pub fn reasoning_models(&self) -> Vec<(&str, &ApiModelInfo)> {
278 let mut result = Vec::new();
279 for (provider_id, provider) in &self.providers {
280 if !self.provider_has_api_key(provider_id) {
281 continue;
282 }
283 for model in provider.models.values() {
284 if model.reasoning {
285 result.push((provider_id.as_str(), model));
286 }
287 }
288 }
289 result
290 }
291
292 pub fn recommended_coding_models(&self) -> Vec<(&str, &ApiModelInfo)> {
294 let preferred_ids = [
295 "claude-sonnet-4-20250514",
296 "claude-opus-4-20250514",
297 "gpt-5-codex",
298 "gpt-5.1-codex",
299 "gpt-4o",
300 "gemini-2.5-pro",
301 "deepseek-v3.2",
302 "step-3.5-flash",
303 "z-ai/glm-4.7",
304 ];
305
306 let mut result = Vec::new();
307 for model_id in preferred_ids {
308 if let Some((provider, model)) = self.find_model(model_id) {
309 result.push((provider, model));
310 }
311 }
312 result
313 }
314
315 #[allow(dead_code)]
317 pub fn to_model_info(&self, model: &ApiModelInfo, provider_id: &str) -> super::ModelInfo {
318 super::ModelInfo {
319 id: model.id.clone(),
320 name: model.name.clone(),
321 provider: provider_id.to_string(),
322 context_window: model
323 .limit
324 .as_ref()
325 .map(|l| l.context as usize)
326 .unwrap_or(128_000),
327 max_output_tokens: model.limit.as_ref().map(|l| l.output as usize),
328 supports_vision: model.attachment,
329 supports_tools: model.tool_call,
330 supports_streaming: true,
331 input_cost_per_million: model.cost.as_ref().map(|c| c.input),
332 output_cost_per_million: model.cost.as_ref().map(|c| c.output),
333 }
334 }
335}