sandoichi 0.4.2 → 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 +5 -1
- package/index.mjs +5 -0
- package/package.json +1 -1
- package/src/accounting-cli.mjs +1 -1
- package/src/artifact-lifecycle.mjs +67 -0
- package/src/artifact-recovery.mjs +5 -0
- package/src/artifact-store.mjs +2 -1
- package/src/cache-attribution.mjs +13 -3
- package/src/context-transform.mjs +129 -17
- package/src/core.mjs +283 -34
- package/src/history-archive.mjs +80 -0
- package/src/hook-cli.mjs +17 -1
- package/src/lazy-mcp-gateway.mjs +10 -6
- package/src/mcp-server.mjs +47 -12
- package/src/metrics.mjs +4 -3
- package/src/provider-usage.mjs +103 -23
- package/src/proxy.mjs +12 -5
- package/src/result-disclosure.mjs +8 -2
- package/src/slice.mjs +419 -0
- package/src/statusline.mjs +5 -9
- package/src/telemetry.mjs +101 -19
package/src/mcp-server.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { exposeMcpResult, recoverStoredArtifact } from './artifact-store.mjs';
|
|
|
8
8
|
import { optimizeToolOutput } from './core.mjs';
|
|
9
9
|
import { ARTIFACT_TOOL_NAME } from './result-disclosure.mjs';
|
|
10
10
|
import { PLUGIN_VERSION } from './version.mjs';
|
|
11
|
+
import { createSliceBridge, isSliceTool, SLICE_TOOLS, SliceRpcError } from './slice.mjs';
|
|
11
12
|
|
|
12
13
|
const TOOL = {
|
|
13
14
|
name: 'prepare_tool_output',
|
|
@@ -20,14 +21,16 @@ const TOOL = {
|
|
|
20
21
|
};
|
|
21
22
|
const ARTIFACT_TOOL = {
|
|
22
23
|
name: ARTIFACT_TOOL_NAME,
|
|
23
|
-
description: 'Recover
|
|
24
|
+
description: 'Recover bounded redacted content from an artifact created in this MCP session. Copy artifact.handle exactly into ref (for example, sando:sha256:0123456789abcdef). Omit range fields to select the full artifact; the response remains bounded by maxBytes (default 65536). Otherwise use either 0-based byte offsets or a 1-based inclusive line range, and omit fields for the unused mode.',
|
|
24
25
|
inputSchema: {
|
|
25
26
|
type: 'object', additionalProperties: false, required: ['ref'],
|
|
26
27
|
properties: {
|
|
27
28
|
ref: { type: 'string', pattern: '^sando:sha256:[a-f0-9]{16,64}$' },
|
|
28
|
-
startByte: { type: 'integer', minimum: 0
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
startByte: { type: 'integer', minimum: 0, description: '0-based inclusive byte offset.' },
|
|
30
|
+
endByte: { type: 'integer', minimum: 0, description: '0-based exclusive byte offset.' },
|
|
31
|
+
startLine: { type: 'integer', minimum: 1, description: '1-based inclusive line number.' },
|
|
32
|
+
endLine: { type: 'integer', minimum: 1, description: '1-based inclusive line number.' },
|
|
33
|
+
maxBytes: { type: 'integer', minimum: 1, maximum: 1048576, description: 'Maximum output bytes; omit for the default.' },
|
|
31
34
|
},
|
|
32
35
|
},
|
|
33
36
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
@@ -35,38 +38,70 @@ const ARTIFACT_TOOL = {
|
|
|
35
38
|
const TOOLS = [TOOL, ARTIFACT_TOOL];
|
|
36
39
|
|
|
37
40
|
function response(id, result) { return { jsonrpc: '2.0', id, result }; }
|
|
38
|
-
function error(id, code, message) { return { jsonrpc: '2.0', id: id ?? null, error: { code, message } }; }
|
|
41
|
+
function error(id, code, message, data) { return { jsonrpc: '2.0', id: id ?? null, error: { code, message, ...(data === undefined ? {} : { data }) } }; }
|
|
42
|
+
function requestKey(id) { return `${typeof id}:${JSON.stringify(id)}`; }
|
|
39
43
|
|
|
40
|
-
function dispatch(message) {
|
|
44
|
+
async function dispatch(message, bridge, active) {
|
|
41
45
|
if (!message || message.jsonrpc !== '2.0' || typeof message.method !== 'string') return error(message?.id, -32600, 'Invalid Request');
|
|
46
|
+
if (message.method === 'notifications/cancelled') {
|
|
47
|
+
active.get(requestKey(message.params?.requestId))?.abort();
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
42
50
|
if (message.id === undefined) return null;
|
|
43
51
|
if (message.method === 'initialize') return response(message.id, {
|
|
44
52
|
protocolVersion: message.params?.protocolVersion || '2025-06-18', capabilities: { tools: { listChanged: false } }, serverInfo: { name: 'sando', version: PLUGIN_VERSION },
|
|
45
53
|
});
|
|
46
54
|
if (message.method === 'ping') return response(message.id, {});
|
|
47
|
-
|
|
55
|
+
const tools = [...TOOLS, ...SLICE_TOOLS()];
|
|
56
|
+
if (message.method === 'tools/list') return response(message.id, { tools });
|
|
48
57
|
if (message.method === 'tools/call') {
|
|
49
|
-
if (!
|
|
58
|
+
if (!tools.some((tool) => tool.name === message.params?.name)) return error(message.id, -32602, 'Unknown tool');
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
active.set(requestKey(message.id), controller);
|
|
50
61
|
try {
|
|
62
|
+
if (isSliceTool(message.params.name)) {
|
|
63
|
+
return response(message.id, await bridge.call(message.params.name, message.params.arguments, { signal: controller.signal }));
|
|
64
|
+
}
|
|
51
65
|
const result = message.params.name === TOOL.name
|
|
52
66
|
? optimizeToolOutput(message.params.arguments)
|
|
53
67
|
: recoverStoredArtifact(message.params.arguments);
|
|
54
68
|
const exposed = message.params.name === TOOL.name ? exposeMcpResult(result) : result;
|
|
55
69
|
return response(message.id, { content: [{ type: 'text', text: exposed.inline ?? exposed.content }], structuredContent: exposed, isError: false });
|
|
56
70
|
} catch (cause) {
|
|
71
|
+
if (cause instanceof SliceRpcError) return error(message.id, cause.code, cause.message, cause.data);
|
|
57
72
|
return response(message.id, { content: [{ type: 'text', text: cause instanceof Error ? cause.message : 'invalid tool input' }], isError: true });
|
|
58
|
-
}
|
|
73
|
+
} finally { active.delete(requestKey(message.id)); }
|
|
59
74
|
}
|
|
60
75
|
return error(message.id, -32601, 'Method not found');
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
export function startMcpServer() {
|
|
64
79
|
const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
80
|
+
const bridge = createSliceBridge();
|
|
81
|
+
const active = new Map();
|
|
82
|
+
const pending = new Set();
|
|
65
83
|
lines.on('line', (line) => {
|
|
66
|
-
let
|
|
67
|
-
try {
|
|
68
|
-
|
|
84
|
+
let message;
|
|
85
|
+
try { message = JSON.parse(line); } catch { process.stdout.write(`${JSON.stringify(error(null, -32700, 'Parse error'))}\n`); return; }
|
|
86
|
+
const task = dispatch(message, bridge, active).then((output) => {
|
|
87
|
+
if (output) process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
88
|
+
}).catch(() => process.stdout.write(`${JSON.stringify(error(message?.id, -32603, 'Internal error'))}\n`));
|
|
89
|
+
pending.add(task);
|
|
90
|
+
void task.finally(() => pending.delete(task));
|
|
69
91
|
});
|
|
92
|
+
lines.once('close', async () => { await Promise.allSettled(pending); bridge.close(); });
|
|
93
|
+
let stopping = false;
|
|
94
|
+
const stop = async () => {
|
|
95
|
+
if (stopping) return;
|
|
96
|
+
stopping = true;
|
|
97
|
+
lines.close();
|
|
98
|
+
for (const controller of active.values()) controller.abort();
|
|
99
|
+
bridge.close();
|
|
100
|
+
await Promise.allSettled(pending);
|
|
101
|
+
process.exit(0);
|
|
102
|
+
};
|
|
103
|
+
process.once('SIGTERM', stop);
|
|
104
|
+
process.once('SIGINT', stop);
|
|
70
105
|
}
|
|
71
106
|
|
|
72
107
|
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) startMcpServer();
|
package/src/metrics.mjs
CHANGED
|
@@ -204,13 +204,14 @@ function providerSavings(providerUsage) {
|
|
|
204
204
|
const value = (names) => names.map((name) => providerUsage[name]).find((candidate) => candidate !== undefined);
|
|
205
205
|
const baseline = value(['baselineInputTokens', 'baseline_input_tokens']);
|
|
206
206
|
const optimized = value(['optimizedInputTokens', 'optimized_input_tokens']);
|
|
207
|
+
const reported = value(['reportedSavingsTokens', 'reported_savings_tokens']);
|
|
208
|
+
if (baseline === undefined && optimized === undefined && reported === undefined) return null;
|
|
207
209
|
if (baseline !== undefined || optimized !== undefined) {
|
|
208
210
|
integer(baseline, 'baselineInputTokens');
|
|
209
211
|
integer(optimized, 'optimizedInputTokens');
|
|
210
|
-
return
|
|
212
|
+
return null;
|
|
211
213
|
}
|
|
212
|
-
|
|
213
|
-
if (reported !== undefined) return integer(reported, 'reportedSavingsTokens', { min: -Number.MAX_SAFE_INTEGER });
|
|
214
|
+
integer(reported, 'reportedSavingsTokens', { min: -Number.MAX_SAFE_INTEGER });
|
|
214
215
|
return null;
|
|
215
216
|
}
|
|
216
217
|
|
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) {
|
package/src/proxy.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
2
|
import http from 'node:http';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import tls from 'node:tls';
|
|
4
5
|
import { brotliDecompressSync, gunzipSync, inflateSync, zstdDecompressSync } from 'node:zlib';
|
|
5
6
|
|
|
@@ -31,6 +32,7 @@ function recordProxyTelemetry({ env, provider, transformed, beforeText, afterTex
|
|
|
31
32
|
incrementCounter({
|
|
32
33
|
statePaths,
|
|
33
34
|
day: todayUtc(),
|
|
35
|
+
pluginVersion: PLUGIN_VERSION,
|
|
34
36
|
event: 'proxy_summary',
|
|
35
37
|
provider: telemetryProvider(provider),
|
|
36
38
|
mode: 'enforce',
|
|
@@ -52,7 +54,7 @@ function recordProxyFailure({ env, provider, failureStage }) {
|
|
|
52
54
|
const statePaths = defaultTelemetryStatePaths(env);
|
|
53
55
|
const day = todayUtc();
|
|
54
56
|
recordFailure({
|
|
55
|
-
statePaths, day, event: 'proxy_failure_summary',
|
|
57
|
+
statePaths, day, pluginVersion: PLUGIN_VERSION, event: 'proxy_failure_summary',
|
|
56
58
|
provider: telemetryProvider(provider), failureStage,
|
|
57
59
|
});
|
|
58
60
|
closeFinishedDays({ statePaths, configPath, day, pluginVersion: PLUGIN_VERSION });
|
|
@@ -408,13 +410,18 @@ async function observeSemanticCandidates({ provider, candidates, semanticCompact
|
|
|
408
410
|
export async function createProviderProxy({
|
|
409
411
|
upstream, host = '127.0.0.1', port = 0, policy = {}, maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
410
412
|
semanticCompactor, metricsPath, contextCapturePath, contextCaptureHost, contextSessionKey,
|
|
411
|
-
f1TelemetryPublisher = publishF1Telemetry, transformProviderRequests =
|
|
413
|
+
f1TelemetryPublisher = publishF1Telemetry, transformProviderRequests = false,
|
|
414
|
+
historyArchiveRoot,
|
|
412
415
|
env = process.env,
|
|
413
416
|
} = {}) {
|
|
414
417
|
const upstreamUrl = assertUpstream(upstream);
|
|
415
418
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new TypeError('port is invalid');
|
|
416
419
|
if (!Number.isInteger(maxBodyBytes) || maxBodyBytes < 1024) throw new TypeError('maxBodyBytes is invalid');
|
|
417
420
|
if (typeof transformProviderRequests !== 'boolean') throw new TypeError('transformProviderRequests is invalid');
|
|
421
|
+
if (transformProviderRequests && policy?.strategies?.recoverableArchive === true
|
|
422
|
+
&& (typeof historyArchiveRoot !== 'string' || !path.isAbsolute(historyArchiveRoot))) {
|
|
423
|
+
throw new TypeError('historyArchiveRoot must be an absolute path');
|
|
424
|
+
}
|
|
418
425
|
let lastStats = null;
|
|
419
426
|
let lastRequestAt = null;
|
|
420
427
|
const capturedContextSessions = new Set();
|
|
@@ -497,17 +504,17 @@ export async function createProviderProxy({
|
|
|
497
504
|
const now = Date.now();
|
|
498
505
|
const idleMs = lastRequestAt === null ? null : now - lastRequestAt;
|
|
499
506
|
lastRequestAt = now;
|
|
500
|
-
const transformed = transformProviderRequest({ provider, body: parsed, policy, idleMs });
|
|
507
|
+
const transformed = transformProviderRequest({ provider, body: parsed, policy, idleMs, historyArchiveRoot });
|
|
501
508
|
if (transformed.changed) body = Buffer.from(JSON.stringify(transformed.body));
|
|
502
509
|
const mechanicalContextTrimmedBytes = Math.max(0, rawBody.length - body.length);
|
|
503
510
|
recordProxyTelemetry({
|
|
504
511
|
env, provider, transformed,
|
|
505
512
|
beforeText: rawBody.toString('utf8'), afterText: body.toString('utf8'),
|
|
506
513
|
});
|
|
507
|
-
lastStats = { provider, ...transformed.stats, mechanicalContextTrimmedBytes, changed: transformed.changed, reasons: transformed.reasons };
|
|
514
|
+
lastStats = { provider, ...transformed.stats, mechanicalContextTrimmedBytes, changed: transformed.changed, reasons: transformed.reasons, disclosures: transformed.disclosures };
|
|
508
515
|
recordProvider = provider;
|
|
509
516
|
recordModel = typeof parsed?.model === 'string' ? parsed.model : null;
|
|
510
|
-
recordStats = { ...transformed.stats, mechanicalContextTrimmedBytes };
|
|
517
|
+
recordStats = { ...transformed.stats, mechanicalContextTrimmedBytes, disclosures: transformed.disclosures };
|
|
511
518
|
if (typeof semanticCompactor === 'function') {
|
|
512
519
|
const candidates = listSemanticCandidates({ provider, body: transformed.body });
|
|
513
520
|
const stats = createSemanticStats(candidates);
|
|
@@ -51,7 +51,7 @@ function bytes(value, name) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function buildResultDisclosure({
|
|
54
|
-
toolName, route, reason, inline, redactedText, inputBytes, redactedBytes, artifact,
|
|
54
|
+
toolName, route, reason, inline, redactedText, inputBytes, redactedBytes, artifact, elidedRange,
|
|
55
55
|
} = {}) {
|
|
56
56
|
if (typeof toolName !== 'string' || !toolName || typeof route !== 'string' || !route
|
|
57
57
|
|| typeof reason !== 'string' || !reason || typeof inline !== 'string' || typeof redactedText !== 'string') {
|
|
@@ -79,6 +79,11 @@ export function buildResultDisclosure({
|
|
|
79
79
|
const recovery = !artifact && reason === 'artifact-admission-limit'
|
|
80
80
|
? { mode: 'unavailable', bounded: true }
|
|
81
81
|
: undefined;
|
|
82
|
+
const recoveryCommand = artifact
|
|
83
|
+
? (elidedRange && Number.isInteger(elidedRange.startLine) && Number.isInteger(elidedRange.endLine)
|
|
84
|
+
? `sando artifact get --ref ${artifact.ref} --start-line ${elidedRange.startLine} --end-line ${elidedRange.endLine}`
|
|
85
|
+
: `sando artifact get --ref ${artifact.ref} --max-bytes 65536`)
|
|
86
|
+
: undefined;
|
|
82
87
|
return {
|
|
83
88
|
schema: RESULT_DISCLOSURE_SCHEMA,
|
|
84
89
|
version: RESULT_DISCLOSURE_VERSION,
|
|
@@ -96,9 +101,10 @@ export function buildResultDisclosure({
|
|
|
96
101
|
bytes: artifact.bytes,
|
|
97
102
|
recovery: {
|
|
98
103
|
tool: ARTIFACT_TOOL_NAME,
|
|
99
|
-
command:
|
|
104
|
+
command: recoveryCommand,
|
|
100
105
|
bounded: true,
|
|
101
106
|
},
|
|
107
|
+
...(elidedRange ? { elidedRange } : {}),
|
|
102
108
|
} : null,
|
|
103
109
|
};
|
|
104
110
|
}
|