fix(dashboard): accurately map used models to actual providers
This commit modifies the /api/models endpoint so that when fetching 'used models' for the Cost Management view, it accurately pairs each model with the exact provider it was routed through (by querying SELECT DISTINCT provider, model FROM llm_requests). Previously, it relied on the global registry's mapping, which could falsely attribute usage to unconfigured or alternate providers.
This commit is contained in:
@@ -54,37 +54,6 @@ pub(super) async fn handle_get_models(
|
|||||||
let registry = &state.app_state.model_registry;
|
let registry = &state.app_state.model_registry;
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
|
|
||||||
// If used_only, fetch the set of models that appear in llm_requests
|
|
||||||
let used_models: Option<std::collections::HashSet<String>> =
|
|
||||||
if params.used_only.unwrap_or(false) {
|
|
||||||
match sqlx::query_scalar::<_, String>(
|
|
||||||
"SELECT DISTINCT model FROM llm_requests",
|
|
||||||
)
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(models) => Some(models.into_iter().collect()),
|
|
||||||
Err(_) => Some(std::collections::HashSet::new()),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build filter from query params
|
|
||||||
let filter = ModelFilter {
|
|
||||||
provider: params.provider,
|
|
||||||
search: params.search,
|
|
||||||
modality: params.modality,
|
|
||||||
tool_call: params.tool_call,
|
|
||||||
reasoning: params.reasoning,
|
|
||||||
has_cost: params.has_cost,
|
|
||||||
};
|
|
||||||
let sort_by = params.sort_by.unwrap_or_default();
|
|
||||||
let sort_order = params.sort_order.unwrap_or_default();
|
|
||||||
|
|
||||||
// Get filtered and sorted model entries
|
|
||||||
let entries = registry.list_models(&filter, &sort_by, &sort_order);
|
|
||||||
|
|
||||||
// Load overrides from database
|
// Load overrides from database
|
||||||
let db_models_result =
|
let db_models_result =
|
||||||
sqlx::query("SELECT id, enabled, prompt_cost_per_m, completion_cost_per_m, mapping FROM model_configs")
|
sqlx::query("SELECT id, enabled, prompt_cost_per_m, completion_cost_per_m, mapping FROM model_configs")
|
||||||
@@ -101,16 +70,89 @@ pub(super) async fn handle_get_models(
|
|||||||
|
|
||||||
let mut models_json = Vec::new();
|
let mut models_json = Vec::new();
|
||||||
|
|
||||||
|
if params.used_only.unwrap_or(false) {
|
||||||
|
// EXACT USED MODELS LOGIC
|
||||||
|
let used_pairs_result = sqlx::query(
|
||||||
|
"SELECT DISTINCT provider, model FROM llm_requests",
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(rows) = used_pairs_result {
|
||||||
|
for row in rows {
|
||||||
|
let provider: String = row.get("provider");
|
||||||
|
let m_key: String = row.get("model");
|
||||||
|
|
||||||
|
let provider_name = match provider.as_str() {
|
||||||
|
"openai" => "OpenAI",
|
||||||
|
"gemini" => "Google Gemini",
|
||||||
|
"deepseek" => "DeepSeek",
|
||||||
|
"grok" => "xAI Grok",
|
||||||
|
"ollama" => "Ollama",
|
||||||
|
_ => provider.as_str(),
|
||||||
|
}.to_string();
|
||||||
|
|
||||||
|
let m_meta = registry.find_model(&m_key);
|
||||||
|
|
||||||
|
let mut enabled = true;
|
||||||
|
let mut prompt_cost = m_meta.and_then(|m| m.cost.as_ref().map(|c| c.input)).unwrap_or(0.0);
|
||||||
|
let mut completion_cost = m_meta.and_then(|m| m.cost.as_ref().map(|c| c.output)).unwrap_or(0.0);
|
||||||
|
let cache_read_cost = m_meta.and_then(|m| m.cost.as_ref().and_then(|c| c.cache_read));
|
||||||
|
let cache_write_cost = m_meta.and_then(|m| m.cost.as_ref().and_then(|c| c.cache_write));
|
||||||
|
let mut mapping = None::<String>;
|
||||||
|
|
||||||
|
if let Some(db_row) = db_models.get(&m_key) {
|
||||||
|
enabled = db_row.get("enabled");
|
||||||
|
if let Some(p) = db_row.get::<Option<f64>, _>("prompt_cost_per_m") {
|
||||||
|
prompt_cost = p;
|
||||||
|
}
|
||||||
|
if let Some(c) = db_row.get::<Option<f64>, _>("completion_cost_per_m") {
|
||||||
|
completion_cost = c;
|
||||||
|
}
|
||||||
|
mapping = db_row.get("mapping");
|
||||||
|
}
|
||||||
|
|
||||||
|
models_json.push(serde_json::json!({
|
||||||
|
"id": m_key,
|
||||||
|
"provider": provider,
|
||||||
|
"provider_name": provider_name,
|
||||||
|
"name": m_meta.map(|m| m.name.clone()).unwrap_or_else(|| m_key.clone()),
|
||||||
|
"enabled": enabled,
|
||||||
|
"prompt_cost": prompt_cost,
|
||||||
|
"completion_cost": completion_cost,
|
||||||
|
"cache_read_cost": cache_read_cost,
|
||||||
|
"cache_write_cost": cache_write_cost,
|
||||||
|
"mapping": mapping,
|
||||||
|
"context_limit": m_meta.and_then(|m| m.limit.as_ref().map(|l| l.context)).unwrap_or(0),
|
||||||
|
"output_limit": m_meta.and_then(|m| m.limit.as_ref().map(|l| l.output)).unwrap_or(0),
|
||||||
|
"modalities": m_meta.and_then(|m| m.modalities.as_ref().map(|mo| serde_json::json!({
|
||||||
|
"input": mo.input,
|
||||||
|
"output": mo.output,
|
||||||
|
}))),
|
||||||
|
"tool_call": m_meta.and_then(|m| m.tool_call),
|
||||||
|
"reasoning": m_meta.and_then(|m| m.reasoning),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// REGISTRY LISTING LOGIC
|
||||||
|
// Build filter from query params
|
||||||
|
let filter = ModelFilter {
|
||||||
|
provider: params.provider,
|
||||||
|
search: params.search,
|
||||||
|
modality: params.modality,
|
||||||
|
tool_call: params.tool_call,
|
||||||
|
reasoning: params.reasoning,
|
||||||
|
has_cost: params.has_cost,
|
||||||
|
};
|
||||||
|
let sort_by = params.sort_by.unwrap_or_default();
|
||||||
|
let sort_order = params.sort_order.unwrap_or_default();
|
||||||
|
|
||||||
|
// Get filtered and sorted model entries
|
||||||
|
let entries = registry.list_models(&filter, &sort_by, &sort_order);
|
||||||
|
|
||||||
for entry in &entries {
|
for entry in &entries {
|
||||||
let m_key = entry.model_key;
|
let m_key = entry.model_key;
|
||||||
|
|
||||||
// Skip models not in the used set (when used_only is active)
|
|
||||||
if let Some(ref used) = used_models {
|
|
||||||
if !used.contains(m_key) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let m_meta = entry.metadata;
|
let m_meta = entry.metadata;
|
||||||
|
|
||||||
let mut enabled = true;
|
let mut enabled = true;
|
||||||
@@ -152,6 +194,7 @@ pub(super) async fn handle_get_models(
|
|||||||
"reasoning": m_meta.reasoning,
|
"reasoning": m_meta.reasoning,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Json(ApiResponse::success(serde_json::json!(models_json)))
|
Json(ApiResponse::success(serde_json::json!(models_json)))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user