mirror of
https://github.com/whit3rabbit/anyllm-proxy.git
synced 2026-09-22 00:00:50 +00:00
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:
co-authored by
Claude Sonnet 4.6
parent
7a3587682c
commit
ca7b27cfc6
+7
-7
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user