From d91075912128e5aac338594783bfa282f0b14ff6 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 6 Mar 2026 07:48:09 +0000 Subject: [PATCH] feat: add OpenAI-compatible /v1/chat/completions endpoint --- studio/backend/main.py | 5 +++ studio/backend/routes/inference.py | 37 +++++++++++++++++++ .../src/features/chat/api/chat-api.ts | 2 +- .../src/features/chat/runtime-provider.tsx | 2 +- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/studio/backend/main.py b/studio/backend/main.py index fca8e581d2..696d8a4d10 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -96,6 +96,11 @@ app.include_router(auth_router, prefix="/api/auth", tags=["auth"]) app.include_router(training_router, prefix="/api/train", tags=["training"]) app.include_router(models_router, prefix="/api/models", tags=["models"]) app.include_router(inference_router, prefix="/api/inference", tags=["inference"]) + +# OpenAI-compatible endpoints: mount the same inference router at /v1 +# so external tools (Open WebUI, SillyTavern, etc.) can use the +# standard /v1/chat/completions path. +app.include_router(inference_router, prefix="/v1", tags=["openai-compat"]) app.include_router(datasets_router, prefix="/api/datasets", tags=["datasets"]) app.include_router(data_recipe_router, prefix="/api/data-recipe", tags=["data-recipe"]) app.include_router(export_router, prefix="/api/export", tags=["export"]) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 156b3237a4..aa4b4ed0cd 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -785,3 +785,40 @@ async def openai_chat_completions( backend.reset_generation_state() logger.error(f"Error during OpenAI completion: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) + + +# ===================================================================== +# OpenAI-Compatible Models Listing (/models → /v1/models) +# ===================================================================== + +@router.get("/models") +async def openai_list_models( + current_subject: str = Depends(get_current_subject), +): + """ + OpenAI-compatible model listing endpoint. + + Returns the currently loaded model in the format expected by + OpenAI-compatible clients (``GET /v1/models``). + """ + models = [] + + # Check GGUF backend + llama_backend = get_llama_cpp_backend() + if llama_backend.is_loaded: + models.append({ + "id": llama_backend.model_identifier, + "object": "model", + "owned_by": "local", + }) + + # Check Unsloth backend + backend = get_inference_backend() + if backend.active_model_name: + models.append({ + "id": backend.active_model_name, + "object": "model", + "owned_by": "local", + }) + + return {"object": "list", "data": models} diff --git a/studio/frontend/src/features/chat/api/chat-api.ts b/studio/frontend/src/features/chat/api/chat-api.ts index 5d5a9551ef..4276e618ed 100644 --- a/studio/frontend/src/features/chat/api/chat-api.ts +++ b/studio/frontend/src/features/chat/api/chat-api.ts @@ -99,7 +99,7 @@ export async function* streamChatCompletions( payload: OpenAIChatCompletionsRequest, signal: AbortSignal, ): AsyncGenerator { - const response = await authFetch("/api/inference/chat/completions", { + const response = await authFetch("/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), diff --git a/studio/frontend/src/features/chat/runtime-provider.tsx b/studio/frontend/src/features/chat/runtime-provider.tsx index ce553dcdbf..46e3c697ad 100644 --- a/studio/frontend/src/features/chat/runtime-provider.tsx +++ b/studio/frontend/src/features/chat/runtime-provider.tsx @@ -202,7 +202,7 @@ async function generateTitleWithModel(payload: { return joined.length > 60 ? joined.slice(0, 60).trimEnd() : joined; } - const response = await authFetch("/api/inference/chat/completions", { + const response = await authFetch("/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({