@shi-yangyang/dsh-token-dashboard 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.en.md +166 -0
- package/README.md +165 -2
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/client/client.js +22897 -0
- package/cordis.patch.yml +7 -0
- package/icon.svg +13 -0
- package/lib/host/aggregate.d.ts +61 -0
- package/lib/host/aggregate.js +472 -0
- package/lib/host/fold.d.ts +103 -0
- package/lib/host/fold.js +375 -0
- package/lib/host/index.d.ts +24 -0
- package/lib/host/index.js +116 -0
- package/lib/host/prices.d.ts +42 -0
- package/lib/host/prices.js +118 -0
- package/lib/host/server.d.ts +30 -0
- package/lib/host/server.js +177 -0
- package/lib/host/source.d.ts +37 -0
- package/lib/host/source.js +179 -0
- package/lib/host/types.d.ts +133 -0
- package/lib/host/types.js +9 -0
- package/lib/host/verify.d.ts +14 -0
- package/lib/host/verify.js +79 -0
- package/lib/shared/builtin-prices.d.ts +27 -0
- package/lib/shared/builtin-prices.generated.d.ts +7 -0
- package/lib/shared/builtin-prices.generated.js +1433 -0
- package/lib/shared/builtin-prices.js +111 -0
- package/lib/shared/dates.d.ts +23 -0
- package/lib/shared/dates.js +79 -0
- package/lib/shared/export.d.ts +13 -0
- package/lib/shared/export.js +113 -0
- package/lib/shared/format.d.ts +23 -0
- package/lib/shared/format.js +108 -0
- package/lib/shared/model.d.ts +37 -0
- package/lib/shared/model.js +87 -0
- package/lib/shared/pricing.d.ts +70 -0
- package/lib/shared/pricing.js +192 -0
- package/lib/shared/range.d.ts +34 -0
- package/lib/shared/range.js +93 -0
- package/lib/shared/types.d.ts +168 -0
- package/lib/shared/types.js +10 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +93 -4
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in list prices, per one million tokens, in USD.
|
|
3
|
+
*
|
|
4
|
+
* DeepSeek rates are the official peak-hour prices published on 2026-10-30:
|
|
5
|
+
* off-peak is half. Other entries mirror the public OpenRouter model list.
|
|
6
|
+
* They are estimates, not a provider invoice, and the user can override any
|
|
7
|
+
* rate in the dashboard.
|
|
8
|
+
*/
|
|
9
|
+
import { GENERATED_PRICES, GENERATED_PRICE_UPDATED } from './builtin-prices.generated.js';
|
|
10
|
+
export const BUILTIN_PRICE_UPDATED = GENERATED_PRICE_UPDATED;
|
|
11
|
+
export const BUILTIN_PRICE_SOURCE = 'DeepSeek official + OpenRouter snapshot';
|
|
12
|
+
export const BUILTIN_PRICES = [
|
|
13
|
+
{
|
|
14
|
+
id: 'deepseek-flash',
|
|
15
|
+
aliases: [
|
|
16
|
+
'deepseek-v4.1-flash',
|
|
17
|
+
'deepseek-v4.1-flash-1017',
|
|
18
|
+
'deepseek-v4-flash',
|
|
19
|
+
'deepseek-v4.1-flash-vision-exp',
|
|
20
|
+
'deepseek-chat',
|
|
21
|
+
],
|
|
22
|
+
price: { currency: 'USD', input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0 },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'deepseek-v4-pro',
|
|
26
|
+
aliases: [
|
|
27
|
+
'deepseek-v4-pro-0813',
|
|
28
|
+
'deepseek-v4.1-pro',
|
|
29
|
+
'deepseek-v4',
|
|
30
|
+
'deepseek-pro',
|
|
31
|
+
'deepseek-reasoner',
|
|
32
|
+
],
|
|
33
|
+
price: { currency: 'USD', input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0 },
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
const ALIASES = {
|
|
37
|
+
'deepseek-v4.1-flash': 'deepseek-flash',
|
|
38
|
+
'deepseek-v4.1-flash-1017': 'deepseek-flash',
|
|
39
|
+
'deepseek-v4-flash': 'deepseek-flash',
|
|
40
|
+
'deepseek-v4.1-flash-vision-exp': 'deepseek-flash',
|
|
41
|
+
'deepseek-chat': 'deepseek-flash',
|
|
42
|
+
'deepseek-v4-pro-0813': 'deepseek-v4-pro',
|
|
43
|
+
'deepseek-v4.1-pro': 'deepseek-v4-pro',
|
|
44
|
+
'deepseek-v4': 'deepseek-v4-pro',
|
|
45
|
+
'deepseek-pro': 'deepseek-v4-pro',
|
|
46
|
+
'deepseek-reasoner': 'deepseek-v4-pro',
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Every useful lookup form for one model id: the raw id, the id without its
|
|
50
|
+
* vendor prefix, without a `:free`/`:batch` variant tag, without a trailing
|
|
51
|
+
* date stamp, and the canonical alias.
|
|
52
|
+
*/
|
|
53
|
+
export function modelKeyCandidates(model) {
|
|
54
|
+
const raw = model.trim().toLowerCase();
|
|
55
|
+
if (raw === '')
|
|
56
|
+
return [];
|
|
57
|
+
const values = new Set([raw]);
|
|
58
|
+
const add = (value) => {
|
|
59
|
+
if (value !== '')
|
|
60
|
+
values.add(value);
|
|
61
|
+
};
|
|
62
|
+
add(raw.replace(/^[^/]+\//, ''));
|
|
63
|
+
for (const value of [...values]) {
|
|
64
|
+
const clean = value.replace(/:(free|batch)$/, '');
|
|
65
|
+
add(clean);
|
|
66
|
+
add(clean
|
|
67
|
+
.replace(/[-_](20\d{2})[-_]?(\d{2})[-_]?(\d{2})$/, '')
|
|
68
|
+
.replace(/[-_](\d{8})$/, '')
|
|
69
|
+
.replace(/[-_](\d{4})$/, ''));
|
|
70
|
+
}
|
|
71
|
+
for (const value of [...values]) {
|
|
72
|
+
const alias = ALIASES[value];
|
|
73
|
+
if (alias !== undefined)
|
|
74
|
+
add(alias);
|
|
75
|
+
}
|
|
76
|
+
return [...values];
|
|
77
|
+
}
|
|
78
|
+
const BUILTIN_BY_KEY = new Map();
|
|
79
|
+
// Hand-maintained DeepSeek aliases and official rates win over the generated
|
|
80
|
+
// OpenRouter snapshot; generated entries then fill every other model.
|
|
81
|
+
for (const entry of BUILTIN_PRICES) {
|
|
82
|
+
for (const key of [entry.id, ...entry.aliases]) {
|
|
83
|
+
for (const candidate of modelKeyCandidates(key)) {
|
|
84
|
+
if (!BUILTIN_BY_KEY.has(candidate))
|
|
85
|
+
BUILTIN_BY_KEY.set(candidate, entry.price);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const entry of GENERATED_PRICES) {
|
|
90
|
+
for (const candidate of modelKeyCandidates(entry.id)) {
|
|
91
|
+
if (!BUILTIN_BY_KEY.has(candidate))
|
|
92
|
+
BUILTIN_BY_KEY.set(candidate, entry.price);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Resolve a built-in list price by model id, ignoring provider/date variants. */
|
|
96
|
+
export function builtinPriceFor(model) {
|
|
97
|
+
for (const key of modelKeyCandidates(model)) {
|
|
98
|
+
const price = BUILTIN_BY_KEY.get(key);
|
|
99
|
+
if (price !== undefined)
|
|
100
|
+
return { ...price };
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
/** Prefer standard entries over batch and free variants when both match. */
|
|
105
|
+
export function modelVariantRank(id) {
|
|
106
|
+
if (id.includes(':free'))
|
|
107
|
+
return 2;
|
|
108
|
+
if (id.includes(':batch'))
|
|
109
|
+
return 1;
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Calendar helpers. Bucket keys are local-calendar labels, never UTC instants. */
|
|
2
|
+
/** `YYYY-MM-DD` in the host's local time zone. */
|
|
3
|
+
export declare function dayKey(ms: number): string;
|
|
4
|
+
/** `YYYY-MM-DDTHH` in the host's local time zone. */
|
|
5
|
+
export declare function hourKey(ms: number): string;
|
|
6
|
+
/** Today's local calendar date. */
|
|
7
|
+
export declare function todayKey(now?: number): string;
|
|
8
|
+
/** The process's IANA time zone, or `UTC` when the runtime cannot report one. */
|
|
9
|
+
export declare function currentTimezone(): string;
|
|
10
|
+
/** Shift a calendar key by a whole number of days. */
|
|
11
|
+
export declare function addDays(key: string, delta: number): string;
|
|
12
|
+
/** Inclusive list of calendar keys from `from` to `to` (bounded by caller). */
|
|
13
|
+
export declare function enumerateDays(from: string, to: string, max?: number): string[];
|
|
14
|
+
/** The trailing `count` calendar keys ending at `today`, ascending. */
|
|
15
|
+
export declare function lastDays(today: string, count: number): string[];
|
|
16
|
+
/** All 24 hour keys for one calendar day, ascending. */
|
|
17
|
+
export declare function dayHours(today: string): string[];
|
|
18
|
+
/** Whole days between two calendar keys, at least 1. */
|
|
19
|
+
export declare function spanDays(from: string, to: string): number;
|
|
20
|
+
/** Validate a `YYYY-MM-DD` string and its calendar meaning. */
|
|
21
|
+
export declare function isDayKey(value: unknown): value is string;
|
|
22
|
+
/** `HH:00` label from an hour bucket key. */
|
|
23
|
+
export declare function hourLabel(key: string): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Calendar helpers. Bucket keys are local-calendar labels, never UTC instants. */
|
|
2
|
+
/** `YYYY-MM-DD` in the host's local time zone. */
|
|
3
|
+
export function dayKey(ms) {
|
|
4
|
+
const date = new Date(ms);
|
|
5
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
6
|
+
}
|
|
7
|
+
/** `YYYY-MM-DDTHH` in the host's local time zone. */
|
|
8
|
+
export function hourKey(ms) {
|
|
9
|
+
const date = new Date(ms);
|
|
10
|
+
return `${dayKey(ms)}T${pad(date.getHours())}`;
|
|
11
|
+
}
|
|
12
|
+
/** Today's local calendar date. */
|
|
13
|
+
export function todayKey(now = Date.now()) {
|
|
14
|
+
return dayKey(now);
|
|
15
|
+
}
|
|
16
|
+
/** The process's IANA time zone, or `UTC` when the runtime cannot report one. */
|
|
17
|
+
export function currentTimezone() {
|
|
18
|
+
try {
|
|
19
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return 'UTC';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Shift a calendar key by a whole number of days. */
|
|
26
|
+
export function addDays(key, delta) {
|
|
27
|
+
const date = new Date(`${key}T12:00:00Z`);
|
|
28
|
+
date.setUTCDate(date.getUTCDate() + delta);
|
|
29
|
+
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
|
|
30
|
+
}
|
|
31
|
+
/** Inclusive list of calendar keys from `from` to `to` (bounded by caller). */
|
|
32
|
+
export function enumerateDays(from, to, max = 400) {
|
|
33
|
+
const result = [];
|
|
34
|
+
let cursor = from;
|
|
35
|
+
for (let index = 0; index <= max; index += 1) {
|
|
36
|
+
result.push(cursor);
|
|
37
|
+
if (cursor === to)
|
|
38
|
+
return result;
|
|
39
|
+
cursor = addDays(cursor, 1);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
/** The trailing `count` calendar keys ending at `today`, ascending. */
|
|
44
|
+
export function lastDays(today, count) {
|
|
45
|
+
const from = addDays(today, -(Math.max(1, count) - 1));
|
|
46
|
+
return enumerateDays(from, today, Math.max(1, count));
|
|
47
|
+
}
|
|
48
|
+
/** All 24 hour keys for one calendar day, ascending. */
|
|
49
|
+
export function dayHours(today) {
|
|
50
|
+
const result = [];
|
|
51
|
+
for (let hour = 0; hour < 24; hour += 1)
|
|
52
|
+
result.push(`${today}T${pad(hour)}`);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/** Whole days between two calendar keys, at least 1. */
|
|
56
|
+
export function spanDays(from, to) {
|
|
57
|
+
const start = Date.parse(`${from}T12:00:00Z`);
|
|
58
|
+
const end = Date.parse(`${to}T12:00:00Z`);
|
|
59
|
+
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
60
|
+
return 1;
|
|
61
|
+
return Math.max(1, Math.round((end - start) / 86_400_000) + 1);
|
|
62
|
+
}
|
|
63
|
+
/** Validate a `YYYY-MM-DD` string and its calendar meaning. */
|
|
64
|
+
export function isDayKey(value) {
|
|
65
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
66
|
+
return false;
|
|
67
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
68
|
+
if (year === undefined || month === undefined || day === undefined)
|
|
69
|
+
return false;
|
|
70
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
71
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
72
|
+
}
|
|
73
|
+
/** `HH:00` label from an hour bucket key. */
|
|
74
|
+
export function hourLabel(key) {
|
|
75
|
+
return `${key.slice(11, 13)}:00`;
|
|
76
|
+
}
|
|
77
|
+
function pad(value) {
|
|
78
|
+
return value < 10 ? `0${value}` : String(value);
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export helpers. Only aggregated statistics are exported: no prompts,
|
|
3
|
+
* assistant text, tool arguments, file contents, credentials, or cwd paths.
|
|
4
|
+
*/
|
|
5
|
+
import type { ModelUsage, StatsPayload, TokenBuckets } from './types.js';
|
|
6
|
+
/** Global summary CSV: metrics plus one row per model. */
|
|
7
|
+
export declare function summaryCsv(payload: StatsPayload, models: ModelUsage[], totals: TokenBuckets, rangeLabel: string): string;
|
|
8
|
+
/** Session detail CSV, one row per session, lifetime totals. */
|
|
9
|
+
export declare function sessionsCsv(payload: StatsPayload): string;
|
|
10
|
+
/** Machine-readable Token statistics JSON with cwd fields removed. */
|
|
11
|
+
export declare function statsJson(payload: StatsPayload): string;
|
|
12
|
+
/** Escape one CSV cell, including a formula-injection guard. */
|
|
13
|
+
export declare function csvCell(value: string): string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export helpers. Only aggregated statistics are exported: no prompts,
|
|
3
|
+
* assistant text, tool arguments, file contents, credentials, or cwd paths.
|
|
4
|
+
*/
|
|
5
|
+
import { cacheHitRate, totalTokens } from './model.js';
|
|
6
|
+
/** Global summary CSV: metrics plus one row per model. */
|
|
7
|
+
export function summaryCsv(payload, models, totals, rangeLabel) {
|
|
8
|
+
const total = totalTokens(totals);
|
|
9
|
+
const rows = [
|
|
10
|
+
['Metric', 'Value'],
|
|
11
|
+
['Generated At', new Date(payload.generatedAt).toISOString()],
|
|
12
|
+
['Range', rangeLabel],
|
|
13
|
+
['Timezone', payload.timezone],
|
|
14
|
+
['Sessions', String(payload.sessions.total)],
|
|
15
|
+
['Sessions With Usage', String(payload.sessions.withUsage)],
|
|
16
|
+
['Model Requests', String(payload.requests)],
|
|
17
|
+
['Total Tokens', String(total)],
|
|
18
|
+
['Uncached Input Tokens', String(totals.uncachedInputTokens)],
|
|
19
|
+
['Cache Read Tokens', String(totals.cacheReadTokens)],
|
|
20
|
+
['Cache Write Tokens', String(totals.cacheWriteTokens)],
|
|
21
|
+
['Output Tokens', String(totals.outputTokens)],
|
|
22
|
+
['Cache Hit Rate (%)', (cacheHitRate(totals) * 100).toFixed(2)],
|
|
23
|
+
[],
|
|
24
|
+
[
|
|
25
|
+
'Model',
|
|
26
|
+
'Provider',
|
|
27
|
+
'Total Tokens',
|
|
28
|
+
'Uncached Input Tokens',
|
|
29
|
+
'Cache Read Tokens',
|
|
30
|
+
'Cache Write Tokens',
|
|
31
|
+
'Output Tokens',
|
|
32
|
+
'Requests',
|
|
33
|
+
'Share (%)',
|
|
34
|
+
],
|
|
35
|
+
];
|
|
36
|
+
for (const model of models) {
|
|
37
|
+
const modelTotal = totalTokens(model);
|
|
38
|
+
rows.push([
|
|
39
|
+
model.model === '' ? 'Unknown' : model.model,
|
|
40
|
+
model.provider,
|
|
41
|
+
String(modelTotal),
|
|
42
|
+
String(model.uncachedInputTokens),
|
|
43
|
+
String(model.cacheReadTokens),
|
|
44
|
+
String(model.cacheWriteTokens),
|
|
45
|
+
String(model.outputTokens),
|
|
46
|
+
String(model.requests),
|
|
47
|
+
total === 0 ? '0.00' : ((modelTotal / total) * 100).toFixed(2),
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
return `${rows.map(row => row.map(csvCell).join(',')).join('\r\n')}\r\n`;
|
|
51
|
+
}
|
|
52
|
+
/** Session detail CSV, one row per session, lifetime totals. */
|
|
53
|
+
export function sessionsCsv(payload) {
|
|
54
|
+
const rows = [[
|
|
55
|
+
'Session ID',
|
|
56
|
+
'Title',
|
|
57
|
+
'Project',
|
|
58
|
+
'Models',
|
|
59
|
+
'Created At',
|
|
60
|
+
'Last Activity',
|
|
61
|
+
'Total Tokens',
|
|
62
|
+
'Uncached Input Tokens',
|
|
63
|
+
'Cache Read Tokens',
|
|
64
|
+
'Cache Write Tokens',
|
|
65
|
+
'Output Tokens',
|
|
66
|
+
'Requests',
|
|
67
|
+
'Cache Hit Rate (%)',
|
|
68
|
+
'Live',
|
|
69
|
+
'Origin',
|
|
70
|
+
'Seeded',
|
|
71
|
+
]];
|
|
72
|
+
for (const session of payload.sessionRows) {
|
|
73
|
+
const models = session.models
|
|
74
|
+
.map(model => (model.model === '' ? 'Unknown' : model.model))
|
|
75
|
+
.join('; ');
|
|
76
|
+
rows.push([
|
|
77
|
+
session.id,
|
|
78
|
+
session.title ?? '',
|
|
79
|
+
session.project,
|
|
80
|
+
models,
|
|
81
|
+
new Date(session.createdAt).toISOString(),
|
|
82
|
+
new Date(session.lastActivity).toISOString(),
|
|
83
|
+
String(totalTokens(session.totals)),
|
|
84
|
+
String(session.totals.uncachedInputTokens),
|
|
85
|
+
String(session.totals.cacheReadTokens),
|
|
86
|
+
String(session.totals.cacheWriteTokens),
|
|
87
|
+
String(session.totals.outputTokens),
|
|
88
|
+
String(session.requests),
|
|
89
|
+
(session.cacheHitRate * 100).toFixed(2),
|
|
90
|
+
session.live ? 'yes' : 'no',
|
|
91
|
+
session.origin,
|
|
92
|
+
session.seeded ? 'yes' : 'no',
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
return `${rows.map(row => row.map(csvCell).join(',')).join('\r\n')}\r\n`;
|
|
96
|
+
}
|
|
97
|
+
/** Machine-readable Token statistics JSON with cwd fields removed. */
|
|
98
|
+
export function statsJson(payload) {
|
|
99
|
+
const sanitized = {
|
|
100
|
+
...payload,
|
|
101
|
+
sessionRows: payload.sessionRows.map(session => ({ ...session, cwd: null })),
|
|
102
|
+
};
|
|
103
|
+
return `${JSON.stringify(sanitized, null, 2)}\n`;
|
|
104
|
+
}
|
|
105
|
+
/** Escape one CSV cell, including a formula-injection guard. */
|
|
106
|
+
export function csvCell(value) {
|
|
107
|
+
let text = value;
|
|
108
|
+
if (/^[=+\-@\t\r]/.test(text))
|
|
109
|
+
text = `'${text}`;
|
|
110
|
+
if (/[",\r\n]/.test(text))
|
|
111
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
112
|
+
return text;
|
|
113
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Presentation helpers. All functions are pure and locale-optional. */
|
|
2
|
+
/** Exact grouped integer. */
|
|
3
|
+
export declare function formatNumber(value: number, locale?: string): string;
|
|
4
|
+
/** Compact token count for cards and axes: 12.4K, 3.1M, 1.2B. */
|
|
5
|
+
export declare function formatCompact(value: number, locale?: string): string;
|
|
6
|
+
/** Ratio in [0,1] rendered as a percentage. */
|
|
7
|
+
export declare function formatPercent(ratio: number, digits?: number): string;
|
|
8
|
+
/** Average output speed; null renders as an em dash. */
|
|
9
|
+
export declare function formatSpeed(value: number | null): string;
|
|
10
|
+
/** Average first-token latency; null renders as an em dash. */
|
|
11
|
+
export declare function formatFirstToken(value: number | null): string;
|
|
12
|
+
/** Local clock time (HH:mm:ss) for the request log. */
|
|
13
|
+
export declare function formatClock(ms: number, locale?: string): string;
|
|
14
|
+
/** Medium date + short time for timestamps. */
|
|
15
|
+
export declare function formatDateTime(ms: number, locale?: string): string;
|
|
16
|
+
/** Short calendar-day label for a `YYYY-MM-DD` bucket key. */
|
|
17
|
+
export declare function formatDayLabel(key: string, locale?: string): string;
|
|
18
|
+
/** `14:00` from a `YYYY-MM-DDTHH` bucket key. */
|
|
19
|
+
export declare function formatHourLabel(key: string): string;
|
|
20
|
+
/** Relative time (e.g. "3 hours ago") with an absolute fallback. */
|
|
21
|
+
export declare function formatRelative(ms: number, now?: number, locale?: string): string;
|
|
22
|
+
/** Width in CSS pixels for a compact size label. */
|
|
23
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Presentation helpers. All functions are pure and locale-optional. */
|
|
2
|
+
/** Exact grouped integer. */
|
|
3
|
+
export function formatNumber(value, locale) {
|
|
4
|
+
if (!Number.isFinite(value))
|
|
5
|
+
return '0';
|
|
6
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }).format(value);
|
|
7
|
+
}
|
|
8
|
+
/** Compact token count for cards and axes: 12.4K, 3.1M, 1.2B. */
|
|
9
|
+
export function formatCompact(value, locale) {
|
|
10
|
+
if (!Number.isFinite(value) || value === 0)
|
|
11
|
+
return '0';
|
|
12
|
+
if (Math.abs(value) < 1_000)
|
|
13
|
+
return formatNumber(value, locale);
|
|
14
|
+
try {
|
|
15
|
+
return new Intl.NumberFormat(locale, {
|
|
16
|
+
notation: 'compact',
|
|
17
|
+
compactDisplay: 'short',
|
|
18
|
+
maximumFractionDigits: value >= 1_000_000 ? 2 : 1,
|
|
19
|
+
}).format(value);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return formatNumber(value, locale);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Ratio in [0,1] rendered as a percentage. */
|
|
26
|
+
export function formatPercent(ratio, digits = 1) {
|
|
27
|
+
if (!Number.isFinite(ratio))
|
|
28
|
+
return '0%';
|
|
29
|
+
const value = ratio * 100;
|
|
30
|
+
const rounded = digits <= 0 ? Math.round(value) : Number(value.toFixed(digits));
|
|
31
|
+
return `${rounded}%`;
|
|
32
|
+
}
|
|
33
|
+
/** Average output speed; null renders as an em dash. */
|
|
34
|
+
export function formatSpeed(value) {
|
|
35
|
+
return value === null || !Number.isFinite(value) ? '—' : `≈${Math.round(value)} tok/s`;
|
|
36
|
+
}
|
|
37
|
+
/** Average first-token latency; null renders as an em dash. */
|
|
38
|
+
export function formatFirstToken(value) {
|
|
39
|
+
return value === null || !Number.isFinite(value) ? '—' : `≈${(value / 1_000).toFixed(1)}s`;
|
|
40
|
+
}
|
|
41
|
+
/** Local clock time (HH:mm:ss) for the request log. */
|
|
42
|
+
export function formatClock(ms, locale) {
|
|
43
|
+
const date = new Date(ms);
|
|
44
|
+
if (!Number.isFinite(date.getTime()))
|
|
45
|
+
return '—';
|
|
46
|
+
try {
|
|
47
|
+
return new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return date.toISOString().slice(11, 19);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Medium date + short time for timestamps. */
|
|
54
|
+
export function formatDateTime(ms, locale) {
|
|
55
|
+
const date = new Date(ms);
|
|
56
|
+
if (!Number.isFinite(date.getTime()))
|
|
57
|
+
return '—';
|
|
58
|
+
try {
|
|
59
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }).format(date);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return date.toISOString();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Short calendar-day label for a `YYYY-MM-DD` bucket key. */
|
|
66
|
+
export function formatDayLabel(key, locale) {
|
|
67
|
+
const date = new Date(`${key}T12:00:00`);
|
|
68
|
+
if (!Number.isFinite(date.getTime()))
|
|
69
|
+
return key;
|
|
70
|
+
try {
|
|
71
|
+
return new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric' }).format(date);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return key;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** `14:00` from a `YYYY-MM-DDTHH` bucket key. */
|
|
78
|
+
export function formatHourLabel(key) {
|
|
79
|
+
return `${key.slice(11, 13)}:00`;
|
|
80
|
+
}
|
|
81
|
+
/** Relative time (e.g. "3 hours ago") with an absolute fallback. */
|
|
82
|
+
export function formatRelative(ms, now = Date.now(), locale) {
|
|
83
|
+
const delta = ms - now;
|
|
84
|
+
if (!Number.isFinite(delta))
|
|
85
|
+
return '—';
|
|
86
|
+
try {
|
|
87
|
+
const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
|
|
88
|
+
const table = [
|
|
89
|
+
['year', 365 * 86_400_000],
|
|
90
|
+
['month', 30 * 86_400_000],
|
|
91
|
+
['day', 86_400_000],
|
|
92
|
+
['hour', 3_600_000],
|
|
93
|
+
['minute', 60_000],
|
|
94
|
+
];
|
|
95
|
+
for (const [unit, size] of table) {
|
|
96
|
+
if (Math.abs(delta) >= size)
|
|
97
|
+
return formatter.format(Math.round(delta / size), unit);
|
|
98
|
+
}
|
|
99
|
+
return formatter.format(0, 'second');
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return formatDateTime(ms, locale);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Width in CSS pixels for a compact size label. */
|
|
106
|
+
export function clamp(value, min, max) {
|
|
107
|
+
return Math.min(max, Math.max(min, value));
|
|
108
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Token bucket arithmetic shared by Host folding and Client range selection. */
|
|
2
|
+
import type { ModelUsage, TokenBuckets } from './types.js';
|
|
3
|
+
/** Key used when an event carries no reliable provider/model identity. */
|
|
4
|
+
export declare const UNKNOWN_MODEL_KEY = "unknown";
|
|
5
|
+
/** An all-zero bucket set. */
|
|
6
|
+
export declare function emptyBuckets(): TokenBuckets;
|
|
7
|
+
/** Add every field of `source` into `target` in place. */
|
|
8
|
+
export declare function addBuckets(target: TokenBuckets, source: TokenBuckets): void;
|
|
9
|
+
/** The official total: the four disjoint buckets summed, never doubled. */
|
|
10
|
+
export declare function totalTokens(buckets: TokenBuckets): number;
|
|
11
|
+
/** Prompt tokens: uncached input plus both cache directions (no output). */
|
|
12
|
+
export declare function inputTokens(buckets: TokenBuckets): number;
|
|
13
|
+
/** Cache tokens: reads plus writes. */
|
|
14
|
+
export declare function cacheTokens(buckets: TokenBuckets): number;
|
|
15
|
+
/**
|
|
16
|
+
* Cache hit rate over the prompt side.
|
|
17
|
+
*
|
|
18
|
+
* `cacheRead / (uncachedInput + cacheRead + cacheWrite)`, so every prompt
|
|
19
|
+
* token appears exactly once in the denominator. Zero prompt tokens yield 0.
|
|
20
|
+
*/
|
|
21
|
+
export declare function cacheHitRate(buckets: TokenBuckets): number;
|
|
22
|
+
/** Stable identity key for one provider/model route. */
|
|
23
|
+
export declare function modelKeyOf(provider: string, model: string): string;
|
|
24
|
+
/** Human label for a model row, with the raw id preserved. */
|
|
25
|
+
export declare function modelLabel(usage: Pick<ModelUsage, 'model' | 'provider'>): string;
|
|
26
|
+
/** Clone a bucket set. */
|
|
27
|
+
export declare function cloneBuckets(buckets: TokenBuckets): TokenBuckets;
|
|
28
|
+
/** A new empty mutable model row. */
|
|
29
|
+
export declare function emptyModel(provider: string, model: string): ModelUsage;
|
|
30
|
+
/** Add one sample into a model row in place. */
|
|
31
|
+
export declare function addModelUsage(target: ModelUsage, source: TokenBuckets, requests: number): void;
|
|
32
|
+
/** Sort model rows by total tokens descending, then name. */
|
|
33
|
+
export declare function sortModels(models: ModelUsage[]): ModelUsage[];
|
|
34
|
+
/** Compare two bucket sets field by field. */
|
|
35
|
+
export declare function bucketsEqual(left: TokenBuckets, right: TokenBuckets): boolean;
|
|
36
|
+
/** Subtract every field of `source` from `target` in place. */
|
|
37
|
+
export declare function subtractBuckets(target: TokenBuckets, source: TokenBuckets): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Token bucket arithmetic shared by Host folding and Client range selection. */
|
|
2
|
+
/** Key used when an event carries no reliable provider/model identity. */
|
|
3
|
+
export const UNKNOWN_MODEL_KEY = 'unknown';
|
|
4
|
+
/** An all-zero bucket set. */
|
|
5
|
+
export function emptyBuckets() {
|
|
6
|
+
return { uncachedInputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 };
|
|
7
|
+
}
|
|
8
|
+
/** Add every field of `source` into `target` in place. */
|
|
9
|
+
export function addBuckets(target, source) {
|
|
10
|
+
target.uncachedInputTokens += source.uncachedInputTokens;
|
|
11
|
+
target.cacheReadTokens += source.cacheReadTokens;
|
|
12
|
+
target.cacheWriteTokens += source.cacheWriteTokens;
|
|
13
|
+
target.outputTokens += source.outputTokens;
|
|
14
|
+
}
|
|
15
|
+
/** The official total: the four disjoint buckets summed, never doubled. */
|
|
16
|
+
export function totalTokens(buckets) {
|
|
17
|
+
return buckets.uncachedInputTokens + buckets.cacheReadTokens + buckets.cacheWriteTokens + buckets.outputTokens;
|
|
18
|
+
}
|
|
19
|
+
/** Prompt tokens: uncached input plus both cache directions (no output). */
|
|
20
|
+
export function inputTokens(buckets) {
|
|
21
|
+
return buckets.uncachedInputTokens + buckets.cacheReadTokens + buckets.cacheWriteTokens;
|
|
22
|
+
}
|
|
23
|
+
/** Cache tokens: reads plus writes. */
|
|
24
|
+
export function cacheTokens(buckets) {
|
|
25
|
+
return buckets.cacheReadTokens + buckets.cacheWriteTokens;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Cache hit rate over the prompt side.
|
|
29
|
+
*
|
|
30
|
+
* `cacheRead / (uncachedInput + cacheRead + cacheWrite)`, so every prompt
|
|
31
|
+
* token appears exactly once in the denominator. Zero prompt tokens yield 0.
|
|
32
|
+
*/
|
|
33
|
+
export function cacheHitRate(buckets) {
|
|
34
|
+
const prompt = inputTokens(buckets);
|
|
35
|
+
return prompt === 0 ? 0 : buckets.cacheReadTokens / prompt;
|
|
36
|
+
}
|
|
37
|
+
/** Stable identity key for one provider/model route. */
|
|
38
|
+
export function modelKeyOf(provider, model) {
|
|
39
|
+
if (model === '')
|
|
40
|
+
return UNKNOWN_MODEL_KEY;
|
|
41
|
+
return `${provider}::${model}`;
|
|
42
|
+
}
|
|
43
|
+
/** Human label for a model row, with the raw id preserved. */
|
|
44
|
+
export function modelLabel(usage) {
|
|
45
|
+
if (usage.model === '')
|
|
46
|
+
return 'Unknown';
|
|
47
|
+
return usage.model;
|
|
48
|
+
}
|
|
49
|
+
/** Clone a bucket set. */
|
|
50
|
+
export function cloneBuckets(buckets) {
|
|
51
|
+
return {
|
|
52
|
+
uncachedInputTokens: buckets.uncachedInputTokens,
|
|
53
|
+
cacheReadTokens: buckets.cacheReadTokens,
|
|
54
|
+
cacheWriteTokens: buckets.cacheWriteTokens,
|
|
55
|
+
outputTokens: buckets.outputTokens,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** A new empty mutable model row. */
|
|
59
|
+
export function emptyModel(provider, model) {
|
|
60
|
+
return { key: modelKeyOf(provider, model), provider, model, requests: 0, ...emptyBuckets() };
|
|
61
|
+
}
|
|
62
|
+
/** Add one sample into a model row in place. */
|
|
63
|
+
export function addModelUsage(target, source, requests) {
|
|
64
|
+
addBuckets(target, source);
|
|
65
|
+
target.requests += requests;
|
|
66
|
+
}
|
|
67
|
+
/** Sort model rows by total tokens descending, then name. */
|
|
68
|
+
export function sortModels(models) {
|
|
69
|
+
return models.sort((left, right) => {
|
|
70
|
+
const delta = totalTokens(right) - totalTokens(left);
|
|
71
|
+
return delta !== 0 ? delta : left.model.localeCompare(right.model);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Compare two bucket sets field by field. */
|
|
75
|
+
export function bucketsEqual(left, right) {
|
|
76
|
+
return left.uncachedInputTokens === right.uncachedInputTokens
|
|
77
|
+
&& left.cacheReadTokens === right.cacheReadTokens
|
|
78
|
+
&& left.cacheWriteTokens === right.cacheWriteTokens
|
|
79
|
+
&& left.outputTokens === right.outputTokens;
|
|
80
|
+
}
|
|
81
|
+
/** Subtract every field of `source` from `target` in place. */
|
|
82
|
+
export function subtractBuckets(target, source) {
|
|
83
|
+
target.uncachedInputTokens -= source.uncachedInputTokens;
|
|
84
|
+
target.cacheReadTokens -= source.cacheReadTokens;
|
|
85
|
+
target.cacheWriteTokens -= source.cacheWriteTokens;
|
|
86
|
+
target.outputTokens -= source.outputTokens;
|
|
87
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional cost estimation. Prices are supplied by the user and stored only in
|
|
3
|
+
* the browser's localStorage; the plugin ships no price table and never infers
|
|
4
|
+
* a rate. All amounts are labelled "Estimated Cost".
|
|
5
|
+
*/
|
|
6
|
+
import type { ModelUsage, TokenBuckets } from './types.js';
|
|
7
|
+
/** One model's user-configured rates, in `currency` per one million tokens. */
|
|
8
|
+
export interface ModelPrice {
|
|
9
|
+
currency: string;
|
|
10
|
+
input: number;
|
|
11
|
+
output: number;
|
|
12
|
+
cacheRead: number;
|
|
13
|
+
cacheWrite: number;
|
|
14
|
+
}
|
|
15
|
+
/** Pricing keyed by the same `provider::model` key the usage rows use. */
|
|
16
|
+
export type PricingTable = Record<string, ModelPrice>;
|
|
17
|
+
/** Suggested currencies; the editor accepts any short code as well. */
|
|
18
|
+
export declare const SUGGESTED_CURRENCIES: readonly ["CNY", "USD", "EUR", "JPY"];
|
|
19
|
+
/** A cost row for one model. */
|
|
20
|
+
export interface ModelCostRow {
|
|
21
|
+
key: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
model: string;
|
|
24
|
+
tokens: TokenBuckets;
|
|
25
|
+
requests: number;
|
|
26
|
+
priced: boolean;
|
|
27
|
+
currency: string | null;
|
|
28
|
+
amount: number | null;
|
|
29
|
+
missing: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Aggregate estimate across models. */
|
|
32
|
+
export interface CostEstimate {
|
|
33
|
+
rows: ModelCostRow[];
|
|
34
|
+
totals: Array<{
|
|
35
|
+
currency: string;
|
|
36
|
+
amount: number;
|
|
37
|
+
}>;
|
|
38
|
+
pricedModels: number;
|
|
39
|
+
unpricedModels: number;
|
|
40
|
+
}
|
|
41
|
+
/** A fetched online price table plus where and when it came from. */
|
|
42
|
+
export interface OnlinePricingState {
|
|
43
|
+
prices: PricingTable;
|
|
44
|
+
source: string;
|
|
45
|
+
fetchedAt: number;
|
|
46
|
+
}
|
|
47
|
+
/** Resolve user overrides over online prices over built-in list prices. */
|
|
48
|
+
export declare function defaultsForModels(models: readonly Pick<ModelUsage, 'key' | 'model'>[], online: Readonly<PricingTable>): PricingTable;
|
|
49
|
+
/** User overrides win per model; the defaults stay for untouched models. */
|
|
50
|
+
export declare function mergePricing(defaults: PricingTable, overrides: PricingTable): PricingTable;
|
|
51
|
+
/** True when two price rows carry identical currency and rates. */
|
|
52
|
+
export declare function samePrice(left: ModelPrice | undefined, right: ModelPrice | undefined): boolean;
|
|
53
|
+
/** Parse the persisted online table defensively. */
|
|
54
|
+
export declare function parseOnlinePricing(raw: string | null | undefined): OnlinePricingState | null;
|
|
55
|
+
/** Serialize the online table and its provenance. */
|
|
56
|
+
export declare function serializeOnlinePricing(state: OnlinePricingState): string;
|
|
57
|
+
/** True only when every rate and the currency are present and valid. */
|
|
58
|
+
export declare function isPriceConfigured(price: ModelPrice | undefined): price is ModelPrice;
|
|
59
|
+
/** Estimated cost for one model, or null when no usable price exists. */
|
|
60
|
+
export declare function costOf(usage: Pick<TokenBuckets, 'uncachedInputTokens' | 'cacheReadTokens' | 'cacheWriteTokens' | 'outputTokens'>, price: ModelPrice | undefined): number | null;
|
|
61
|
+
/** Build the per-model estimate and currency totals for the current range. */
|
|
62
|
+
export declare function estimateCost(models: ModelUsage[], table: PricingTable): CostEstimate;
|
|
63
|
+
/** Parse a persisted pricing table defensively; unknown or invalid rows are dropped. */
|
|
64
|
+
export declare function parsePricing(raw: string | null | undefined): PricingTable;
|
|
65
|
+
/** Serialize a table for localStorage. */
|
|
66
|
+
export declare function serializePricing(table: PricingTable): string;
|
|
67
|
+
/** Normalize one untrusted pricing value. */
|
|
68
|
+
export declare function normalizePrice(value: unknown): ModelPrice | null;
|
|
69
|
+
/** Format an amount using its currency, with more precision for tiny values. */
|
|
70
|
+
export declare function formatAmount(amount: number, currency: string): string;
|