feat(server): add /v1/models endpoint for OpenAI-compatible model discovery
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

Open WebUI and other OpenAI-compatible clients call GET /v1/models to
discover available models. Lists all models from enabled providers via
the model registry, respects disabled models, and handles Ollama models
from TOML config.
This commit is contained in:
2026-03-02 14:06:31 -05:00
parent e38f012b23
commit 88aae389d2

View File

@@ -3,7 +3,7 @@ use axum::{
extract::State, extract::State,
response::IntoResponse, response::IntoResponse,
response::sse::{Event, Sse}, response::sse::{Event, Sse},
routing::post, routing::{get, post},
}; };
use futures::stream::StreamExt; use futures::stream::StreamExt;
use std::sync::Arc; use std::sync::Arc;
@@ -24,6 +24,7 @@ use crate::{
pub fn router(state: AppState) -> Router { pub fn router(state: AppState) -> Router {
Router::new() Router::new()
.route("/v1/chat/completions", post(chat_completions)) .route("/v1/chat/completions", post(chat_completions))
.route("/v1/models", get(list_models))
.layer(axum::middleware::from_fn_with_state( .layer(axum::middleware::from_fn_with_state(
state.clone(), state.clone(),
rate_limiting::middleware::rate_limit_middleware, rate_limiting::middleware::rate_limit_middleware,
@@ -31,6 +32,60 @@ pub fn router(state: AppState) -> Router {
.with_state(state) .with_state(state)
} }
/// GET /v1/models — OpenAI-compatible model listing.
/// Returns all models from enabled providers so clients like Open WebUI can
/// discover which models are available through the proxy.
async fn list_models(
State(state): State<AppState>,
_auth: AuthenticatedClient,
) -> Result<Json<serde_json::Value>, AppError> {
let registry = &state.model_registry;
let providers = state.provider_manager.get_all_providers().await;
let mut models = Vec::new();
for provider in &providers {
let provider_name = provider.name();
// Find this provider's models in the registry
if let Some(provider_info) = registry.providers.get(provider_name) {
for (model_id, meta) in &provider_info.models {
// Skip disabled models via the config cache
if let Some(cfg) = state.model_config_cache.get(model_id).await {
if !cfg.enabled {
continue;
}
}
models.push(serde_json::json!({
"id": model_id,
"object": "model",
"created": 0,
"owned_by": provider_name,
"name": meta.name,
}));
}
}
// For Ollama, models are configured in the TOML, not the registry
if provider_name == "ollama" {
for model_id in &state.config.providers.ollama.models {
models.push(serde_json::json!({
"id": model_id,
"object": "model",
"created": 0,
"owned_by": "ollama",
}));
}
}
}
Ok(Json(serde_json::json!({
"object": "list",
"data": models
})))
}
async fn get_model_cost( async fn get_model_cost(
model: &str, model: &str,
prompt_tokens: u32, prompt_tokens: u32,