token-usage-dashboard 1.0.6
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/LICENSE +21 -0
- package/README.md +151 -0
- package/bin/ai-usage-dashboard.js +92 -0
- package/build/client/_app/immutable/assets/0.fjR0gTLF.css +1 -0
- package/build/client/_app/immutable/assets/0.fjR0gTLF.css.br +0 -0
- package/build/client/_app/immutable/assets/0.fjR0gTLF.css.gz +0 -0
- package/build/client/_app/immutable/assets/2.BABRsmGA.css +1 -0
- package/build/client/_app/immutable/assets/2.BABRsmGA.css.br +0 -0
- package/build/client/_app/immutable/assets/2.BABRsmGA.css.gz +0 -0
- package/build/client/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/build/client/_app/immutable/chunks/Bjy-W4x2.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bjy-W4x2.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DC6tN2WI.js +3 -0
- package/build/client/_app/immutable/chunks/DC6tN2WI.js.br +0 -0
- package/build/client/_app/immutable/chunks/DC6tN2WI.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DRLSLo2t.js +2 -0
- package/build/client/_app/immutable/chunks/DRLSLo2t.js.br +0 -0
- package/build/client/_app/immutable/chunks/DRLSLo2t.js.gz +0 -0
- package/build/client/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/build/client/_app/immutable/chunks/xihTtKlq.js.br +1 -0
- package/build/client/_app/immutable/chunks/xihTtKlq.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.C7Gg5PnI.js +2 -0
- package/build/client/_app/immutable/entry/app.C7Gg5PnI.js.br +0 -0
- package/build/client/_app/immutable/entry/app.C7Gg5PnI.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.fbJ-_JRS.js +1 -0
- package/build/client/_app/immutable/entry/start.fbJ-_JRS.js.br +0 -0
- package/build/client/_app/immutable/entry/start.fbJ-_JRS.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.BNwlucmP.js +1 -0
- package/build/client/_app/immutable/nodes/0.BNwlucmP.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.BNwlucmP.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.Dj3Fzim2.js +1 -0
- package/build/client/_app/immutable/nodes/1.Dj3Fzim2.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Dj3Fzim2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.Csi9nKxw.js +2 -0
- package/build/client/_app/immutable/nodes/2.Csi9nKxw.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.Csi9nKxw.js.gz +0 -0
- package/build/client/_app/version.json +1 -0
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/client/favicon.svg +1 -0
- package/build/client/favicon.svg.br +0 -0
- package/build/client/favicon.svg.gz +0 -0
- package/build/client/robots.txt +2 -0
- package/build/client/robots.txt.br +3 -0
- package/build/client/robots.txt.gz +0 -0
- package/build/env.js +70 -0
- package/build/handler.js +22 -0
- package/build/index.js +145 -0
- package/build/server/chunks/chunks/client.js-C1_n_a2s.js +28 -0
- package/build/server/chunks/chunks/env.js-l0sNRNKZ.js +2 -0
- package/build/server/chunks/chunks/exports.js-Dd48p7Ih.js +263 -0
- package/build/server/chunks/chunks/internal.js-b-1wrnfA.js +1201 -0
- package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js +22 -0
- package/build/server/chunks/chunks/queries.js-aTxz7ubg.js +676 -0
- package/build/server/chunks/chunks/range.js-CLYFtcRE.js +117 -0
- package/build/server/chunks/chunks/refresh.js-DM1hnB2Y.js +107 -0
- package/build/server/chunks/chunks/request.js-B3j1_Icg.js +20 -0
- package/build/server/chunks/chunks/server.js-mTMwhapa.js +5374 -0
- package/build/server/chunks/chunks/shared.js-7DyfRqo3.js +255 -0
- package/build/server/chunks/chunks/utils.js-DzwqnkBT.js +1049 -0
- package/build/server/chunks/entries/endpoints/api/refresh/_server.ts.js-CJa6pKKD.js +21 -0
- package/build/server/chunks/entries/endpoints/api/settings/_server.ts.js-DQt7Bc2q.js +24 -0
- package/build/server/chunks/entries/endpoints/api/usage/_server.ts.js-kInjrhtE.js +17 -0
- package/build/server/chunks/entries/fallbacks/error.svelte.js-CVKDebWa.js +68 -0
- package/build/server/chunks/entries/hooks.server.js-BbojNy1X.js +16 -0
- package/build/server/chunks/entries/pages/_layout.svelte.js-C9kkUOBf.js +9 -0
- package/build/server/chunks/entries/pages/_page.server.ts.js-Oa3Sfml2.js +17 -0
- package/build/server/chunks/entries/pages/_page.svelte.js-D91hy9R4.js +1255 -0
- package/build/server/chunks/handler-br42Ds2q.js +1475 -0
- package/build/server/chunks/index.js-CBAFVV7O.js +3972 -0
- package/build/server/chunks/manifest.js-s70ajt69.js +63 -0
- package/build/server/chunks/nodes/0.js-Bzngkdxl.js +9 -0
- package/build/server/chunks/nodes/1.js-Cvu2_eGw.js +9 -0
- package/build/server/chunks/nodes/2.js-7xvomkGu.js +20 -0
- package/build/shims.js +33 -0
- package/package.json +64 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//#region src/lib/pricing.ts
|
|
2
|
+
/** Dollars per million. Anthropic bills cache reads at a tenth and 5m writes at 1.25x. */
|
|
3
|
+
var anthropic = (input, output) => ({
|
|
4
|
+
input,
|
|
5
|
+
output,
|
|
6
|
+
cacheRead: input * .1,
|
|
7
|
+
cacheWrite: input * 1.25
|
|
8
|
+
});
|
|
9
|
+
/** OpenAI discounts cached input and charges nothing to put it there. */
|
|
10
|
+
var openai = (input, output) => ({
|
|
11
|
+
input,
|
|
12
|
+
output,
|
|
13
|
+
cacheRead: input * .1,
|
|
14
|
+
cacheWrite: 0
|
|
15
|
+
});
|
|
16
|
+
var RATES = {
|
|
17
|
+
"claude-fable-5": anthropic(10, 50),
|
|
18
|
+
"claude-opus-5": anthropic(5, 25),
|
|
19
|
+
"claude-opus-4-8": anthropic(5, 25),
|
|
20
|
+
"claude-opus-4-7": anthropic(5, 25),
|
|
21
|
+
"claude-opus-4-6": anthropic(5, 25),
|
|
22
|
+
"claude-opus-4-5": anthropic(5, 25),
|
|
23
|
+
"claude-opus-4-1": anthropic(15, 75),
|
|
24
|
+
"claude-sonnet-5": anthropic(3, 15),
|
|
25
|
+
"claude-sonnet-4-6": anthropic(3, 15),
|
|
26
|
+
"claude-sonnet-4-5": anthropic(3, 15),
|
|
27
|
+
"claude-sonnet-4": anthropic(3, 15),
|
|
28
|
+
"claude-haiku-4-5": anthropic(1, 5),
|
|
29
|
+
"claude-3-5-haiku": anthropic(.8, 4),
|
|
30
|
+
"gpt-5.6-sol-pro": openai(2.5, 15),
|
|
31
|
+
"gpt-5.6-sol": openai(2.5, 15),
|
|
32
|
+
"gpt-5.6-terra": openai(2, 12),
|
|
33
|
+
"gpt-5.6-luna": openai(.2, 1.2),
|
|
34
|
+
"gpt-5": openai(1.25, 10),
|
|
35
|
+
"gpt-5-mini": openai(.25, 2),
|
|
36
|
+
"gpt-5-nano": openai(.05, .4),
|
|
37
|
+
"gpt-4.1": openai(2, 8),
|
|
38
|
+
"gpt-4o": openai(2.5, 10),
|
|
39
|
+
"gemini-3-pro": openai(2, 12),
|
|
40
|
+
"gemini-2.5-pro": openai(1.25, 10),
|
|
41
|
+
"gemini-2.5-flash": openai(.3, 2.5)
|
|
42
|
+
};
|
|
43
|
+
/** Longest matching family, so a dated snapshot prices like the model it is. */
|
|
44
|
+
function rateFor(model, overrides = {}) {
|
|
45
|
+
const table = {
|
|
46
|
+
...RATES,
|
|
47
|
+
...overrides
|
|
48
|
+
};
|
|
49
|
+
if (table[model]) return table[model];
|
|
50
|
+
const name = model.includes("/") ? model.slice(model.indexOf("/") + 1) : model;
|
|
51
|
+
if (table[name]) return table[name];
|
|
52
|
+
const match = Object.keys(table).filter((key) => name.startsWith(key) && /^[-@:.]/.test(name.slice(key.length))).sort((a, b) => b.length - a.length)[0];
|
|
53
|
+
return match ? table[match] : null;
|
|
54
|
+
}
|
|
55
|
+
/** Null rather than zero for a model we have no price for, so the UI can own up to it. */
|
|
56
|
+
function costOf(model, tokens, overrides = {}) {
|
|
57
|
+
const rate = rateFor(model, overrides);
|
|
58
|
+
if (!rate) return null;
|
|
59
|
+
return (tokens.input * rate.input + tokens.output * rate.output + tokens.cache_read * rate.cacheRead + tokens.cache_write * rate.cacheWrite) / 1e6;
|
|
60
|
+
}
|
|
61
|
+
var totalOf = (tokens) => tokens.input + tokens.output + tokens.cache_read + tokens.cache_write;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/lib/range.ts
|
|
64
|
+
var RANGES = [
|
|
65
|
+
{
|
|
66
|
+
id: "today",
|
|
67
|
+
label: "Today",
|
|
68
|
+
days: 1
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "7d",
|
|
72
|
+
label: "7 days",
|
|
73
|
+
days: 7
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "30d",
|
|
77
|
+
label: "30 days",
|
|
78
|
+
days: 30
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "90d",
|
|
82
|
+
label: "90 days",
|
|
83
|
+
days: 90
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "1y",
|
|
87
|
+
label: "12 months",
|
|
88
|
+
days: 365
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "all",
|
|
92
|
+
label: "All time",
|
|
93
|
+
days: 0
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
var rangeLabel = (id) => RANGES.find((range) => range.id === id)?.label ?? "30 days";
|
|
97
|
+
/** Whole local days, so the daily charts and the totals above them agree. */
|
|
98
|
+
function resolveRange(id, now = Date.now()) {
|
|
99
|
+
const range = RANGES.find((entry) => entry.id === id) ?? RANGES[2];
|
|
100
|
+
const to = Math.ceil(now / 6e4) * 6e4;
|
|
101
|
+
if (!range.days) return {
|
|
102
|
+
from: 0,
|
|
103
|
+
to,
|
|
104
|
+
range: range.id
|
|
105
|
+
};
|
|
106
|
+
const start = new Date(now);
|
|
107
|
+
start.setHours(0, 0, 0, 0);
|
|
108
|
+
start.setDate(start.getDate() - (range.days - 1));
|
|
109
|
+
return {
|
|
110
|
+
from: start.getTime(),
|
|
111
|
+
to,
|
|
112
|
+
range: range.id
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { RANGES as R, rangeLabel as a, RATES as b, costOf as c, resolveRange as r, totalOf as t };
|
|
117
|
+
//# sourceMappingURL=range.js-CLYFtcRE.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { s as settings, a as scan, r as report, d as describe, b as db, c as resolve, u as usage, e as config } from './queries.js-aTxz7ubg.js';
|
|
2
|
+
import { r as resolveRange } from './range.js-CLYFtcRE.js';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
//#region src/lib/server/windows.ts
|
|
7
|
+
var ENDPOINT = "https://api.anthropic.com/api/oauth/usage";
|
|
8
|
+
var LABELS = {
|
|
9
|
+
five_hour: ["Claude 5-hour", 300],
|
|
10
|
+
seven_day: ["Claude 7-day", 10080],
|
|
11
|
+
seven_day_opus: ["Claude 7-day (Opus)", 10080],
|
|
12
|
+
seven_day_oauth_apps: ["Claude 7-day (apps)", 10080]
|
|
13
|
+
};
|
|
14
|
+
var EVERY = 6e5;
|
|
15
|
+
var AFTER_429 = 18e5;
|
|
16
|
+
var nextAt = 0;
|
|
17
|
+
var percent = (window) => window.used_percentage ?? window.utilization;
|
|
18
|
+
function resets(window) {
|
|
19
|
+
if (!window.resets_at) return null;
|
|
20
|
+
const at = typeof window.resets_at === "number" ? /* @__PURE__ */ new Date(window.resets_at * 1e3) : new Date(window.resets_at);
|
|
21
|
+
return Number.isNaN(at.getTime()) ? null : at.getTime();
|
|
22
|
+
}
|
|
23
|
+
async function token() {
|
|
24
|
+
const path = join(config.claudeDir, ".credentials.json");
|
|
25
|
+
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
26
|
+
const oauth = raw.claudeAiOauth ?? raw.oauth ?? raw;
|
|
27
|
+
const access = oauth.accessToken ?? oauth.access_token;
|
|
28
|
+
if (!access) throw new Error(`no OAuth access token in ${path}`);
|
|
29
|
+
return access;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Subscription windows straight from the account, which is the only place they exist. Asked for
|
|
33
|
+
* far less often than the scan runs, since the endpoint rate-limits and the numbers barely move.
|
|
34
|
+
*/
|
|
35
|
+
async function refreshClaudeWindows(force = false) {
|
|
36
|
+
if (!settings().claudeWindows) return;
|
|
37
|
+
if (!force && Date.now() < nextAt) return;
|
|
38
|
+
nextAt = Date.now() + EVERY;
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(ENDPOINT, {
|
|
41
|
+
headers: {
|
|
42
|
+
authorization: `Bearer ${await token()}`,
|
|
43
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
44
|
+
accept: "application/json"
|
|
45
|
+
},
|
|
46
|
+
signal: AbortSignal.timeout(1e4)
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) throw new Error(`the usage endpoint answered ${response.status}`);
|
|
49
|
+
const body = await response.json();
|
|
50
|
+
const insert = db().prepare("insert into windows (id, tool, label, used_percent, window_minutes, resets_at, detail, seen_at) values (?, ?, ?, ?, ?, ?, ?, ?) on conflict(id) do update set used_percent = excluded.used_percent, resets_at = excluded.resets_at, seen_at = excluded.seen_at");
|
|
51
|
+
const seenAt = Date.now();
|
|
52
|
+
let found = 0;
|
|
53
|
+
for (const [key, [label, minutes]] of Object.entries(LABELS)) {
|
|
54
|
+
const window = body[key];
|
|
55
|
+
if (!window || percent(window) === void 0) continue;
|
|
56
|
+
insert.run(`claude-${key}`, "claude-code", label, percent(window), minutes, resets(window), "", seenAt);
|
|
57
|
+
found += 1;
|
|
58
|
+
}
|
|
59
|
+
if (!found) throw new Error("the usage endpoint returned no windows we recognise");
|
|
60
|
+
resolve("windows:claude");
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const limited = describe(error).includes("429");
|
|
63
|
+
if (limited) nextAt = Date.now() + AFTER_429;
|
|
64
|
+
report({
|
|
65
|
+
id: "windows:claude",
|
|
66
|
+
level: "info",
|
|
67
|
+
scope: "Claude",
|
|
68
|
+
message: limited ? "Anthropic is rate limiting the windows check" : "Could not read your 5-hour and 7-day windows",
|
|
69
|
+
hint: limited ? "Trying again in half an hour. Anything shown is the last good read." : `${describe(error)}. Sign in with Claude Code, or turn the check off in settings.`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/lib/server/refresh.ts
|
|
75
|
+
var timer = null;
|
|
76
|
+
async function refresh(force = false) {
|
|
77
|
+
await Promise.all([scan().catch((error) => report({
|
|
78
|
+
id: "scan",
|
|
79
|
+
level: "error",
|
|
80
|
+
scope: "Scan",
|
|
81
|
+
message: "The scan stopped early",
|
|
82
|
+
hint: describe(error)
|
|
83
|
+
})), refreshClaudeWindows(force)]);
|
|
84
|
+
warm();
|
|
85
|
+
}
|
|
86
|
+
/** Pay for the first rollup here, not on the next visitor's page load. */
|
|
87
|
+
function warm() {
|
|
88
|
+
const { from, to, range } = resolveRange(settings().range);
|
|
89
|
+
usage({
|
|
90
|
+
from,
|
|
91
|
+
to,
|
|
92
|
+
range,
|
|
93
|
+
tools: [],
|
|
94
|
+
models: [],
|
|
95
|
+
projects: []
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function boot() {
|
|
99
|
+
if (timer) return;
|
|
100
|
+
const every = Math.max(15, settings().scanIntervalSec) * 1e3;
|
|
101
|
+
timer = setInterval(() => void refresh(), every);
|
|
102
|
+
timer.unref?.();
|
|
103
|
+
refresh();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { boot as b, refresh as r };
|
|
107
|
+
//# sourceMappingURL=refresh.js-DM1hnB2Y.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { s as settings } from './queries.js-aTxz7ubg.js';
|
|
2
|
+
import { r as resolveRange } from './range.js-CLYFtcRE.js';
|
|
3
|
+
|
|
4
|
+
//#region src/lib/server/request.ts
|
|
5
|
+
function filtersFrom(url) {
|
|
6
|
+
const id = url.searchParams.get("range") || settings().range;
|
|
7
|
+
const { from, to, range } = resolveRange(id);
|
|
8
|
+
const list = (name) => (url.searchParams.get(name) || "").split(",").filter(Boolean).slice(0, 200);
|
|
9
|
+
return {
|
|
10
|
+
from,
|
|
11
|
+
to,
|
|
12
|
+
range,
|
|
13
|
+
tools: list("tools"),
|
|
14
|
+
models: list("models"),
|
|
15
|
+
projects: list("projects")
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { filtersFrom as f };
|
|
20
|
+
//# sourceMappingURL=request.js-B3j1_Icg.js.map
|