aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--NOTICE4
-rw-r--r--deckit/modules/llm.py11
-rw-r--r--static/deck.css16
-rw-r--r--static/logos/claude.svg1
-rw-r--r--static/logos/opencode.svg1
-rw-r--r--static/modules/llm.js12
-rw-r--r--tests/test_llm.py9
7 files changed, 46 insertions, 8 deletions
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 @@
+<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}},