diff options
| -rw-r--r-- | deckit/modules/llm.py | 20 | ||||
| -rw-r--r-- | static/modules/llm.js | 13 | ||||
| -rw-r--r-- | tests/test_llm.py | 18 |
3 files changed, 45 insertions, 6 deletions
diff --git a/deckit/modules/llm.py b/deckit/modules/llm.py index fb2366f..310eabf 100644 --- a/deckit/modules/llm.py +++ b/deckit/modules/llm.py @@ -19,6 +19,24 @@ def _fetch_for(base): return fetch +def _opencode(rows) -> dict: + """Aggregate /api/summary's per-model rows into opencode's cost and tokens.""" + picked = [r for r in rows if isinstance(r, dict) and r.get("program") == "opencode"] + models = [ + {"provider": r.get("provider") or "", "model": r.get("model") or "", + "cost": r.get("total_cost_usd") or 0, "tokens": r.get("tokens") or 0, + "calls": r.get("calls") or 0} + for r in picked + ] + models.sort(key=lambda r: r["cost"], reverse=True) + return { + "cost": round(sum(r["cost"] for r in models), 4), + "tokens": sum(r["tokens"] for r in models), + "calls": sum(r["calls"] for r in models), + "models": models, + } + + def make(cfg: dict) -> Module: base = cfg.get("llm", {}).get("url", "http://127.0.0.1:8420").rstrip("/") fetch = cfg.get("fetch") or _fetch_for(base) @@ -48,7 +66,7 @@ def make(cfg: dict) -> Module: "extra_utilization": latest.get("extra_utilization"), "extra_currency": latest.get("extra_currency"), }, - "opencode": data["/api/summary"], + "opencode": _opencode(data.get("/api/summary") or []), } return Module(name="llm", title="LLM usage", state=state) diff --git a/static/modules/llm.js b/static/modules/llm.js index 093c27c..a1b775b 100644 --- a/static/modules/llm.js +++ b/static/modules/llm.js @@ -1,6 +1,13 @@ const num = (v) => (v == null ? null : Number(v)); const pct = (v) => (num(v) == null ? "—" : `${Math.round(num(v))}%`); const money = (v, cur) => (num(v) == null ? "—" : `${num(v).toFixed(2)}${cur ? " " + cur : ""}`); +const tokens = (n) => { + if (n == null) return "—"; + if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`; + if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`; + if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`; + return String(n); +}; const clock = (sec) => (sec ? new Date(sec * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""); function line(label, value, percent) { @@ -49,7 +56,11 @@ export default { line(`5h window ${clock(c.five_hour_resets_at)}`, pct(c.five_hour_pct), c.five_hour_pct), line(`7d window ${clock(c.seven_day_resets_at)}`, pct(c.seven_day_pct), c.seven_day_pct), line("Month", money(c.extra_used, c.extra_currency)), - line("opencode spend", money(o.total_cost)), + line("opencode spend", money(o.cost)), + line("opencode tokens", `${tokens(o.tokens)} · ${o.calls ?? 0} calls`), ); + for (const m of (o.models ?? []).slice(0, 4)) { + box.append(line(m.model || m.provider, money(m.cost))); + } }, }; diff --git a/tests/test_llm.py b/tests/test_llm.py index 9cdfcf6..354b5fe 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -17,20 +17,30 @@ class Down(unittest.TestCase): class Up(unittest.TestCase): - def test_merges_endpoints(self): + def test_aggregates_opencode_and_ignores_other_programs(self): data = {"/api/claude_limits": [{"context_pct": 12}], "/api/claude_session": {"context_used": 1200}, - "/api/summary": {"total_cost": 1.5}} + "/api/summary": [ + {"program": "opencode", "provider": "deepseek", "model": "v4", + "total_cost_usd": 1.5, "tokens": 1000, "calls": 3}, + {"program": "opencode", "provider": "together", "model": "qwen", + "total_cost_usd": 0.5, "tokens": 500, "calls": 1}, + {"program": "assistant", "provider": "x", "model": "y", + "total_cost_usd": 9.0, "tokens": 9999, "calls": 9}, + ]} st = llm.make({"llm": {"url": "http://x"}, "fetch": lambda u, timeout=2: data[u]}).state() self.assertFalse(st["offline"]) self.assertEqual(st["claude"]["context_pct"], 12) - self.assertEqual(st["opencode"]["total_cost"], 1.5) + self.assertEqual(st["opencode"]["cost"], 2.0) + self.assertEqual(st["opencode"]["tokens"], 1500) + self.assertEqual(st["opencode"]["calls"], 4) + self.assertEqual(st["opencode"]["models"][0]["model"], "v4") def test_handles_the_real_latest_shape(self): data = {"/api/claude_limits": {"latest": {"context_pct": 42, "five_hour_pct": 10}}, "/api/claude_session": {}, - "/api/summary": {}} + "/api/summary": []} st = llm.make({"llm": {"url": "http://x"}, "fetch": lambda u, timeout=2: data[u]}).state() self.assertEqual(st["claude"]["context_pct"], 42) |
