diff --git a/tests/entrypoints/openai/test_dashboard.py b/tests/entrypoints/openai/test_dashboard.py index bb6203c03e2..112de6a0f8f 100644 --- a/tests/entrypoints/openai/test_dashboard.py +++ b/tests/entrypoints/openai/test_dashboard.py @@ -192,17 +192,42 @@ class TestDashboardRouter: # Should return a dict (may be empty if no metrics available) assert isinstance(data, dict) - def test_dashboard_api_collect_env(self, client): - """Test GET /dashboard/api/collect-env returns env info.""" - response = client.get("/dashboard/api/collect-env") + def test_dashboard_api_metrics_with_lora(self, app): + """Test /dashboard/api/metrics includes LoRA info when available.""" + # Mock serving models with LoRA adapters + mock_base_model = MagicMock() + mock_base_model.id = "base-model" + mock_base_model.root = "base-model" + mock_base_model.parent = None + + mock_lora_adapter = MagicMock() + mock_lora_adapter.id = "lora-adapter-1" + mock_lora_adapter.root = "lora-adapter-1" + mock_lora_adapter.parent = "base-model" + + mock_models_response = MagicMock() + mock_models_response.data = [mock_base_model, mock_lora_adapter] + + mock_serving_models = AsyncMock() + mock_serving_models.show_available_models = AsyncMock( + return_value=mock_models_response + ) + + app.state.openai_serving_models = mock_serving_models + + client = TestClient(app) + response = client.get("/dashboard/api/metrics") assert response.status_code == 200 data = response.json() - assert "status" in data - assert "output" in data - # Status should be success or error - assert data["status"] in ("success", "error") + # Should have internal stats with LoRA info + assert "internal" in data + assert "lora" in data["internal"] + assert data["internal"]["lora"]["count"] == 1 + assert len(data["internal"]["lora"]["adapters"]) == 1 + assert data["internal"]["lora"]["adapters"][0]["id"] == "lora-adapter-1" + assert data["internal"]["lora"]["adapters"][0]["parent"] == "base-model" class TestAttachRouter: diff --git a/vllm/entrypoints/serve/dashboard/api_router.py b/vllm/entrypoints/serve/dashboard/api_router.py index 7c49ae87c57..158f1a365fc 100644 --- a/vllm/entrypoints/serve/dashboard/api_router.py +++ b/vllm/entrypoints/serve/dashboard/api_router.py @@ -167,7 +167,11 @@ async def dashboard_info(request: Request) -> JSONResponse: @router.get("/dashboard/api/metrics") async def dashboard_metrics(request: Request) -> JSONResponse: - """Get metrics for dashboard display.""" + """Get metrics for dashboard display. + + Returns both Prometheus metrics and internal engine stats that are only + accessible in-process (not available via external /metrics endpoint). + """ metrics: dict = {} # Try to get metrics from prometheus registry @@ -206,33 +210,49 @@ async def dashboard_metrics(request: Request) -> JSONResponse: except Exception: pass + # Get internal engine stats (only available in-process) + internal: dict = {} + try: + engine_client = getattr(request.app.state, "engine_client", None) + if engine_client is not None: + # Try to get internal stats from the engine + internal_stats = getattr(engine_client, "get_internal_stats", None) + if internal_stats is not None: + stats = await internal_stats() + if stats: + internal = stats + except Exception as e: + logger.debug("Failed to get internal engine stats: %s", e) + + # Try to get LoRA adapter info from serving models + try: + serving_models = getattr(request.app.state, "openai_serving_models", None) + if serving_models is not None: + lora_stats = await _get_lora_stats(serving_models) + if lora_stats: + internal["lora"] = lora_stats + except Exception as e: + logger.debug("Failed to get LoRA stats: %s", e) + + if internal: + metrics["internal"] = internal + return JSONResponse(content=metrics) -@router.get("/dashboard/api/collect-env") -async def dashboard_collect_env() -> JSONResponse: - """Collect environment information for debugging. - - This runs the same collection as `vllm collect-env` CLI command. - Useful for users to copy environment info when reporting issues. - """ - import asyncio - from concurrent.futures import ThreadPoolExecutor - +async def _get_lora_stats(serving_models) -> dict | None: + """Get LoRA adapter statistics from serving models.""" try: - from vllm.collect_env import get_pretty_env_info - - # Run in thread pool since it executes subprocess commands - loop = asyncio.get_event_loop() - with ThreadPoolExecutor() as executor: - env_info = await loop.run_in_executor(executor, get_pretty_env_info) - - return JSONResponse(content={"output": env_info, "status": "success"}) - except Exception as e: - logger.warning("Failed to collect environment info: %s", e) - return JSONResponse( - content={"output": str(e), "status": "error"}, status_code=500 - ) + models_response = await serving_models.show_available_models() + lora_adapters = [m for m in models_response.data if m.parent is not None] + if lora_adapters: + return { + "count": len(lora_adapters), + "adapters": [{"id": a.id, "parent": a.parent} for a in lora_adapters], + } + except Exception: + pass + return None def attach_router(app: FastAPI) -> None: diff --git a/vllm/entrypoints/serve/dashboard/static/index.html b/vllm/entrypoints/serve/dashboard/static/index.html index a82ad0e444b..6aed488c402 100644 --- a/vllm/entrypoints/serve/dashboard/static/index.html +++ b/vllm/entrypoints/serve/dashboard/static/index.html @@ -101,82 +101,23 @@ grid-template-rows: auto 1fr; grid-template-columns: 280px 1fr 280px; grid-template-areas: - "metrics metrics metrics" + "topbar topbar topbar" "left center right"; height: 100vh; gap: 12px; padding: 12px; } - /* Top Metrics Bar */ - .metrics-bar { - grid-area: metrics; + /* Top Bar */ + .top-bar { + grid-area: topbar; display: flex; align-items: center; - gap: 4px; + gap: 8px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); - padding: 6px 12px; - } - - .metric-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 0; - padding: 2px 6px; - border-right: 1px solid var(--border); - transition: background 0.2s ease; - flex: 1 1 auto; - min-width: 0; - } - - .metric-item:last-child { - border-right: none; - } - - .metric-item:hover { - background: var(--hover-bg); - } - - .metric-item .label { - font-size: 9px; - color: var(--muted-foreground); - text-transform: uppercase; - font-weight: 500; - letter-spacing: 0.02em; - } - - .metric-item .value { - font-size: 12px; - font-weight: 700; - font-family: var(--font-mono); - color: var(--vllm-blue); - } - - .metric-item .value.green { color: var(--success); } - .metric-item .value.yellow { color: var(--vllm-yellow); } - .metric-item .value.red { color: var(--error); } - - .metric-item .mini-bar { - width: 32px; - height: 3px; - background: var(--muted); - border-radius: 2px; - overflow: hidden; - margin-top: 2px; - } - - .metric-item .mini-bar .fill { - height: 100%; - background: var(--vllm-blue); - border-radius: 2px; - transition: width 0.3s ease; - } - - .metric-item .mini-bar .fill.green { - background: var(--success); + padding: 8px 12px; } .status-indicator { @@ -185,7 +126,6 @@ gap: 6px; padding-right: 10px; border-right: 1px solid var(--border); - flex-shrink: 0; } .status-indicator .dot { @@ -209,13 +149,10 @@ font-weight: 600; color: var(--vllm-yellow); padding: 0 8px; - border-right: 1px solid var(--border); - max-width: 180px; + max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - flex-shrink: 1; - min-width: 60px; } .theme-toggle { @@ -228,7 +165,6 @@ border-radius: var(--radius); cursor: pointer; transition: all 0.2s ease; - flex-shrink: 0; } .theme-toggle:hover { @@ -298,6 +234,13 @@ flex-direction: column; } + .left-panel-section { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + } /* Config section */ .config-scroll { @@ -730,179 +673,10 @@ 50% { opacity: 0.5; } } - /* Modal */ - .modal-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.7); - z-index: 9999; - align-items: center; - justify-content: center; - backdrop-filter: blur(4px); - } - - .modal-overlay.visible { - display: flex; - animation: fade-in 0.2s ease-out; - } - - .modal { - background: var(--card); - border: 1px solid var(--border); - border-radius: var(--radius); - width: 90%; - max-width: 800px; - max-height: 85vh; - display: flex; - flex-direction: column; - box-shadow: 0 20px 60px var(--shadow-color); - } - - .modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 20px; - border-bottom: 1px solid var(--border); - background: linear-gradient(90deg, rgba(48, 162, 255, 0.08) 0%, transparent 100%); - } - - .modal-title { - font-size: 14px; - font-weight: 600; - color: var(--vllm-blue); - display: flex; - align-items: center; - gap: 8px; - } - - .modal-actions { - display: flex; - gap: 8px; - } - - .modal-btn { - padding: 6px 12px; - border-radius: calc(var(--radius) - 2px); - font-size: 11px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - gap: 6px; - } - - .modal-btn-primary { - background: var(--vllm-blue); - border: none; - color: white; - } - - .modal-btn-primary:hover { - background: var(--vllm-blue-hover); - } - - .modal-btn-secondary { - background: var(--muted); - border: 1px solid var(--border); - color: var(--foreground); - } - - .modal-btn-secondary:hover { - border-color: var(--vllm-blue); - } - - .modal-btn-close { - background: transparent; - border: none; - color: var(--muted-foreground); - padding: 6px; - font-size: 18px; - line-height: 1; - } - - .modal-btn-close:hover { - color: var(--error); - } - - .modal-body { - flex: 1; - overflow-y: auto; - padding: 16px 20px; - } - - .modal-body pre { - font-family: var(--font-mono); - font-size: 11px; - line-height: 1.5; - color: var(--foreground); - white-space: pre-wrap; - word-break: break-word; - margin: 0; - } - - .modal-loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 60px 20px; - color: var(--muted-foreground); - gap: 16px; - } - - .modal-loading .spinner { - width: 32px; - height: 32px; - border: 3px solid var(--muted); - border-top-color: var(--vllm-blue); - border-radius: 50%; - animation: spin 0.8s linear infinite; - } - - @keyframes spin { - to { transform: rotate(360deg); } - } - .copy-success { color: var(--success) !important; } - /* Env button */ - .env-btn { - margin-left: auto; - display: flex; - align-items: center; - gap: 6px; - padding: 6px 10px; - background: var(--muted); - border: 1px solid var(--border); - border-radius: var(--radius); - cursor: pointer; - transition: all 0.2s ease; - font-size: 12px; - color: var(--muted-foreground); - font-weight: 500; - margin-right: 8px; - } - - .env-btn:hover { - background: var(--input); - border-color: var(--vllm-blue); - color: var(--vllm-blue); - } - - .env-btn svg { - width: 14px; - height: 14px; - fill: currentColor; - } - .github-link { display: flex; align-items: center; @@ -930,8 +704,8 @@
- -
+ +
-
-
- Running - 0 -
-
- Waiting - 0 -
-
- Requests - 0 -
-
- Tokens - 0 -
-
- TTFT - - -
-
- E2E Latency - - -
-
- Prefill - - -
-
- Decode - - -
-
- KV Cache - 0% -
-
-
- Cache Hit - - -
-
- +
- +
-
- Configuration - +
+
+ Configuration + +
+
+
+ Loading... +
+
-
-
- Loading... +
+
+ Environment + +
+
+
+ Loading... +
@@ -1082,13 +818,12 @@
- +
- Environment - + Runtime Stats
-
+
Loading...
@@ -1099,39 +834,6 @@
- - -