From e38ddbd1b082fbfe436442db98cbe90e7f1924ed Mon Sep 17 00:00:00 2001 From: lambda Date: Sun, 21 Dec 2025 17:13:38 +0530 Subject: [PATCH 1/2] refactor(models): use models.dev API for model discovery --- README.md | 4 +- crates/rullm-cli/src/cli_client.rs | 12 -- crates/rullm-cli/src/commands/mod.rs | 2 +- crates/rullm-cli/src/commands/models.rs | 166 ++++++------------ crates/rullm-cli/src/provider.rs | 10 ++ crates/rullm-core/examples/README.md | 28 ++- crates/rullm-core/examples/google_simple.rs | 12 +- crates/rullm-core/examples/openai_config.rs | 14 -- .../examples/openai_conversation.rs | 14 -- crates/rullm-core/examples/openai_simple.rs | 12 +- .../rullm-core/examples/test_all_providers.rs | 113 +++--------- .../src/providers/anthropic/client.rs | 62 ------- .../rullm-core/src/providers/google/client.rs | 50 ------ .../rullm-core/src/providers/openai/client.rs | 47 ----- .../src/providers/openai_compatible/mod.rs | 55 ------ 15 files changed, 108 insertions(+), 493 deletions(-) diff --git a/README.md b/README.md index 554549a0..8643cc89 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ rullm --system "You are a helpful assistant." "Summarize this text" # List available models (shows only chat models, with your aliases) rullm models list -# Update model list for all providers with API keys +# Update model list from models.dev (no API keys required) rullm models update # Manage aliases @@ -129,4 +129,4 @@ source <(COMPLETE=bash ./target/debug/rullm) # zsh source <(COMPLETE=zsh ./target/debug/rullm) -``` \ No newline at end of file +``` diff --git a/crates/rullm-cli/src/cli_client.rs b/crates/rullm-cli/src/cli_client.rs index 743105fb..e22a692d 100644 --- a/crates/rullm-cli/src/cli_client.rs +++ b/crates/rullm-cli/src/cli_client.rs @@ -486,18 +486,6 @@ impl CliClient { } } - /// Get available models for the provider - pub async fn available_models(&self) -> Result, LlmError> { - match self { - Self::OpenAI { client, .. } => client.list_models().await, - Self::Anthropic { client, .. } => client.list_models().await, - Self::Google { client, .. } => client.list_models().await, - Self::Groq { client, .. } | Self::OpenRouter { client, .. } => { - client.available_models().await - } - } - } - /// Get provider name pub fn provider_name(&self) -> &'static str { match self { diff --git a/crates/rullm-cli/src/commands/mod.rs b/crates/rullm-cli/src/commands/mod.rs index 718f5453..a1964484 100644 --- a/crates/rullm-cli/src/commands/mod.rs +++ b/crates/rullm-cli/src/commands/mod.rs @@ -37,7 +37,7 @@ const CHAT_EXAMPLES: &str = r#"EXAMPLES: const MODELS_EXAMPLES: &str = r#"EXAMPLES: rullm models list # List cached models - rullm models update -m openai/gpt-4 # Fetch OpenAI models + rullm models update # Fetch latest models rullm models default openai/gpt-4o # Set default model rullm models clear # Clear model cache"#; diff --git a/crates/rullm-cli/src/commands/models.rs b/crates/rullm-cli/src/commands/models.rs index f66439ab..680d7713 100644 --- a/crates/rullm-cli/src/commands/models.rs +++ b/crates/rullm-cli/src/commands/models.rs @@ -1,14 +1,13 @@ -use crate::cli_client::CliClient; use anyhow::Result; use chrono::Utc; use clap::{Args, Subcommand}; -use rullm_core::LlmError; +use serde::Deserialize; +use std::collections::HashMap; use strum::IntoEnumIterator; use crate::{ aliases::UserAliasConfig, args::{Cli, CliConfig}, - client, commands::{ModelsCache, format_duration}, constants::{ALIASES_CONFIG_FILE, MODEL_FILE_NAME}, output::OutputLevel, @@ -23,14 +22,14 @@ pub struct ModelsArgs { #[derive(Subcommand)] pub enum ModelsAction { - /// List available models for the current provider (default) + /// List cached models List, /// Set a default model that will be used when --model is not supplied Default { /// Model identifier in the form provider:model-name (e.g. openai:gpt-4o) model: Option, }, - /// Fetch fresh models from all providers with available API keys and update local cache + /// Fetch fresh models from models.dev and update local cache Update, /// Clear the local models cache Clear, @@ -41,7 +40,7 @@ impl ModelsArgs { &self, output_level: OutputLevel, cli_config: &mut CliConfig, - cli: &Cli, + _cli: &Cli, ) -> Result<()> { match &self.action { ModelsAction::List => { @@ -67,34 +66,28 @@ impl ModelsArgs { } } ModelsAction::Update => { - // List of supported providers - let providers = Provider::iter(); - let mut updated = vec![]; - let mut skipped = vec![]; - - for provider in providers { - let provider = format!("{provider}"); - // Try to create a client for this provider - let model_hint = format!("{provider}:dummy"); // dummy model name, just to get the client - let client = match client::from_model(&model_hint, cli, cli_config).await { - Ok(c) => c, - Err(_) => { - skipped.push(provider); - continue; - } - }; - match update_models(cli_config, &client, output_level).await { - Ok(_) => updated.push(provider), - Err(_) => skipped.push(provider), - } + let supported: Vec<&str> = Provider::iter().map(|p| p.models_dev_id()).collect(); + + crate::output::progress("Fetching models from models.dev...", output_level); + + let models = fetch_models_from_models_dev(&supported).await?; + if models.is_empty() { + anyhow::bail!("No models returned by models.dev"); } - if !skipped.is_empty() { - crate::output::note( - &format!("Skipped (no API key or error): {}", skipped.join(", ")), - output_level, - ); + let cache = ModelsCache::new(models); + let path = cli_config.data_base_path.join(MODEL_FILE_NAME); + + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; } + + std::fs::write(&path, serde_json::to_string_pretty(&cache)?)?; + + crate::output::success( + &format!("Updated {} models", cache.models.len()), + output_level, + ); } ModelsAction::Clear => { clear_models_cache(cli_config, output_level)?; @@ -216,85 +209,6 @@ pub fn clear_models_cache(cli_config: &CliConfig, output_level: OutputLevel) -> Ok(()) } -pub async fn update_models( - cli_config: &mut CliConfig, - client: &CliClient, - output_level: OutputLevel, -) -> Result<(), LlmError> { - crate::output::progress( - &format!( - "Fetching models from {}...", - crate::output::format_provider(client.provider_name()) - ), - output_level, - ); - - let mut models = client.available_models().await.map_err(|e| { - crate::output::error(&format!("Failed to fetch models: {e}"), output_level); - e - })?; - - if models.is_empty() { - crate::output::error("No models returned by provider", output_level); - return Err(LlmError::model( - "No models returned by provider".to_string(), - )); - } - - models.sort(); - models.dedup(); - - _cache_models(cli_config, client.provider_name(), &models).map_err(|e| { - crate::output::error(&format!("Failed to update models cache: {e}"), output_level); - LlmError::unknown(e.to_string()) - })?; - - crate::output::success( - &format!( - "Updated {} models for {}", - models.len(), - client.provider_name() - ), - output_level, - ); - - Ok(()) -} - -fn _cache_models(cli_config: &CliConfig, provider_name: &str, models: &[String]) -> Result<()> { - use std::fs; - - let path = cli_config.data_base_path.join(MODEL_FILE_NAME); - // TODO: we shouldn't need to do this here, this should be done while cli_config is created - // TODO: Remove if we already do this. - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - - // Load existing cache if present - let mut entries = if let Ok(Some(cache)) = load_models_cache(cli_config) { - cache.models - } else { - Vec::new() - }; - - // Remove all entries for this provider - let prefix = format!("{}:", provider_name.to_lowercase()); - entries.retain(|m| !m.starts_with(&prefix)); - - // Add new models for this provider - let new_entries: Vec = models - .iter() - .map(|m| format!("{}:{}", provider_name.to_lowercase(), m)) - .collect(); - entries.extend(new_entries); - - let cache = ModelsCache::new(entries); - let json = serde_json::to_string_pretty(&cache)?; - fs::write(path, json)?; - Ok(()) -} - pub(crate) fn load_models_cache(cli_config: &CliConfig) -> Result> { use std::fs; @@ -314,3 +228,35 @@ pub(crate) fn load_models_cache(cli_config: &CliConfig) -> Result, +} + +#[derive(Deserialize)] +struct ModelsDevModel { + #[serde(default)] + id: Option, +} + +async fn fetch_models_from_models_dev(supported_providers: &[&str]) -> Result> { + let response = reqwest::get("https://models.dev/api.json") + .await? + .error_for_status()?; + let providers: HashMap = response.json().await?; + + let mut all_models = Vec::new(); + for provider_id in supported_providers { + if let Some(provider) = providers.get(*provider_id) { + for (model_id, model) in &provider.models { + let id = model.id.as_deref().unwrap_or(model_id); + all_models.push(format!("{provider_id}:{id}")); + } + } + } + + all_models.sort(); + all_models.dedup(); + Ok(all_models) +} diff --git a/crates/rullm-cli/src/provider.rs b/crates/rullm-cli/src/provider.rs index b6362e05..db7ae82c 100644 --- a/crates/rullm-cli/src/provider.rs +++ b/crates/rullm-cli/src/provider.rs @@ -90,4 +90,14 @@ impl Provider { Provider::Google => "GOOGLE_AI_API_KEY", } } + + pub fn models_dev_id(&self) -> &'static str { + match self { + Provider::OpenAI => "openai", + Provider::Groq => "groq", + Provider::OpenRouter => "openrouter", + Provider::Anthropic => "anthropic", + Provider::Google => "google", + } + } } diff --git a/crates/rullm-core/examples/README.md b/crates/rullm-core/examples/README.md index 3028f325..f9cfbc50 100644 --- a/crates/rullm-core/examples/README.md +++ b/crates/rullm-core/examples/README.md @@ -348,7 +348,6 @@ match provider.chat_completion(request).await { - **`provider.chat_completion(request)`** - Send chat completion - **`provider.health_check()`** - Test API connectivity -- **`provider.available_models()`** - Get supported models - **`config.validate()`** - Validate configuration ### Supported Models @@ -391,7 +390,7 @@ match provider.chat_completion(request).await { ## Test All Providers (`test_all_providers.rs`) -Comprehensive test that validates all LLM providers and their `available_models` functionality: +Comprehensive test that validates all LLM providers with health checks: ```bash # Set up your API keys @@ -405,31 +404,26 @@ cargo run --example test_all_providers **Features:** - Tests OpenAI, Anthropic, and Google providers -- Calls `available_models()` for each provider -- Validates expected model patterns - Performs health checks - Provides detailed success/failure reporting - Gracefully handles missing API keys **Sample Output:** ``` -šŸš€ Testing All LLM Providers and Their Available Models +šŸš€ Testing All LLM Providers šŸ” Testing OpenAI Provider... - Provider name: openai Health check: āœ… Passed - Expected model 'gpt-4': āœ… Found - Expected model 'gpt-3.5-turbo': āœ… Found -āœ… OpenAI: Found 5 models +āœ… OpenAI: Health check passed šŸ“Š SUMMARY: -ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” -│ Provider │ Status │ Models │ -ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ -│ OpenAI │ āœ… Pass │ 5 models │ -│ Anthropic │ āœ… Pass │ 5 models │ -│ Google │ āœ… Pass │ 5 models │ -ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Provider │ Status │ +ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ +│ OpenAI │ āœ… Pass │ +│ Anthropic │ āœ… Pass │ +│ Google │ āœ… Pass │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ šŸŽ‰ All providers are working correctly! ``` @@ -438,4 +432,4 @@ Use this example for: - Verifying your API keys work - Testing network connectivity - Validating provider implementations -- CI/CD pipeline health checks \ No newline at end of file +- CI/CD pipeline health checks diff --git a/crates/rullm-core/examples/google_simple.rs b/crates/rullm-core/examples/google_simple.rs index 6c888378..12d0c9a4 100644 --- a/crates/rullm-core/examples/google_simple.rs +++ b/crates/rullm-core/examples/google_simple.rs @@ -125,17 +125,7 @@ async fn main() -> Result<(), Box> { } } - // 7. List models - println!("\nšŸ“‹ Available models:"); - let models = client.list_models().await?; - for (i, model) in models.iter().take(5).enumerate() { - println!(" {}. {}", i + 1, model); - } - if models.len() > 5 { - println!(" ... and {} more", models.len() - 5); - } - - // 8. Health check + // 7. Health check match client.health_check().await { Ok(_) => println!("\nāœ… Google AI is healthy"), Err(e) => println!("\nāŒ Health check failed: {e}"), diff --git a/crates/rullm-core/examples/openai_config.rs b/crates/rullm-core/examples/openai_config.rs index 2ba628d7..20457f8f 100644 --- a/crates/rullm-core/examples/openai_config.rs +++ b/crates/rullm-core/examples/openai_config.rs @@ -72,20 +72,6 @@ async fn main() -> Result<(), Box> { Err(e) => println!(" āŒ Health check failed: {e}"), } - // Get available models - match client.list_models().await { - Ok(models) => { - println!(" Available models (first 5):"); - for (i, model) in models.iter().take(5).enumerate() { - println!(" {}. {}", i + 1, model); - } - if models.len() > 5 { - println!(" ... and {} more", models.len() - 5); - } - } - Err(e) => println!(" āŒ Error getting models: {e}"), - } - // Make a simple request println!("\n Testing chat completion..."); let mut test_request = ChatCompletionRequest::new( diff --git a/crates/rullm-core/examples/openai_conversation.rs b/crates/rullm-core/examples/openai_conversation.rs index 6419c78a..fa630ff3 100644 --- a/crates/rullm-core/examples/openai_conversation.rs +++ b/crates/rullm-core/examples/openai_conversation.rs @@ -23,20 +23,6 @@ async fn main() -> Result<(), Box> { // Configure OpenAI client from environment let client = OpenAIClient::from_env()?; - // Check available models - println!("Available models:"); - match client.list_models().await { - Ok(models) => { - for (i, model) in models.iter().take(10).enumerate() { - println!(" {}. {}", i + 1, model); - } - if models.len() > 10 { - println!(" ... and {} more", models.len() - 10); - } - } - Err(e) => println!("Error getting models: {e}"), - } - // Health check match client.health_check().await { Ok(_) => println!("āœ… Client is healthy\n"), diff --git a/crates/rullm-core/examples/openai_simple.rs b/crates/rullm-core/examples/openai_simple.rs index 25c8f243..aa8c6c9f 100644 --- a/crates/rullm-core/examples/openai_simple.rs +++ b/crates/rullm-core/examples/openai_simple.rs @@ -121,17 +121,7 @@ async fn main() -> Result<(), Box> { creative_response.choices[0].finish_reason ); - // 6. List models - println!("\nšŸ“‹ Available models:"); - let models = client.list_models().await?; - for (i, model) in models.iter().take(5).enumerate() { - println!(" {}. {}", i + 1, model); - } - if models.len() > 5 { - println!(" ... and {} more", models.len() - 5); - } - - // 7. Health check + // 6. Health check match client.health_check().await { Ok(_) => println!("\nāœ… OpenAI API is healthy"), Err(e) => println!("\nāŒ Health check failed: {e}"), diff --git a/crates/rullm-core/examples/test_all_providers.rs b/crates/rullm-core/examples/test_all_providers.rs index 67a948a3..921af2c5 100644 --- a/crates/rullm-core/examples/test_all_providers.rs +++ b/crates/rullm-core/examples/test_all_providers.rs @@ -6,7 +6,7 @@ use std::env; #[tokio::main] async fn main() -> Result<(), Box> { - println!("šŸš€ Testing All LLM Providers and Their Available Models\n"); + println!("šŸš€ Testing All LLM Providers\n"); // Test results tracking let mut results = Vec::new(); @@ -14,22 +14,13 @@ async fn main() -> Result<(), Box> { // 1. Test OpenAI Provider println!("šŸ” Testing OpenAI Provider..."); match test_openai_provider().await { - Ok(models) => { - println!("āœ… OpenAI: Found {} models", models.len()); - println!( - " Models (first 5): {}", - models - .iter() - .take(5) - .map(|s| s.as_str()) - .collect::>() - .join(", ") - ); - results.push(("OpenAI", true, models.len())); + Ok(()) => { + println!("āœ… OpenAI: Health check passed"); + results.push(("OpenAI", true)); } Err(e) => { println!("āŒ OpenAI: Failed - {e}"); - results.push(("OpenAI", false, 0)); + results.push(("OpenAI", false)); } } println!(); @@ -37,22 +28,13 @@ async fn main() -> Result<(), Box> { // 2. Test Anthropic Provider println!("šŸ” Testing Anthropic Provider..."); match test_anthropic_provider().await { - Ok(models) => { - println!("āœ… Anthropic: Found {} models", models.len()); - println!( - " Models (first 5): {}", - models - .iter() - .take(5) - .map(|s| s.as_str()) - .collect::>() - .join(", ") - ); - results.push(("Anthropic", true, models.len())); + Ok(()) => { + println!("āœ… Anthropic: Health check passed"); + results.push(("Anthropic", true)); } Err(e) => { println!("āŒ Anthropic: Failed - {e}"); - results.push(("Anthropic", false, 0)); + results.push(("Anthropic", false)); } } println!(); @@ -60,43 +42,29 @@ async fn main() -> Result<(), Box> { // 3. Test Google Provider println!("šŸ” Testing Google Provider..."); match test_google_provider().await { - Ok(models) => { - println!("āœ… Google: Found {} models", models.len()); - println!( - " Models (first 5): {}", - models - .iter() - .take(5) - .map(|s| s.as_str()) - .collect::>() - .join(", ") - ); - results.push(("Google", true, models.len())); + Ok(()) => { + println!("āœ… Google: Health check passed"); + results.push(("Google", true)); } Err(e) => { println!("āŒ Google: Failed - {e}"); - results.push(("Google", false, 0)); + results.push(("Google", false)); } } println!(); // Summary println!("šŸ“Š SUMMARY:"); - println!("ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”"); - println!("│ Provider │ Status │ Models │"); - println!("ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤"); - for (provider, success, model_count) in &results { + println!("ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”"); + println!("│ Provider │ Status │"); + println!("ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤"); + for (provider, success) in &results { let status = if *success { "āœ… Pass" } else { "āŒ Fail" }; - let models = if *success { - format!("{model_count} models") - } else { - "N/A".to_string() - }; - println!("│ {provider:11} │ {status:6} │ {models:11} │"); + println!("│ {provider:11} │ {status:6} │"); } - println!("ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜"); + println!("ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜"); - let successful_providers = results.iter().filter(|(_, success, _)| *success).count(); + let successful_providers = results.iter().filter(|(_, success)| *success).count(); let total_providers = results.len(); if successful_providers == total_providers { @@ -110,7 +78,7 @@ async fn main() -> Result<(), Box> { Ok(()) } -async fn test_openai_provider() -> Result, Box> { +async fn test_openai_provider() -> Result<(), Box> { let api_key = env::var("OPENAI_API_KEY").map_err(|_| "OPENAI_API_KEY environment variable not set")?; @@ -123,23 +91,10 @@ async fn test_openai_provider() -> Result, Box println!(" Health check: āš ļø Warning - {e}"), } - // Get available models - let models = client.list_models().await?; - - // Verify we have expected models - let expected_models = ["gpt-4", "gpt-3.5-turbo"]; - for expected in &expected_models { - if models.iter().any(|m| m.contains(expected)) { - println!(" Expected model '{expected}': āœ… Found"); - } else { - println!(" Expected model '{expected}': āš ļø Not found in list"); - } - } - - Ok(models) + Ok(()) } -async fn test_anthropic_provider() -> Result, Box> { +async fn test_anthropic_provider() -> Result<(), Box> { let api_key = env::var("ANTHROPIC_API_KEY") .map_err(|_| "ANTHROPIC_API_KEY environment variable not set")?; @@ -152,13 +107,10 @@ async fn test_anthropic_provider() -> Result, Box println!(" Health check: āš ļø Warning - {e}"), } - // Get available models - let models = client.list_models().await?; - - Ok(models) + Ok(()) } -async fn test_google_provider() -> Result, Box> { +async fn test_google_provider() -> Result<(), Box> { let api_key = env::var("GOOGLE_API_KEY").map_err(|_| "GOOGLE_API_KEY environment variable not set")?; @@ -171,18 +123,5 @@ async fn test_google_provider() -> Result, Box println!(" Health check: āš ļø Warning - {e}"), } - // Get available models - let models = client.list_models().await?; - - // Verify we have expected models - let expected_models = ["gemini", "flash", "pro"]; - for expected in &expected_models { - if models.iter().any(|m| m.contains(expected)) { - println!(" Expected model pattern '{expected}': āœ… Found"); - } else { - println!(" Expected model pattern '{expected}': āš ļø Not found in list"); - } - } - - Ok(models) + Ok(()) } diff --git a/crates/rullm-core/src/providers/anthropic/client.rs b/crates/rullm-core/src/providers/anthropic/client.rs index bd08bc21..4bda4938 100644 --- a/crates/rullm-core/src/providers/anthropic/client.rs +++ b/crates/rullm-core/src/providers/anthropic/client.rs @@ -186,68 +186,6 @@ impl AnthropicClient { Ok(tokens) } - /// List available models - pub async fn list_models(&self) -> Result, LlmError> { - let url = format!("{}/v1/models", self.base_url); - - let mut req = self.client.get(&url); - for (key, value) in self.config.headers() { - req = req.header(key, value); - } - - let response = req.send().await?; - - if !response.status().is_success() { - return Err(LlmError::api( - "anthropic", - "Failed to fetch available models", - Some(response.status().to_string()), - None, - )); - } - - let json: serde_json::Value = response - .json() - .await - .map_err(|e| LlmError::serialization("Failed to parse models response", Box::new(e)))?; - - let models_array = json - .get("data") - .and_then(|d| d.as_array()) - .or_else(|| json.get("models").and_then(|m| m.as_array())) - .ok_or_else(|| { - LlmError::serialization( - "Invalid models response format", - Box::new(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Missing data array", - )), - ) - })?; - - let models: Vec = models_array - .iter() - .filter_map(|m| { - m.get("id") - .and_then(|id| id.as_str()) - .or_else(|| m.get("name").and_then(|name| name.as_str())) - .or_else(|| m.get("model").and_then(|model| model.as_str())) - .map(|s| s.to_string()) - }) - .collect(); - - if models.is_empty() { - return Err(LlmError::api( - "anthropic", - "No models found in response", - None, - None, - )); - } - - Ok(models) - } - /// Health check pub async fn health_check(&self) -> Result<(), LlmError> { // Anthropic doesn't have a dedicated health endpoint diff --git a/crates/rullm-core/src/providers/google/client.rs b/crates/rullm-core/src/providers/google/client.rs index adbdde1f..51207923 100644 --- a/crates/rullm-core/src/providers/google/client.rs +++ b/crates/rullm-core/src/providers/google/client.rs @@ -144,56 +144,6 @@ impl GoogleClient { }))) } - /// List available models - pub async fn list_models(&self) -> Result, LlmError> { - let url = format!("{}/models?key={}", self.base_url, self.config.api_key()); - - let mut req = self.client.get(&url); - for (key, value) in self.config.headers() { - req = req.header(key, value); - } - - let response = req.send().await?; - - if !response.status().is_success() { - return Err(LlmError::api( - "google", - "Failed to fetch available models", - Some(response.status().to_string()), - None, - )); - } - - let json: serde_json::Value = response - .json() - .await - .map_err(|e| LlmError::serialization("Failed to parse models response", Box::new(e)))?; - - let models_array = json - .get("models") - .and_then(|m| m.as_array()) - .ok_or_else(|| { - LlmError::serialization( - "Invalid models response format", - Box::new(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Missing models array", - )), - ) - })?; - - let models: Vec = models_array - .iter() - .filter_map(|m| { - m.get("name") - .and_then(|v| v.as_str()) - .map(|s| s.split('/').next_back().unwrap_or(s).to_string()) - }) - .collect(); - - Ok(models) - } - /// Health check pub async fn health_check(&self) -> Result<(), LlmError> { let url = format!("{}/models?key={}", self.base_url, self.config.api_key()); diff --git a/crates/rullm-core/src/providers/openai/client.rs b/crates/rullm-core/src/providers/openai/client.rs index 2ba78faa..5e1c0d65 100644 --- a/crates/rullm-core/src/providers/openai/client.rs +++ b/crates/rullm-core/src/providers/openai/client.rs @@ -147,53 +147,6 @@ impl OpenAIClient { }))) } - /// List available models - pub async fn list_models(&self) -> Result, LlmError> { - let url = format!("{}/models", self.base_url); - - let mut req = self.client.get(&url); - for (key, value) in self.config.headers() { - req = req.header(key, value); - } - - let response = req.send().await?; - - if !response.status().is_success() { - return Err(LlmError::api( - "openai", - "Failed to fetch available models", - Some(response.status().to_string()), - None, - )); - } - - let json: serde_json::Value = response - .json() - .await - .map_err(|e| LlmError::serialization("Failed to parse models response", Box::new(e)))?; - - let models_array = json.get("data").and_then(|d| d.as_array()).ok_or_else(|| { - LlmError::serialization( - "Invalid models response format", - Box::new(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Missing data array", - )), - ) - })?; - - let models: Vec = models_array - .iter() - .filter_map(|m| { - m.get("id") - .and_then(|id| id.as_str()) - .map(|s| s.to_string()) - }) - .collect(); - - Ok(models) - } - /// Health check pub async fn health_check(&self) -> Result<(), LlmError> { let url = format!("{}/models", self.base_url); diff --git a/crates/rullm-core/src/providers/openai_compatible/mod.rs b/crates/rullm-core/src/providers/openai_compatible/mod.rs index 0fe42db7..c21dcae9 100644 --- a/crates/rullm-core/src/providers/openai_compatible/mod.rs +++ b/crates/rullm-core/src/providers/openai_compatible/mod.rs @@ -200,61 +200,6 @@ impl OpenAICompatibleProvider { }) } - /// Get list of available models - pub async fn available_models(&self) -> Result, LlmError> { - let url = format!("{}/models", self.config.base_url()); - - let mut req = self.client.get(&url); - for (key, value) in self.config.headers() { - req = req.header(&key, &value); - } - let resp = req.send().await?; - - if !resp.status().is_success() { - return Err(LlmError::api( - self.identity.name, - "Failed to fetch available models", - Some(resp.status().to_string()), - None, - )); - } - - let json: serde_json::Value = resp - .json() - .await - .map_err(|e| LlmError::serialization("Failed to parse models response", Box::new(e)))?; - - let models_array = json.get("data").and_then(|d| d.as_array()).ok_or_else(|| { - LlmError::serialization( - "Invalid models response format", - Box::new(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Missing data array", - )), - ) - })?; - - let models: Vec = models_array - .iter() - .filter_map(|m| { - m.get("id") - .and_then(|id| id.as_str()) - .map(|s| s.to_string()) - }) - .collect(); - - if models.is_empty() { - return Err(LlmError::api( - self.identity.name, - "No models found in response", - None, - None, - )); - } - - Ok(models) - } - /// Health check pub async fn health_check(&self) -> Result<(), LlmError> { let url = format!("{}/models", self.config.base_url()); From 0ab49c64f32a8011e884c35d7ac374b534e4b463 Mon Sep 17 00:00:00 2001 From: lambda Date: Sun, 21 Dec 2025 18:14:02 +0530 Subject: [PATCH 2/2] refactor(models): remove redundant models_dev_id method --- README.md | 2 +- crates/rullm-cli/src/commands/models.rs | 16 +++++++++++----- crates/rullm-cli/src/provider.rs | 10 ---------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8643cc89..f730a811 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ rullm --system "You are a helpful assistant." "Summarize this text" # List available models (shows only chat models, with your aliases) rullm models list -# Update model list from models.dev (no API keys required) +# Update model list rullm models update # Manage aliases diff --git a/crates/rullm-cli/src/commands/models.rs b/crates/rullm-cli/src/commands/models.rs index 680d7713..a03eadbe 100644 --- a/crates/rullm-cli/src/commands/models.rs +++ b/crates/rullm-cli/src/commands/models.rs @@ -3,6 +3,7 @@ use chrono::Utc; use clap::{Args, Subcommand}; use serde::Deserialize; use std::collections::HashMap; +use std::time::Duration; use strum::IntoEnumIterator; use crate::{ @@ -66,7 +67,7 @@ impl ModelsArgs { } } ModelsAction::Update => { - let supported: Vec<&str> = Provider::iter().map(|p| p.models_dev_id()).collect(); + let supported: Vec = Provider::iter().map(|p| p.to_string()).collect(); crate::output::progress("Fetching models from models.dev...", output_level); @@ -236,19 +237,24 @@ struct ModelsDevProvider { #[derive(Deserialize)] struct ModelsDevModel { - #[serde(default)] id: Option, } -async fn fetch_models_from_models_dev(supported_providers: &[&str]) -> Result> { - let response = reqwest::get("https://models.dev/api.json") +async fn fetch_models_from_models_dev(supported_providers: &[String]) -> Result> { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(10)) + .build()?; + + let response = client + .get("https://models.dev/api.json") + .send() .await? .error_for_status()?; let providers: HashMap = response.json().await?; let mut all_models = Vec::new(); for provider_id in supported_providers { - if let Some(provider) = providers.get(*provider_id) { + if let Some(provider) = providers.get(provider_id) { for (model_id, model) in &provider.models { let id = model.id.as_deref().unwrap_or(model_id); all_models.push(format!("{provider_id}:{id}")); diff --git a/crates/rullm-cli/src/provider.rs b/crates/rullm-cli/src/provider.rs index db7ae82c..b6362e05 100644 --- a/crates/rullm-cli/src/provider.rs +++ b/crates/rullm-cli/src/provider.rs @@ -90,14 +90,4 @@ impl Provider { Provider::Google => "GOOGLE_AI_API_KEY", } } - - pub fn models_dev_id(&self) -> &'static str { - match self { - Provider::OpenAI => "openai", - Provider::Groq => "groq", - Provider::OpenRouter => "openrouter", - Provider::Anthropic => "anthropic", - Provider::Google => "google", - } - } }