front: /v1/models served from system worker; quant poll only while a job runs

#1 /v1/models: the front fanned out to every engine's /v1/models (slow during
generation). It now fetches the full list once from the coderai-system worker
(config-based list_models, off the GPU engines) — added a /v1/models route to the
system app — with a last-good cache and per-engine fallback if the worker is down.

#3 quant status: the models page polled /admin/api/quantize-status (engine) every
5s unconditionally. It now polls only while a quant job is actually running (and
the tab is visible); startQuantize re-arms it. No idle engine hits.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DDv7BchtZQWsnPG6Jm49m
parent 858d393a
...@@ -2341,6 +2341,7 @@ async function startQuantize(){ ...@@ -2341,6 +2341,7 @@ async function startQuantize(){
_renderQuantStatus(null, {available:false, capabilities:j.job.caps||{}}); _renderQuantStatus(null, {available:false, capabilities:j.job.caps||{}});
return; return;
} }
_quantActive = true; // resume polling while this job runs
_refreshQuantStatus(path); _refreshQuantStatus(path);
}catch(e){ }catch(e){
if(el) el.innerHTML = '<span style="color:var(--danger,#dc2626)">Failed to start: '+esc(String(e))+'</span>'; if(el) el.innerHTML = '<span style="color:var(--danger,#dc2626)">Failed to start: '+esc(String(e))+'</span>';
...@@ -2747,11 +2748,13 @@ function _instanceBadge(lookupPaths, cfgMax){ ...@@ -2747,11 +2748,13 @@ function _instanceBadge(lookupPaths, cfgMax){
// ---- Quant-job badges on the model list (polled; re-renders only on change) ---- // ---- Quant-job badges on the model list (polled; re-renders only on change) ----
let _quantJobs = {}; let _quantJobs = {};
let _quantSig = ''; let _quantSig = '';
let _quantActive = false; // any quant job currently running → keep polling the engine
async function refreshQuantJobs(){ async function refreshQuantJobs(){
try{ try{
const r = await pageFetch('/admin/api/quantize-status'); const r = await pageFetch('/admin/api/quantize-status');
_quantJobs = (await r.json()).jobs || {}; _quantJobs = (await r.json()).jobs || {};
}catch(e){ /* keep last-known on transient error */ } }catch(e){ /* keep last-known on transient error */ }
_quantActive = Object.values(_quantJobs).some(j => j && j.status === 'running');
// Signature buckets progress to 5% so we only re-render on meaningful change. // Signature buckets progress to 5% so we only re-render on meaningful change.
return Object.entries(_quantJobs) return Object.entries(_quantJobs)
.map(([k,v])=>`${k}:${v.status}:${Math.round((v.progress||0)*20)}`).sort().join('|'); .map(([k,v])=>`${k}:${v.status}:${Math.round((v.progress||0)*20)}`).sort().join('|');
...@@ -2777,8 +2780,11 @@ async function refreshLocal(){ ...@@ -2777,8 +2780,11 @@ async function refreshLocal(){
loadCachedModels(); loadCachedModels();
} }
// Poll quant jobs; re-render the list only when a job's state/progress changes. // Poll quant jobs ONLY while one is running (and the tab is visible) — quant runs
// on the engine, so don't poll it every 5s when there's nothing to watch.
setInterval(async () => { setInterval(async () => {
if (document.visibilityState === 'hidden') return;
if (!_quantActive) return;
const sig = await refreshQuantJobs(); const sig = await refreshQuantJobs();
if (sig !== _quantSig) { _quantSig = sig; loadCachedModels(); } if (sig !== _quantSig) { _quantSig = sig; loadCachedModels(); }
}, 5000); }, 5000);
......
...@@ -162,12 +162,30 @@ class FrontProxy: ...@@ -162,12 +162,30 @@ class FrontProxy:
self._broker = None self._broker = None
async def collect_models(self, headers): async def collect_models(self, headers):
"""Union of every healthy engine's /v1/models. Each engine registers only """Full node model list. Served from the coderai-system worker (config-based
the models the front assigned to it, so the union is the full set with no list_models, off the GPU engines) in a single fast call, so /v1/models never
duplicates. Returns ("ok", {...}) or ("passthrough", httpx.Response) when an fans out to — or stalls on — a busy engine. Falls back to a per-engine union
auth/error response should be relayed instead.""" only if the worker is unavailable. Returns ("ok", {...}) or ("passthrough",
httpx.Response)."""
sysw = self._system_engine()
if sysw is not None:
try:
r = await self._short.get(sysw.url + "/v1/models", headers=headers)
if r.status_code == 200:
data = r.json().get("data") or []
self._engine_models_cache["__system__"] = data # last-good
return ("ok", {"object": "list", "data": data})
except Exception:
pass
# Worker briefly unreachable: serve its last-good list if we have one.
cached = self._engine_models_cache.get("__system__")
if cached:
return ("ok", {"object": "list", "data": cached})
seen, order, relay = {}, [], None seen, order, relay = {}, [], None
for e in self.registry.all(): for e in self.registry.all():
if getattr(e, "role", "engine") == "system":
continue
models = None models = None
if e.healthy: if e.healthy:
try: try:
......
...@@ -79,5 +79,26 @@ def build_system_app(config, config_dir, internal_port: int = 0) -> FastAPI: ...@@ -79,5 +79,26 @@ def build_system_app(config, config_dir, internal_port: int = 0) -> FastAPI:
return JSONResponse({"ok": False, "error": str(exc)}, status_code=500) return JSONResponse({"ok": False, "error": str(exc)}, status_code=500)
return JSONResponse({"ok": True}) return JSONResponse({"ok": True})
@app.get("/v1/models", include_in_schema=False)
async def _v1_models():
"""Full node model list (config-based), so the front can serve /v1/models
from here instead of fanning out to every engine."""
from codai.models.manager import multi_model_manager
out = []
for m in multi_model_manager.list_models(all_engines=True):
if hasattr(m, "model_dump"):
out.append(m.model_dump())
elif hasattr(m, "dict"):
out.append(m.dict())
elif isinstance(m, dict):
out.append(m)
else:
try:
import dataclasses
out.append(dataclasses.asdict(m))
except Exception:
out.append({"id": str(getattr(m, "id", m))})
return JSONResponse({"object": "list", "data": out})
app.include_router(admin_router, tags=["Admin"]) app.include_router(admin_router, tags=["Admin"])
return app return app
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment