aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-09 19:02:04 +0100
committerDanilo M. <danix@danix.xyz>2026-11-09 19:02:04 +0100
commitc906d90dcf0abdfb347bd67d6f33a2c45ae32e66 (patch)
tree8d2bf424aff57e76a3adcbf79e797cacd248da7a
parent84ac47a8ab196ee20ad778219bfa13df48f87241 (diff)
downloaddeck-it-c906d90dcf0abdfb347bd67d6f33a2c45ae32e66.tar.gz
deck-it-c906d90dcf0abdfb347bd67d6f33a2c45ae32e66.zip
fix: aggregate opencode rows from /api/summary
-rw-r--r--deckit/modules/llm.py20
-rw-r--r--static/modules/llm.js13
-rw-r--r--tests/test_llm.py18
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)