sandoichi 0.4.1 → 0.5.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 +6 -2
- package/index.mjs +75 -0
- package/package.json +1 -1
- package/src/accounting-cli.mjs +1 -1
- package/src/artifact-cli.mjs +67 -0
- package/src/artifact-lifecycle.mjs +67 -0
- package/src/artifact-recovery.mjs +138 -0
- package/src/artifact-store.mjs +44 -0
- package/src/cache-attribution.mjs +13 -3
- package/src/context-audit-cli.mjs +104 -0
- package/src/context-capture.mjs +200 -0
- package/src/context-classifier.mjs +142 -0
- package/src/context-footprint.mjs +299 -0
- package/src/context-transform.mjs +146 -17
- package/src/core.mjs +300 -35
- package/src/f1-telemetry.mjs +80 -0
- package/src/f4-telemetry.mjs +183 -0
- package/src/gateway-gate-cli.mjs +88 -0
- package/src/gateway-gate.mjs +412 -0
- package/src/history-archive.mjs +80 -0
- package/src/history-disclosure.mjs +70 -0
- package/src/hook-cli.mjs +17 -1
- package/src/lazy-mcp-gateway-stdio.mjs +59 -0
- package/src/lazy-mcp-gateway.mjs +295 -0
- package/src/mcp-server.mjs +72 -11
- package/src/metrics.mjs +4 -3
- package/src/provider-usage.mjs +103 -23
- package/src/proxy.mjs +435 -19
- package/src/result-disclosure.mjs +115 -0
- package/src/slice.mjs +419 -0
- package/src/statusline.mjs +5 -9
- package/src/telemetry.mjs +155 -29
package/src/provider-usage.mjs
CHANGED
|
@@ -11,6 +11,8 @@ const LOCK_WAIT_MS = 10;
|
|
|
11
11
|
const LOCK_ATTEMPTS = 250;
|
|
12
12
|
const STALE_LOCK_MS = 30_000;
|
|
13
13
|
const COST_SCOPES = new Set(['session', 'event']);
|
|
14
|
+
const COST_SOURCES = new Set(['host-reported', 'provider-reported']);
|
|
15
|
+
const AGGREGATIONS = new Set(['session']);
|
|
14
16
|
|
|
15
17
|
export const PROVIDER_USAGE_SCHEMA = SCHEMA;
|
|
16
18
|
export const PROVIDER_USAGE_VERSION = VERSION;
|
|
@@ -71,11 +73,12 @@ function reportedCost(value) {
|
|
|
71
73
|
function attachReportedCost(records, totalCostUsd) {
|
|
72
74
|
if (!records.length || !usd(totalCostUsd)) return records;
|
|
73
75
|
return records.map((item, index) => index === records.length - 1
|
|
74
|
-
? { ...item, totalCostUsd, costScope: 'session' } : item);
|
|
76
|
+
? { ...item, totalCostUsd, costScope: 'session', costSource: 'host-reported' } : item);
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
function usageRecord({ host, source, sourceKey, sessionId, turnId, at, inputTokens, cachedInputTokens = 0,
|
|
78
|
-
cacheWriteInputTokens = 0, outputTokens, reasoningOutputTokens = 0, totalCostUsd,
|
|
80
|
+
cacheWriteInputTokens = 0, outputTokens, reasoningOutputTokens = 0, totalCostUsd, costSource,
|
|
81
|
+
aggregation, turnCount, arm, experimentId, workloadId, identityAt }) {
|
|
79
82
|
if (!text(host) || !text(source) || !text(sourceKey)
|
|
80
83
|
|| (sessionId !== null && !text(sessionId)) || (turnId !== null && !text(turnId))
|
|
81
84
|
|| !text(at) || !counter(inputTokens) || !counter(cachedInputTokens)
|
|
@@ -83,8 +86,10 @@ function usageRecord({ host, source, sourceKey, sessionId, turnId, at, inputToke
|
|
|
83
86
|
|| !counter(outputTokens) || !counter(reasoningOutputTokens) || reasoningOutputTokens > outputTokens) return null;
|
|
84
87
|
const totalTokens = safeSum(inputTokens, outputTokens);
|
|
85
88
|
if (totalTokens === null) return null;
|
|
86
|
-
const identity = JSON.stringify({ host, source, sourceKey,
|
|
87
|
-
|
|
89
|
+
const identity = JSON.stringify({ host, source, sourceKey,
|
|
90
|
+
...(identityAt === undefined ? { sessionId, turnId } : { at: identityAt }),
|
|
91
|
+
inputTokens, cachedInputTokens, cacheWriteInputTokens, outputTokens, reasoningOutputTokens,
|
|
92
|
+
totalTokens, aggregation, turnCount, arm, experimentId, workloadId });
|
|
88
93
|
const result = {
|
|
89
94
|
eventKey: `usage:${host}:${sha256(identity)}`,
|
|
90
95
|
schema: SCHEMA, version: VERSION, host, source,
|
|
@@ -93,15 +98,18 @@ function usageRecord({ host, source, sourceKey, sessionId, turnId, at, inputToke
|
|
|
93
98
|
reasoningOutputTokens, totalTokens,
|
|
94
99
|
};
|
|
95
100
|
if (totalCostUsd !== undefined && usd(totalCostUsd)) result.totalCostUsd = totalCostUsd;
|
|
101
|
+
if (costSource !== undefined) result.costSource = costSource;
|
|
102
|
+
if (aggregation !== undefined) result.aggregation = aggregation;
|
|
103
|
+
if (turnCount !== undefined) result.turnCount = turnCount;
|
|
96
104
|
if (arm !== undefined) result.arm = arm;
|
|
97
105
|
if (experimentId !== undefined) result.experimentId = experimentId;
|
|
98
106
|
if (workloadId !== undefined) result.workloadId = workloadId;
|
|
99
107
|
return result;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
function claudeUsageRecord(usage, { source, sourceKey, sessionId = null, turnId = null, at,
|
|
111
|
+
arm, experimentId, workloadId, aggregation, turnCount, identityAt } = {}) {
|
|
112
|
+
if (!record(usage)) return null;
|
|
105
113
|
const inputTokens = usage.input_tokens;
|
|
106
114
|
const cachedInputTokens = optionalCounter(usage.cache_read_input_tokens);
|
|
107
115
|
const cacheWriteInputTokens = optionalCounter(usage.cache_creation_input_tokens);
|
|
@@ -112,13 +120,37 @@ function claudeRecord(value, index, { sessionId = null, turnId = null, now, arm,
|
|
|
112
120
|
const totalTokens = usage.total_tokens === undefined ? safeSum(totalInputTokens, outputTokens) : usage.total_tokens;
|
|
113
121
|
if (!counter(totalTokens) || totalTokens !== totalInputTokens + outputTokens) return null;
|
|
114
122
|
return usageRecord({
|
|
115
|
-
host: 'claude', source
|
|
116
|
-
sessionId, turnId: value.turn_id ?? turnId, at: isoDate(value.timestamp, now),
|
|
123
|
+
host: 'claude', source, sourceKey, sessionId, turnId, at,
|
|
117
124
|
inputTokens: totalInputTokens, cachedInputTokens, cacheWriteInputTokens, outputTokens,
|
|
125
|
+
aggregation, turnCount, identityAt,
|
|
118
126
|
arm, experimentId, workloadId,
|
|
119
127
|
});
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
function claudeRecord(value, index, { sessionId = null, turnId = null, now, arm, experimentId, workloadId } = {}) {
|
|
131
|
+
if (value.type !== 'assistant' || !record(value.message?.usage)) return null;
|
|
132
|
+
const identityAt = value.timestamp === undefined ? undefined : isoDate(value.timestamp, now);
|
|
133
|
+
return claudeUsageRecord(value.message.usage, {
|
|
134
|
+
source: 'claude-transcript', sourceKey: value.message.id ?? value.uuid ?? value.request_id ?? value.timestamp ?? String(index),
|
|
135
|
+
sessionId, turnId: value.turn_id ?? turnId, at: isoDate(value.timestamp, now), identityAt,
|
|
136
|
+
arm, experimentId, workloadId,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function claudeResultRecord(value, entries, { sessionId = null, arm, experimentId, workloadId, now } = {}) {
|
|
141
|
+
const identityTimestamp = value.timestamp ?? entries.slice().reverse().map((entry) => entry.value.timestamp).find(text);
|
|
142
|
+
const at = identityTimestamp ?? now;
|
|
143
|
+
const messages = new Set(entries
|
|
144
|
+
.filter(({ value: entry }) => entry.type === 'assistant' && record(entry.message?.usage))
|
|
145
|
+
.map(({ value: entry }, index) => entry.message.id ?? entry.uuid ?? entry.request_id ?? String(index)));
|
|
146
|
+
const turnCount = counter(value.num_turns ?? value.numTurns) ? value.num_turns ?? value.numTurns : messages.size || undefined;
|
|
147
|
+
return claudeUsageRecord(value.usage, {
|
|
148
|
+
source: 'claude-result', sourceKey: sessionId === null ? value.uuid ?? value.request_id ?? 'final' : 'session',
|
|
149
|
+
sessionId, turnId: null, at: isoDate(at, now), identityAt: identityTimestamp === undefined ? undefined : isoDate(identityTimestamp, now),
|
|
150
|
+
aggregation: 'session', turnCount, arm, experimentId, workloadId,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
122
154
|
function codexRecord(value, index, { sessionId = null, turnId = null, now, arm, experimentId, workloadId } = {}) {
|
|
123
155
|
const usage = value.type === 'turn.completed'
|
|
124
156
|
? value.usage
|
|
@@ -135,9 +167,10 @@ function codexRecord(value, index, { sessionId = null, turnId = null, now, arm,
|
|
|
135
167
|
|| !counter(outputTokens) || reasoningOutputTokens === null) return null;
|
|
136
168
|
const totalTokens = usage.total_tokens === undefined ? safeSum(inputTokens, outputTokens) : usage.total_tokens;
|
|
137
169
|
if (!counter(totalTokens) || totalTokens !== inputTokens + outputTokens) return null;
|
|
170
|
+
const identityAt = value.timestamp === undefined ? undefined : isoDate(value.timestamp, now);
|
|
138
171
|
return usageRecord({
|
|
139
172
|
host: 'codex', source: 'codex-transcript', sourceKey: value.turn_id ?? value.id ?? value.timestamp ?? String(index),
|
|
140
|
-
sessionId, turnId: value.turn_id ?? value.id ?? (value.timestamp ? `at:${value.timestamp}` : turnId), at: isoDate(value.timestamp, now),
|
|
173
|
+
sessionId, turnId: value.turn_id ?? value.id ?? (value.timestamp ? `at:${value.timestamp}` : turnId), at: isoDate(value.timestamp, now), identityAt,
|
|
141
174
|
inputTokens, cachedInputTokens, cacheWriteInputTokens, outputTokens, reasoningOutputTokens,
|
|
142
175
|
arm, experimentId, workloadId,
|
|
143
176
|
});
|
|
@@ -145,9 +178,18 @@ function codexRecord(value, index, { sessionId = null, turnId = null, now, arm,
|
|
|
145
178
|
|
|
146
179
|
export function parseClaudeTranscript(textValue, options = {}) {
|
|
147
180
|
const entries = jsonLines(textValue);
|
|
148
|
-
const
|
|
181
|
+
const unique = new Map();
|
|
182
|
+
entries.forEach(({ value }, index) => {
|
|
183
|
+
const result = claudeRecord(value, index, options);
|
|
184
|
+
if (!result) return;
|
|
185
|
+
const messageId = text(value.message?.id);
|
|
186
|
+
const key = messageId ? `message:${messageId}` : result.eventKey;
|
|
187
|
+
unique.set(key, result);
|
|
188
|
+
});
|
|
189
|
+
const resultEntry = entries.findLast(({ value }) => value.type === 'result' && record(value.usage));
|
|
149
190
|
const totalCostUsd = options.totalCostUsd ?? entries.slice().reverse().map(({ value }) => reportedCost(value)).find(usd);
|
|
150
|
-
|
|
191
|
+
const aggregate = resultEntry ? claudeResultRecord(resultEntry.value, entries, options) : null;
|
|
192
|
+
return attachReportedCost(aggregate ? [aggregate] : [...unique.values()], totalCostUsd);
|
|
151
193
|
}
|
|
152
194
|
|
|
153
195
|
export function parseCodexTranscript(textValue, options = {}) {
|
|
@@ -188,6 +230,9 @@ function validateUsage(value) {
|
|
|
188
230
|
|| (value.experimentId !== undefined && !text(value.experimentId))
|
|
189
231
|
|| (value.workloadId !== undefined && !text(value.workloadId))
|
|
190
232
|
|| (value.totalCostUsd !== undefined && !usd(value.totalCostUsd))
|
|
233
|
+
|| (value.costSource !== undefined && !COST_SOURCES.has(value.costSource))
|
|
234
|
+
|| (value.aggregation !== undefined && !AGGREGATIONS.has(value.aggregation))
|
|
235
|
+
|| (value.turnCount !== undefined && !counter(value.turnCount))
|
|
191
236
|
|| (value.costScope !== undefined && (!COST_SCOPES.has(value.costScope) || value.totalCostUsd === undefined))) {
|
|
192
237
|
throw new Error('provider usage record is invalid');
|
|
193
238
|
}
|
|
@@ -251,11 +296,19 @@ export function appendProviderUsage({ storagePath = defaultProviderUsagePath(),
|
|
|
251
296
|
const state = readProviderUsage(filePath);
|
|
252
297
|
const existing = new Map(state.records.map((item, index) => [item.eventKey, index]));
|
|
253
298
|
for (const item of records) {
|
|
254
|
-
const index =
|
|
299
|
+
const index = item.aggregation === 'session'
|
|
300
|
+
? item.sessionId === null
|
|
301
|
+
? existing.get(item.eventKey) ?? -1
|
|
302
|
+
: state.records.findIndex((candidate) => candidate.aggregation === 'session'
|
|
303
|
+
&& candidate.host === item.host && candidate.source === item.source && candidate.sessionId === item.sessionId)
|
|
304
|
+
: existing.get(item.eventKey);
|
|
255
305
|
if (index === undefined) {
|
|
256
306
|
existing.set(item.eventKey, state.records.length);
|
|
257
307
|
state.records.push(item);
|
|
258
|
-
} else if (
|
|
308
|
+
} else if (index < 0) {
|
|
309
|
+
existing.set(item.eventKey, state.records.length);
|
|
310
|
+
state.records.push(item);
|
|
311
|
+
} else if (item.aggregation === 'session' || item.costScope === 'session') {
|
|
259
312
|
state.records[index] = item;
|
|
260
313
|
}
|
|
261
314
|
}
|
|
@@ -278,9 +331,20 @@ export function collectProviderUsage({ host, transcriptPath, sessionId = null, t
|
|
|
278
331
|
}
|
|
279
332
|
|
|
280
333
|
export function buildProviderUsageReport(state, { sessionId, pricing } = {}) {
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
334
|
+
const selected = validateState(state).records.filter((item) => sessionId === undefined || item.sessionId === sessionId);
|
|
335
|
+
const aggregateSessions = new Set(selected
|
|
336
|
+
.filter((item) => item.aggregation === 'session' && item.sessionId !== null)
|
|
337
|
+
.map(sessionKey));
|
|
338
|
+
const records = selected.filter((item) => item.aggregation === 'session'
|
|
339
|
+
|| item.sessionId === null || !aggregateSessions.has(sessionKey(item)));
|
|
340
|
+
const sessions = new Set(records.map(sessionKey));
|
|
341
|
+
const turns = new Set(records.map(turnKey));
|
|
342
|
+
const aggregateRecords = records.filter((item) => item.aggregation === 'session');
|
|
343
|
+
const aggregateCounts = aggregateRecords.map((item) => item.turnCount);
|
|
344
|
+
const nonAggregateTurns = new Set(records.filter((item) => item.aggregation !== 'session').map(turnKey));
|
|
345
|
+
const turnCount = aggregateRecords.length
|
|
346
|
+
? aggregateCounts.every(counter) ? safeSum(...aggregateCounts, nonAggregateTurns.size) : null
|
|
347
|
+
: turns.size;
|
|
284
348
|
const sum = (field) => records.reduce((total, item) => total + item[field], 0);
|
|
285
349
|
const weightedCostUnits = records.reduce((total, item) => total + computeWeightedUsage(item, pricing).costUnits, 0);
|
|
286
350
|
const freshInputTokens = records.reduce((total, item) => total + item.inputTokens - item.cachedInputTokens - item.cacheWriteInputTokens, 0);
|
|
@@ -289,7 +353,7 @@ export function buildProviderUsageReport(state, { sessionId, pricing } = {}) {
|
|
|
289
353
|
const effectiveRate = totalCostUsd !== null && totalTokens(records) > 0
|
|
290
354
|
? totalCostUsd / totalTokens(records) * 1_000_000 : null;
|
|
291
355
|
const cost = {
|
|
292
|
-
status: billing.complete ?
|
|
356
|
+
status: billing.complete ? billing.source : billing.partial ? billing.source : 'unavailable',
|
|
293
357
|
coverage: billing.complete ? 'complete' : billing.partial ? 'partial' : 'none',
|
|
294
358
|
totalCostUsd,
|
|
295
359
|
effectiveRateUsdPerMillionTokens: effectiveRate,
|
|
@@ -299,18 +363,28 @@ export function buildProviderUsageReport(state, { sessionId, pricing } = {}) {
|
|
|
299
363
|
inputTokens: sum('inputTokens'), cachedInputTokens: sum('cachedInputTokens'),
|
|
300
364
|
cacheWriteInputTokens: sum('cacheWriteInputTokens'), freshInputTokens,
|
|
301
365
|
outputTokens: sum('outputTokens'), reasoningOutputTokens: sum('reasoningOutputTokens'), totalTokens: sum('totalTokens'),
|
|
302
|
-
turnCount
|
|
366
|
+
turnCount, weightedCostUnits,
|
|
303
367
|
weightedCost: { source: 'weighted-estimate', costUnits: weightedCostUnits },
|
|
304
368
|
cost,
|
|
305
369
|
totalCostUsd,
|
|
306
|
-
providerReportedCostUsd: totalCostUsd,
|
|
370
|
+
providerReportedCostUsd: billing.source === 'provider-reported' && billing.complete ? totalCostUsd : null,
|
|
307
371
|
sessionBlendedEffectiveRateUsdPerMillionTokens: effectiveRate,
|
|
308
372
|
costSource: cost.status,
|
|
309
373
|
};
|
|
310
374
|
}
|
|
311
375
|
|
|
312
376
|
function billingKey(item) {
|
|
313
|
-
return
|
|
377
|
+
return sessionKey(item);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function sessionKey(item) {
|
|
381
|
+
return item.sessionId === null
|
|
382
|
+
? `${item.host}\0unknown:${item.eventKey}`
|
|
383
|
+
: `${item.host}\0${item.sessionId}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function turnKey(item, index) {
|
|
387
|
+
return `${sessionKey(item)}\0${item.turnId ?? `record:${index}`}`;
|
|
314
388
|
}
|
|
315
389
|
|
|
316
390
|
function reportedCostSummary(records) {
|
|
@@ -321,18 +395,24 @@ function reportedCostSummary(records) {
|
|
|
321
395
|
groups.set(billingKey(item), entries);
|
|
322
396
|
});
|
|
323
397
|
const totals = [];
|
|
398
|
+
const sources = new Set();
|
|
324
399
|
for (const entries of groups.values()) {
|
|
325
400
|
const sessionCosts = entries.filter((item) => item.costScope === 'session' && usd(item.totalCostUsd));
|
|
326
401
|
if (sessionCosts.length) {
|
|
327
402
|
const latest = sessionCosts.reduce((left, right) => right.at >= left.at ? right : left);
|
|
328
403
|
totals.push(latest.totalCostUsd);
|
|
404
|
+
sources.add(latest.costSource ?? 'unknown');
|
|
329
405
|
continue;
|
|
330
406
|
}
|
|
331
|
-
if (entries.every((item) => usd(item.totalCostUsd)))
|
|
407
|
+
if (entries.every((item) => usd(item.totalCostUsd))) {
|
|
408
|
+
totals.push(sumUsd(entries.map((item) => item.totalCostUsd)));
|
|
409
|
+
entries.forEach((item) => sources.add(item.costSource ?? 'unknown'));
|
|
410
|
+
}
|
|
332
411
|
}
|
|
333
412
|
const complete = groups.size > 0 && totals.length === groups.size && !totals.includes(null);
|
|
334
413
|
const totalCostUsd = complete ? sumUsd(totals) : null;
|
|
335
|
-
|
|
414
|
+
const source = sources.size === 1 ? [...sources][0] : sources.size ? 'mixed' : null;
|
|
415
|
+
return { complete: complete && totalCostUsd !== null, partial: totals.length > 0, totalCostUsd, source: source ?? 'unknown' };
|
|
336
416
|
}
|
|
337
417
|
|
|
338
418
|
function totalTokens(records) {
|