opencode-plugboard 0.16.0 → 0.18.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.
- package/README.md +458 -39
- package/index.ts +40 -15
- package/package.json +37 -8
- package/rpc.ts +3 -0
- package/src/anthropic/request.ts +1 -1
- package/src/anthropic/response.ts +38 -3
- package/src/anthropic/route.ts +13 -6
- package/src/anthropic/tokens.ts +42 -0
- package/src/config.ts +47 -2
- package/src/dashboard/app.css +209 -0
- package/src/dashboard/app.js +444 -0
- package/src/dashboard/assets.ts +72 -0
- package/src/dashboard/page.ts +87 -0
- package/src/discovery.ts +14 -2
- package/src/gemini/route.ts +22 -2
- package/src/openai/route.ts +42 -6
- package/src/rpc.ts +65 -0
- package/src/runtime.ts +62 -12
- package/src/session/host.ts +24 -0
- package/src/tui-format.ts +217 -0
- package/src/tui.tsx +166 -0
- package/src/usage.ts +672 -0
- package/tui.ts +4 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/* plugboard usage dashboard (#141). Mobile first; light and dark follow prefers-color-scheme. */
|
|
2
|
+
:root {
|
|
3
|
+
color-scheme: light dark;
|
|
4
|
+
--bg: #f6f7f9;
|
|
5
|
+
--fg: #1b1f24;
|
|
6
|
+
--muted: #5b6470;
|
|
7
|
+
--card: #ffffff;
|
|
8
|
+
--line: #d8dde3;
|
|
9
|
+
--accent: #2f6fdb;
|
|
10
|
+
--bad: #b42318;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@media (prefers-color-scheme: dark) {
|
|
14
|
+
:root {
|
|
15
|
+
--bg: #0e1116;
|
|
16
|
+
--fg: #e6edf3;
|
|
17
|
+
--muted: #9aa4b2;
|
|
18
|
+
--card: #161b22;
|
|
19
|
+
--line: #30363d;
|
|
20
|
+
--accent: #6ea8ff;
|
|
21
|
+
--bad: #ff7b72;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
* {
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[hidden] {
|
|
30
|
+
display: none !important;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
body {
|
|
34
|
+
margin: 0;
|
|
35
|
+
font: 16px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
36
|
+
background: var(--bg);
|
|
37
|
+
color: var(--fg);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
header.top,
|
|
41
|
+
main {
|
|
42
|
+
max-width: 64rem;
|
|
43
|
+
margin: 0 auto;
|
|
44
|
+
padding: 0 0.75rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
header.top {
|
|
48
|
+
padding-top: 1rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
main {
|
|
52
|
+
padding-bottom: 2rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
h1 {
|
|
56
|
+
font-size: 1.4rem;
|
|
57
|
+
margin: 0.25rem 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
h2 {
|
|
61
|
+
font-size: 1.05rem;
|
|
62
|
+
margin: 1.5rem 0 0.5rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.muted {
|
|
66
|
+
color: var(--muted);
|
|
67
|
+
font-size: 0.9rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.error {
|
|
71
|
+
color: var(--bad);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.card {
|
|
75
|
+
background: var(--card);
|
|
76
|
+
border: 1px solid var(--line);
|
|
77
|
+
border-radius: 0.6rem;
|
|
78
|
+
padding: 0.9rem 1rem;
|
|
79
|
+
margin-top: 0.75rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
input,
|
|
83
|
+
button {
|
|
84
|
+
font: inherit;
|
|
85
|
+
min-height: 2.5rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
input {
|
|
89
|
+
flex: 1;
|
|
90
|
+
min-width: 0;
|
|
91
|
+
padding: 0.5rem 0.6rem;
|
|
92
|
+
border: 1px solid var(--line);
|
|
93
|
+
border-radius: 0.4rem;
|
|
94
|
+
background: var(--bg);
|
|
95
|
+
color: var(--fg);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
button {
|
|
99
|
+
padding: 0.5rem 0.8rem;
|
|
100
|
+
border: 1px solid var(--line);
|
|
101
|
+
border-radius: 0.4rem;
|
|
102
|
+
background: var(--card);
|
|
103
|
+
color: var(--fg);
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
button.primary,
|
|
108
|
+
button[aria-pressed="true"] {
|
|
109
|
+
background: var(--accent);
|
|
110
|
+
border-color: var(--accent);
|
|
111
|
+
color: #fff;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
label {
|
|
115
|
+
display: block;
|
|
116
|
+
margin-bottom: 0.4rem;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
form p {
|
|
120
|
+
margin-bottom: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.row,
|
|
124
|
+
.toolbar {
|
|
125
|
+
display: flex;
|
|
126
|
+
gap: 0.5rem;
|
|
127
|
+
flex-wrap: wrap;
|
|
128
|
+
align-items: center;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.toolbar {
|
|
132
|
+
justify-content: space-between;
|
|
133
|
+
margin-top: 0.75rem;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.ranges {
|
|
137
|
+
display: flex;
|
|
138
|
+
gap: 0.25rem;
|
|
139
|
+
flex-wrap: wrap;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.cards {
|
|
143
|
+
display: grid;
|
|
144
|
+
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
|
145
|
+
gap: 0.6rem;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.stat {
|
|
149
|
+
margin-top: 0;
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: 0.15rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.stat strong {
|
|
156
|
+
font-size: 1.35rem;
|
|
157
|
+
font-variant-numeric: tabular-nums;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.label {
|
|
161
|
+
color: var(--muted);
|
|
162
|
+
font-size: 0.85rem;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.scroll {
|
|
166
|
+
overflow-x: auto;
|
|
167
|
+
-webkit-overflow-scrolling: touch;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
table {
|
|
171
|
+
width: 100%;
|
|
172
|
+
border-collapse: collapse;
|
|
173
|
+
font-variant-numeric: tabular-nums;
|
|
174
|
+
font-size: 0.92rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
th,
|
|
178
|
+
td {
|
|
179
|
+
padding: 0.4rem 0.5rem;
|
|
180
|
+
border-bottom: 1px solid var(--line);
|
|
181
|
+
text-align: left;
|
|
182
|
+
white-space: nowrap;
|
|
183
|
+
vertical-align: top;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
th.num,
|
|
187
|
+
td.num {
|
|
188
|
+
text-align: right;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
td.wrap {
|
|
192
|
+
white-space: normal;
|
|
193
|
+
word-break: break-word;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
th button {
|
|
197
|
+
padding: 0;
|
|
198
|
+
min-height: 0;
|
|
199
|
+
border: 0;
|
|
200
|
+
background: none;
|
|
201
|
+
color: inherit;
|
|
202
|
+
font-weight: 600;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.chart {
|
|
206
|
+
min-height: 220px;
|
|
207
|
+
margin-top: 0.5rem;
|
|
208
|
+
overflow: hidden;
|
|
209
|
+
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* plugboard usage dashboard (#141). Plain browser script, no build step, served as /dashboard/app.js.
|
|
3
|
+
*
|
|
4
|
+
* Data comes only from GET /v1/usage, with the key the person typed (kept in sessionStorage). Every
|
|
5
|
+
* value that came from the server (app names, model ids) reaches the page through textContent or
|
|
6
|
+
* attributes; nothing here writes markup from a string. The page sends no inline script or style (see page.ts).
|
|
7
|
+
*/
|
|
8
|
+
(function () {
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
var KEY_STORAGE = "plugboard.usage.key";
|
|
12
|
+
var COLORS = ["#2f6fdb", "#e0701b", "#2a9d5b", "#b43fa6", "#c99700", "#d6336c", "#15877f", "#6b5ce7"];
|
|
13
|
+
var CHART_HEIGHT = 220;
|
|
14
|
+
var MODEL_ROWS = 20;
|
|
15
|
+
var dark = window.matchMedia("(prefers-color-scheme: dark)");
|
|
16
|
+
|
|
17
|
+
var state = { days: 7, report: null, sort: { key: "costUsd", dir: -1 }, charts: [] };
|
|
18
|
+
|
|
19
|
+
function $(id) {
|
|
20
|
+
return document.getElementById(id);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** An element with static attributes and text children (strings become text nodes). */
|
|
24
|
+
function h(tag, attrs) {
|
|
25
|
+
var node = document.createElement(tag);
|
|
26
|
+
var children = Array.prototype.slice.call(arguments, 2);
|
|
27
|
+
Object.keys(attrs || {}).forEach(function (name) {
|
|
28
|
+
node.setAttribute(name, attrs[name]);
|
|
29
|
+
});
|
|
30
|
+
children.forEach(function (child) {
|
|
31
|
+
if (child !== null && child !== undefined) node.append(child);
|
|
32
|
+
});
|
|
33
|
+
return node;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------- formatting
|
|
37
|
+
|
|
38
|
+
var integer = new Intl.NumberFormat(undefined);
|
|
39
|
+
var compact = new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 });
|
|
40
|
+
var dollars = new Intl.NumberFormat(undefined, { style: "currency", currency: "USD", maximumFractionDigits: 2 });
|
|
41
|
+
|
|
42
|
+
function fmtCount(n) {
|
|
43
|
+
return n >= 10000 ? compact.format(n) : integer.format(n);
|
|
44
|
+
}
|
|
45
|
+
function fmtCost(usd) {
|
|
46
|
+
if (usd > 0 && usd < 0.005) return "<$0.01";
|
|
47
|
+
return dollars.format(usd);
|
|
48
|
+
}
|
|
49
|
+
function fmtMs(ms) {
|
|
50
|
+
if (ms === null || ms === undefined) return "n/a";
|
|
51
|
+
if (ms >= 1000) return (ms / 1000).toFixed(ms >= 10000 ? 0 : 1) + " s";
|
|
52
|
+
return ms + " ms";
|
|
53
|
+
}
|
|
54
|
+
function fmtTime(iso) {
|
|
55
|
+
return iso ? new Date(iso).toLocaleString() : "never";
|
|
56
|
+
}
|
|
57
|
+
function fmtRate(hits, misses) {
|
|
58
|
+
var total = hits + misses;
|
|
59
|
+
return total === 0 ? "n/a" : ((hits / total) * 100).toFixed(1) + "%";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function tokensOf(counts) {
|
|
63
|
+
var t = counts.tokens;
|
|
64
|
+
return t.input + t.cacheRead + t.cacheWrite + t.output + t.reasoning;
|
|
65
|
+
}
|
|
66
|
+
function errorsOf(counts) {
|
|
67
|
+
var e = counts.errors;
|
|
68
|
+
return e.client + e.server + e.timeout + e.disconnected;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The UTC calendar day `offset` days from today, YYYY-MM-DD. */
|
|
72
|
+
function utcDay(offset) {
|
|
73
|
+
var d = new Date();
|
|
74
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
75
|
+
d.setUTCDate(d.getUTCDate() + offset);
|
|
76
|
+
return d.toISOString().slice(0, 10);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------- view state
|
|
80
|
+
|
|
81
|
+
function setStatus(text) {
|
|
82
|
+
$("status").textContent = text;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function showSignIn(message) {
|
|
86
|
+
destroyCharts();
|
|
87
|
+
state.report = null;
|
|
88
|
+
$("panel").hidden = true;
|
|
89
|
+
$("signin").hidden = false;
|
|
90
|
+
$("auth-error").hidden = !message;
|
|
91
|
+
$("auth-error").textContent = message || "";
|
|
92
|
+
$("key").focus();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function showPanel() {
|
|
96
|
+
$("signin").hidden = true;
|
|
97
|
+
$("panel").hidden = false;
|
|
98
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-days]"), function (button) {
|
|
99
|
+
button.setAttribute("aria-pressed", String(Number(button.getAttribute("data-days")) === state.days));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------- loading
|
|
104
|
+
|
|
105
|
+
async function load() {
|
|
106
|
+
var stored = sessionStorage.getItem(KEY_STORAGE);
|
|
107
|
+
if (stored === null) return showSignIn("");
|
|
108
|
+
showPanel();
|
|
109
|
+
var from = utcDay(-(state.days - 1));
|
|
110
|
+
var to = utcDay(0);
|
|
111
|
+
setStatus("Loading " + from + " to " + to + " (UTC)...");
|
|
112
|
+
var response;
|
|
113
|
+
try {
|
|
114
|
+
response = await fetch("/v1/usage?from=" + from + "&to=" + to, {
|
|
115
|
+
headers: stored === "" ? {} : { authorization: "Bearer " + stored },
|
|
116
|
+
cache: "no-store",
|
|
117
|
+
credentials: "omit",
|
|
118
|
+
referrerPolicy: "no-referrer",
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
setStatus("Could not reach plugboard. Check the connection and refresh.");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (response.status === 401 || response.status === 403) {
|
|
125
|
+
sessionStorage.removeItem(KEY_STORAGE);
|
|
126
|
+
showSignIn(response.status === 401 ? "That key was not accepted." : "This key may not read usage.");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
setStatus("plugboard answered " + response.status + ". Refresh to try again.");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
state.report = await response.json();
|
|
134
|
+
render();
|
|
135
|
+
setStatus("Updated " + new Date().toLocaleTimeString() + ". " + from + " to " + to + " (UTC days).");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function render() {
|
|
139
|
+
if (!state.report) return;
|
|
140
|
+
renderTotals();
|
|
141
|
+
renderApps();
|
|
142
|
+
renderCharts();
|
|
143
|
+
renderModels();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------- totals
|
|
147
|
+
|
|
148
|
+
function stat(label, value, note) {
|
|
149
|
+
return h("div", { class: "card stat" }, h("span", { class: "label" }, label), h("strong", null, value), note ? h("span", { class: "muted" }, note) : null);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderTotals() {
|
|
153
|
+
var t = state.report.totals;
|
|
154
|
+
var e = t.errors;
|
|
155
|
+
var cards = [
|
|
156
|
+
stat("Requests", fmtCount(t.requests), null),
|
|
157
|
+
stat("Tokens", fmtCount(tokensOf(t)), "input " + fmtCount(t.tokens.input) + ", output " + fmtCount(t.tokens.output + t.tokens.reasoning)),
|
|
158
|
+
stat("Estimated cost", fmtCost(t.costUsd), t.unpricedRequests > 0 ? t.unpricedRequests + " call(s) had no price" : null),
|
|
159
|
+
stat("Errors", fmtCount(errorsOf(t)), "client " + e.client + ", server " + e.server + ", timeout " + e.timeout + ", disconnected " + e.disconnected),
|
|
160
|
+
stat("Cache hit rate", fmtRate(t.cacheHits, t.cacheMisses), t.cacheHits + " hits, " + t.cacheMisses + " misses"),
|
|
161
|
+
stat("Fallbacks", fmtCount(t.fallbacks), "retries on another target"),
|
|
162
|
+
stat("Rate limited (429)", fmtCount(t.limited), null),
|
|
163
|
+
stat("p95 latency", t.latency.p95Ms === null ? "n/a" : "\u2264 " + fmtMs(t.latency.p95Ms), "estimate from buckets"),
|
|
164
|
+
];
|
|
165
|
+
$("totals").replaceChildren(...cards);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------- tables
|
|
169
|
+
|
|
170
|
+
function sortable(label, key) {
|
|
171
|
+
var active = state.sort.key === key;
|
|
172
|
+
var arrow = active ? (state.sort.dir < 0 ? " \u2193" : " \u2191") : "";
|
|
173
|
+
var button = h("button", { type: "button", "data-sort": key }, label + arrow);
|
|
174
|
+
return h("th", { class: "num", "aria-sort": active ? (state.sort.dir < 0 ? "descending" : "ascending") : "none" }, button);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
var SORT_VALUE = {
|
|
178
|
+
requests: function (t) {
|
|
179
|
+
return t.requests;
|
|
180
|
+
},
|
|
181
|
+
tokens: tokensOf,
|
|
182
|
+
costUsd: function (t) {
|
|
183
|
+
return t.costUsd;
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
function renderApps() {
|
|
188
|
+
var apps = state.report.apps;
|
|
189
|
+
var names = Object.keys(apps).sort();
|
|
190
|
+
var table = $("apps");
|
|
191
|
+
table.replaceChildren();
|
|
192
|
+
if (names.length === 0) {
|
|
193
|
+
table.append(h("tr", null, h("td", null, "No usage in this range.")));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
var value = SORT_VALUE[state.sort.key] || SORT_VALUE.costUsd;
|
|
197
|
+
names.sort(function (a, b) {
|
|
198
|
+
return (value(apps[a].totals) - value(apps[b].totals)) * state.sort.dir || (a < b ? -1 : a > b ? 1 : 0);
|
|
199
|
+
});
|
|
200
|
+
table.append(
|
|
201
|
+
h(
|
|
202
|
+
"thead",
|
|
203
|
+
null,
|
|
204
|
+
h(
|
|
205
|
+
"tr",
|
|
206
|
+
null,
|
|
207
|
+
h("th", null, "App"),
|
|
208
|
+
sortable("Requests", "requests"),
|
|
209
|
+
sortable("Tokens", "tokens"),
|
|
210
|
+
sortable("Cost", "costUsd"),
|
|
211
|
+
h("th", { class: "num" }, "Errors"),
|
|
212
|
+
h("th", { class: "num" }, "p95"),
|
|
213
|
+
h("th", null, "Last request"),
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
var body = h("tbody", null);
|
|
218
|
+
names.forEach(function (name) {
|
|
219
|
+
var t = apps[name].totals;
|
|
220
|
+
body.append(
|
|
221
|
+
h(
|
|
222
|
+
"tr",
|
|
223
|
+
null,
|
|
224
|
+
h("td", null, name),
|
|
225
|
+
h("td", { class: "num" }, integer.format(t.requests)),
|
|
226
|
+
h("td", { class: "num" }, fmtCount(tokensOf(t))),
|
|
227
|
+
h("td", { class: "num" }, fmtCost(t.costUsd)),
|
|
228
|
+
h("td", { class: "num" }, integer.format(errorsOf(t))),
|
|
229
|
+
h("td", { class: "num" }, t.latency.p95Ms === null ? "n/a" : "\u2264 " + fmtMs(t.latency.p95Ms)),
|
|
230
|
+
h("td", null, fmtTime(t.lastRequestAt)),
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
table.append(body);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function renderModels() {
|
|
238
|
+
var totals = {};
|
|
239
|
+
Object.keys(state.report.apps).forEach(function (name) {
|
|
240
|
+
var models = state.report.apps[name].models;
|
|
241
|
+
Object.keys(models).forEach(function (model) {
|
|
242
|
+
var sum = totals[model] || { requests: 0, tokens: 0, costUsd: 0 };
|
|
243
|
+
totals[model] = { requests: sum.requests + models[model].requests, tokens: sum.tokens + models[model].tokens, costUsd: sum.costUsd + models[model].costUsd };
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
var rows = Object.keys(totals)
|
|
247
|
+
.map(function (model) {
|
|
248
|
+
return { model: model, usage: totals[model] };
|
|
249
|
+
})
|
|
250
|
+
.sort(function (a, b) {
|
|
251
|
+
return b.usage.costUsd - a.usage.costUsd || b.usage.requests - a.usage.requests || (a.model < b.model ? -1 : 1);
|
|
252
|
+
});
|
|
253
|
+
var table = $("models");
|
|
254
|
+
table.replaceChildren();
|
|
255
|
+
$("models-note").textContent = rows.length > MODEL_ROWS ? "Top " + MODEL_ROWS + " of " + rows.length + " models, by cost." : rows.length === 0 ? "No model calls in this range." : "";
|
|
256
|
+
if (rows.length === 0) return;
|
|
257
|
+
table.append(h("thead", null, h("tr", null, h("th", null, "Model"), h("th", { class: "num" }, "Requests"), h("th", { class: "num" }, "Tokens"), h("th", { class: "num" }, "Cost"))));
|
|
258
|
+
var body = h("tbody", null);
|
|
259
|
+
rows.slice(0, MODEL_ROWS).forEach(function (row) {
|
|
260
|
+
body.append(
|
|
261
|
+
h(
|
|
262
|
+
"tr",
|
|
263
|
+
null,
|
|
264
|
+
h("td", { class: "wrap" }, row.model),
|
|
265
|
+
h("td", { class: "num" }, integer.format(row.usage.requests)),
|
|
266
|
+
h("td", { class: "num" }, fmtCount(row.usage.tokens)),
|
|
267
|
+
h("td", { class: "num" }, fmtCost(row.usage.costUsd)),
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
table.append(body);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------- charts
|
|
275
|
+
|
|
276
|
+
function destroyCharts() {
|
|
277
|
+
state.charts.forEach(function (chart) {
|
|
278
|
+
chart.destroy();
|
|
279
|
+
});
|
|
280
|
+
state.charts = [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** One value per UTC day of the range, for every app that has records. Days without records are 0. */
|
|
284
|
+
function dailySeries(metric) {
|
|
285
|
+
var report = state.report;
|
|
286
|
+
var dates = [];
|
|
287
|
+
for (var at = Date.parse(report.from + "T00:00:00Z"); at <= Date.parse(report.to + "T00:00:00Z"); at += 86400000) {
|
|
288
|
+
dates.push(new Date(at).toISOString().slice(0, 10));
|
|
289
|
+
}
|
|
290
|
+
var names = Object.keys(report.apps).sort();
|
|
291
|
+
var series = names.map(function (name) {
|
|
292
|
+
var byDate = {};
|
|
293
|
+
report.apps[name].days.forEach(function (day) {
|
|
294
|
+
byDate[day.date] = metric(day);
|
|
295
|
+
});
|
|
296
|
+
return dates.map(function (date) {
|
|
297
|
+
return byDate[date] || 0;
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
xs: dates.map(function (date) {
|
|
302
|
+
return Date.parse(date + "T00:00:00Z") / 1000;
|
|
303
|
+
}),
|
|
304
|
+
names: names,
|
|
305
|
+
series: series,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function renderChart(id, metric, fmt) {
|
|
310
|
+
var container = $(id);
|
|
311
|
+
container.replaceChildren();
|
|
312
|
+
var data = dailySeries(metric);
|
|
313
|
+
var hasData = data.series.some(function (values) {
|
|
314
|
+
return values.some(function (v) {
|
|
315
|
+
return v > 0;
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
if (!hasData) {
|
|
319
|
+
container.append(h("p", { class: "muted" }, "No usage in this range."));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!window.uPlot) {
|
|
323
|
+
container.append(h("p", { class: "muted" }, "Charts need uplot.js, which did not load."));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
var ink = dark.matches ? "#c9d1d9" : "#3d4551";
|
|
327
|
+
var grid = dark.matches ? "#30363d" : "#e3e6ea";
|
|
328
|
+
var options = {
|
|
329
|
+
width: Math.max(260, container.clientWidth),
|
|
330
|
+
height: CHART_HEIGHT,
|
|
331
|
+
legend: { show: true },
|
|
332
|
+
scales: {
|
|
333
|
+
x: { time: true },
|
|
334
|
+
// Counts and costs start at zero, whatever the smallest value is.
|
|
335
|
+
y: {
|
|
336
|
+
range: function (u, min, max) {
|
|
337
|
+
return [0, max > 0 ? max * 1.1 : 1];
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
axes: [
|
|
342
|
+
{ stroke: ink, grid: { stroke: grid }, ticks: { stroke: grid } },
|
|
343
|
+
{
|
|
344
|
+
stroke: ink,
|
|
345
|
+
grid: { stroke: grid },
|
|
346
|
+
ticks: { stroke: grid },
|
|
347
|
+
values: function (u, splits) {
|
|
348
|
+
return splits.map(function (v) {
|
|
349
|
+
return fmt(v);
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
series: [
|
|
355
|
+
{},
|
|
356
|
+
...data.names.map(function (name, i) {
|
|
357
|
+
return {
|
|
358
|
+
label: name,
|
|
359
|
+
stroke: COLORS[i % COLORS.length],
|
|
360
|
+
width: 2,
|
|
361
|
+
points: { show: true, size: 5 },
|
|
362
|
+
value: function (u, v) {
|
|
363
|
+
return v === null || v === undefined ? "" : fmt(v);
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}),
|
|
367
|
+
],
|
|
368
|
+
};
|
|
369
|
+
var plot = new window.uPlot(options, [data.xs].concat(data.series), container);
|
|
370
|
+
state.charts.push(plot);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function renderCharts() {
|
|
374
|
+
destroyCharts();
|
|
375
|
+
renderChart("chart-requests", function (day) {
|
|
376
|
+
return day.requests;
|
|
377
|
+
}, function (v) {
|
|
378
|
+
return fmtCount(v);
|
|
379
|
+
});
|
|
380
|
+
renderChart("chart-cost", function (day) {
|
|
381
|
+
return day.costUsd;
|
|
382
|
+
}, function (v) {
|
|
383
|
+
return v >= 1 ? dollars.format(v) : "$" + v.toFixed(3);
|
|
384
|
+
});
|
|
385
|
+
renderChart("chart-tokens", tokensOf, function (v) {
|
|
386
|
+
return fmtCount(v);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function resizeCharts() {
|
|
391
|
+
state.charts.forEach(function (chart) {
|
|
392
|
+
var container = chart.root.parentNode;
|
|
393
|
+
chart.setSize({ width: Math.max(260, container.clientWidth), height: CHART_HEIGHT });
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------- events
|
|
398
|
+
|
|
399
|
+
$("key-form").addEventListener("submit", function (event) {
|
|
400
|
+
event.preventDefault();
|
|
401
|
+
sessionStorage.setItem(KEY_STORAGE, $("key").value.trim());
|
|
402
|
+
$("key").value = "";
|
|
403
|
+
load();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-days]"), function (button) {
|
|
407
|
+
button.addEventListener("click", function () {
|
|
408
|
+
state.days = Number(button.getAttribute("data-days"));
|
|
409
|
+
load();
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
$("refresh").addEventListener("click", function () {
|
|
414
|
+
load();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
$("sign-out").addEventListener("click", function () {
|
|
418
|
+
sessionStorage.removeItem(KEY_STORAGE);
|
|
419
|
+
showSignIn("");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
$("apps").addEventListener("click", function (event) {
|
|
423
|
+
var button = event.target.closest("button[data-sort]");
|
|
424
|
+
if (!button || !state.report) return;
|
|
425
|
+
var key = button.getAttribute("data-sort");
|
|
426
|
+
state.sort = { key: key, dir: state.sort.key === key ? -state.sort.dir : -1 };
|
|
427
|
+
renderApps();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
dark.addEventListener("change", function () {
|
|
431
|
+
if (state.report) {
|
|
432
|
+
renderTotals();
|
|
433
|
+
renderCharts();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
var resizeTimer = null;
|
|
438
|
+
window.addEventListener("resize", function () {
|
|
439
|
+
clearTimeout(resizeTimer);
|
|
440
|
+
resizeTimer = setTimeout(resizeCharts, 150);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
load();
|
|
444
|
+
})();
|