From 33060a4aab87a79d222b10d088e01acf6ac8be29 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 9 Nov 2026 19:31:13 +0100 Subject: feat: opencode session spend and brand marks in the LLM card --- NOTICE | 4 ++++ deckit/modules/llm.py | 11 +++++++++-- static/deck.css | 16 +++++++++++++++- static/logos/claude.svg | 1 + static/logos/opencode.svg | 1 + static/modules/llm.js | 12 +++++++++--- tests/test_llm.py | 9 +++++++-- 7 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 static/logos/claude.svg create mode 100644 static/logos/opencode.svg diff --git a/NOTICE b/NOTICE index 981facc..25324c1 100644 --- a/NOTICE +++ b/NOTICE @@ -10,3 +10,7 @@ setup and the stdlib HTTP server are adapted from tabdeck. Gridstack.js (https://gridstackjs.com) is vendored under `static/vendor/gridstack/`, MIT License. Copyright (c) 2019-2025 Alain Dumesny and contributors. + +The Claude and OpenCode brand marks under `static/logos/` are from Simple Icons +(https://simpleicons.org), released under CC0 1.0. The marks themselves are +trademarks of their respective owners. diff --git a/deckit/modules/llm.py b/deckit/modules/llm.py index fb45097..1b40a6f 100644 --- a/deckit/modules/llm.py +++ b/deckit/modules/llm.py @@ -49,8 +49,15 @@ def _opencode_session(sessions) -> dict: continue rows = block.get("rows") or [] top = max(rows, key=lambda r: r.get("calls") or 0) if rows else {} - return {"model": top.get("model"), "provider": top.get("provider")} - return {"model": None, "provider": None} + return { + "model": top.get("model"), + "provider": top.get("provider"), + "session_cost": round(sum(r.get("cost_usd") or 0 for r in rows), 4), + "session_tokens": sum(r.get("tokens") or 0 for r in rows), + "session_calls": sum(r.get("calls") or 0 for r in rows), + } + return {"model": None, "provider": None, "session_cost": None, + "session_tokens": 0, "session_calls": 0} def make(cfg: dict) -> Module: diff --git a/static/deck.css b/static/deck.css index 79bf374..6f9e49a 100644 --- a/static/deck.css +++ b/static/deck.css @@ -266,7 +266,20 @@ select { border: 1px solid var(--udt-surface1); } .llm-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } -.llm-head .ico { width: 18px; height: 18px; color: var(--udt-accent); flex: none; } +.brand { + width: 18px; + height: 18px; + flex: none; + background: var(--udt-accent); + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + -webkit-mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; +} +.brand.claude { -webkit-mask-image: url(/logos/claude.svg); mask-image: url(/logos/claude.svg); } +.brand.opencode { -webkit-mask-image: url(/logos/opencode.svg); mask-image: url(/logos/opencode.svg); } .llm-head h3 { font-size: 12px; text-transform: uppercase; @@ -291,6 +304,7 @@ select { .gbar { height: 6px; background: var(--udt-surface2); border-radius: 3px; overflow: hidden; } .gbar > i { display: block; height: 100%; background: var(--udt-accent); } .lbig { display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border-radius: 12px; background: var(--udt-surface1); } +.lpair { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .good { color: var(--udt-green); } .bad { color: var(--udt-red); } diff --git a/static/logos/claude.svg b/static/logos/claude.svg new file mode 100644 index 0000000..29bb61b --- /dev/null +++ b/static/logos/claude.svg @@ -0,0 +1 @@ +Claude \ No newline at end of file diff --git a/static/logos/opencode.svg b/static/logos/opencode.svg new file mode 100644 index 0000000..b090155 --- /dev/null +++ b/static/logos/opencode.svg @@ -0,0 +1 @@ +OpenCode \ No newline at end of file diff --git a/static/modules/llm.js b/static/modules/llm.js index 9d05f29..1465a30 100644 --- a/static/modules/llm.js +++ b/static/modules/llm.js @@ -8,8 +8,8 @@ const tokens = (n) => { return String(n); }; -const CLAUDE_ICON = ''; -const OPENCODE_ICON = ''; +const CLAUDE_ICON = ''; +const OPENCODE_ICON = ''; function section(icon, title, model) { const sec = document.createElement("section"); @@ -87,7 +87,13 @@ export default { box.append(claude); const opencode = section(OPENCODE_ICON, "opencode", o.model); - opencode.append(stat("spend", money(o.cost), `${tokens(o.tokens)} · ${o.calls ?? 0} calls`)); + const pair = document.createElement("div"); + pair.className = "lpair"; + pair.append( + stat("session", money(o.session_cost), `${tokens(o.session_tokens)} · ${o.session_calls ?? 0} calls`), + stat("total", money(o.cost), `${tokens(o.tokens)} · ${o.calls ?? 0} calls`), + ); + opencode.append(pair); box.append(opencode); }, }; diff --git a/tests/test_llm.py b/tests/test_llm.py index ea28b08..d64aef1 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -30,8 +30,10 @@ class Up(unittest.TestCase): ], "/api/last_session": {"sessions": [ {"program": "opencode", "rows": [ - {"provider": "together", "model": "qwen", "calls": 1}, - {"provider": "deepseek", "model": "v4", "calls": 3}, + {"provider": "together", "model": "qwen", "calls": 1, + "cost_usd": 0.2, "tokens": 200}, + {"provider": "deepseek", "model": "v4", "calls": 3, + "cost_usd": 0.3, "tokens": 300}, ]}, ]}} st = llm.make({"llm": {"url": "http://x"}, @@ -43,6 +45,9 @@ class Up(unittest.TestCase): self.assertEqual(st["opencode"]["tokens"], 1500) self.assertEqual(st["opencode"]["calls"], 4) self.assertEqual(st["opencode"]["model"], "v4") + self.assertEqual(st["opencode"]["session_cost"], 0.5) + self.assertEqual(st["opencode"]["session_calls"], 4) + self.assertEqual(st["opencode"]["session_tokens"], 500) def test_handles_the_real_latest_shape(self): data = {"/api/claude_limits": {"latest": {"context_pct": 42, "five_hour_pct": 10}}, -- cgit v1.2.3