Skip to content

Commit e38ddbd

Browse files
committed
refactor(models): use models.dev API for model discovery
1 parent 372b409 commit e38ddbd

15 files changed

Lines changed: 108 additions & 493 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ rullm --system "You are a helpful assistant." "Summarize this text"
6666
# List available models (shows only chat models, with your aliases)
6767
rullm models list
6868

69-
# Update model list for all providers with API keys
69+
# Update model list from models.dev (no API keys required)
7070
rullm models update
7171

7272
# Manage aliases
@@ -129,4 +129,4 @@ source <(COMPLETE=bash ./target/debug/rullm)
129129

130130
# zsh
131131
source <(COMPLETE=zsh ./target/debug/rullm)
132-
```
132+
```

crates/rullm-cli/src/cli_client.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -486,18 +486,6 @@ impl CliClient {
486486
}
487487
}
488488

489-
/// Get available models for the provider
490-
pub async fn available_models(&self) -> Result<Vec<String>, LlmError> {
491-
match self {
492-
Self::OpenAI { client, .. } => client.list_models().await,
493-
Self::Anthropic { client, .. } => client.list_models().await,
494-
Self::Google { client, .. } => client.list_models().await,
495-
Self::Groq { client, .. } | Self::OpenRouter { client, .. } => {
496-
client.available_models().await
497-
}
498-
}
499-
}
500-
501489
/// Get provider name
502490
pub fn provider_name(&self) -> &'static str {
503491
match self {

crates/rullm-cli/src/commands/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const CHAT_EXAMPLES: &str = r#"EXAMPLES:
3737

3838
const MODELS_EXAMPLES: &str = r#"EXAMPLES:
3939
rullm models list # List cached models
40-
rullm models update -m openai/gpt-4 # Fetch OpenAI models
40+
rullm models update # Fetch latest models
4141
rullm models default openai/gpt-4o # Set default model
4242
rullm models clear # Clear model cache"#;
4343

crates/rullm-cli/src/commands/models.rs

Lines changed: 56 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
use crate::cli_client::CliClient;
21
use anyhow::Result;
32
use chrono::Utc;
43
use clap::{Args, Subcommand};
5-
use rullm_core::LlmError;
4+
use serde::Deserialize;
5+
use std::collections::HashMap;
66
use strum::IntoEnumIterator;
77

88
use crate::{
99
aliases::UserAliasConfig,
1010
args::{Cli, CliConfig},
11-
client,
1211
commands::{ModelsCache, format_duration},
1312
constants::{ALIASES_CONFIG_FILE, MODEL_FILE_NAME},
1413
output::OutputLevel,
@@ -23,14 +22,14 @@ pub struct ModelsArgs {
2322

2423
#[derive(Subcommand)]
2524
pub enum ModelsAction {
26-
/// List available models for the current provider (default)
25+
/// List cached models
2726
List,
2827
/// Set a default model that will be used when --model is not supplied
2928
Default {
3029
/// Model identifier in the form provider:model-name (e.g. openai:gpt-4o)
3130
model: Option<String>,
3231
},
33-
/// Fetch fresh models from all providers with available API keys and update local cache
32+
/// Fetch fresh models from models.dev and update local cache
3433
Update,
3534
/// Clear the local models cache
3635
Clear,
@@ -41,7 +40,7 @@ impl ModelsArgs {
4140
&self,
4241
output_level: OutputLevel,
4342
cli_config: &mut CliConfig,
44-
cli: &Cli,
43+
_cli: &Cli,
4544
) -> Result<()> {
4645
match &self.action {
4746
ModelsAction::List => {
@@ -67,34 +66,28 @@ impl ModelsArgs {
6766
}
6867
}
6968
ModelsAction::Update => {
70-
// List of supported providers
71-
let providers = Provider::iter();
72-
let mut updated = vec![];
73-
let mut skipped = vec![];
74-
75-
for provider in providers {
76-
let provider = format!("{provider}");
77-
// Try to create a client for this provider
78-
let model_hint = format!("{provider}:dummy"); // dummy model name, just to get the client
79-
let client = match client::from_model(&model_hint, cli, cli_config).await {
80-
Ok(c) => c,
81-
Err(_) => {
82-
skipped.push(provider);
83-
continue;
84-
}
85-
};
86-
match update_models(cli_config, &client, output_level).await {
87-
Ok(_) => updated.push(provider),
88-
Err(_) => skipped.push(provider),
89-
}
69+
let supported: Vec<&str> = Provider::iter().map(|p| p.models_dev_id()).collect();
70+
71+
crate::output::progress("Fetching models from models.dev...", output_level);
72+
73+
let models = fetch_models_from_models_dev(&supported).await?;
74+
if models.is_empty() {
75+
anyhow::bail!("No models returned by models.dev");
9076
}
9177

92-
if !skipped.is_empty() {
93-
crate::output::note(
94-
&format!("Skipped (no API key or error): {}", skipped.join(", ")),
95-
output_level,
96-
);
78+
let cache = ModelsCache::new(models);
79+
let path = cli_config.data_base_path.join(MODEL_FILE_NAME);
80+
81+
if let Some(parent) = path.parent() {
82+
std::fs::create_dir_all(parent)?;
9783
}
84+
85+
std::fs::write(&path, serde_json::to_string_pretty(&cache)?)?;
86+
87+
crate::output::success(
88+
&format!("Updated {} models", cache.models.len()),
89+
output_level,
90+
);
9891
}
9992
ModelsAction::Clear => {
10093
clear_models_cache(cli_config, output_level)?;
@@ -216,85 +209,6 @@ pub fn clear_models_cache(cli_config: &CliConfig, output_level: OutputLevel) ->
216209
Ok(())
217210
}
218211

219-
pub async fn update_models(
220-
cli_config: &mut CliConfig,
221-
client: &CliClient,
222-
output_level: OutputLevel,
223-
) -> Result<(), LlmError> {
224-
crate::output::progress(
225-
&format!(
226-
"Fetching models from {}...",
227-
crate::output::format_provider(client.provider_name())
228-
),
229-
output_level,
230-
);
231-
232-
let mut models = client.available_models().await.map_err(|e| {
233-
crate::output::error(&format!("Failed to fetch models: {e}"), output_level);
234-
e
235-
})?;
236-
237-
if models.is_empty() {
238-
crate::output::error("No models returned by provider", output_level);
239-
return Err(LlmError::model(
240-
"No models returned by provider".to_string(),
241-
));
242-
}
243-
244-
models.sort();
245-
models.dedup();
246-
247-
_cache_models(cli_config, client.provider_name(), &models).map_err(|e| {
248-
crate::output::error(&format!("Failed to update models cache: {e}"), output_level);
249-
LlmError::unknown(e.to_string())
250-
})?;
251-
252-
crate::output::success(
253-
&format!(
254-
"Updated {} models for {}",
255-
models.len(),
256-
client.provider_name()
257-
),
258-
output_level,
259-
);
260-
261-
Ok(())
262-
}
263-
264-
fn _cache_models(cli_config: &CliConfig, provider_name: &str, models: &[String]) -> Result<()> {
265-
use std::fs;
266-
267-
let path = cli_config.data_base_path.join(MODEL_FILE_NAME);
268-
// TODO: we shouldn't need to do this here, this should be done while cli_config is created
269-
// TODO: Remove if we already do this.
270-
if let Some(parent) = path.parent() {
271-
fs::create_dir_all(parent)?;
272-
}
273-
274-
// Load existing cache if present
275-
let mut entries = if let Ok(Some(cache)) = load_models_cache(cli_config) {
276-
cache.models
277-
} else {
278-
Vec::new()
279-
};
280-
281-
// Remove all entries for this provider
282-
let prefix = format!("{}:", provider_name.to_lowercase());
283-
entries.retain(|m| !m.starts_with(&prefix));
284-
285-
// Add new models for this provider
286-
let new_entries: Vec<String> = models
287-
.iter()
288-
.map(|m| format!("{}:{}", provider_name.to_lowercase(), m))
289-
.collect();
290-
entries.extend(new_entries);
291-
292-
let cache = ModelsCache::new(entries);
293-
let json = serde_json::to_string_pretty(&cache)?;
294-
fs::write(path, json)?;
295-
Ok(())
296-
}
297-
298212
pub(crate) fn load_models_cache(cli_config: &CliConfig) -> Result<Option<ModelsCache>> {
299213
use std::fs;
300214

@@ -314,3 +228,35 @@ pub(crate) fn load_models_cache(cli_config: &CliConfig) -> Result<Option<ModelsC
314228
// Old format doesn't have timestamp info
315229
Ok(None)
316230
}
231+
232+
#[derive(Deserialize)]
233+
struct ModelsDevProvider {
234+
models: HashMap<String, ModelsDevModel>,
235+
}
236+
237+
#[derive(Deserialize)]
238+
struct ModelsDevModel {
239+
#[serde(default)]
240+
id: Option<String>,
241+
}
242+
243+
async fn fetch_models_from_models_dev(supported_providers: &[&str]) -> Result<Vec<String>> {
244+
let response = reqwest::get("https://models.dev/api.json")
245+
.await?
246+
.error_for_status()?;
247+
let providers: HashMap<String, ModelsDevProvider> = response.json().await?;
248+
249+
let mut all_models = Vec::new();
250+
for provider_id in supported_providers {
251+
if let Some(provider) = providers.get(*provider_id) {
252+
for (model_id, model) in &provider.models {
253+
let id = model.id.as_deref().unwrap_or(model_id);
254+
all_models.push(format!("{provider_id}:{id}"));
255+
}
256+
}
257+
}
258+
259+
all_models.sort();
260+
all_models.dedup();
261+
Ok(all_models)
262+
}

crates/rullm-cli/src/provider.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,14 @@ impl Provider {
9090
Provider::Google => "GOOGLE_AI_API_KEY",
9191
}
9292
}
93+
94+
pub fn models_dev_id(&self) -> &'static str {
95+
match self {
96+
Provider::OpenAI => "openai",
97+
Provider::Groq => "groq",
98+
Provider::OpenRouter => "openrouter",
99+
Provider::Anthropic => "anthropic",
100+
Provider::Google => "google",
101+
}
102+
}
93103
}

crates/rullm-core/examples/README.md

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,6 @@ match provider.chat_completion(request).await {
348348

349349
- **`provider.chat_completion(request)`** - Send chat completion
350350
- **`provider.health_check()`** - Test API connectivity
351-
- **`provider.available_models()`** - Get supported models
352351
- **`config.validate()`** - Validate configuration
353352

354353
### Supported Models
@@ -391,7 +390,7 @@ match provider.chat_completion(request).await {
391390

392391
## Test All Providers (`test_all_providers.rs`)
393392

394-
Comprehensive test that validates all LLM providers and their `available_models` functionality:
393+
Comprehensive test that validates all LLM providers with health checks:
395394

396395
```bash
397396
# Set up your API keys
@@ -405,31 +404,26 @@ cargo run --example test_all_providers
405404

406405
**Features:**
407406
- Tests OpenAI, Anthropic, and Google providers
408-
- Calls `available_models()` for each provider
409-
- Validates expected model patterns
410407
- Performs health checks
411408
- Provides detailed success/failure reporting
412409
- Gracefully handles missing API keys
413410

414411
**Sample Output:**
415412
```
416-
🚀 Testing All LLM Providers and Their Available Models
413+
🚀 Testing All LLM Providers
417414
418415
🔍 Testing OpenAI Provider...
419-
Provider name: openai
420416
Health check: ✅ Passed
421-
Expected model 'gpt-4': ✅ Found
422-
Expected model 'gpt-3.5-turbo': ✅ Found
423-
✅ OpenAI: Found 5 models
417+
✅ OpenAI: Health check passed
424418
425419
📊 SUMMARY:
426-
┌─────────────┬────────┬─────────────
427-
│ Provider │ Status │ Models │
428-
├─────────────┼────────┼─────────────
429-
│ OpenAI │ ✅ Pass │ 5 models │
430-
│ Anthropic │ ✅ Pass │ 5 models │
431-
│ Google │ ✅ Pass │ 5 models │
432-
└─────────────┴────────┴─────────────
420+
┌─────────────┬────────┐
421+
│ Provider │ Status │
422+
├─────────────┼────────┤
423+
│ OpenAI │ ✅ Pass │
424+
│ Anthropic │ ✅ Pass │
425+
│ Google │ ✅ Pass │
426+
└─────────────┴────────┘
433427
434428
🎉 All providers are working correctly!
435429
```
@@ -438,4 +432,4 @@ Use this example for:
438432
- Verifying your API keys work
439433
- Testing network connectivity
440434
- Validating provider implementations
441-
- CI/CD pipeline health checks
435+
- CI/CD pipeline health checks

crates/rullm-core/examples/google_simple.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
125125
}
126126
}
127127

128-
// 7. List models
129-
println!("\n📋 Available models:");
130-
let models = client.list_models().await?;
131-
for (i, model) in models.iter().take(5).enumerate() {
132-
println!(" {}. {}", i + 1, model);
133-
}
134-
if models.len() > 5 {
135-
println!(" ... and {} more", models.len() - 5);
136-
}
137-
138-
// 8. Health check
128+
// 7. Health check
139129
match client.health_check().await {
140130
Ok(_) => println!("\n✅ Google AI is healthy"),
141131
Err(e) => println!("\n❌ Health check failed: {e}"),

crates/rullm-core/examples/openai_config.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
7272
Err(e) => println!(" ❌ Health check failed: {e}"),
7373
}
7474

75-
// Get available models
76-
match client.list_models().await {
77-
Ok(models) => {
78-
println!(" Available models (first 5):");
79-
for (i, model) in models.iter().take(5).enumerate() {
80-
println!(" {}. {}", i + 1, model);
81-
}
82-
if models.len() > 5 {
83-
println!(" ... and {} more", models.len() - 5);
84-
}
85-
}
86-
Err(e) => println!(" ❌ Error getting models: {e}"),
87-
}
88-
8975
// Make a simple request
9076
println!("\n Testing chat completion...");
9177
let mut test_request = ChatCompletionRequest::new(

0 commit comments

Comments
 (0)