From fac11c318b841e6c6076e144637776ef3c4a7f80 Mon Sep 17 00:00:00 2001 From: kukirin-ui <325067280+kukirin-ui@users.noreply.github.com> Date: Wed, 14 Oct 2026 04:56:17 +0200 Subject: [PATCH] Recipe status bar: model gen/prompt t/s from llama-server timings, KB/s only during fetch, battery temp --- server/server.py | 19 ++++++++++++++++--- server/ui.html | 5 ++++- tests/test_server.py | 11 ++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/server/server.py b/server/server.py index 7c0d316..cf9f8b7 100644 --- a/server/server.py +++ b/server/server.py @@ -470,11 +470,20 @@ def llm_complete(system, user, max_tokens, timeout=300): except ValueError: err = r.text[:300] raise RuntimeError(f"Model error ({r.status_code}): {err or 'unknown error'}") - return strip_think(r.json()["choices"][0]["message"].get("content") or "") + data = r.json() + tm = data.get("timings") or {} + try: + if tm.get("predicted_per_second"): + RECIPE_LIVE["gen_tps"] = f"{float(tm['predicted_per_second']):.1f} t/s" + if tm.get("prompt_per_second"): + RECIPE_LIVE["prompt_tps"] = f"{float(tm['prompt_per_second']):.1f} t/s" + except (TypeError, ValueError): + pass + return strip_think(data["choices"][0]["message"].get("content") or "") RECIPE_STOP = {"flag": False} -RECIPE_LIVE = {"speed": "", "size": ""} # live status bar (not written into the log) +RECIPE_LIVE = {"speed": "", "size": "", "phase": "", "gen_tps": "", "prompt_tps": ""} # live status bar (not written into the log) BATTERY_TEMP_FILE = os.environ.get("BATTERY_TEMP_FILE", "/sys/class/power_supply/battery/temp") FETCH_LIMIT = 2_000_000 @@ -630,6 +639,7 @@ def recipe_gen(url, title, text, raw, max_links=5, depth=1, ctx=2048, crawl="bal if len(text) < 20: yield "[No text to analyze.]" return + RECIPE_LIVE.update(phase="fetch", gen_tps="", prompt_tps="") cache.save_state(step="crawling", crawl=crawl, title=title) pages = [(url or "(pasted text)", title, text, raw)] visited = {url} if url else set() @@ -672,6 +682,7 @@ def recipe_gen(url, title, text, raw, max_links=5, depth=1, ctx=2048, crawl="bal cached_facts = sum(1 for p in pages if cache.get_facts(p[0], fkey) is not None) if cached_pages or cached_facts: yield f"> Resuming: {cached_pages}/{len(pages)} pages cached, {cached_facts}/{len(pages)} facts cached.\n" + RECIPE_LIVE["phase"] = "model" cache.save_state(step="extracting", pages=[p[0] for p in pages]) facts, used = [], [] for n, (u, tt, t, _) in enumerate(pages, 1): @@ -1213,7 +1224,9 @@ def _job_file(): def _job_public(j): - live = {"speed": RECIPE_LIVE.get("speed") or "n/a", "size": RECIPE_LIVE.get("size") or "", "temp": battery_temp()} + live = {"phase": RECIPE_LIVE.get("phase") or "", "gen_tps": RECIPE_LIVE.get("gen_tps") or "", + "prompt_tps": RECIPE_LIVE.get("prompt_tps") or "", "speed": RECIPE_LIVE.get("speed") or "", + "size": RECIPE_LIVE.get("size") or "", "temp": battery_temp()} return {k: j.get(k) for k in ("id", "url", "crawl", "status", "started", "finished")} | {"text": "".join(j["log"]), "live": live} diff --git a/server/ui.html b/server/ui.html index 763703e..b660a75 100644 --- a/server/ui.html +++ b/server/ui.html @@ -345,7 +345,10 @@

High-Compliance Consulting & Community Support

const out=$("out"),atEnd=out.scrollTop+out.clientHeight>=out.scrollHeight-20; const lv=d.live||{},bar=$("job-bar"); bar.style.display=d.status==="running"?"block":"none"; - bar.textContent=`⬇ ${lv.speed||"n/a"}${lv.size?" ("+lv.size+")":""} · 🌡 Battery ${lv.temp||"n/a"}`; + const parts=[`⚡ Gen ${lv.gen_tps||"–"}`+(lv.prompt_tps?` · Prompt ${lv.prompt_tps}`:"")]; + if(lv.phase==="fetch"&&lv.speed)parts.push(`⬇ ${lv.speed}${lv.size?" ("+lv.size+")":""}`); + parts.push(`🌡 Battery ${lv.temp||"n/a"}`); + bar.textContent=parts.join(" · "); out.textContent=`[Recipe job ${d.id} · ${d.crawl||""} · ${st}]\n${d.url||""}\n\n`+(d.text||""); if(atEnd)out.scrollTop=out.scrollHeight; } diff --git a/tests/test_server.py b/tests/test_server.py index 0b4b96b..7f232ba 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -245,7 +245,7 @@ def llm(s, u, m, timeout=300): if j["status"] != "running": break time.sleep(0.05) - assert j["status"] == "stopped" and "KB/s" in j["live"]["speed"] and j["live"]["temp"] == "34.5°C" + assert j["status"] == "stopped" and "KB/s" in j["live"]["speed"] and j["live"]["phase"] == "model" and j["live"]["temp"] == "34.5°C" assert "KB/s" not in j["text"] and "°C" not in j["text"] # live bar only, not in the log assert "skipped (no useful text)" in j["text"] first_fetches = len(fetched) @@ -267,3 +267,12 @@ def test_battery_temp_fallback(monkeypatch, tmp_path): monkeypatch.setattr(server, "BATTERY_TEMP_FILE", str(tmp_path / "missing")) monkeypatch.setattr(server.shutil, "which", lambda n: None) assert server.battery_temp() == "n/a" + + +def test_llm_complete_records_timings(monkeypatch): + resp = MagicMock(status_code=200) + resp.json.return_value = {"choices": [{"message": {"content": "ok"}}], + "timings": {"predicted_per_second": 20.44, "prompt_per_second": 182.1}} + monkeypatch.setattr(server.requests, "post", lambda *a, **k: resp) + assert server.llm_complete("s", "u", 5) == "ok" + assert server.RECIPE_LIVE["gen_tps"] == "20.4 t/s" and server.RECIPE_LIVE["prompt_tps"] == "182.1 t/s"