From 9841c2b3d238e5139dc7cf0274e98eb9468e3ee8 Mon Sep 17 00:00:00 2001
From: remsky
Date: Sun, 12 Jul 2026 11:14:04 -0600
Subject: [PATCH] fix(debug-endpoints): gate /debug/* introspection routes
behind ENABLE_DEBUG_ENDPOINTS, document opt-in flags
---
CHANGELOG.md | 9 ++++
README.md | 12 ++---
api/src/core/config.py | 3 ++
api/src/main.py | 2 +-
api/src/routers/debug.py | 74 ++++++-------------------------
api/tests/test_debug_endpoints.py | 26 +++++++++++
docker/cpu/docker-compose.yml | 4 +-
docker/gpu/docker-compose.yml | 2 +
docker/rocm/docker-compose.yml | 2 +
9 files changed, 66 insertions(+), 68 deletions(-)
create mode 100644 api/tests/test_debug_endpoints.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5114fe05..2cf6f3ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,8 +5,17 @@ Notable changes to this project will be documented in this file.
Per-PR attribution and contributor credits are published automatically on the corresponding GitHub release page; this file is the curated, human-readable summary.
## [v0.6.0] - Unreleased
+### Breaking changes
+- `POST /dev/unload` is off by default; set `ALLOW_DEV_UNLOAD=true` to enable, otherwise returns 403. Shipped open in v0.5.0, now opt-in (#483).
+- `/debug/*` routes also set off by default, continuation of above; to avoid unintentional exposure of internals (stack traces, temp storage, CPU/mem/GPU); set `ENABLE_DEBUG_ENDPOINTS=true` to enable, otherwise 403's.
+- Removed lingering deprecated `/debug/session_pools`
+
+### Changed
+- Documented API stability: `/v1/*` is the stable surface; `/dev/*` and `/debug/*` are operational helpers that may change or move behind flags between minor releases.
+
### Fixed
- OpenAI voice aliases pointed at legacy v0.19 voicepacks that sound degraded on the v1.0 model. Added the proper v1.0 `bf_isabella` and repointed `nova` (`bf_v0isabella` -> `bf_isabella`), `alloy`, `ash`, `coral`, `echo` to their v1.0 voices. The `v0*` voices stay available by explicit name. (#479)
+- `/dev/captioned_speech` returned `timestamps: null` for non-English espeak voices (es/fr/it/hi/pt). Word timestamps are now derived from the model's own phoneme durations (`pred_dur`), so they match the audio exactly; falls back to the old behavior when word counts can't be reconciled. English path unchanged, ja/zh keep previous behavior. (#484)
## [v0.5.0] - 2026-06-06
### Added
diff --git a/README.md b/README.md
index f3e01790..e46a04b5 100644
--- a/README.md
+++ b/README.md
@@ -3,14 +3,14 @@
# _`FastKoko`_
-[](./CHANGELOG.md) []()
-[]()
+[](./CHANGELOG.md) []()
+[]()
[](https://github.com/hexgrad/kokoro)
[](https://github.com/hexgrad/misaki)
[](https://huggingface.co/hexgrad/Kokoro-82M/commit/9901c2b79161b6e898b7ea857ae5298f47b8b0d6)
-[](https://huggingface.co/spaces/Remsky/FastKoko) [](https://github.com/remsky?tab=packages&repo_name=Kokoro-FastAPI)
+[](https://huggingface.co/spaces/Remsky/FastKoko) [](https://github.com/remsky?tab=packages&repo_name=Kokoro-FastAPI)
@@ -582,14 +582,16 @@ The city of [Worcester](/wˈʊstər/) is easy. [pause:1s] See?
Debug Endpoints
-Monitor system state and resource usage with these endpoints:
+Monitor system state and resource usage with these endpoints. The `/debug/*` routes expose host and process internals, so they are off by default; set `ENABLE_DEBUG_ENDPOINTS=true` to enable.
- `/debug/threads` - Get thread information and stack traces
- `/debug/storage` - Monitor temp file and output directory usage
- `/debug/system` - Get system information (CPU, memory, GPU)
-- `POST /dev/unload` - Release model from VRAM; reloads lazily on next request
+- `POST /dev/unload` - Release model from VRAM; reloads lazily on next request. Off by default; set `ALLOW_DEV_UNLOAD=true` to enable
Useful for debugging resource exhaustion or performance issues.
+
+Stability: the `/v1/*` OpenAI-compatible routes are the stable API. `/dev/*` and `/debug/*` are operational helpers, and may change or move behind flags between minor releases.
diff --git a/api/src/core/config.py b/api/src/core/config.py
index c6a896eb..6bb54c1f 100644
--- a/api/src/core/config.py
+++ b/api/src/core/config.py
@@ -41,6 +41,9 @@ class Settings(BaseSettings):
False # Whether to allow saving combined voices locally
)
allow_dev_unload: bool = False # Whether to expose the POST /dev/unload endpoint
+ enable_debug_endpoints: bool = (
+ False # Whether to expose /debug/* host and process introspection routes
+ )
# Container absolute paths
model_dir: str = "/app/api/src/models" # Absolute path in container
diff --git a/api/src/main.py b/api/src/main.py
index 11bd3e5b..89a7759b 100644
--- a/api/src/main.py
+++ b/api/src/main.py
@@ -135,7 +135,7 @@ async def lifespan(app: FastAPI):
# Include routers
app.include_router(openai_router, prefix="/v1")
app.include_router(dev_router) # Development endpoints
-app.include_router(debug_router) # Debug endpoints
+app.include_router(debug_router) # Debug endpoints (403 unless enabled)
if settings.enable_web_player:
app.include_router(web_router, prefix="/web") # Web player static files
diff --git a/api/src/routers/debug.py b/api/src/routers/debug.py
index 8acb9fd7..c3476765 100644
--- a/api/src/routers/debug.py
+++ b/api/src/routers/debug.py
@@ -1,10 +1,11 @@
import threading
-import time
from datetime import datetime
import psutil
import torch
-from fastapi import APIRouter
+from fastapi import APIRouter, Depends, HTTPException
+
+from ..core.config import settings
try:
import GPUtil
@@ -13,7 +14,16 @@
except ImportError:
GPU_AVAILABLE = False
-router = APIRouter(tags=["debug"])
+
+async def _require_debug_enabled():
+ if not settings.enable_debug_endpoints:
+ raise HTTPException(
+ status_code=403,
+ detail={"error": "Debug endpoints are disabled"},
+ )
+
+
+router = APIRouter(tags=["debug"], dependencies=[Depends(_require_debug_enabled)])
@router.get("/debug/threads")
@@ -149,61 +159,3 @@ async def get_system_info():
"network": network_info,
"gpu": gpu_info,
}
-
-
-@router.get("/debug/session_pools")
-async def get_session_pool_info():
- """Get information about ONNX session pools."""
- from ..inference.model_manager import get_manager
-
- manager = await get_manager()
- pools = manager._session_pools
- current_time = time.time()
-
- pool_info = {}
-
- # Get CPU pool info
- if "onnx_cpu" in pools:
- cpu_pool = pools["onnx_cpu"]
- pool_info["cpu"] = {
- "active_sessions": len(cpu_pool._sessions),
- "max_sessions": cpu_pool._max_size,
- "sessions": [
- {"model": path, "age_seconds": current_time - info.last_used}
- for path, info in cpu_pool._sessions.items()
- ],
- }
-
- # Get GPU pool info
- if "onnx_gpu" in pools:
- gpu_pool = pools["onnx_gpu"]
- pool_info["gpu"] = {
- "active_sessions": len(gpu_pool._sessions),
- "max_streams": gpu_pool._max_size,
- "available_streams": len(gpu_pool._available_streams),
- "sessions": [
- {
- "model": path,
- "age_seconds": current_time - info.last_used,
- "stream_id": info.stream_id,
- }
- for path, info in gpu_pool._sessions.items()
- ],
- }
-
- # Add GPU memory info if available
- if GPU_AVAILABLE:
- try:
- gpus = GPUtil.getGPUs()
- if gpus:
- gpu = gpus[0] # Assume first GPU
- pool_info["gpu"]["memory"] = {
- "total_mb": gpu.memoryTotal,
- "used_mb": gpu.memoryUsed,
- "free_mb": gpu.memoryFree,
- "percent_used": (gpu.memoryUsed / gpu.memoryTotal) * 100,
- }
- except Exception:
- pass
-
- return pool_info
diff --git a/api/tests/test_debug_endpoints.py b/api/tests/test_debug_endpoints.py
new file mode 100644
index 00000000..091017bc
--- /dev/null
+++ b/api/tests/test_debug_endpoints.py
@@ -0,0 +1,26 @@
+"""Tests for the /debug/* opt-in gate."""
+
+from unittest.mock import patch
+
+from fastapi.testclient import TestClient
+
+from api.src.core.config import settings
+from api.src.main import app
+
+client = TestClient(app)
+
+
+def test_debug_endpoints_403_when_disabled():
+ """Disabled by default: every /debug/* route returns 403."""
+ for path in ["/debug/threads", "/debug/storage", "/debug/system"]:
+ response = client.get(path)
+ assert response.status_code == 403
+ assert response.json()["detail"]["error"] == "Debug endpoints are disabled"
+
+
+def test_debug_endpoints_200_when_enabled():
+ with patch.object(settings, "enable_debug_endpoints", True):
+ response = client.get("/debug/threads")
+
+ assert response.status_code == 200
+ assert "total_threads" in response.json()
diff --git a/docker/cpu/docker-compose.yml b/docker/cpu/docker-compose.yml
index a793ace9..3013ca7b 100644
--- a/docker/cpu/docker-compose.yml
+++ b/docker/cpu/docker-compose.yml
@@ -20,7 +20,9 @@ services:
- ONNX_ARENA_EXTEND_STRATEGY=kNextPowerOfTwo
- API_LOG_LEVEL=DEBUG
- DOWNLOAD_MODEL=true
-
+ # - ALLOW_DEV_UNLOAD=true
+ # - ENABLE_DEBUG_ENDPOINTS=true
+
# # Gradio UI service [Comment out everything below if you don't need it]
# gradio-ui:
# image: ghcr.io/remsky/kokoro-fastapi-ui:v${VERSION}
diff --git a/docker/gpu/docker-compose.yml b/docker/gpu/docker-compose.yml
index 94413a28..c068e732 100644
--- a/docker/gpu/docker-compose.yml
+++ b/docker/gpu/docker-compose.yml
@@ -21,6 +21,8 @@ services:
- PYTHONUNBUFFERED=1
- API_LOG_LEVEL=DEBUG
- DOWNLOAD_MODEL=true
+ # - ALLOW_DEV_UNLOAD=true
+ # - ENABLE_DEBUG_ENDPOINTS=true
deploy:
resources:
reservations:
diff --git a/docker/rocm/docker-compose.yml b/docker/rocm/docker-compose.yml
index 73c94ff5..ed8f2f30 100644
--- a/docker/rocm/docker-compose.yml
+++ b/docker/rocm/docker-compose.yml
@@ -27,6 +27,8 @@ services:
- 8880:8880
environment:
- USE_GPU=true
+ # - ALLOW_DEV_UNLOAD=true
+ # - ENABLE_DEBUG_ENDPOINTS=true
- TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1
# MIOPEN_FIND_MODE=2 is set by default in the Dockerfile. It reuses the
# on-disk find DB and skips the expensive per-process kernel search.