diff options
| -rw-r--r-- | NOTICE | 4 | ||||
| -rw-r--r-- | deckit/modules/llm.py | 11 | ||||
| -rw-r--r-- | static/deck.css | 16 | ||||
| -rw-r--r-- | static/logos/claude.svg | 1 | ||||
| -rw-r--r-- | static/logos/opencode.svg | 1 | ||||
| -rw-r--r-- | static/modules/llm.js | 12 | ||||
| -rw-r--r-- | tests/test_llm.py | 9 |
7 files changed, 46 insertions, 8 deletions
@@ -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 @@ +<svg fill="#D97757" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z"/></svg>
\ 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 @@ +<svg fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenCode</title><path d="M22 24H2V0h20zM17 4.8H7v14.4h10z"/></svg>
\ 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 = '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3 1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2L12 3Z"/></svg>'; -const OPENCODE_ICON = '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="m7 10 2 2-2 2"/><path d="M12.5 14H17"/></svg>'; +const CLAUDE_ICON = '<span class="brand claude"></span>'; +const OPENCODE_ICON = '<span class="brand opencode"></span>'; 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}}, |
