fix: add catalog routes, complete CatalogProvider type, group providers by status

- Copy catalog.rs from main; add pub mod catalog + GET routes in admin router
- Add price_per_million_for_model to cost module (missing from this branch)
- Expand CatalogProvider with display_name, status, capabilities, model_count
- BackendForm provider dropdown: use display_name, group by status via optgroup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
whit3rabbit
2026-04-08 13:45:57 -05:00
co-authored by Claude Sonnet 4.6
parent 7a3587682c
commit ca7b27cfc6
6 changed files with 179 additions and 11 deletions
File diff suppressed because one or more lines are too long
+13 -1
View File
@@ -243,11 +243,23 @@ export interface EnvImportError {
export interface CatalogProvider {
id: string
name: string
display_name: string
name?: string // keep for backwards compat if anything uses it
protocol: string
auth: string
status: 'implemented' | 'wired' | 'stub'
default_base_url: string
env_vars: string[]
litellm_prefix: string
capabilities: {
chat_completions: boolean
streaming: boolean
tool_use: boolean
embeddings: boolean
vision: boolean
batch: boolean
}
model_count: number
}
// --- Managed backends ---
@@ -128,9 +128,17 @@ export function BackendForm({ initial, onSuccess, onCancel }: BackendFormProps)
{providers.length === 0 && (
<option value="">Loading providers</option>
)}
{providers.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
{(['implemented', 'wired', 'stub'] as const).map(status => {
const group = providers.filter(p => p.status === status)
if (group.length === 0) return null
return (
<optgroup key={status} label={status.charAt(0).toUpperCase() + status.slice(1)}>
{group.map(p => (
<option key={p.id} value={p.id}>{p.display_name}</option>
))}
</optgroup>
)
})}
</select>
</div>
+133
View File
@@ -0,0 +1,133 @@
use anyllm_providers::{
all_providers, get_provider, list_models,
model::ModelStatus,
provider::{AuthKind, ProviderProtocol, ProviderStatus},
};
use axum::{extract::Path, http::StatusCode, response::IntoResponse, Json};
fn protocol_str(p: ProviderProtocol) -> &'static str {
match p {
ProviderProtocol::OpenAICompat => "openai_compat",
ProviderProtocol::AzureOpenAI => "azure_openai",
ProviderProtocol::VertexAI => "vertex_ai",
ProviderProtocol::GeminiOpenAI => "gemini_openai",
ProviderProtocol::GeminiNative => "gemini_native",
ProviderProtocol::AnthropicNative => "anthropic_native",
ProviderProtocol::BedrockNative => "bedrock_native",
ProviderProtocol::Custom => "custom",
}
}
fn auth_str(a: AuthKind) -> &'static str {
match a {
AuthKind::Bearer => "bearer",
AuthKind::GoogleApiKey => "google_api_key",
AuthKind::AzureApiKey => "azure_api_key",
AuthKind::AwsSigV4 => "aws_sigv4",
AuthKind::None => "none",
}
}
fn provider_status_str(s: ProviderStatus) -> &'static str {
match s {
ProviderStatus::Implemented => "implemented",
ProviderStatus::Wired => "wired",
ProviderStatus::Stub => "stub",
}
}
fn model_status_str(s: ModelStatus) -> &'static str {
match s {
ModelStatus::Available => "available",
ModelStatus::Deprecated => "deprecated",
ModelStatus::Stub => "stub",
}
}
/// GET /admin/api/catalog/providers
///
/// Returns all registered providers with metadata from the compile-time registry.
/// No SharedState needed — data is all static.
pub(super) async fn list_providers() -> impl IntoResponse {
let providers: Vec<serde_json::Value> = all_providers()
.map(|p| {
let model_count = list_models(p.id).len();
serde_json::json!({
"id": p.id,
"display_name": p.display_name,
"protocol": protocol_str(p.protocol),
"auth": auth_str(p.auth),
"status": provider_status_str(p.status),
"default_base_url": p.default_base_url,
"env_vars": p.env_vars,
"litellm_prefix": p.litellm_prefix,
"capabilities": {
"chat_completions": p.capabilities.chat_completions,
"streaming": p.capabilities.streaming,
"tool_use": p.capabilities.tool_use,
"embeddings": p.capabilities.embeddings,
"vision": p.capabilities.vision,
"batch": p.capabilities.batch,
},
"model_count": model_count,
})
})
.collect();
Json(serde_json::json!({ "providers": providers })).into_response()
}
/// GET /admin/api/catalog/providers/{id}/models
///
/// Returns all static models for the given provider, enriched with pricing data
/// from the embedded pricing table. Returns 404 for unknown provider ids.
pub(super) async fn list_provider_models(Path(provider_id): Path<String>) -> impl IntoResponse {
if !super::is_safe_model_name(&provider_id) {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "invalid provider id" })),
)
.into_response();
}
if get_provider(&provider_id).is_none() {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "provider not found" })),
)
.into_response();
}
let models: Vec<serde_json::Value> = list_models(&provider_id)
.iter()
.map(|m| {
let pricing = crate::cost::price_per_million_for_model(m.id).map(|(inp, out)| {
serde_json::json!({
"input_per_million_tokens": inp,
"output_per_million_tokens": out,
})
});
serde_json::json!({
"id": m.id,
"context_window": m.context_window,
"max_output_tokens": m.max_output_tokens,
"status": model_status_str(m.status),
"capabilities": {
"streaming": m.capabilities.streaming,
"tool_use": m.capabilities.tool_use,
"vision": m.capabilities.vision,
"extended_thinking": m.capabilities.extended_thinking,
},
"pricing": pricing,
})
})
.collect();
let has_models = !models.is_empty();
Json(serde_json::json!({
"provider_id": provider_id,
"has_models": has_models,
"models": models,
}))
.into_response()
}
+6
View File
@@ -1,6 +1,7 @@
// Admin server routes. Served on a separate localhost-only listener.
pub mod audit;
pub mod catalog;
pub mod config;
pub mod env;
pub mod keys;
@@ -420,6 +421,11 @@ pub fn admin_router(shared: SharedState, token: Arc<zeroize::Zeroizing<String>>)
"/admin/api/mcp-servers/{name}",
delete(mcp::remove_mcp_server),
)
.route("/admin/api/catalog/providers", get(catalog::list_providers))
.route(
"/admin/api/catalog/providers/:id/models",
get(catalog::list_provider_models),
)
.route("/admin/api/status", get(status::get_status))
.route("/admin/api/traffic", get(traffic::get_traffic))
.route("/admin/api/uptime", get(uptime::get_uptime))
+9
View File
@@ -104,6 +104,15 @@ pub fn pricing() -> &'static ModelPricing {
&PRICING
}
/// Return (input_per_million, output_per_million) for a model, or None if unknown.
/// Costs are scaled to per-million for human-readable display; the underlying
/// table stores per-token values.
pub fn price_per_million_for_model(model_id: &str) -> Option<(f64, f64)> {
pricing()
.price_for_model(model_id)
.map(|(i, o)| (i * 1_000_000.0, o * 1_000_000.0))
}
/// A single pricing record loaded from the JSON pricing table.
/// `model_pattern` supports exact match and longest-prefix matching.
#[derive(Debug, Clone, serde::Deserialize)]