aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-09 18:18:10 +0100
committerDanilo M. <danix@danix.xyz>2026-11-09 18:18:10 +0100
commit2e86bcdc2132e7772e20e9f4426e3f079a07e5e0 (patch)
tree6ad406071097fee9b7347c1b8605188d17de99a7
parente2ddaeb9a9271069a36b77112110fa8e646ab02f (diff)
downloaddeck-it-2e86bcdc2132e7772e20e9f4426e3f079a07e5e0.tar.gz
deck-it-2e86bcdc2132e7772e20e9f4426e3f079a07e5e0.zip
feat: llm usage panel
-rw-r--r--deckit/modules/llm.py22
-rw-r--r--static/deck.css3
-rw-r--r--static/modules/llm.js55
-rw-r--r--tests/test_frontend.py14
4 files changed, 83 insertions, 11 deletions
diff --git a/deckit/modules/llm.py b/deckit/modules/llm.py
index 2e1cda4..fb2366f 100644
--- a/deckit/modules/llm.py
+++ b/deckit/modules/llm.py
@@ -29,16 +29,24 @@ def make(cfg: dict) -> Module:
except (OSError, ValueError):
return {"offline": True}
limits = data["/api/claude_limits"]
- latest = limits[-1] if isinstance(limits, list) and limits else (
- limits if isinstance(limits, dict) else {})
+ if isinstance(limits, dict):
+ latest = limits.get("latest") or {}
+ elif isinstance(limits, list) and limits:
+ latest = limits[-1]
+ else:
+ latest = {}
return {
"offline": False,
"claude": {
- "context_pct": latest.get("context_pct", data["/api/claude_session"].get("context_used")),
- "five_hour": latest.get("five_hour"),
- "seven_day": latest.get("seven_day"),
- "monthly_used": latest.get("extra_used_credits"),
- "monthly_limit": latest.get("extra_monthly_limit"),
+ "context_pct": latest.get("context_pct"),
+ "five_hour_pct": latest.get("five_hour_pct"),
+ "five_hour_resets_at": latest.get("five_hour_resets_at"),
+ "seven_day_pct": latest.get("seven_day_pct"),
+ "seven_day_resets_at": latest.get("seven_day_resets_at"),
+ "extra_used": latest.get("extra_used_credits"),
+ "extra_limit": latest.get("extra_monthly_limit"),
+ "extra_utilization": latest.get("extra_utilization"),
+ "extra_currency": latest.get("extra_currency"),
},
"opencode": data["/api/summary"],
}
diff --git a/static/deck.css b/static/deck.css
index c2512d5..e4728a5 100644
--- a/static/deck.css
+++ b/static/deck.css
@@ -77,6 +77,9 @@ select {
padding: 8px;
}
+.llm .meter { height: 6px; background: var(--udt-surface1); border-radius: 3px; overflow: hidden; margin: 2px 0 8px; }
+.llm .meter > i { display: block; height: 100%; background: var(--udt-accent); }
+
.good { color: var(--udt-green); }
.bad { color: var(--udt-red); }
.warn { color: var(--udt-yellow); }
diff --git a/static/modules/llm.js b/static/modules/llm.js
new file mode 100644
index 0000000..093c27c
--- /dev/null
+++ b/static/modules/llm.js
@@ -0,0 +1,55 @@
+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 clock = (sec) => (sec ? new Date(sec * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "");
+
+function line(label, value, percent) {
+ const row = document.createElement("div");
+ row.className = "row";
+ const name = document.createElement("span");
+ name.textContent = label;
+ const val = document.createElement("span");
+ val.className = "num";
+ val.textContent = value;
+ row.append(name, val);
+ const wrap = document.createElement("div");
+ wrap.append(row);
+ if (percent != null) {
+ const meter = document.createElement("div");
+ meter.className = "meter";
+ const fill = document.createElement("i");
+ fill.style.width = `${Math.max(0, Math.min(100, Number(percent)))}%`;
+ meter.append(fill);
+ wrap.append(meter);
+ }
+ return wrap;
+}
+
+export default {
+ title: "LLM usage",
+ interval: 5,
+
+ mount(el) {
+ const box = document.createElement("div");
+ box.className = "llm";
+ el.append(box);
+ },
+
+ update(state, el) {
+ const box = el.querySelector(".llm");
+ box.textContent = "";
+ if (state.offline) {
+ box.textContent = "llm-cost-dashboard offline";
+ return;
+ }
+ const c = state.claude ?? {};
+ const o = state.opencode ?? {};
+ box.append(
+ line("Context", pct(c.context_pct), c.context_pct),
+ 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)),
+ );
+ },
+};
diff --git a/tests/test_frontend.py b/tests/test_frontend.py
index 94ea88f..fb4df7e 100644
--- a/tests/test_frontend.py
+++ b/tests/test_frontend.py
@@ -60,12 +60,18 @@ class Frontend(unittest.TestCase):
self.assertEqual(status, 200)
self.assertIn(b"--udt-", body)
- def test_obs_panel_served(self):
- status, body = self.get("/modules/obs.js")
- self.assertEqual(status, 200)
+ def assert_panel(self, name):
+ status, body = self.get(f"/modules/{name}.js")
+ self.assertEqual(status, 200, name)
text = body.decode()
for part in ("export default", "mount", "update"):
- self.assertIn(part, text)
+ self.assertIn(part, text, name)
+
+ def test_obs_panel_served(self):
+ self.assert_panel("obs")
+
+ def test_llm_panel_served(self):
+ self.assert_panel("llm")
if __name__ == "__main__":