Swap canonical model from openrouter to models.dev (#6625)
This commit is contained in:
@@ -901,8 +901,8 @@ fn estimate_cost_usd(
|
||||
) -> Option<f64> {
|
||||
let canonical_model = maybe_get_canonical_model(provider, model)?;
|
||||
|
||||
let input_cost_per_token = canonical_model.pricing.prompt?;
|
||||
let output_cost_per_token = canonical_model.pricing.completion?;
|
||||
let input_cost_per_token = canonical_model.cost.input? / 1_000_000.0;
|
||||
let output_cost_per_token = canonical_model.cost.output? / 1_000_000.0;
|
||||
|
||||
let input_cost = input_cost_per_token * input_tokens as f64;
|
||||
let output_cost = output_cost_per_token * output_tokens as f64;
|
||||
|
||||
@@ -482,17 +482,17 @@ pub async fn get_pricing(
|
||||
|
||||
let mut pricing_data = Vec::new();
|
||||
|
||||
if let (Some(input_cost), Some(output_cost)) = (
|
||||
canonical_model.pricing.prompt,
|
||||
canonical_model.pricing.completion,
|
||||
) {
|
||||
if let (Some(input_cost), Some(output_cost)) =
|
||||
(canonical_model.cost.input, canonical_model.cost.output)
|
||||
{
|
||||
pricing_data.push(PricingData {
|
||||
provider: query.provider.clone(),
|
||||
model: query.model.clone(),
|
||||
input_token_cost: input_cost,
|
||||
output_token_cost: output_cost,
|
||||
// Canonical model costs are per million tokens, convert to per-token
|
||||
input_token_cost: input_cost / 1_000_000.0,
|
||||
output_token_cost: output_cost / 1_000_000.0,
|
||||
currency: "$".to_string(),
|
||||
context_length: Some(canonical_model.context_length as u32),
|
||||
context_length: Some(canonical_model.limit.context as u32),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,9 +42,9 @@ pub struct ModelInfo {
|
||||
pub name: String,
|
||||
/// The maximum context length this model supports
|
||||
pub context_limit: usize,
|
||||
/// Cost per token for input (optional)
|
||||
/// Cost per token for input in USD (optional)
|
||||
pub input_token_cost: Option<f64>,
|
||||
/// Cost per token for output (optional)
|
||||
/// Cost per token for output in USD (optional)
|
||||
pub output_token_cost: Option<f64>,
|
||||
/// Currency for the costs (default: "$")
|
||||
pub currency: Option<String>,
|
||||
@@ -456,15 +456,40 @@ pub trait Provider: Send + Sync {
|
||||
|
||||
let provider_name = self.get_name();
|
||||
|
||||
let recommended_models: Vec<String> = all_models
|
||||
// Get all text-capable models with their release dates
|
||||
let mut models_with_dates: Vec<(String, Option<String>)> = all_models
|
||||
.iter()
|
||||
.filter(|model| {
|
||||
map_to_canonical_model(provider_name, model, registry)
|
||||
.and_then(|canonical_id| registry.get(&canonical_id))
|
||||
.map(|m| m.input_modalities.contains(&"text".to_string()))
|
||||
.unwrap_or(false)
|
||||
.filter_map(|model| {
|
||||
let canonical_id = map_to_canonical_model(provider_name, model, registry)?;
|
||||
|
||||
let (provider, model_name) = canonical_id.split_once('/')?;
|
||||
let canonical_model = registry.get(provider, model_name)?;
|
||||
|
||||
if !canonical_model
|
||||
.modalities
|
||||
.input
|
||||
.contains(&crate::providers::canonical::Modality::Text)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let release_date = canonical_model.release_date.clone();
|
||||
|
||||
Some((model.clone(), release_date))
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Sort by release date (most recent first), then alphabetically for models without dates
|
||||
models_with_dates.sort_by(|a, b| match (&a.1, &b.1) {
|
||||
(Some(date_a), Some(date_b)) => date_b.cmp(date_a),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => a.0.cmp(&b.0),
|
||||
});
|
||||
|
||||
let recommended_models: Vec<String> = models_with_dates
|
||||
.into_iter()
|
||||
.map(|(name, _)| name)
|
||||
.collect();
|
||||
|
||||
if recommended_models.is_empty() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// Build canonical models from OpenRouter API
|
||||
/// Build canonical models from models.dev API
|
||||
///
|
||||
/// This script fetches models from OpenRouter and converts them to canonical format.
|
||||
/// This script fetches models from models.dev and converts them to canonical format.
|
||||
/// By default, it also checks which models from top providers are properly mapped.
|
||||
///
|
||||
/// Usage:
|
||||
@@ -10,7 +10,7 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use goose::providers::canonical::{
|
||||
canonical_name, CanonicalModel, CanonicalModelRegistry, Pricing,
|
||||
canonical_name, CanonicalModel, CanonicalModelRegistry, Limit, Modalities, Modality, Pricing,
|
||||
};
|
||||
use goose::providers::{canonical::ModelMapping, create_with_named_model};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -18,20 +18,35 @@ use serde_json::Value;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const OPENROUTER_API_URL: &str = "https://openrouter.ai/api/v1/models";
|
||||
const MODELS_DEV_API_URL: &str = "https://models.dev/api.json";
|
||||
|
||||
// Providers to include in canonical models
|
||||
const ALLOWED_PROVIDERS: &[&str] = &[
|
||||
"anthropic",
|
||||
"google",
|
||||
"openai",
|
||||
"meta-llama",
|
||||
"mistralai",
|
||||
"x-ai",
|
||||
"openrouter",
|
||||
"llama",
|
||||
"mistral",
|
||||
"xai",
|
||||
"deepseek",
|
||||
"cohere",
|
||||
"ai21",
|
||||
"qwen",
|
||||
"azure",
|
||||
"amazon-bedrock",
|
||||
"venice",
|
||||
"google-vertex",
|
||||
];
|
||||
|
||||
// Normalize provider names from models.dev to our canonical format
|
||||
fn normalize_provider_name(provider: &str) -> &str {
|
||||
match provider {
|
||||
"llama" => "meta-llama",
|
||||
"xai" => "x-ai",
|
||||
"mistral" => "mistralai",
|
||||
_ => provider,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
@@ -51,6 +66,7 @@ struct MappingEntry {
|
||||
provider: String,
|
||||
model: String,
|
||||
canonical: String,
|
||||
recommended: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -92,12 +108,16 @@ impl MappingReport {
|
||||
provider_name: &str,
|
||||
fetched_models: Vec<String>,
|
||||
mappings: Vec<ModelMapping>,
|
||||
recommended_models: Vec<String>,
|
||||
) {
|
||||
let mapping_map: HashMap<String, String> = mappings
|
||||
.iter()
|
||||
.map(|m| (m.provider_model.clone(), m.canonical_model.clone()))
|
||||
.collect();
|
||||
|
||||
let recommended_set: std::collections::HashSet<String> =
|
||||
recommended_models.into_iter().collect();
|
||||
|
||||
for model in &fetched_models {
|
||||
if !mapping_map.contains_key(model) {
|
||||
self.unmapped_models.push(ProviderModelPair {
|
||||
@@ -113,6 +133,7 @@ impl MappingReport {
|
||||
provider: provider_name.to_string(),
|
||||
model: model.clone(),
|
||||
canonical: canonical.clone(),
|
||||
recommended: recommended_set.contains(model),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -307,220 +328,167 @@ impl MappingReport {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn build_canonical_models() -> Result<()> {
|
||||
println!("Fetching models from OpenRouter API...");
|
||||
println!("Fetching models from models.dev API...");
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(OPENROUTER_API_URL)
|
||||
.get(MODELS_DEV_API_URL)
|
||||
.header("User-Agent", "goose/canonical-builder")
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to fetch from OpenRouter API")?;
|
||||
.context("Failed to fetch from models.dev API")?;
|
||||
|
||||
let json: Value = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse OpenRouter response")?;
|
||||
.context("Failed to parse models.dev response")?;
|
||||
|
||||
let models = json["data"]
|
||||
.as_array()
|
||||
.context("Expected 'data' array in OpenRouter response")?
|
||||
.clone();
|
||||
let providers_obj = json
|
||||
.as_object()
|
||||
.context("Expected object in models.dev response")?;
|
||||
|
||||
println!("Processing {} models from OpenRouter...", models.len());
|
||||
|
||||
// First pass: Group models by canonical ID and track the one with shortest name
|
||||
let mut canonical_groups: HashMap<String, &Value> = HashMap::new();
|
||||
let mut shortest_names: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for model in &models {
|
||||
let id = model["id"].as_str().unwrap();
|
||||
let name = model["name"].as_str().context("Model missing id field")?;
|
||||
|
||||
// Skip OpenRouter-specific pricing variants (:free, :nitro)
|
||||
// Keep :extended since it has different context length
|
||||
if id.contains(":free") || id.contains(":nitro") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let canonical_id = canonical_name("openrouter", id);
|
||||
|
||||
let provider = canonical_id.split('/').next().unwrap_or("");
|
||||
if !ALLOWED_PROVIDERS.contains(&provider) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let prompt_cost = model
|
||||
.get("pricing")
|
||||
.and_then(|p| p.get("prompt"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let completion_cost = model
|
||||
.get("pricing")
|
||||
.and_then(|p| p.get("completion"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let has_paid_pricing = prompt_cost > 0.0 || completion_cost > 0.0;
|
||||
|
||||
if let Some(existing_model) = canonical_groups.get(&canonical_id) {
|
||||
let existing_name = shortest_names.get(&canonical_id).unwrap();
|
||||
|
||||
let existing_prompt = existing_model
|
||||
.get("pricing")
|
||||
.and_then(|p| p.get("prompt"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let existing_completion = existing_model
|
||||
.get("pricing")
|
||||
.and_then(|p| p.get("completion"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let existing_has_paid = existing_prompt > 0.0 || existing_completion > 0.0;
|
||||
|
||||
let should_replace = if has_paid_pricing != existing_has_paid {
|
||||
has_paid_pricing // Prefer the one with paid pricing
|
||||
} else {
|
||||
name.len() < existing_name.len() // Both same pricing tier, prefer shorter name
|
||||
};
|
||||
|
||||
if should_replace {
|
||||
println!(
|
||||
" Updating {} from '{}' (paid: {}) to '{}' (paid: {})",
|
||||
canonical_id,
|
||||
existing_model["id"].as_str().unwrap(),
|
||||
existing_has_paid,
|
||||
id,
|
||||
has_paid_pricing
|
||||
);
|
||||
shortest_names.insert(canonical_id.clone(), name.to_string());
|
||||
canonical_groups.insert(canonical_id, model);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
" Adding: {} (from {}, paid: {})",
|
||||
canonical_id, id, has_paid_pricing
|
||||
);
|
||||
shortest_names.insert(canonical_id.clone(), name.to_string());
|
||||
canonical_groups.insert(canonical_id, model);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out beta/preview variants if non-beta version exists
|
||||
let beta_suffixes = ["-beta", "-preview", "-alpha"];
|
||||
let mut to_remove = Vec::new();
|
||||
|
||||
for canonical_id in canonical_groups.keys() {
|
||||
for suffix in &beta_suffixes {
|
||||
if canonical_id.ends_with(suffix) {
|
||||
// Check if non-beta version exists
|
||||
let base_id = canonical_id.strip_suffix(suffix).unwrap();
|
||||
if canonical_groups.contains_key(base_id) {
|
||||
println!(
|
||||
" Filtering out {} (non-beta version {} exists)",
|
||||
canonical_id, base_id
|
||||
);
|
||||
to_remove.push(canonical_id.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id in to_remove {
|
||||
canonical_groups.remove(&id);
|
||||
shortest_names.remove(&id);
|
||||
}
|
||||
|
||||
// Second pass: Build the registry with the selected models
|
||||
let mut registry = CanonicalModelRegistry::new();
|
||||
let mut total_models = 0;
|
||||
|
||||
for (canonical_id, model) in canonical_groups.iter() {
|
||||
let name = shortest_names.get(canonical_id).unwrap();
|
||||
for provider_key in ALLOWED_PROVIDERS {
|
||||
if let Some(provider_data) = providers_obj.get(*provider_key) {
|
||||
let models = provider_data["models"]
|
||||
.as_object()
|
||||
.context(format!("Provider {} missing models object", provider_key))?;
|
||||
|
||||
let context_length = model["context_length"].as_u64().unwrap_or(128_000) as usize;
|
||||
let normalized_provider = normalize_provider_name(provider_key);
|
||||
|
||||
let max_completion_tokens = model
|
||||
.get("top_provider")
|
||||
.and_then(|tp| tp.get("max_completion_tokens"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as usize);
|
||||
println!(
|
||||
"\nProcessing {} ({} models)...",
|
||||
normalized_provider,
|
||||
models.len()
|
||||
);
|
||||
|
||||
let mut input_modalities: Vec<String> = model
|
||||
.get("architecture")
|
||||
.and_then(|arch| arch.get("input_modalities"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec!["text".to_string()]);
|
||||
input_modalities.sort();
|
||||
for (model_id, model_data) in models {
|
||||
// Skip models without pricing information
|
||||
let cost_data = match model_data.get("cost") {
|
||||
Some(c) if !c.is_null() => c,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let mut output_modalities: Vec<String> = model
|
||||
.get("architecture")
|
||||
.and_then(|arch| arch.get("output_modalities"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec!["text".to_string()]);
|
||||
output_modalities.sort();
|
||||
let name = model_data["name"]
|
||||
.as_str()
|
||||
.context(format!("Model {} missing name", model_id))?;
|
||||
|
||||
let supports_tools = model
|
||||
.get("supported_parameters")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|params| params.iter().any(|param| param.as_str() == Some("tools")))
|
||||
.unwrap_or(false);
|
||||
// Use canonical_name to normalize the model ID (strips date stamps, etc.)
|
||||
// This deduplicates different versions of the same model
|
||||
let canonical_id = canonical_name(normalized_provider, model_id);
|
||||
|
||||
let pricing_obj = model
|
||||
.get("pricing")
|
||||
.context("Model missing pricing field")?;
|
||||
let pricing = Pricing {
|
||||
prompt: pricing_obj
|
||||
.get("prompt")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok()),
|
||||
completion: pricing_obj
|
||||
.get("completion")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok()),
|
||||
request: pricing_obj
|
||||
.get("request")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok()),
|
||||
image: pricing_obj
|
||||
.get("image")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok()),
|
||||
};
|
||||
let family = model_data
|
||||
.get("family")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let canonical_model = CanonicalModel {
|
||||
id: canonical_id.clone(),
|
||||
name: name.to_string(),
|
||||
context_length,
|
||||
max_completion_tokens,
|
||||
input_modalities,
|
||||
output_modalities,
|
||||
supports_tools,
|
||||
pricing,
|
||||
};
|
||||
let attachment = model_data.get("attachment").and_then(|v| v.as_bool());
|
||||
|
||||
registry.register(canonical_model);
|
||||
let reasoning = model_data.get("reasoning").and_then(|v| v.as_bool());
|
||||
|
||||
let tool_call = model_data
|
||||
.get("tool_call")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
let temperature = model_data.get("temperature").and_then(|v| v.as_bool());
|
||||
|
||||
let knowledge = model_data
|
||||
.get("knowledge")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let release_date = model_data
|
||||
.get("release_date")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let last_updated = model_data
|
||||
.get("last_updated")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let modalities = Modalities {
|
||||
input: model_data
|
||||
.get("modalities")
|
||||
.and_then(|m| m.get("input"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.filter_map(|s| {
|
||||
serde_json::from_value(serde_json::Value::String(s.to_string()))
|
||||
.ok()
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec![Modality::Text]),
|
||||
output: model_data
|
||||
.get("modalities")
|
||||
.and_then(|m| m.get("output"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.filter_map(|s| {
|
||||
serde_json::from_value(serde_json::Value::String(s.to_string()))
|
||||
.ok()
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec![Modality::Text]),
|
||||
};
|
||||
|
||||
let open_weights = model_data.get("open_weights").and_then(|v| v.as_bool());
|
||||
|
||||
let cost = Pricing {
|
||||
input: cost_data.get("input").and_then(|v| v.as_f64()),
|
||||
output: cost_data.get("output").and_then(|v| v.as_f64()),
|
||||
cache_read: cost_data.get("cache_read").and_then(|v| v.as_f64()),
|
||||
cache_write: cost_data.get("cache_write").and_then(|v| v.as_f64()),
|
||||
};
|
||||
|
||||
let limit = Limit {
|
||||
context: model_data
|
||||
.get("limit")
|
||||
.and_then(|l| l.get("context"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(128_000) as usize,
|
||||
output: model_data
|
||||
.get("limit")
|
||||
.and_then(|l| l.get("output"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as usize),
|
||||
};
|
||||
|
||||
let canonical_model = CanonicalModel {
|
||||
id: canonical_id.clone(),
|
||||
name: name.to_string(),
|
||||
family,
|
||||
attachment,
|
||||
reasoning,
|
||||
tool_call,
|
||||
temperature,
|
||||
knowledge,
|
||||
release_date,
|
||||
last_updated,
|
||||
modalities,
|
||||
open_weights,
|
||||
cost,
|
||||
limit,
|
||||
};
|
||||
|
||||
// Extract the normalized model name (everything after "provider/")
|
||||
let model_name = canonical_id
|
||||
.strip_prefix(&format!("{}/", normalized_provider))
|
||||
.unwrap_or(model_id);
|
||||
registry.register(normalized_provider, model_name, canonical_model);
|
||||
total_models += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
@@ -528,7 +496,7 @@ async fn build_canonical_models() -> Result<()> {
|
||||
registry.to_file(&output_path)?;
|
||||
println!(
|
||||
"\n✓ Wrote {} models to {}",
|
||||
registry.count(),
|
||||
total_models,
|
||||
output_path.display()
|
||||
);
|
||||
|
||||
@@ -538,7 +506,7 @@ async fn build_canonical_models() -> Result<()> {
|
||||
async fn check_provider(
|
||||
provider_name: &str,
|
||||
model_for_init: &str,
|
||||
) -> Result<(Vec<String>, Vec<ModelMapping>)> {
|
||||
) -> Result<(Vec<String>, Vec<ModelMapping>, Vec<String>)> {
|
||||
println!("Checking provider: {}", provider_name);
|
||||
|
||||
let provider = match create_with_named_model(provider_name, model_for_init).await {
|
||||
@@ -546,7 +514,7 @@ async fn check_provider(
|
||||
Err(e) => {
|
||||
println!(" ⚠ Failed to create provider: {}", e);
|
||||
println!(" This is expected if credentials are not configured.");
|
||||
return Ok((Vec::new(), Vec::new()));
|
||||
return Ok((Vec::new(), Vec::new(), Vec::new()));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -566,6 +534,18 @@ async fn check_provider(
|
||||
}
|
||||
};
|
||||
|
||||
let recommended_models = match provider.fetch_recommended_models().await {
|
||||
Ok(Some(models)) => {
|
||||
println!(" ✓ Found {} recommended models", models.len());
|
||||
models
|
||||
}
|
||||
Ok(None) => Vec::new(),
|
||||
Err(e) => {
|
||||
println!(" ⚠ Failed to fetch recommended models: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
let mut mappings = Vec::new();
|
||||
for model in &fetched_models {
|
||||
match provider.map_to_canonical_model(model).await {
|
||||
@@ -582,7 +562,7 @@ async fn check_provider(
|
||||
}
|
||||
println!(" ✓ Found {} mappings", mappings.len());
|
||||
|
||||
Ok((fetched_models, mappings))
|
||||
Ok((fetched_models, mappings, recommended_models))
|
||||
}
|
||||
|
||||
async fn check_canonical_mappings() -> Result<()> {
|
||||
@@ -596,15 +576,20 @@ async fn check_canonical_mappings() -> Result<()> {
|
||||
("openai", "gpt-4"),
|
||||
("openrouter", "anthropic/claude-3.5-sonnet"),
|
||||
("google", "gemini-1.5-pro-002"),
|
||||
("databricks", "claude-3-5-sonnet-20241022"),
|
||||
("tetrate", "claude-3-5-sonnet-computer-use"),
|
||||
("xai", "grok-code-fast-1"),
|
||||
("azure_openai", "gpt-4o"),
|
||||
("aws_bedrock", "anthropic.claude-3-5-sonnet-20241022-v2:0"),
|
||||
("venice", "llama-3.3-70b"),
|
||||
("gcp_vertex_ai", "gemini-1.5-pro-002"),
|
||||
];
|
||||
|
||||
let mut report = MappingReport::new();
|
||||
|
||||
for (provider_name, default_model) in providers {
|
||||
let (fetched, mappings) = check_provider(provider_name, default_model).await?;
|
||||
report.add_provider_results(provider_name, fetched, mappings);
|
||||
let (fetched, mappings, recommended) = check_provider(provider_name, default_model).await?;
|
||||
report.add_provider_results(provider_name, fetched, mappings, recommended);
|
||||
println!();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ mod model;
|
||||
mod name_builder;
|
||||
mod registry;
|
||||
|
||||
pub use model::{CanonicalModel, Pricing};
|
||||
pub use model::{CanonicalModel, Limit, Modalities, Modality, Pricing};
|
||||
pub use name_builder::{canonical_name, map_to_canonical_model, strip_version_suffix};
|
||||
pub use registry::CanonicalModelRegistry;
|
||||
|
||||
@@ -23,6 +23,13 @@ impl ModelMapping {
|
||||
|
||||
pub fn maybe_get_canonical_model(provider: &str, model: &str) -> Option<CanonicalModel> {
|
||||
let registry = CanonicalModelRegistry::bundled().ok()?;
|
||||
|
||||
// map_to_canonical_model returns the canonical ID (provider/model)
|
||||
// Parse it to get provider and model parts for registry lookup
|
||||
let canonical_id = map_to_canonical_model(provider, model, registry)?;
|
||||
registry.get(&canonical_id).cloned()
|
||||
if let Some((canon_provider, canon_model)) = canonical_id.split_once('/') {
|
||||
registry.get(canon_provider, canon_model).cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,120 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Pricing information for a model (all costs in USD per token)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Pricing {
|
||||
/// Cost per prompt token
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompt: Option<f64>,
|
||||
|
||||
/// Cost per completion token
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completion: Option<f64>,
|
||||
|
||||
/// Cost per request
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub request: Option<f64>,
|
||||
|
||||
/// Cost per image
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image: Option<f64>,
|
||||
/// Modality types for model input/output
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Modality {
|
||||
Text,
|
||||
Image,
|
||||
Audio,
|
||||
Video,
|
||||
Pdf,
|
||||
}
|
||||
|
||||
fn deserialize_modalities<'de, D>(deserializer: D) -> Result<Vec<Modality>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let strings: Vec<String> = Vec::deserialize(deserializer)?;
|
||||
Ok(strings
|
||||
.into_iter()
|
||||
.filter_map(|s| serde_json::from_value(serde_json::Value::String(s)).ok())
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Modalities {
|
||||
/// Input modalities (e.g., [Text, Image, Pdf])
|
||||
#[serde(default, deserialize_with = "deserialize_modalities")]
|
||||
pub input: Vec<Modality>,
|
||||
|
||||
/// Output modalities (e.g., [Text])
|
||||
#[serde(default, deserialize_with = "deserialize_modalities")]
|
||||
pub output: Vec<Modality>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Pricing {
|
||||
/// Cost in USD per million input tokens
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input: Option<f64>,
|
||||
|
||||
/// Cost in USD per million output tokens
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output: Option<f64>,
|
||||
|
||||
/// Cost per million cached read tokens
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read: Option<f64>,
|
||||
|
||||
/// Cost per million cached write tokens
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_write: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Limit {
|
||||
/// Maximum context window size in tokens
|
||||
pub context: usize,
|
||||
|
||||
/// Maximum output/completion tokens
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output: Option<usize>,
|
||||
}
|
||||
|
||||
/// Canonical representation of a model
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CanonicalModel {
|
||||
/// Model identifier (e.g., "anthropic/claude-3-5-sonnet" or "openai/gpt-4o:extended")
|
||||
/// Model identifier (e.g., "anthropic/claude-3-5-sonnet")
|
||||
pub id: String,
|
||||
|
||||
/// Human-readable name (e.g., "Claude 3.5 Sonnet")
|
||||
/// Human-readable name (e.g., "Claude Sonnet 3.5 v2")
|
||||
pub name: String,
|
||||
|
||||
/// Maximum context window size in tokens
|
||||
pub context_length: usize,
|
||||
|
||||
/// Maximum completion tokens
|
||||
/// Model family (e.g., "claude-sonnet", "gpt")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_completion_tokens: Option<usize>,
|
||||
pub family: Option<String>,
|
||||
|
||||
/// Input modalities supported (e.g., ["text", "image"])
|
||||
#[serde(default)]
|
||||
pub input_modalities: Vec<String>,
|
||||
/// Whether the model supports attachments
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attachment: Option<bool>,
|
||||
|
||||
/// Output modalities supported (e.g., ["text"])
|
||||
#[serde(default)]
|
||||
pub output_modalities: Vec<String>,
|
||||
/// Whether the model supports reasoning/thinking
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning: Option<bool>,
|
||||
|
||||
/// Whether the model supports tool calling
|
||||
#[serde(default)]
|
||||
pub supports_tools: bool,
|
||||
pub tool_call: bool,
|
||||
|
||||
/// Pricing for this model
|
||||
pub pricing: Pricing,
|
||||
/// Whether the model supports temperature parameter
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<bool>,
|
||||
|
||||
/// Knowledge cutoff date (e.g., "2024-04-30")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub knowledge: Option<String>,
|
||||
|
||||
/// Release date (e.g., "2024-10-22")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub release_date: Option<String>,
|
||||
|
||||
/// Last updated date (e.g., "2024-10-22")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_updated: Option<String>,
|
||||
|
||||
/// Input and output modalities
|
||||
#[serde(default)]
|
||||
pub modalities: Modalities,
|
||||
|
||||
/// Whether the model has open weights
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub open_weights: Option<bool>,
|
||||
|
||||
/// Pricing information
|
||||
#[serde(default)]
|
||||
pub cost: Pricing,
|
||||
|
||||
/// Token limits
|
||||
#[serde(default)]
|
||||
pub limit: Limit,
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
// Patterns for normalizing version numbers and stripping suffixes
|
||||
static NORMALIZE_VERSION_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"-(\d)-(\d)(-|$)").unwrap());
|
||||
|
||||
static STRIP_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
|
||||
vec![
|
||||
Regex::new(r"-latest$").unwrap(),
|
||||
Regex::new(r"-preview(-\d+)*$").unwrap(),
|
||||
Regex::new(r"-exp(-\d+)*$").unwrap(),
|
||||
Regex::new(r":exacto$").unwrap(),
|
||||
Regex::new(r"-\d{8}$").unwrap(),
|
||||
Regex::new(r"-\d{4}$").unwrap(),
|
||||
Regex::new(r"-\d{4}-\d{2}-\d{2}$").unwrap(),
|
||||
Regex::new(r"-v\d+(\.\d+)*$").unwrap(),
|
||||
Regex::new(r"-\d{3,}$").unwrap(),
|
||||
Regex::new(r"-bedrock$").unwrap(),
|
||||
Regex::new(r"-reasoning$").unwrap(),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -33,23 +31,22 @@ static CLAUDE_PATTERNS: Lazy<Vec<(Regex, Regex, &'static str)>> = Lazy::new(|| {
|
||||
/// Build canonical model name from provider and model identifiers
|
||||
pub fn canonical_name(provider: &str, model: &str) -> String {
|
||||
let model_base = strip_version_suffix(model);
|
||||
|
||||
// OpenRouter models are already in canonical format
|
||||
if provider == "openrouter" {
|
||||
model_base
|
||||
} else {
|
||||
format!("{}/{}", provider, model_base)
|
||||
}
|
||||
format!("{}/{}", provider, model_base)
|
||||
}
|
||||
|
||||
/// Try to build a canonical name and check if it exists in the registry
|
||||
fn try_canonical(
|
||||
provider: &str,
|
||||
model: &str,
|
||||
registry: &super::CanonicalModelRegistry,
|
||||
) -> Option<String> {
|
||||
let candidate = canonical_name(provider, model);
|
||||
registry.get(&candidate).map(|_| candidate)
|
||||
fn is_meta_provider(provider: &str) -> bool {
|
||||
matches!(provider, "databricks" | "tetrate" | "bedrock" | "azure")
|
||||
}
|
||||
|
||||
fn map_provider_name(provider: &str) -> &str {
|
||||
match provider {
|
||||
// Goose provider names that differ from models.dev names
|
||||
"xai" => "x-ai",
|
||||
"azure_openai" => "azure",
|
||||
"aws_bedrock" => "amazon-bedrock",
|
||||
"gcp_vertex_ai" => "google-vertex",
|
||||
_ => provider,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to map a provider/model pair to a canonical model
|
||||
@@ -58,53 +55,51 @@ pub fn map_to_canonical_model(
|
||||
model: &str,
|
||||
registry: &super::CanonicalModelRegistry,
|
||||
) -> Option<String> {
|
||||
// Try direct mapping first
|
||||
if let Some(candidate) = try_canonical(provider, model, registry) {
|
||||
return Some(candidate);
|
||||
let registry_provider = map_provider_name(provider);
|
||||
|
||||
// For normal providers (anthropic, openai, google, openrouter, etc.), just do direct lookup
|
||||
if !is_meta_provider(provider) {
|
||||
let normalized_model = strip_version_suffix(model);
|
||||
if let Some(canonical) = registry.get(registry_provider, &normalized_model) {
|
||||
return Some(canonical.id.clone());
|
||||
}
|
||||
// Also try original model name
|
||||
if let Some(canonical) = registry.get(registry_provider, model) {
|
||||
return Some(canonical.id.clone());
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Try with common prefixes stripped
|
||||
// For hosting/meta-providers do string matching magic to figure out the real provider and model
|
||||
let model_stripped = strip_common_prefixes(model);
|
||||
if model_stripped != model {
|
||||
if let Some(candidate) = try_canonical(provider, &model_stripped, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// Try word-order swapping for Claude models (claude-4-opus ↔ claude-opus-4)
|
||||
if let Some(swapped) = swap_claude_word_order(&model_stripped) {
|
||||
if let Some(candidate) = try_canonical(provider, &swapped, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
|
||||
if is_hosting_provider(provider) {
|
||||
if let Some(inferred) = infer_provider_from_model(&swapped) {
|
||||
if let Some(candidate) = try_canonical(inferred, &swapped, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
if let Some(inferred_provider) = infer_provider_from_model(&swapped) {
|
||||
let normalized = strip_version_suffix(&swapped);
|
||||
if let Some(canonical) = registry.get(inferred_provider, &normalized) {
|
||||
return Some(canonical.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For hosting providers, try to infer the real provider from model name patterns
|
||||
if is_hosting_provider(provider) {
|
||||
if let Some(inferred_provider) = infer_provider_from_model(&model_stripped) {
|
||||
if let Some(candidate) = try_canonical(inferred_provider, &model_stripped, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(inferred) = infer_provider_from_model(model) {
|
||||
if let Some(candidate) = try_canonical(inferred, model, registry) {
|
||||
return Some(candidate);
|
||||
}
|
||||
if let Some(inferred_provider) = infer_provider_from_model(&model_stripped) {
|
||||
let normalized = strip_version_suffix(&model_stripped);
|
||||
if let Some(canonical) = registry.get(inferred_provider, &normalized) {
|
||||
return Some(canonical.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(inferred_provider) = infer_provider_from_model(model) {
|
||||
let normalized = strip_version_suffix(model);
|
||||
if let Some(canonical) = registry.get(inferred_provider, &normalized) {
|
||||
return Some(canonical.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// For provider-prefixed models like "databricks-meta-llama-3-1-70b"
|
||||
if let Some((extracted_provider, extracted_model)) = extract_provider_prefix(&model_stripped) {
|
||||
if let Some(candidate) = try_canonical(extracted_provider, extracted_model, registry) {
|
||||
return Some(candidate);
|
||||
let normalized = strip_version_suffix(extracted_model);
|
||||
if let Some(canonical) = registry.get(extracted_provider, &normalized) {
|
||||
return Some(canonical.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,13 +127,6 @@ fn swap_claude_word_order(model: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_hosting_provider(provider: &str) -> bool {
|
||||
matches!(
|
||||
provider,
|
||||
"databricks" | "openrouter" | "azure" | "bedrock" | "chatgpt_codex"
|
||||
)
|
||||
}
|
||||
|
||||
/// Infer the real provider from model name patterns
|
||||
fn infer_provider_from_model(model: &str) -> Option<&'static str> {
|
||||
let model_lower = model.to_lowercase();
|
||||
@@ -316,10 +304,10 @@ mod tests {
|
||||
Some("openai/gpt-4-turbo".to_string())
|
||||
);
|
||||
|
||||
// === OpenRouter (already canonical format) ===
|
||||
// === OpenRouter ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("openrouter", "anthropic/claude-3.5-sonnet", r),
|
||||
Some("anthropic/claude-3.5-sonnet".to_string())
|
||||
map_to_canonical_model("openrouter", "anthropic/claude-sonnet-4.5", r),
|
||||
Some("openrouter/anthropic/claude-sonnet-4.5".to_string())
|
||||
);
|
||||
|
||||
// === Anthropic Claude - basic ===
|
||||
@@ -412,8 +400,8 @@ mod tests {
|
||||
|
||||
// === Meta Llama ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "meta-llama-3-1-70b-instruct", r),
|
||||
Some("meta-llama/llama-3.1-70b-instruct".to_string())
|
||||
map_to_canonical_model("databricks", "meta-llama-3-3-70b-instruct", r),
|
||||
Some("meta-llama/llama-3.3-70b-instruct".to_string())
|
||||
);
|
||||
|
||||
// === Mistral variants ===
|
||||
@@ -422,8 +410,8 @@ mod tests {
|
||||
Some("mistralai/codestral".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "ministral-8b", r),
|
||||
Some("mistralai/ministral-8b".to_string())
|
||||
map_to_canonical_model("databricks", "ministral-3b", r),
|
||||
Some("mistralai/ministral-3b".to_string())
|
||||
);
|
||||
|
||||
// === DeepSeek ===
|
||||
@@ -432,18 +420,8 @@ mod tests {
|
||||
Some("deepseek/deepseek-chat".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "deepseek-r1", r),
|
||||
Some("deepseek/deepseek-r1".to_string())
|
||||
);
|
||||
|
||||
// === Qwen ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "qwen-2-5-72b-instruct", r),
|
||||
Some("qwen/qwen-2.5-72b-instruct".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "goose-qwen-2-5-72b-instruct", r),
|
||||
Some("qwen/qwen-2.5-72b-instruct".to_string())
|
||||
map_to_canonical_model("databricks", "deepseek-reasoner", r),
|
||||
Some("deepseek/deepseek-reasoner".to_string())
|
||||
);
|
||||
|
||||
// === Grok (X.AI) ===
|
||||
@@ -460,23 +438,14 @@ mod tests {
|
||||
Some("x-ai/grok-4-fast".to_string())
|
||||
);
|
||||
|
||||
// === Jamba (AI21) ===
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "jamba-large-1-7", r),
|
||||
Some("ai21/jamba-large-1.7".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "databricks-jamba-large-1-7", r),
|
||||
Some("ai21/jamba-large-1.7".to_string())
|
||||
);
|
||||
|
||||
// === Cohere Command ===
|
||||
// Note: version suffix "-2024" is stripped by canonical_name
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "command-r-plus-08", r),
|
||||
map_to_canonical_model("databricks", "command-r-plus-08-2024", r),
|
||||
Some("cohere/command-r-plus-08".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "goose-command-r-08", r),
|
||||
map_to_canonical_model("databricks", "goose-command-r-08-2024", r),
|
||||
Some("cohere/command-r-08".to_string())
|
||||
);
|
||||
|
||||
@@ -494,17 +463,13 @@ mod tests {
|
||||
Some("google/gemini-2.5-flash".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "mistralai-mistral-large", r),
|
||||
Some("mistralai/mistral-large".to_string())
|
||||
map_to_canonical_model("databricks", "mistralai-codestral", r),
|
||||
Some("mistralai/codestral".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "deepseek-deepseek-chat", r),
|
||||
Some("deepseek/deepseek-chat".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "qwen-qwen-2-5-72b-instruct", r),
|
||||
Some("qwen/qwen-2.5-72b-instruct".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
map_to_canonical_model("databricks", "x-ai-grok-3", r),
|
||||
Some("x-ai/grok-3".to_string())
|
||||
|
||||
@@ -13,7 +13,12 @@ static BUNDLED_REGISTRY: Lazy<Result<CanonicalModelRegistry>> = Lazy::new(|| {
|
||||
|
||||
let mut registry = CanonicalModelRegistry::new();
|
||||
for model in models {
|
||||
registry.register(model);
|
||||
// Extract provider and model from id (format: "provider/model")
|
||||
if let Some((provider, model_name)) = model.id.split_once('/') {
|
||||
let provider = provider.to_string();
|
||||
let model_name = model_name.to_string();
|
||||
registry.register(&provider, &model_name, model);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(registry)
|
||||
@@ -21,7 +26,8 @@ static BUNDLED_REGISTRY: Lazy<Result<CanonicalModelRegistry>> = Lazy::new(|| {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CanonicalModelRegistry {
|
||||
models: HashMap<String, CanonicalModel>,
|
||||
// Key: (provider, model) tuple
|
||||
models: HashMap<(String, String), CanonicalModel>,
|
||||
}
|
||||
|
||||
impl CanonicalModelRegistry {
|
||||
@@ -46,7 +52,11 @@ impl CanonicalModelRegistry {
|
||||
|
||||
let mut registry = Self::new();
|
||||
for model in models {
|
||||
registry.register(model);
|
||||
if let Some((provider, model_name)) = model.id.split_once('/') {
|
||||
let provider = provider.to_string();
|
||||
let model_name = model_name.to_string();
|
||||
registry.register(&provider, &model_name, model);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(registry)
|
||||
@@ -64,12 +74,13 @@ impl CanonicalModelRegistry {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register(&mut self, model: CanonicalModel) {
|
||||
self.models.insert(model.id.clone(), model);
|
||||
pub fn register(&mut self, provider: &str, model: &str, canonical_model: CanonicalModel) {
|
||||
self.models
|
||||
.insert((provider.to_string(), model.to_string()), canonical_model);
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&CanonicalModel> {
|
||||
self.models.get(name)
|
||||
pub fn get(&self, provider: &str, model: &str) -> Option<&CanonicalModel> {
|
||||
self.models.get(&(provider.to_string(), model.to_string()))
|
||||
}
|
||||
|
||||
pub fn all_models(&self) -> Vec<&CanonicalModel> {
|
||||
@@ -79,10 +90,6 @@ impl CanonicalModelRegistry {
|
||||
pub fn count(&self) -> usize {
|
||||
self.models.len()
|
||||
}
|
||||
|
||||
pub fn contains(&self, name: &str) -> bool {
|
||||
self.models.contains_key(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CanonicalModelRegistry {
|
||||
|
||||
@@ -90,6 +90,7 @@ fn check_context_length_exceeded(text: &str) -> bool {
|
||||
"max_tokens",
|
||||
"decrease input length",
|
||||
"context limit",
|
||||
"maximum prompt length",
|
||||
];
|
||||
let text_lower = text.to_lowercase();
|
||||
check_phrases
|
||||
|
||||
@@ -176,4 +176,34 @@ impl Provider for XaiProvider {
|
||||
|
||||
stream_openai_compat(response, log)
|
||||
}
|
||||
|
||||
async fn fetch_supported_models(&self) -> Result<Option<Vec<String>>, ProviderError> {
|
||||
let response = self.api_client.response_get(None, "models").await?;
|
||||
let json = handle_response_openai_compat(response).await?;
|
||||
|
||||
if let Some(err_obj) = json.get("error") {
|
||||
let msg = err_obj
|
||||
.get("message")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown error");
|
||||
return Err(ProviderError::Authentication(msg.to_string()));
|
||||
}
|
||||
|
||||
let data = json.get("data").and_then(|v| v.as_array());
|
||||
match data {
|
||||
Some(arr) => {
|
||||
let mut models: Vec<String> = arr
|
||||
.iter()
|
||||
.filter_map(|m| {
|
||||
m.get("id")
|
||||
.and_then(|id| id.as_str())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.collect();
|
||||
models.sort();
|
||||
Ok(Some(models))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,10 +247,10 @@ impl ProviderTester {
|
||||
dbg!(&result);
|
||||
println!("===================");
|
||||
|
||||
if self.name.to_lowercase() == "ollama" {
|
||||
if self.name.to_lowercase() == "ollama" || self.name.to_lowercase() == "openrouter" {
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Expected to succeed because of default truncation"
|
||||
"Expected to succeed because of default truncation or large context window"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -4868,7 +4868,7 @@
|
||||
"input_token_cost": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "Cost per token for input (optional)",
|
||||
"description": "Cost per token for input in USD (optional)",
|
||||
"nullable": true
|
||||
},
|
||||
"name": {
|
||||
@@ -4878,7 +4878,7 @@
|
||||
"output_token_cost": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "Cost per token for output (optional)",
|
||||
"description": "Cost per token for output in USD (optional)",
|
||||
"nullable": true
|
||||
},
|
||||
"supports_cache_control": {
|
||||
|
||||
@@ -573,7 +573,7 @@ export type ModelInfo = {
|
||||
*/
|
||||
currency?: string | null;
|
||||
/**
|
||||
* Cost per token for input (optional)
|
||||
* Cost per token for input in USD (optional)
|
||||
*/
|
||||
input_token_cost?: number | null;
|
||||
/**
|
||||
@@ -581,7 +581,7 @@ export type ModelInfo = {
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Cost per token for output (optional)
|
||||
* Cost per token for output in USD (optional)
|
||||
*/
|
||||
output_token_cost?: number | null;
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user