sandoichi 0.1.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.md +25 -0
- package/index.mjs +54 -0
- package/package.json +27 -0
- package/src/active-session.mjs +136 -0
- package/src/cache-attribution.mjs +182 -0
- package/src/context-transform.mjs +492 -0
- package/src/core.mjs +258 -0
- package/src/history-budget.mjs +36 -0
- package/src/history-dedupe.mjs +98 -0
- package/src/history-shake.mjs +66 -0
- package/src/history-structure.mjs +28 -0
- package/src/hook-cli.mjs +118 -0
- package/src/mcp-server.mjs +45 -0
- package/src/metrics-cli.mjs +34 -0
- package/src/metrics.mjs +390 -0
- package/src/provider-ledger.mjs +138 -0
- package/src/provider-usage.mjs +224 -0
- package/src/proxy-metrics.mjs +34 -0
- package/src/proxy.mjs +212 -0
- package/src/routing.mjs +89 -0
- package/src/secret-redaction.mjs +21 -0
- package/src/semantic-compactor.mjs +218 -0
- package/src/statusline.mjs +100 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { buildMetricsReport, defaultMetricsPath, formatMetricsReport, readMetrics } from './metrics.mjs';
|
|
7
|
+
|
|
8
|
+
function option(argv, name) {
|
|
9
|
+
const index = argv.indexOf(`--${name}`);
|
|
10
|
+
return index === -1 ? undefined : argv[index + 1];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function runMetricsCli({ argv = process.argv.slice(2), env = process.env, stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
14
|
+
if (argv.includes('--help')) {
|
|
15
|
+
stdout.write('Usage: node metrics-cli.mjs [--json] [--path ABSOLUTE_PATH] [--timezone IANA_ZONE] [--session SESSION_ID]\n');
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const storagePath = option(argv, 'path') || defaultMetricsPath(env);
|
|
20
|
+
const timezone = option(argv, 'timezone');
|
|
21
|
+
const state = readMetrics(storagePath, timezone ? { timezone } : {});
|
|
22
|
+
const report = buildMetricsReport(state, {
|
|
23
|
+
sessionId: option(argv, 'session'),
|
|
24
|
+
});
|
|
25
|
+
stdout.write(argv.includes('--json') ? `${JSON.stringify(report, null, 2)}\n` : formatMetricsReport(report));
|
|
26
|
+
return report;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
stderr.write(`sando metrics: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) runMetricsCli();
|
package/src/metrics.mjs
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const SCHEMA = 'sando-metrics/v1';
|
|
7
|
+
const REPORT_SCHEMA = 'sando-report/v1';
|
|
8
|
+
const VERSION = 1;
|
|
9
|
+
const LOCK_WAIT_MS = 10;
|
|
10
|
+
const LOCK_ATTEMPTS = 250;
|
|
11
|
+
const STALE_LOCK_MS = 30_000;
|
|
12
|
+
|
|
13
|
+
function sha256(value) {
|
|
14
|
+
return createHash('sha256').update(value).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function defaultTimezone() {
|
|
18
|
+
return new Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateTimezone(timezone) {
|
|
22
|
+
if (typeof timezone !== 'string' || !timezone) throw new Error('timezone is invalid');
|
|
23
|
+
try { new Intl.DateTimeFormat('en-US', { timeZone: timezone }).format(); }
|
|
24
|
+
catch { throw new Error('timezone is invalid'); }
|
|
25
|
+
return timezone;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolvePath(storagePath) {
|
|
29
|
+
const value = storagePath ?? defaultMetricsPath();
|
|
30
|
+
if (typeof value !== 'string' || !path.isAbsolute(value)) throw new Error('metrics path must be absolute');
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function optionalString(value) {
|
|
35
|
+
return typeof value === 'string' && value ? value : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function integer(value, name, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
39
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`metrics input has invalid ${name}`);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function dateValue(value, fallback) {
|
|
44
|
+
const date = value === undefined ? new Date(fallback) : new Date(value);
|
|
45
|
+
if (Number.isNaN(date.getTime())) throw new Error('timestamp is invalid');
|
|
46
|
+
return date;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function localParts(date, timezone) {
|
|
50
|
+
const parts = Object.fromEntries(new Intl.DateTimeFormat('en-US-u-nu-latn', {
|
|
51
|
+
timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'short',
|
|
52
|
+
}).formatToParts(date).filter(({ type }) => type !== 'literal').map(({ type, value }) => [type, value]));
|
|
53
|
+
return { year: Number(parts.year), month: Number(parts.month), day: Number(parts.day) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isoWeek({ year, month, day }) {
|
|
57
|
+
const current = new Date(Date.UTC(year, month - 1, day));
|
|
58
|
+
const weekday = current.getUTCDay() || 7;
|
|
59
|
+
current.setUTCDate(current.getUTCDate() + 4 - weekday);
|
|
60
|
+
const weekYear = current.getUTCFullYear();
|
|
61
|
+
const firstThursday = new Date(Date.UTC(weekYear, 0, 4));
|
|
62
|
+
const week = 1 + Math.round((current - firstThursday) / 604800000);
|
|
63
|
+
return `${weekYear}-W${String(week).padStart(2, '0')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function periodKey(date, timezone, period) {
|
|
67
|
+
const parts = localParts(date, timezone);
|
|
68
|
+
if (period === 'daily') return `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`;
|
|
69
|
+
if (period === 'monthly') return `${parts.year}-${String(parts.month).padStart(2, '0')}`;
|
|
70
|
+
return isoWeek(parts);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function emptyState(timezone) {
|
|
74
|
+
return { schema: SCHEMA, version: VERSION, timezone, records: [] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateRecord(record) {
|
|
78
|
+
if (!record || typeof record !== 'object' || typeof record.eventKey !== 'string'
|
|
79
|
+
|| !record.eventKey || typeof record.receiptDigest !== 'string' || !record.receiptDigest
|
|
80
|
+
|| typeof record.at !== 'string' || typeof record.host !== 'string' || !record.host) {
|
|
81
|
+
throw new Error('metrics state contains an invalid record');
|
|
82
|
+
}
|
|
83
|
+
if (Number.isNaN(new Date(record.at).getTime())) throw new Error('metrics state contains an invalid timestamp');
|
|
84
|
+
integer(record.estimatedInputTokens, 'estimatedInputTokens');
|
|
85
|
+
integer(record.estimatedInlineTokens, 'estimatedInlineTokens');
|
|
86
|
+
if (!Number.isSafeInteger(record.estimatedTransformSavingsTokens)) throw new Error('metrics state contains invalid savings');
|
|
87
|
+
if (record.estimatedTransformSavingsTokens !== record.estimatedInputTokens - record.estimatedInlineTokens) {
|
|
88
|
+
throw new Error('metrics state contains invalid savings');
|
|
89
|
+
}
|
|
90
|
+
if (record.providerReportedSavingsTokens !== null
|
|
91
|
+
&& !Number.isSafeInteger(record.providerReportedSavingsTokens)) throw new Error('metrics state contains invalid provider savings');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateState(value, requestedTimezone) {
|
|
95
|
+
if (!value || typeof value !== 'object' || value.schema !== SCHEMA || value.version !== VERSION
|
|
96
|
+
|| !Array.isArray(value.records)) throw new Error('metrics state is invalid');
|
|
97
|
+
validateTimezone(value.timezone);
|
|
98
|
+
if (requestedTimezone && requestedTimezone !== value.timezone) throw new Error('metrics timezone mismatch');
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const seenReceipts = new Set();
|
|
101
|
+
for (const record of value.records) {
|
|
102
|
+
validateRecord(record);
|
|
103
|
+
if (seen.has(record.eventKey)) throw new Error('metrics state contains duplicate events');
|
|
104
|
+
const receiptKey = `${record.host}\0${record.receiptDigest}`;
|
|
105
|
+
if (!record.eventKey.startsWith('event:') && seenReceipts.has(receiptKey)) {
|
|
106
|
+
throw new Error('metrics state contains duplicate receipts');
|
|
107
|
+
}
|
|
108
|
+
seen.add(record.eventKey);
|
|
109
|
+
if (!record.eventKey.startsWith('event:')) seenReceipts.add(receiptKey);
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ensureDirectory(directory) {
|
|
115
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
116
|
+
const stat = fs.lstatSync(directory);
|
|
117
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('metrics directory is unsafe');
|
|
118
|
+
fs.chmodSync(directory, 0o700);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function assertRegularFile(filePath) {
|
|
122
|
+
const stat = fs.lstatSync(filePath, { throwIfNoEntry: false });
|
|
123
|
+
if (stat && (!stat.isFile() || stat.isSymbolicLink())) throw new Error('metrics file is unsafe');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function waitForLock() {
|
|
127
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_WAIT_MS);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function withLock(lockPath, operation) {
|
|
131
|
+
let handle;
|
|
132
|
+
for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt += 1) {
|
|
133
|
+
try {
|
|
134
|
+
handle = fs.openSync(lockPath, 'wx', 0o600);
|
|
135
|
+
fs.writeSync(handle, `${process.pid}\n`);
|
|
136
|
+
break;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
139
|
+
const stat = fs.statSync(lockPath, { throwIfNoEntry: false });
|
|
140
|
+
if (stat && Date.now() - stat.mtimeMs > STALE_LOCK_MS) fs.rmSync(lockPath, { force: true });
|
|
141
|
+
else waitForLock();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (handle === undefined) throw new Error('metrics lock timeout');
|
|
145
|
+
try { return operation(); }
|
|
146
|
+
finally {
|
|
147
|
+
fs.closeSync(handle);
|
|
148
|
+
fs.rmSync(lockPath, { force: true });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function atomicWrite(filePath, value) {
|
|
153
|
+
assertRegularFile(filePath);
|
|
154
|
+
const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
155
|
+
let handle;
|
|
156
|
+
let renamed = false;
|
|
157
|
+
try {
|
|
158
|
+
handle = fs.openSync(temporary, 'wx', 0o600);
|
|
159
|
+
fs.writeFileSync(handle, `${JSON.stringify(value, null, 2)}\n`);
|
|
160
|
+
fs.fsyncSync(handle);
|
|
161
|
+
fs.closeSync(handle);
|
|
162
|
+
handle = undefined;
|
|
163
|
+
fs.chmodSync(temporary, 0o600);
|
|
164
|
+
fs.renameSync(temporary, filePath);
|
|
165
|
+
renamed = true;
|
|
166
|
+
} finally {
|
|
167
|
+
try {
|
|
168
|
+
if (handle !== undefined) fs.closeSync(handle);
|
|
169
|
+
} finally {
|
|
170
|
+
if (!renamed) fs.rmSync(temporary, { force: true });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const directoryHandle = fs.openSync(path.dirname(filePath), 'r');
|
|
175
|
+
try { fs.fsyncSync(directoryHandle); } finally { fs.closeSync(directoryHandle); }
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function defaultMetricsPath(env = process.env) {
|
|
180
|
+
const configured = env.SANDO_METRICS_PATH;
|
|
181
|
+
if (configured !== undefined) return resolvePath(configured);
|
|
182
|
+
const stateHome = env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state');
|
|
183
|
+
if (!path.isAbsolute(stateHome)) throw new Error('state directory must be absolute');
|
|
184
|
+
return path.join(stateHome, 'sando', 'metrics.json');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function readMetrics(storagePath = defaultMetricsPath(), { timezone } = {}) {
|
|
188
|
+
const filePath = resolvePath(storagePath);
|
|
189
|
+
const requestedTimezone = timezone === undefined ? undefined : validateTimezone(timezone);
|
|
190
|
+
const exists = fs.existsSync(filePath);
|
|
191
|
+
if (!exists) return emptyState(requestedTimezone || defaultTimezone());
|
|
192
|
+
assertRegularFile(filePath);
|
|
193
|
+
let value;
|
|
194
|
+
try { value = JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
195
|
+
catch { throw new Error('metrics state is invalid'); }
|
|
196
|
+
return validateState(value, requestedTimezone);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function providerSavings(providerUsage) {
|
|
200
|
+
if (providerUsage === undefined || providerUsage === null) return null;
|
|
201
|
+
if (!providerUsage || typeof providerUsage !== 'object' || Array.isArray(providerUsage)) {
|
|
202
|
+
throw new Error('metrics input has invalid provider usage');
|
|
203
|
+
}
|
|
204
|
+
const value = (names) => names.map((name) => providerUsage[name]).find((candidate) => candidate !== undefined);
|
|
205
|
+
const baseline = value(['baselineInputTokens', 'baseline_input_tokens']);
|
|
206
|
+
const optimized = value(['optimizedInputTokens', 'optimized_input_tokens']);
|
|
207
|
+
if (baseline !== undefined || optimized !== undefined) {
|
|
208
|
+
integer(baseline, 'baselineInputTokens');
|
|
209
|
+
integer(optimized, 'optimizedInputTokens');
|
|
210
|
+
return baseline - optimized;
|
|
211
|
+
}
|
|
212
|
+
const reported = value(['reportedSavingsTokens', 'reported_savings_tokens']);
|
|
213
|
+
if (reported !== undefined) return integer(reported, 'reportedSavingsTokens', { min: -Number.MAX_SAFE_INTEGER });
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function makeRecord({ host, event, receipt, optimization, now }) {
|
|
218
|
+
if (typeof host !== 'string' || !host || !event || typeof event !== 'object'
|
|
219
|
+
|| typeof receipt?.digest !== 'string' || !receipt.digest || !optimization?.stats
|
|
220
|
+
|| typeof optimization.inline !== 'string') throw new Error('metrics input is invalid');
|
|
221
|
+
const stats = optimization.stats;
|
|
222
|
+
const estimatedInputTokens = integer(stats.estimatedInputTokens, 'estimatedInputTokens');
|
|
223
|
+
const estimatedInlineTokens = integer(stats.estimatedInlineTokens, 'estimatedInlineTokens');
|
|
224
|
+
const eventId = optionalString(event.eventId);
|
|
225
|
+
const eventKey = eventId
|
|
226
|
+
? `event:${sha256(`${host}\0${eventId}`)}`
|
|
227
|
+
: `receipt:${sha256(`${host}\0${receipt.digest}`)}`;
|
|
228
|
+
const timestamp = dateValue(event.timestamp, now).toISOString();
|
|
229
|
+
return {
|
|
230
|
+
eventKey,
|
|
231
|
+
receiptDigest: receipt.digest,
|
|
232
|
+
at: timestamp,
|
|
233
|
+
host,
|
|
234
|
+
sessionId: optionalString(event.sessionId),
|
|
235
|
+
client: optionalString(event.client),
|
|
236
|
+
clientVersion: optionalString(event.clientVersion),
|
|
237
|
+
model: optionalString(event.model),
|
|
238
|
+
estimatedInputTokens,
|
|
239
|
+
estimatedInlineTokens,
|
|
240
|
+
estimatedTransformSavingsTokens: estimatedInputTokens - estimatedInlineTokens,
|
|
241
|
+
providerReportedSavingsTokens: providerSavings(event.providerUsage),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function recordMetrics({ storagePath = defaultMetricsPath(), timezone, host, event, receipt, optimization, now = new Date() } = {}) {
|
|
246
|
+
const filePath = resolvePath(storagePath);
|
|
247
|
+
const directory = path.dirname(filePath);
|
|
248
|
+
ensureDirectory(directory);
|
|
249
|
+
const lockPath = `${filePath}.lock`;
|
|
250
|
+
return withLock(lockPath, () => {
|
|
251
|
+
const state = readMetrics(filePath, { timezone });
|
|
252
|
+
const record = makeRecord({ host, event, receipt, optimization, now });
|
|
253
|
+
if (state.records.some((candidate) => candidate.eventKey === record.eventKey
|
|
254
|
+
|| (!record.eventKey.startsWith('event:')
|
|
255
|
+
&& candidate.host === record.host && candidate.receiptDigest === record.receiptDigest))) return state;
|
|
256
|
+
state.records.push(record);
|
|
257
|
+
atomicWrite(filePath, state);
|
|
258
|
+
return state;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function sessionKey(record) {
|
|
263
|
+
return `${record.host}\0${record.sessionId ?? '<unknown>'}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function addSafe(left, right) {
|
|
267
|
+
const total = left + right;
|
|
268
|
+
if (!Number.isSafeInteger(total)) throw new Error('metrics aggregate overflow');
|
|
269
|
+
return total;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function sum(records, field) {
|
|
273
|
+
return records.reduce((total, record) => addSafe(total, record[field]), 0);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function providerSum(records) {
|
|
277
|
+
const values = records.filter((record) => record.providerReportedSavingsTokens !== null);
|
|
278
|
+
return values.length ? sum(values, 'providerReportedSavingsTokens') : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function bucket(period, records) {
|
|
282
|
+
const groups = new Set(records.map(sessionKey));
|
|
283
|
+
return {
|
|
284
|
+
period,
|
|
285
|
+
eventCount: records.length,
|
|
286
|
+
sessionCount: groups.size,
|
|
287
|
+
estimatedTransformSavingsTokens: sum(records, 'estimatedTransformSavingsTokens'),
|
|
288
|
+
providerReportedSavingsTokens: providerSum(records),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function sessionSummary(records) {
|
|
293
|
+
if (!records.length) return null;
|
|
294
|
+
const first = records[0];
|
|
295
|
+
return {
|
|
296
|
+
id: first.sessionId,
|
|
297
|
+
host: first.host,
|
|
298
|
+
client: first.client,
|
|
299
|
+
clientVersion: first.clientVersion,
|
|
300
|
+
model: first.model,
|
|
301
|
+
eventCount: records.length,
|
|
302
|
+
estimatedTransformSavingsTokens: sum(records, 'estimatedTransformSavingsTokens'),
|
|
303
|
+
providerReportedSavingsTokens: providerSum(records),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function averageBySession(records) {
|
|
308
|
+
const sessions = new Map();
|
|
309
|
+
for (const record of records) {
|
|
310
|
+
const key = sessionKey(record);
|
|
311
|
+
const group = sessions.get(key) || [];
|
|
312
|
+
group.push(record);
|
|
313
|
+
sessions.set(key, group);
|
|
314
|
+
}
|
|
315
|
+
const providerSessions = [...sessions.values()].filter((group) => providerSum(group) !== null);
|
|
316
|
+
return {
|
|
317
|
+
sessionCount: sessions.size,
|
|
318
|
+
providerSessionCount: providerSessions.length,
|
|
319
|
+
estimatedTransformSavingsTokens: sessions.size ? sum(records, 'estimatedTransformSavingsTokens') / sessions.size : 0,
|
|
320
|
+
providerReportedSavingsTokens: providerSessions.length
|
|
321
|
+
? providerSessions.reduce((total, group) => addSafe(total, providerSum(group)), 0) / providerSessions.length
|
|
322
|
+
: null,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function periodReport(records, timezone, period, now) {
|
|
327
|
+
const currentPeriod = periodKey(now, timezone, period);
|
|
328
|
+
const grouped = new Map();
|
|
329
|
+
for (const record of records) {
|
|
330
|
+
const key = periodKey(new Date(record.at), timezone, period);
|
|
331
|
+
const group = grouped.get(key) || [];
|
|
332
|
+
group.push(record);
|
|
333
|
+
grouped.set(key, group);
|
|
334
|
+
}
|
|
335
|
+
const history = [...grouped.keys()].sort().map((key) => bucket(key, grouped.get(key)));
|
|
336
|
+
return { current: bucket(currentPeriod, grouped.get(currentPeriod) || []), history };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function buildMetricsReport(state, { now = new Date(), sessionId } = {}) {
|
|
340
|
+
const value = validateState(state);
|
|
341
|
+
const date = dateValue(now, now);
|
|
342
|
+
const records = [...value.records].sort((left, right) => left.at.localeCompare(right.at));
|
|
343
|
+
const groups = new Map();
|
|
344
|
+
for (const record of records) {
|
|
345
|
+
const key = sessionKey(record);
|
|
346
|
+
const group = groups.get(key) || [];
|
|
347
|
+
group.push(record);
|
|
348
|
+
groups.set(key, group);
|
|
349
|
+
}
|
|
350
|
+
let currentGroup;
|
|
351
|
+
if (sessionId !== undefined) {
|
|
352
|
+
const matching = records.filter((record) => record.sessionId === sessionId);
|
|
353
|
+
currentGroup = matching.length ? groups.get(sessionKey(matching[matching.length - 1])) : undefined;
|
|
354
|
+
} else if (records.length) {
|
|
355
|
+
currentGroup = groups.get(sessionKey(records[records.length - 1]));
|
|
356
|
+
}
|
|
357
|
+
const { period: _period, ...cumulative } = bucket('all-time', records);
|
|
358
|
+
return {
|
|
359
|
+
schema: REPORT_SCHEMA,
|
|
360
|
+
timezone: value.timezone,
|
|
361
|
+
currentSession: sessionSummary(currentGroup || []),
|
|
362
|
+
averagePerSession: averageBySession(records),
|
|
363
|
+
cumulative,
|
|
364
|
+
periods: {
|
|
365
|
+
daily: periodReport(records, value.timezone, 'daily', date),
|
|
366
|
+
weekly: periodReport(records, value.timezone, 'weekly', date),
|
|
367
|
+
monthly: periodReport(records, value.timezone, 'monthly', date),
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function tokenLine(value) {
|
|
373
|
+
return value === null ? 'unavailable' : `${value} tokens`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function formatMetricsReport(report) {
|
|
377
|
+
const lines = [
|
|
378
|
+
`Sando savings (timezone: ${report.timezone})`,
|
|
379
|
+
`Current session: ${report.currentSession ? report.currentSession.id ?? 'unknown' : 'none'}`,
|
|
380
|
+
`Current session estimated transform savings: ${tokenLine(report.currentSession?.estimatedTransformSavingsTokens ?? 0)}`,
|
|
381
|
+
`Average estimated transform savings per session: ${tokenLine(report.averagePerSession.estimatedTransformSavingsTokens)} (${report.averagePerSession.sessionCount} sessions)`,
|
|
382
|
+
`Cumulative estimated transform savings: ${tokenLine(report.cumulative.estimatedTransformSavingsTokens)}`,
|
|
383
|
+
`Cumulative provider-reported savings: ${tokenLine(report.cumulative.providerReportedSavingsTokens)}`,
|
|
384
|
+
`Daily (${report.periods.daily.current.period}) estimated transform savings: ${tokenLine(report.periods.daily.current.estimatedTransformSavingsTokens)}`,
|
|
385
|
+
`ISO week (${report.periods.weekly.current.period}) estimated transform savings: ${tokenLine(report.periods.weekly.current.estimatedTransformSavingsTokens)}`,
|
|
386
|
+
`Monthly (${report.periods.monthly.current.period}) estimated transform savings: ${tokenLine(report.periods.monthly.current.estimatedTransformSavingsTokens)}`,
|
|
387
|
+
];
|
|
388
|
+
if (!report.cumulative.eventCount) lines.push('No Sando events recorded.');
|
|
389
|
+
return `${lines.join('\n')}\n`;
|
|
390
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const SCHEMA = 'sando-provider-ledger/v1';
|
|
2
|
+
const VERSION = 1;
|
|
3
|
+
const PERIODS = new Set(['session', 'day', 'week', 'month']);
|
|
4
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
export const PROVIDER_LEDGER_SCHEMA = SCHEMA;
|
|
7
|
+
export const PROVIDER_LEDGER_VERSION = VERSION;
|
|
8
|
+
|
|
9
|
+
function record(value) {
|
|
10
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function text(value) {
|
|
14
|
+
return typeof value === 'string' && value.length > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function counter(value) {
|
|
18
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function safeSum(...values) {
|
|
22
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
23
|
+
return Number.isSafeInteger(total) ? total : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function dateValue(value) {
|
|
27
|
+
if (typeof value !== 'string') return null;
|
|
28
|
+
const date = new Date(value);
|
|
29
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function usageValue(usage) {
|
|
33
|
+
if (!record(usage) || !Object.hasOwn(usage, 'promptTokens') || !Object.hasOwn(usage, 'outputTokens')
|
|
34
|
+
|| !counter(usage.promptTokens) || !counter(usage.outputTokens)) return null;
|
|
35
|
+
const cacheReadTokens = usage.cacheReadTokens === undefined ? 0 : usage.cacheReadTokens;
|
|
36
|
+
const cacheWriteTokens = usage.cacheWriteTokens === undefined ? 0 : usage.cacheWriteTokens;
|
|
37
|
+
if (!counter(cacheReadTokens) || !counter(cacheWriteTokens)) return null;
|
|
38
|
+
const inputTokens = safeSum(usage.promptTokens, usage.outputTokens);
|
|
39
|
+
const cachedTokens = safeSum(cacheReadTokens, cacheWriteTokens);
|
|
40
|
+
if (inputTokens === null || cachedTokens === null || cachedTokens > usage.promptTokens) return null;
|
|
41
|
+
return {
|
|
42
|
+
promptTokens: usage.promptTokens,
|
|
43
|
+
outputTokens: usage.outputTokens,
|
|
44
|
+
cacheReadTokens,
|
|
45
|
+
cacheWriteTokens,
|
|
46
|
+
effectiveInputTokens: usage.promptTokens - cacheReadTokens,
|
|
47
|
+
totalTokens: inputTokens,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createProviderLedgerEntry(input) {
|
|
52
|
+
if (!record(input) || !text(input.provider) || !text(input.sessionId)
|
|
53
|
+
|| (input.model !== undefined && input.model !== null && !text(input.model))) return null;
|
|
54
|
+
const at = dateValue(input.at);
|
|
55
|
+
const usage = usageValue(input.usage);
|
|
56
|
+
const rawUsage = input.rawUsage === undefined ? input.usage : input.rawUsage;
|
|
57
|
+
if (!at || !usage || !record(rawUsage)) return null;
|
|
58
|
+
try {
|
|
59
|
+
return {
|
|
60
|
+
schema: SCHEMA,
|
|
61
|
+
version: VERSION,
|
|
62
|
+
provider: input.provider,
|
|
63
|
+
model: input.model ?? null,
|
|
64
|
+
sessionId: input.sessionId,
|
|
65
|
+
at: at.toISOString(),
|
|
66
|
+
usage,
|
|
67
|
+
rawUsage: structuredClone(rawUsage),
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validEntry(entry) {
|
|
75
|
+
if (!record(entry) || entry.schema !== SCHEMA || entry.version !== VERSION
|
|
76
|
+
|| !text(entry.provider) || !text(entry.sessionId)
|
|
77
|
+
|| (entry.model !== null && !text(entry.model))) return false;
|
|
78
|
+
const at = dateValue(entry.at);
|
|
79
|
+
const usage = usageValue(entry.usage);
|
|
80
|
+
return Boolean(at && usage && entry.at === at.toISOString() && record(entry.rawUsage)
|
|
81
|
+
&& Object.keys(entry.usage).length === 6
|
|
82
|
+
&& Object.entries(usage).every(([field, value]) => entry.usage[field] === value));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isoWeek(date) {
|
|
86
|
+
const current = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
87
|
+
const weekday = current.getUTCDay() || 7;
|
|
88
|
+
current.setUTCDate(current.getUTCDate() + 4 - weekday);
|
|
89
|
+
const year = current.getUTCFullYear();
|
|
90
|
+
const firstThursday = new Date(Date.UTC(year, 0, 4));
|
|
91
|
+
const week = 1 + Math.round((current - firstThursday) / WEEK_MS);
|
|
92
|
+
return `${year}-W${String(week).padStart(2, '0')}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function periodKey(entry, period) {
|
|
96
|
+
if (period === 'session') return entry.sessionId;
|
|
97
|
+
const date = new Date(entry.at);
|
|
98
|
+
const year = date.getUTCFullYear();
|
|
99
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
100
|
+
if (period === 'month') return `${year}-${month}`;
|
|
101
|
+
if (period === 'week') return isoWeek(date);
|
|
102
|
+
return `${year}-${month}-${String(date.getUTCDate()).padStart(2, '0')}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function addUsage(left, right) {
|
|
106
|
+
const usage = {};
|
|
107
|
+
for (const field of ['promptTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'effectiveInputTokens', 'totalTokens']) {
|
|
108
|
+
usage[field] = safeSum(left[field], right[field]);
|
|
109
|
+
if (usage[field] === null) throw new Error('provider ledger aggregate overflow');
|
|
110
|
+
}
|
|
111
|
+
return usage;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function aggregateProviderLedger(entries, period = 'session') {
|
|
115
|
+
if (!Array.isArray(entries)) throw new TypeError('provider ledger entries must be an array');
|
|
116
|
+
if (!PERIODS.has(period)) throw new TypeError('provider ledger period is invalid');
|
|
117
|
+
const groups = new Map();
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (!validEntry(entry)) throw new TypeError('invalid provider ledger entry');
|
|
120
|
+
const key = periodKey(entry, period);
|
|
121
|
+
const current = groups.get(key);
|
|
122
|
+
groups.set(key, current ? {
|
|
123
|
+
period: key,
|
|
124
|
+
entryCount: current.entryCount + 1,
|
|
125
|
+
usage: addUsage(current.usage, entry.usage),
|
|
126
|
+
} : {
|
|
127
|
+
period: key,
|
|
128
|
+
entryCount: 1,
|
|
129
|
+
usage: entry.usage,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
schema: SCHEMA,
|
|
134
|
+
version: VERSION,
|
|
135
|
+
period,
|
|
136
|
+
buckets: [...groups.values()].sort((left, right) => left.period.localeCompare(right.period)),
|
|
137
|
+
};
|
|
138
|
+
}
|