use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelCost {
pub input: f64,
pub output: f64,
#[serde(default)]
pub cache_read: Option<f64>,
#[serde(default)]
pub cache_write: Option<f64>,
#[serde(default)]
pub reasoning: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelLimit {
#[serde(default)]
pub context: u64,
#[serde(default)]
pub output: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelModalities {
#[serde(default)]
pub input: Vec<String>,
#[serde(default)]
pub output: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiModelInfo {
pub id: String,
pub name: String,
#[serde(default)]
pub family: Option<String>,
#[serde(default)]
pub attachment: bool,
#[serde(default)]
pub reasoning: bool,
#[serde(default)]
pub tool_call: bool,
#[serde(default)]
pub structured_output: Option<bool>,
#[serde(default)]
pub temperature: Option<bool>,
#[serde(default)]
pub knowledge: Option<String>,
#[serde(default)]
pub release_date: Option<String>,
#[serde(default)]
pub last_updated: Option<String>,
#[serde(default)]
pub modalities: Option<ModelModalities>,
#[serde(default)]
pub open_weights: bool,
#[serde(default)]
pub cost: Option<ModelCost>,
#[serde(default)]
pub limit: Option<ModelLimit>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderInfo {
pub id: String,
#[serde(default)]
pub env: Vec<String>,
#[serde(default)]
pub npm: Option<String>,
#[serde(default)]
pub api: Option<String>,
pub name: String,
#[serde(default)]
pub doc: Option<String>,
#[serde(default)]
pub models: HashMap<String, ApiModelInfo>,
}
pub type ModelsApiResponse = HashMap<String, ProviderInfo>;
#[derive(Debug, Clone, Default)]
pub struct ModelCatalog {
providers: HashMap<String, ProviderInfo>,
}
#[allow(dead_code)]
impl ModelCatalog {
pub fn new() -> Self {
Self::default()
}
pub async fn fetch() -> anyhow::Result<Self> {
const MODELS_URL: &str = "https://models.dev/api.json";
tracing::info!("Fetching models from {}", MODELS_URL);
let response = reqwest::get(MODELS_URL).await?;
let providers: ModelsApiResponse = response.json().await?;
tracing::info!("Loaded {} providers", providers.len());
Ok(Self { providers })
}
#[allow(dead_code)]
pub async fn fetch_from(url: &str) -> anyhow::Result<Self> {
let response = reqwest::get(url).await?;
let providers: ModelsApiResponse = response.json().await?;
Ok(Self { providers })
}
pub fn provider_has_api_key(&self, provider_id: &str) -> bool {
if let Some(manager) = crate::secrets::secrets_manager() {
let cache = manager.cache.try_read();
if let Ok(cache) = cache {
return cache.contains_key(provider_id);
}
}
false
}
pub async fn check_provider_api_key_async(&self, provider_id: &str) -> bool {
crate::secrets::has_api_key(provider_id).await
}
pub async fn preload_available_providers(&self) -> Vec<String> {
let mut available = Vec::new();
if let Some(manager) = crate::secrets::secrets_manager() {
if let Ok(providers) = manager.list_configured_providers().await {
for provider_id in providers {
if manager.has_api_key(&provider_id).await {
available.push(provider_id);
}
}
}
}
available
}
pub fn available_providers(&self) -> Vec<&str> {
self.providers
.keys()
.filter(|id| self.provider_has_api_key(id))
.map(|s| s.as_str())
.collect()
}
#[allow(dead_code)]
pub async fn available_providers_async(&self) -> Vec<String> {
let mut available = Vec::new();
for provider_id in self.providers.keys() {
if self.check_provider_api_key_async(provider_id).await {
available.push(provider_id.clone());
}
}
available
}
pub fn get_provider(&self, provider_id: &str) -> Option<&ProviderInfo> {
self.providers.get(provider_id)
}
pub fn get_available_provider(&self, provider_id: &str) -> Option<&ProviderInfo> {
if self.provider_has_api_key(provider_id) {
self.providers.get(provider_id)
} else {
None
}
}
pub fn get_model(&self, provider_id: &str, model_id: &str) -> Option<&ApiModelInfo> {
self.providers
.get(provider_id)
.and_then(|p| p.models.get(model_id))
}
pub fn get_available_model(&self, provider_id: &str, model_id: &str) -> Option<&ApiModelInfo> {
if self.provider_has_api_key(provider_id) {
self.get_model(provider_id, model_id)
} else {
None
}
}
pub fn find_model(&self, model_id: &str) -> Option<(&str, &ApiModelInfo)> {
for (provider_id, provider) in &self.providers {
if !self.provider_has_api_key(provider_id) {
continue;
}
if let Some(model) = provider.models.get(model_id) {
return Some((provider_id, model));
}
}
None
}
pub fn find_model_any(&self, model_id: &str) -> Option<(&str, &ApiModelInfo)> {
for (provider_id, provider) in &self.providers {
if let Some(model) = provider.models.get(model_id) {
return Some((provider_id, model));
}
}
None
}
#[allow(dead_code)]
pub fn provider_ids(&self) -> Vec<&str> {
self.providers.keys().map(|s| s.as_str()).collect()
}
pub fn all_providers(&self) -> &HashMap<String, ProviderInfo> {
&self.providers
}
pub fn models_for_provider(&self, provider_id: &str) -> Vec<&ApiModelInfo> {
if !self.provider_has_api_key(provider_id) {
return Vec::new();
}
self.providers
.get(provider_id)
.map(|p| p.models.values().collect())
.unwrap_or_default()
}
pub fn tool_capable_models(&self) -> Vec<(&str, &ApiModelInfo)> {
let mut result = Vec::new();
for (provider_id, provider) in &self.providers {
if !self.provider_has_api_key(provider_id) {
continue;
}
for model in provider.models.values() {
if model.tool_call {
result.push((provider_id.as_str(), model));
}
}
}
result
}
pub fn reasoning_models(&self) -> Vec<(&str, &ApiModelInfo)> {
let mut result = Vec::new();
for (provider_id, provider) in &self.providers {
if !self.provider_has_api_key(provider_id) {
continue;
}
for model in provider.models.values() {
if model.reasoning {
result.push((provider_id.as_str(), model));
}
}
}
result
}
pub fn recommended_coding_models(&self) -> Vec<(&str, &ApiModelInfo)> {
let preferred_ids = [
"claude-sonnet-4-6",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
"gpt-5-codex",
"gpt-5.1-codex",
"gpt-4o",
"gemini-3.1-pro-preview",
"gemini-2.5-pro",
"deepseek-v3.2",
"step-3.5-flash",
"glm-5",
"z-ai/glm-5",
];
let mut result = Vec::new();
for model_id in preferred_ids {
if let Some((provider, model)) = self.find_model(model_id) {
result.push((provider, model));
}
}
result
}
#[allow(dead_code)]
pub fn to_model_info(&self, model: &ApiModelInfo, provider_id: &str) -> super::ModelInfo {
super::ModelInfo {
id: model.id.clone(),
name: model.name.clone(),
provider: provider_id.to_string(),
context_window: model
.limit
.as_ref()
.map(|l| l.context as usize)
.unwrap_or(128_000),
max_output_tokens: model.limit.as_ref().map(|l| l.output as usize),
supports_vision: model.attachment,
supports_tools: model.tool_call,
supports_streaming: true,
input_cost_per_million: model.cost.as_ref().map(|c| c.input),
output_cost_per_million: model.cost.as_ref().map(|c| c.output),
}
}
}