Expose DuckLM runtime status

This commit is contained in:
2026-05-22 07:45:47 +08:00
parent ff98224eb6
commit 8452673994
8 changed files with 319 additions and 9 deletions
+53 -2
View File
@@ -6,7 +6,7 @@ from pathlib import Path
from typing import Any, Literal
import uvicorn
from fastapi import FastAPI, HTTPException, Request
from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@@ -165,7 +165,31 @@ def create_app() -> FastAPI:
return {"status": "ok"}
@app.get("/v1/status")
async def status() -> dict[str, Any]:
async def status(probe: bool = Query(False)) -> dict[str, Any]:
role_configs = model_client.list_roles()
endpoints = sorted(
{
f"{role_config['provider']}:{role_config['base_url']}:{role_config['model']}"
for role_config in role_configs.values()
}
)
llama_status: dict[str, Any] = {"probed": False, "ok": None, "roles": {}}
vector_status: dict[str, Any] = {
**vector_memory.config_status(),
"probed": False,
"ok": None,
}
if probe:
model_ping = await model_client.ping()
llama_status = {
"probed": True,
"ok": all(role.get("ok") for role in model_ping.values()) if model_ping else False,
"roles": model_ping,
}
vector_status = {
**await vector_memory.health(),
"probed": True,
}
return {
"name": "DuckLM",
"version": "0.1.0",
@@ -173,6 +197,33 @@ def create_app() -> FastAPI:
"api_port": settings.api_port,
"workspace": settings.workspace,
"db_path": settings.db_path,
"api": {
"host": settings.api_host,
"port": settings.api_port,
"base_url": f"http://{settings.api_host}:{settings.api_port}",
},
"paths": {
"workspace": settings.workspace,
"db_path": settings.db_path,
"models_config": str(model_client.config_path),
},
"token_budget": {
"ctx_size": settings.ctx_size,
"max_input_tokens": settings.max_input_tokens,
"max_recent_events_tokens": settings.max_recent_events_tokens,
"max_memory_tokens": settings.max_memory_tokens,
"max_skill_tokens": settings.max_skill_tokens,
},
"models": {
"default_provider": model_client.default_provider,
"roles": role_configs,
"endpoints": endpoints,
},
"services": {
"duck_api": {"ok": True, "probed": True},
"llama": llama_status,
"vector_memory": vector_status,
},
}
@app.get("/v1/models/roles")
+37
View File
@@ -94,6 +94,43 @@ class VectorMemory:
response.raise_for_status()
return response.json().get("result", [])
def config_status(self) -> dict[str, Any]:
if self.embeddings_base_url:
embedding_source = f"remote:{self.embeddings_base_url}"
elif self._local_model_path:
embedding_source = f"local:{self._local_model_path}"
else:
embedding_source = "none"
return {
"configured": bool(self.qdrant_url and embedding_source != "none"),
"qdrant_url": self.qdrant_url,
"collection": self.collection_name,
"embedding_source": embedding_source,
}
async def health(self) -> dict[str, Any]:
"""Probe Qdrant without loading the local embedding model."""
import time
status = self.config_status()
started = time.perf_counter()
try:
async with httpx.AsyncClient(timeout=5.0, trust_env=False) as client:
response = await client.get(f"{self.qdrant_url}/")
response.raise_for_status()
return {
**status,
"ok": True,
"latency_ms": round((time.perf_counter() - started) * 1000, 1),
}
except httpx.HTTPError as exc:
return {
**status,
"ok": False,
"error": str(exc),
"latency_ms": round((time.perf_counter() - started) * 1000, 1),
}
async def _embed(self, text: str) -> list[float]:
"""Generate embeddings using local model or remote endpoint."""
# Prefer local model if available