From 88aae389d23ba6542861334cb027ae7a25ed7cb6 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Mon, 2 Mar 2026 14:06:31 -0500 Subject: [PATCH] feat(server): add /v1/models endpoint for OpenAI-compatible model discovery 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. --- src/server/mod.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/server/mod.rs b/src/server/mod.rs index 5541ed0c..f2d01a0a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -3,7 +3,7 @@ use axum::{ extract::State, response::IntoResponse, response::sse::{Event, Sse}, - routing::post, + routing::{get, post}, }; use futures::stream::StreamExt; use std::sync::Arc; @@ -24,6 +24,7 @@ use crate::{ pub fn router(state: AppState) -> Router { Router::new() .route("/v1/chat/completions", post(chat_completions)) + .route("/v1/models", get(list_models)) .layer(axum::middleware::from_fn_with_state( state.clone(), rate_limiting::middleware::rate_limit_middleware, @@ -31,6 +32,60 @@ pub fn router(state: AppState) -> Router { .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, + _auth: AuthenticatedClient, +) -> Result, 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( model: &str, prompt_tokens: u32,