@krtclcdy/tersio 2.9.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ :root {
2
+ --bg: #f4f4f5; --panel: #ffffff; --ink: #18181b; --dim: #52525b;
3
+ --line: rgba(24, 24, 27, .12); --accent: #047857; --accent-soft: rgba(4, 120, 87, .12);
4
+ --track: rgba(24, 24, 27, .08); --grid: rgba(4, 120, 87, .07);
5
+ --cell-0: rgba(24, 24, 27, .07);
6
+ --cell-1: rgba(4, 120, 87, .3); --cell-2: rgba(4, 120, 87, .55);
7
+ --cell-3: rgba(4, 120, 87, .8); --cell-4: #047857;
8
+ }
9
+ html[data-theme="light"] { color-scheme: light; }
10
+ html[data-theme="dark"] { color-scheme: dark; }
11
+ @media (prefers-color-scheme: dark) {
12
+ :root:not([data-theme="light"]) {
13
+ --bg: #09090b; --panel: #101014; --ink: #f4f4f5; --dim: #a1a1aa;
14
+ --line: rgba(255, 255, 255, .09); --accent: #34d399; --accent-soft: rgba(52, 211, 153, .1);
15
+ --track: rgba(255, 255, 255, .07); --grid: rgba(52, 211, 153, .05);
16
+ --cell-0: rgba(255, 255, 255, .07);
17
+ --cell-1: rgba(52, 211, 153, .3); --cell-2: rgba(52, 211, 153, .55);
18
+ --cell-3: rgba(52, 211, 153, .8); --cell-4: #34d399;
19
+ }
20
+ }
21
+ body { background: var(--bg); color: var(--ink); }
22
+ .hero-glow { position: absolute; left: 50%; top: 44%; width: min(720px, 90vw); height: 300px;
23
+ transform: translate(-50%, -50%); pointer-events: none;
24
+ background: radial-gradient(ellipse at center, var(--accent-soft) 0%, transparent 65%);
25
+ animation: breathe 5s ease-in-out infinite; }
26
+ @keyframes breathe { 0%, 100% { opacity: .55; transform: translate(-50%, -50%) scale(1); } 50% { opacity: 1; transform: translate(-50%, -50%) scale(1.12); } }
27
+ .herototal { background: linear-gradient(180deg, var(--ink) 55%, var(--accent) 130%);
28
+ -webkit-background-clip: text; background-clip: text;
29
+ -webkit-text-fill-color: transparent; color: transparent; }
30
+ html[data-theme="dark"] {
31
+ --bg: #09090b; --panel: #101014; --ink: #f4f4f5; --dim: #a1a1aa;
32
+ --line: rgba(255, 255, 255, .09); --accent: #34d399; --accent-soft: rgba(52, 211, 153, .1);
33
+ --track: rgba(255, 255, 255, .07); --grid: rgba(52, 211, 153, .05);
34
+ --cell-0: rgba(255, 255, 255, .07);
35
+ --cell-1: rgba(52, 211, 153, .3); --cell-2: rgba(52, 211, 153, .55);
36
+ --cell-3: rgba(52, 211, 153, .8); --cell-4: #34d399;
37
+ }
38
+ .ticker { animation: tick 36s linear infinite; will-change: transform; }
39
+ .ticker:hover { animation-play-state: paused; }
40
+ @keyframes tick { from { transform: translateX(0); } to { transform: translateX(-50%); } }
41
+ .display { font-family: "Outfit", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
42
+ .bg-blueprint {
43
+ background-image: linear-gradient(var(--grid) 1px, transparent 1px),
44
+ linear-gradient(90deg, var(--grid) 1px, transparent 1px);
45
+ background-size: 44px 44px;
46
+ mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 30%, transparent 75%);
47
+ }
48
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; }
49
+ .rowline { border-bottom: 1px solid var(--line); }
50
+ .pill { display: inline-flex; align-items: center; gap: 4px; border-radius: 999px; padding: 3px 10px; font-size: 11px; white-space: nowrap; }
51
+ .pill.good { background: var(--accent-soft); color: var(--accent); }
52
+ .pill.bad { background: rgba(248, 113, 113, .12); color: #f87171; }
53
+ .pill.flat { background: var(--track); color: var(--dim); }
54
+ .fxbtn { background: var(--track); color: var(--ink); border: 0; cursor: pointer; gap: 6px; }
55
+ .fxbtn:focus-visible { outline: 1px solid var(--accent); }
56
+ .fxbtn svg { transition: transform .15s; }
57
+ .fxbtn[aria-expanded="true"] svg { transform: rotate(180deg); }
58
+ .fxpanel { position: absolute; right: 0; top: calc(100% + 6px); z-index: 50; min-width: 132px; padding: 4px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel); box-shadow: 0 8px 28px rgba(0, 0, 0, .28); }
59
+ .fxopt { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; border-radius: 7px; font-size: 11px; cursor: pointer; color: var(--ink); background: transparent; border: 0; text-align: left; }
60
+ .fxopt:hover, .fxopt.active { background: var(--accent-soft); }
61
+ .fxopt:focus-visible { outline: 1px solid var(--accent); }
62
+ .fxopt .tick { margin-left: auto; color: var(--accent); visibility: hidden; }
63
+ .fxopt[aria-selected="true"] .tick { visibility: visible; }
64
+ .bar-fill { background: var(--accent); border-radius: 999px; transform-origin: left; transition: width 1s cubic-bezier(.16, 1, .3, 1); }
65
+ .bar-track { background: var(--track); border-radius: 999px; }
66
+ .live-dot { background: var(--accent); box-shadow: 0 0 0 0 var(--accent-soft); animation: ping 2s cubic-bezier(0, 0, .2, 1) infinite; }
67
+ .thsort { all: unset; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; text-transform: inherit; letter-spacing: inherit; }
68
+ .thsort.num { float: right; }
69
+ .thsort:hover { color: var(--ink); }
70
+ .thsort:focus-visible { outline: 1px solid var(--accent); border-radius: 4px; }
71
+ .thsort .arr { font-size: 10px; color: var(--accent); }
72
+ th[aria-sort="none"] .arr { color: var(--dim); opacity: .55; }
73
+ .pgbtn { min-width: 26px; height: 26px; padding: 0 7px; display: inline-flex; align-items: center; justify-content: center; border-radius: 7px; border: 1px solid var(--line); background: transparent; color: var(--dim); cursor: pointer; }
74
+ .pgbtn:hover:not(:disabled) { color: var(--ink); border-color: var(--accent); }
75
+ .pgbtn.on { background: var(--accent-soft); color: var(--accent); border-color: transparent; font-weight: 700; }
76
+ .pgbtn:disabled { opacity: .35; cursor: default; }
77
+ .pgbtn:focus-visible { outline: 1px solid var(--accent); }
78
+ .rail { position: fixed; left: 14px; top: 50%; transform: translateY(-50%); z-index: 40; flex-direction: column; gap: 5px; }
79
+ .rail button { all: unset; width: 14px; height: 2px; border-radius: 2px; background: var(--track); cursor: pointer; transition: background .2s; }
80
+ .rail button.lit { background: var(--accent); }
81
+ .rail button:hover { background: var(--dim); }
82
+ .rail button:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }
83
+ @keyframes ping { 75%, 100% { box-shadow: 0 0 0 9px transparent; } }
84
+ .rise { opacity: 0; transform: translateY(26px); }
85
+ .rise.in { opacity: 1; transform: none; transition: opacity .7s cubic-bezier(.16, 1, .3, 1), transform .7s cubic-bezier(.16, 1, .3, 1); }
86
+ .tilt { transition: transform .25s cubic-bezier(.16, 1, .3, 1), background .25s; }
87
+ .tilt:hover { transform: translateX(6px); background: var(--accent-soft); }
88
+ .seg button { color: var(--dim); border-radius: 8px; transition: background .2s, color .2s; cursor: pointer; }
89
+ .seg button.on { background: var(--accent-soft); color: var(--ink); }
90
+ .seg button:not(.on):hover { color: var(--ink); }
91
+ .mrow { transition: background .25s; }
92
+ .mrow:hover { background: var(--accent-soft); }
93
+ .stripcell { transition: background .25s; }
94
+ .stripcell:hover { background: var(--accent-soft); }
95
+ button, .tilt, .cell, .stripcell { cursor: pointer; }
96
+ .btn-push { transition: transform .12s, background .2s; }
97
+ .btn-push:hover { background: var(--accent-soft); }
98
+ .btn-push:active { transform: scale(.96); }
99
+ a.social { color: var(--ink); }
100
+ a.social:hover { color: var(--accent); }
101
+ a.social svg { width: 16px; height: 16px; display: block; }
102
+ a.social:hover { outline: 1px solid var(--accent); outline-offset: 2px; }
103
+ .cell { width: 100%; aspect-ratio: 1 / 1; border-radius: 3px; background: var(--cell-0); min-width: 0; }
104
+ .cell.key { width: 11px; height: 11px; aspect-ratio: auto; flex-shrink: 0; }
105
+ .cell.l1 { background: var(--cell-1); } .cell.l2 { background: var(--cell-2); }
106
+ .cell.l3 { background: var(--cell-3); } .cell.l4 { background: var(--cell-4); }
107
+ .brandmark { width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; flex-shrink: 0; color: #fff; }
108
+ .brandmark img { width: 18px; height: 18px; }
109
+ .brandmark.sm { width: 28px; height: 28px; border-radius: 8px; }
110
+ .brandmark.sm img { width: 14px; height: 14px; }
111
+ .brandmark .fb { display: none; width: 18px; height: 18px; }
112
+ .brandmark.sm .fb { width: 14px; height: 14px; }
113
+ .ghostimg { position: absolute; right: -6px; bottom: -10px; width: 88px; height: 88px;
114
+ opacity: .08; pointer-events: none; user-select: none; }
115
+ @media (prefers-color-scheme: light) {
116
+ :root:not([data-theme="dark"]) .ghostimg { opacity: .06; filter: brightness(0); }
117
+ }
118
+ html[data-theme="light"] .ghostimg { opacity: .06; filter: brightness(0); }
119
+ #tip { position: fixed; z-index: 100; pointer-events: none; min-width: 230px; max-width: 300px;
120
+ background: color-mix(in srgb, var(--panel) 94%, transparent);
121
+ -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
122
+ border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px;
123
+ box-shadow: 0 16px 48px rgba(0, 0, 0, .35); opacity: 0; transition: opacity .15s; }
124
+ #tip.show { opacity: 1; }
125
+ #tip .tt { font-size: 11px; text-transform: uppercase; letter-spacing: .14em; font-weight: 700; }
126
+ #tip .tv { font-size: 13px; margin: 2px 0 8px; }
127
+ #tip .tr { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 2.5px 0; }
128
+ #tip .sw { width: 9px; height: 9px; border-radius: 2.5px; flex-shrink: 0; }
129
+ #tip .tn { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
130
+ #tip .tvr { flex-shrink: 0; }
131
+ .modelcard { transition: background .25s; }
132
+ .modelcard:hover { background: var(--accent-soft); }
133
+
134
+ .heatmonth { font-size: 10px; }
135
+ #graphMonths span { overflow: visible; white-space: nowrap; }
136
+ .dock {
137
+ position: sticky; top: 12px; z-index: 50;
138
+ border: 1px solid var(--line); border-radius: 20px;
139
+ box-shadow: 0 1px 2px rgba(0, 0, 0, .08);
140
+ }
141
+ .dock img.brand { border-radius: 10px; }
142
+ .dock {
143
+ background:
144
+ linear-gradient(var(--panel), var(--panel)) padding-box,
145
+ linear-gradient(120deg, var(--accent-soft), var(--line) 30%, var(--line) 70%, var(--accent-soft)) border-box;
146
+ border: 1px solid transparent;
147
+ }
148
+ #reload:hover { box-shadow: 0 0 14px var(--accent-soft); border-color: var(--accent); }
149
+ .spark { display: flex; align-items: flex-end; gap: 3px; }
150
+ .spark > span { flex: 1; min-width: 3px; border-radius: 2px 2px 0 0; background: var(--accent); opacity: .45; }
151
+ .spark > span.top { opacity: 1; box-shadow: 0 0 8px var(--accent-soft); }
152
+ @media (prefers-reduced-motion: reduce) {
153
+ .rise { opacity: 1; transform: none; }
154
+ .live-dot { animation: none; }
155
+ .tilt, .btn-push, .mrow, .bar-fill { transition: none; }
156
+ .hero-glow, .ticker { animation: none; }
157
+ }
@@ -0,0 +1,213 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="dark light">
7
+ <script>(function(){try{var t=localStorage.getItem('tersio-theme');if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
8
+ <title>Tersio Dashboard</title>
9
+ <link rel="icon" type="image/png" href="brand.webp">
10
+ <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
11
+ <script src="https://unpkg.com/lucide@latest"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
14
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/outfit@5/index.css">
15
+ <link rel="stylesheet" href="styles.css">
16
+ </head>
17
+ <body class="min-h-[100dvh] antialiased">
18
+ <div class="bg-blueprint pointer-events-none fixed inset-0" aria-hidden="true"></div>
19
+
20
+ <main class="overflow-x-clip w-full max-w-full">
21
+ <div class="relative mx-auto max-w-7xl px-4 sm:px-6 pb-16">
22
+ <!-- Scroll progress rail: fixed left ticks, accent fill by scroll fraction -->
23
+ <nav id="rail" class="rail hidden md:flex" aria-label="Scroll progress"></nav>
24
+
25
+ <!-- Floating dock navbar -->
26
+ <header class="dock mt-4 flex w-full items-center gap-2 px-3 py-2">
27
+ <a href="#" class="flex shrink-0 items-center gap-2.5 pl-1 pr-2" aria-label="Tersio dashboard home">
28
+ <img src="brand.webp" alt="Tersio" width="32" height="32" class="brand size-8 rounded-full">
29
+ <span class="leading-tight text-left">
30
+ <span class="block font-bold tracking-tight text-sm">TERSIO DASHBOARD</span>
31
+ <span class="mono block text-[11px]" style="color: var(--dim)">live token number feed</span>
32
+ </span>
33
+ </a>
34
+ <span class="mono hidden shrink-0 sm:flex items-center gap-2 text-xs px-2 ml-auto" style="color: var(--dim)">
35
+ <span class="live-dot inline-block size-2 rounded-full"></span>
36
+ <span id="clock">--:--</span>
37
+ </span>
38
+ <span class="hidden sm:block w-px h-5 shrink-0" style="background: var(--line)" aria-hidden="true"></span>
39
+ <button id="theme" class="btn-push flex shrink-0 items-center p-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Toggle dark mode">
40
+ <span id="iconMoon"><i data-lucide="moon" class="size-4"></i></span>
41
+ <span id="iconSun" class="hidden"><i data-lucide="sun" class="size-4"></i></span>
42
+ </button>
43
+ <button id="reload" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Reload data">
44
+ <i data-lucide="refresh-cw" class="size-3.5"></i><span class="hidden md:inline">Reload</span>
45
+ </button>
46
+ </header>
47
+
48
+ <!-- Hero: centered total -->
49
+ <section class="mx-auto max-w-6xl text-center pt-14 pb-10 relative">
50
+ <div class="hero-glow" aria-hidden="true"></div>
51
+ <p class="hero-in mono text-[11px] uppercase tracking-[0.22em] mb-4 relative" style="color: var(--accent)">Live token feed</p>
52
+ <p class="hero-in mono font-bold tracking-tighter leading-none text-7xl md:text-8xl relative herototal" id="total">0</p>
53
+ <div class="hero-in relative mt-6 overflow-hidden" aria-hidden="true" style="mask-image: linear-gradient(90deg, transparent, black 12%, black 88%, transparent)">
54
+ <div class="ticker mono text-xs whitespace-nowrap inline-block" id="ticker" style="color: var(--dim)"></div>
55
+ </div>
56
+ </section>
57
+
58
+ <!-- Savings bento: big USD card + nested right rail -->
59
+ <section class="grid grid-cols-12 grid-flow-dense gap-3" aria-label="Savings">
60
+ <div class="rise col-span-12 lg:col-span-7 lg:row-span-2 rounded-xl p-6 flex flex-col justify-between" style="border: 1px solid var(--line); background: var(--panel)">
61
+ <div>
62
+ <div class="flex items-center justify-between gap-2">
63
+ <p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)" id="costLabel">usd cost</p>
64
+ <span class="relative">
65
+ <button id="fxBtn" class="pill mono fxbtn" aria-haspopup="listbox" aria-expanded="false"><span id="fxCur">🇺🇸 USD</span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
66
+ <div id="fxPanel" class="fxpanel hidden" role="listbox" aria-label="Display currency"></div>
67
+ </span>
68
+ </div>
69
+ <p class="mono font-bold tracking-tighter leading-none mt-2 text-6xl lg:text-7xl" id="usd">-</p>
70
+ </div>
71
+ <div class="mt-4">
72
+ <div class="spark h-16" id="usdSpark"></div>
73
+ <div class="flex gap-4 mt-3 mono text-xs" style="color: var(--dim)">
74
+ <span>avg <span id="usdPerDay" style="color: var(--ink)">-</span>/day</span>
75
+ <span>top <span id="usdTopDay" style="color: var(--ink)">-</span></span>
76
+ <span title="Cache savings per $1 spent, vs full input price">x<span id="usdLeverage" style="color: var(--accent)">-</span> cache leverage</span>
77
+ </div>
78
+ </div>
79
+ <p class="mono text-[11px] mt-3" style="color: var(--dim)" id="usdNote">per-model price table</p>
80
+ </div>
81
+ <div class="col-span-12 lg:col-span-5 lg:row-span-2 grid grid-cols-2 grid-flow-dense gap-3 h-full grid-rows-[auto_1fr]">
82
+ <div class="rise col-span-2 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
83
+ <div class="flex items-center justify-between gap-2">
84
+ <div><p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">saved by cache</p>
85
+ <p class="mono font-bold text-3xl mt-1" style="color: var(--accent)" id="saved">-</p></div>
86
+ <span class="pill flat mono" title="Modeled estimate, not measured">est.</span>
87
+ </div>
88
+ <p class="mono text-[11px] mt-2" style="color: var(--dim)">est. vs full input price</p>
89
+ </div>
90
+ <div class="rise col-span-1 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
91
+ <div class="flex items-center justify-between gap-2">
92
+ <div><p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">co2</p>
93
+ <p class="mono font-bold text-3xl mt-1" id="co2">-</p>
94
+ <p class="mt-2"><span class="pill flat mono" title="Single-stream ceiling divided by 32-way served concurrency">served ÷32</span></p></div>
95
+ </div>
96
+ <p class="mono text-[11px] mt-2" style="color: var(--dim)">est. from output</p>
97
+ </div>
98
+ <div class="rise col-span-1 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
99
+ <div class="flex items-center justify-between gap-2">
100
+ <div><p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">cache share</p>
101
+ <p class="mono font-bold text-3xl mt-1" id="cacheShare">-</p>
102
+ <p class="mt-2"><span class="pill good mono" id="cacheDelta">-</span></p></div>
103
+ </div>
104
+ <p class="mono text-[11px] mt-2" style="color: var(--dim)">of tokens cached</p>
105
+ </div>
106
+ </div>
107
+ </section>
108
+
109
+ <!-- Token strip: nonzero buckets only -->
110
+ <section class="rise mt-3 flex rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" id="tokens" aria-label="Tokens by bucket"></section>
111
+
112
+ <!-- Activity: full-width year heatmap -->
113
+ <section id="activity" class="rise mt-8 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Activity graph">
114
+ <div class="flex items-center gap-2 mb-1">
115
+ <i data-lucide="calendar-days" class="size-4" style="color: var(--accent)"></i>
116
+ <h2 class="display font-bold tracking-tight text-xl">Activity</h2>
117
+ <div class="seg mono text-xs flex items-center gap-1 ml-auto p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="tablist" aria-label="Graph range">
118
+ <button data-gmode="daily" class="on px-3 py-1.5" role="tab">Daily</button>
119
+ <button data-gmode="weekly" class="px-3 py-1.5" role="tab">Weekly</button>
120
+ <button data-gmode="cumulative" class="px-3 py-1.5" role="tab">Cumulative</button>
121
+ </div>
122
+ </div>
123
+ <p class="mono text-xs mb-4" style="color: var(--dim)"><span id="graphRange"></span> · <span id="graphCap">daily values / trailing 12 months</span></p>
124
+ <div class="pb-1">
125
+ <div class="grid w-full" style="grid-template-columns: repeat(53, minmax(0, 1fr)); gap: 3px;" id="graph"></div>
126
+ </div>
127
+ <div class="mono text-[11px] mt-3 flex items-center gap-1.5" style="color: var(--dim)">
128
+ <span>less</span>
129
+ <span class="cell key"></span><span class="cell key l1"></span><span class="cell key l2"></span><span class="cell key l3"></span><span class="cell key l4"></span>
130
+ <span>more</span>
131
+ </div>
132
+ <div class="heatmonth mono mt-2 grid w-full" style="color: var(--dim); grid-template-columns: repeat(53, minmax(0, 1fr)); gap: 3px;" id="graphMonths"></div>
133
+ </section>
134
+
135
+ <!-- Top models: 3 columns -->
136
+ <section class="mt-8" aria-label="Top models">
137
+ <div class="flex items-center gap-2 mb-1">
138
+ <i data-lucide="cpu" class="size-4" style="color: var(--accent)"></i>
139
+ <h2 class="display font-bold tracking-tight text-xl">Top models</h2>
140
+ <span class="mono text-xs ml-auto" style="color: var(--dim)">usage across sessions</span>
141
+ </div>
142
+ <p class="mono text-xs mb-4" style="color: var(--dim)">week-over-week vs prior 7 days</p>
143
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-3" id="modelCards"></div>
144
+ </section>
145
+
146
+
147
+ <!-- Models list: full width -->
148
+ <section class="rise mt-8" aria-label="All models">
149
+ <div class="flex items-center gap-2 mb-1">
150
+ <i data-lucide="layers" class="size-4" style="color: var(--accent)"></i>
151
+ <h2 class="display font-bold tracking-tight text-xl">Models</h2>
152
+ </div>
153
+ <p class="mono text-xs mb-4" style="color: var(--dim)">ranked by tokens / top 8</p>
154
+ <ol id="models" class="rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)"></ol>
155
+ </section>
156
+
157
+ <!-- Command tools: session tool calls + measured RTK savings, one table -->
158
+ <section id="tools" class="rise mt-8 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Command tools">
159
+ <div class="flex items-center gap-2 mb-1">
160
+ <i data-lucide="terminal" class="size-4" style="color: var(--accent)"></i>
161
+ <h2 class="display font-bold tracking-tight text-xl">Command tools</h2>
162
+ <span class="mono text-xs ml-auto" style="color: var(--dim)" id="toolsScope">tool calls in sessions</span>
163
+ </div>
164
+ <p class="mono text-xs mb-4" style="color: var(--dim)">count · tokens saved · avg rate · avg time · share of executions</p>
165
+ <div class="overflow-x-auto">
166
+ <table class="w-full mono text-[13px]" id="cmdTable">
167
+ <thead>
168
+ <tr class="text-left text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">
169
+ <th class="font-normal text-right pr-3 py-2 w-10">#</th>
170
+ <th class="font-normal py-2 pr-3" aria-sort="none"><button class="thsort" data-sort="name">Tool / Command <span class="arr">⇅</span></button></th>
171
+ <th class="font-normal text-right py-2 pr-3" aria-sort="descending"><button class="thsort num" data-sort="count">Count <span class="arr">↓</span></button></th>
172
+ <th class="font-normal text-right py-2 pr-3" aria-sort="none"><button class="thsort num" data-sort="saved">Saved <span class="arr">⇅</span></button></th>
173
+ <th class="font-normal text-right py-2 pr-3" aria-sort="none"><button class="thsort num" data-sort="avgPct">Avg% <span class="arr">⇅</span></button></th>
174
+ <th class="font-normal text-right py-2 pr-3" aria-sort="none"><button class="thsort num" data-sort="avgMs">Time <span class="arr">⇅</span></button></th>
175
+ <th class="font-normal py-2 min-w-32">Impact</th>
176
+ </tr>
177
+ </thead>
178
+ <tbody id="cmdBody"></tbody>
179
+ </table>
180
+ </div>
181
+ <p id="emptyCmd" class="mono text-sm py-6 text-center hidden" style="color: var(--dim)">No tool calls or RTK measurements yet.</p>
182
+ <div id="cmdPager" class="mono text-xs mt-4 flex flex-wrap items-center gap-x-4 gap-y-3" style="color: var(--dim)">
183
+ <span id="cmdRange">Showing 0 of 0</span>
184
+ <span class="seg flex items-center gap-1 p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="group" aria-label="Rows per page">
185
+ <button data-pp="10" class="px-2.5 py-1" role="button">10</button>
186
+ <button data-pp="15" class="on px-2.5 py-1" role="button">15</button>
187
+ <button data-pp="25" class="px-2.5 py-1" role="button">25</button>
188
+ <button data-pp="50" class="px-2.5 py-1" role="button">50</button>
189
+ </span>
190
+ <span class="ml-auto flex items-center gap-1" id="cmdPages" role="navigation" aria-label="Pages"></span>
191
+ </div>
192
+ <p class="mono text-[11px] mt-3" style="color: var(--dim)">saved / avg / time are RTK-metered per command — session tools without metering show –. caveman + ponytail gains are instruction-following, bench-measured in BENCHMARK.md.</p>
193
+ </section>
194
+
195
+ <footer class="mono text-xs mt-12 pt-4" style="color: var(--dim); border-top: 1px solid var(--line)">
196
+
197
+ <footer class="mono text-xs mt-12 pt-4" style="color: var(--dim); border-top: 1px solid var(--line)">
198
+ <div class="flex flex-wrap items-center gap-x-4 gap-y-3">
199
+ <span>© 2026 Tersio. KurutoDenzeru. All rights reserved.</span>
200
+ <span class="pill flat" id="verChip">tersio</span>
201
+ <span class="ml-auto flex items-center gap-1" style="color: var(--ink)">
202
+ <a href="https://github.com/KurutoDenzeru/tersio" target="_blank" rel="noopener" aria-label="GitHub" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a>
203
+ <a href="https://linkedin.com/in/kurtcalacday/" target="_blank" rel="noopener" aria-label="LinkedIn" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg></a>
204
+ <a href="https://instagram.com/krtclcdy/" target="_blank" rel="noopener" aria-label="Instagram" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z"/></svg></a>
205
+ </span>
206
+ </div>
207
+ </footer>
208
+ </div>
209
+ </main>
210
+
211
+ <script src="app.js" defer></script>
212
+ </body>
213
+ </html>
@@ -0,0 +1,88 @@
1
+ // extensions/shared/carbon.ts — CO2/energy footprints, EcoLogits-style.
2
+ //
3
+ // Port of the EcoLogits v0.8.2 LLM impact model (genai-impact/ecologits,
4
+ // MPL-2.0 — see ECO_NOTICE.md), following the same served-estimate approach
5
+ // as honey-for-devs' hooks/eco.js: the faithful single-stream (batch-1)
6
+ // ceiling divided by serving_concurrency for continuous-batching amortization.
7
+ //
8
+ // Model params come from EcoLogits' registry where it covers the model;
9
+ // otherwise a documented class borrow (never a silent guess — every borrow
10
+ // carries its source). Unknown ids fall to the gpt-4o default. Everything
11
+ // rendered from here must carry ~est. and never merge with measured figures.
12
+ // EcoLogits 0.8.2 constants — kWh, kgCO2eq, seconds, GB.
13
+ const Q_BITS = 4;
14
+ const E_ALPHA = 8.91e-8;
15
+ const E_BETA = 1.43e-6;
16
+ const L_ALPHA = 8.02e-4;
17
+ const L_BETA = 2.23e-2;
18
+ const GPU_MEM = 80;
19
+ const GPU_EMB_GWP = 143;
20
+ const SRV_GPUS = 8;
21
+ const SRV_POWER = 1;
22
+ const SRV_EMB_GWP = 3000;
23
+ const LIFETIME = 5 * 365 * 24 * 3600;
24
+ const PUE = 1.2;
25
+ // Per-provider grid intensity, gCO2eq/kWh (location-based research values).
26
+ const GRIDS = {
27
+ anthropic: 500,
28
+ openai: 400,
29
+ google: 330,
30
+ default: 450,
31
+ };
32
+ // Serving amortization: single-stream ceiling ÷ concurrency ≈ served impact.
33
+ export const SERVING_CONCURRENCY = 32;
34
+ // Registry values verbatim from EcoLogits' model_repository; borrows cite
35
+ // the public param disclosure they stand in for (MoE active/total).
36
+ const TABLE = [
37
+ { match: 'opus', active: 625.0, total: 2000, provider: 'anthropic', source: 'registry' },
38
+ { match: 'sonnet', active: 137.5, total: 440, provider: 'anthropic', source: 'registry' },
39
+ { match: 'haiku', active: 18.0, total: 18.0, provider: 'anthropic', source: 'registry' },
40
+ { match: 'gpt-5', active: 93.75, total: 300, provider: 'openai', source: 'registry' },
41
+ { match: 'gpt', active: 137.5, total: 440, provider: 'openai', source: 'registry' },
42
+ { match: 'gemini', active: 137.5, total: 440, provider: 'google', source: 'registry' },
43
+ { match: 'deepseek', active: 37, total: 671, provider: 'default', source: 'borrowed' },
44
+ { match: 'qwen', active: 22, total: 235, provider: 'default', source: 'borrowed' },
45
+ { match: 'glm', active: 37, total: 671, provider: 'default', source: 'borrowed' },
46
+ { match: 'kimi', active: 32, total: 1000, provider: 'default', source: 'borrowed' },
47
+ { match: 'muse', active: 70, total: 70, provider: 'default', source: 'borrowed' },
48
+ { match: 'flash', active: 18.0, total: 18.0, provider: 'default', source: 'borrowed' },
49
+ ];
50
+ const DEFAULT_ENTRY = { active: 137.5, total: 440, provider: 'openai' };
51
+ export function carbonParamsFor(model) {
52
+ const name = model.toLowerCase();
53
+ for (const row of TABLE) {
54
+ if (row.match && name.includes(row.match)) {
55
+ return { activeB: row.active, totalB: row.total, provider: row.provider, source: row.source };
56
+ }
57
+ }
58
+ return { activeB: DEFAULT_ENTRY.active, totalB: DEFAULT_ENTRY.total, provider: DEFAULT_ENTRY.provider, source: 'default' };
59
+ }
60
+ function impacts(activeB, totalB, outTokens, gridKg) {
61
+ const gpuEnergy = outTokens * (E_ALPHA * activeB + E_BETA);
62
+ const latency = outTokens * (L_ALPHA * activeB + L_BETA);
63
+ const gpuCount = Math.ceil(((1.2 * totalB * Q_BITS) / 8 / GPU_MEM));
64
+ const serverEnergy = (latency / 3600) * SRV_POWER * (gpuCount / SRV_GPUS);
65
+ const energyKwh = PUE * (serverEnergy + gpuCount * gpuEnergy);
66
+ const embGwp = (latency / LIFETIME) * ((gpuCount / SRV_GPUS) * SRV_EMB_GWP + gpuCount * GPU_EMB_GWP);
67
+ return { energyKwh, gco2: (energyKwh * gridKg + embGwp) * 1000 };
68
+ }
69
+ export function footprintFor(model, outputTokens) {
70
+ const p = carbonParamsFor(model);
71
+ const gridKg = (GRIDS[p.provider] ?? GRIDS.default) / 1000;
72
+ const ceiling = impacts(p.activeB, p.totalB, Math.max(0, Math.floor(outputTokens)), gridKg);
73
+ const b = SERVING_CONCURRENCY > 0 ? SERVING_CONCURRENCY : 1;
74
+ return {
75
+ energyWh: (ceiling.energyKwh / b) * 1000,
76
+ gco2: ceiling.gco2 / b,
77
+ gco2Ceiling: ceiling.gco2,
78
+ concurrency: b,
79
+ provider: p.provider,
80
+ paramSource: p.source,
81
+ };
82
+ }
83
+ export function co2GramsFor(model, outputTokens) {
84
+ return footprintFor(model, outputTokens).gco2;
85
+ }
86
+ export function energyWhFor(model, outputTokens) {
87
+ return footprintFor(model, outputTokens).energyWh;
88
+ }
@@ -0,0 +1,121 @@
1
+ // extensions/shared/pricing.ts — model pricing with live LiteLLM refresh.
2
+ // Lookup order: exact id in the live cache → built-in substring table → default.
3
+ // Network only in refreshPrices (called from `tersio update`); every reader is
4
+ // sync and offline-safe. Best-effort throughout: pricing never breaks a caller.
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ // Rough list rates (USD per 1M tokens) for the models OMP sessions cite.
9
+ // Refresh against LiteLLM pricing data when adding families.
10
+ const PRICE_TABLE = [
11
+ { match: ':free', price: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
12
+ { match: 'opus', price: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 } },
13
+ { match: 'sonnet', price: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 } },
14
+ { match: 'haiku', price: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 } },
15
+ { match: 'gpt', price: { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 2.5 } },
16
+ { match: 'gemini', price: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 1.25 } },
17
+ { match: 'glm', price: { input: 0.5, output: 1, cacheRead: 0.05, cacheWrite: 0.5 } },
18
+ ];
19
+ export const DEFAULT_PRICE = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
20
+ const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
21
+ const CACHE_TTL_MS = 7 * 24 * 3600 * 1000;
22
+ const KEEP_RE = /claude|sonnet|opus|haiku|gpt|gemini|glm|deepseek|qwen|kimi|mistral|grok|llama|muse|nemotron|ministral|codestral|qwen/i;
23
+ export function pricesCachePath() {
24
+ const override = process.env.TERSIO_PRICES_FILE;
25
+ if (override)
26
+ return override;
27
+ return path.join(os.homedir(), '.omp', 'plugins', 'tersio-prices.json');
28
+ }
29
+ export function pricesUrl() {
30
+ return process.env.TERSIO_PRICES_URL || LITELLM_URL;
31
+ }
32
+ export function loadLivePrices() {
33
+ let raw;
34
+ try {
35
+ raw = fs.readFileSync(pricesCachePath(), 'utf8');
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ try {
41
+ const parsed = JSON.parse(raw);
42
+ if (typeof parsed.fetchedAt !== 'number' || !parsed.exact || typeof parsed.exact !== 'object')
43
+ return null;
44
+ if (Date.now() - parsed.fetchedAt > CACHE_TTL_MS)
45
+ return null;
46
+ return { fetchedAt: parsed.fetchedAt, exact: parsed.exact };
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ function num(v) {
53
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
54
+ }
55
+ export function priceFor(model, live) {
56
+ const table = live === undefined ? loadLivePrices() : live;
57
+ if (table) {
58
+ const hit = table.exact[model] ?? table.exact[model.toLowerCase()];
59
+ if (hit && [hit.input, hit.output, hit.cacheRead, hit.cacheWrite].every((v) => typeof v === 'number')) {
60
+ return { price: hit, known: true, live: true };
61
+ }
62
+ }
63
+ const name = model.toLowerCase();
64
+ for (const row of PRICE_TABLE) {
65
+ if (name.includes(row.match))
66
+ return { price: row.price, known: true, live: false };
67
+ }
68
+ return { price: DEFAULT_PRICE, known: false, live: false };
69
+ }
70
+ // Fetch LiteLLM pricing, keep relevant families, write the cache. False on
71
+ // any failure (offline, timeout, shape change) — callers fall back silently.
72
+ export async function refreshPrices() {
73
+ let res;
74
+ try {
75
+ const ctl = new AbortController();
76
+ const timer = setTimeout(() => ctl.abort(), 15000);
77
+ try {
78
+ res = await fetch(pricesUrl(), { signal: ctl.signal });
79
+ }
80
+ finally {
81
+ clearTimeout(timer);
82
+ }
83
+ if (!res.ok)
84
+ return false;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ let data;
90
+ try {
91
+ data = (await res.json());
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ const exact = {};
97
+ for (const [id, row] of Object.entries(data)) {
98
+ if (!row || typeof row !== 'object' || !KEEP_RE.test(id))
99
+ continue;
100
+ const input = num(row.input_cost_per_token);
101
+ const output = num(row.output_cost_per_token);
102
+ if (input === null || output === null)
103
+ continue;
104
+ exact[id] = {
105
+ input: input * 1e6,
106
+ output: output * 1e6,
107
+ cacheRead: (num(row.cache_read_input_token_cost) ?? input) * 1e6,
108
+ cacheWrite: (num(row.cache_creation_input_token_cost) ?? input) * 1e6,
109
+ };
110
+ }
111
+ if (!Object.keys(exact).length)
112
+ return false;
113
+ try {
114
+ fs.mkdirSync(path.dirname(pricesCachePath()), { recursive: true });
115
+ fs.writeFileSync(pricesCachePath(), JSON.stringify({ fetchedAt: Date.now(), exact }), 'utf8');
116
+ return true;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }