@magnusekdahl/parallix 1.2.0 → 1.2.1
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/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/integrate.js +9 -3
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/package.json +4 -2
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +0 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseClaudeStreamJson = parseClaudeStreamJson;
|
|
4
|
+
exports.extractClaudeTelemetryFromStdout = extractClaudeTelemetryFromStdout;
|
|
3
5
|
// Claude telemetry is read from the Claude CLI's `--output-format stream-json`
|
|
4
6
|
// stdout stream (see claude.js `buildClaudeInvocation`). Unlike Codex — which
|
|
5
7
|
// writes a per-session rollout JSONL to disk — the Claude CLI emits its usage
|
|
@@ -33,27 +35,21 @@
|
|
|
33
35
|
// the stage's total generated output is the SUM of each turn's final value.
|
|
34
36
|
// - `cache_read_input_tokens` / `cache_creation_input_tokens` appear on
|
|
35
37
|
// message_start.usage when prompt caching is active; summed across turns.
|
|
36
|
-
|
|
37
38
|
const PROVIDER = 'anthropic';
|
|
38
|
-
|
|
39
|
-
/** @param {*} value */
|
|
40
39
|
function num(value) {
|
|
41
|
-
|
|
40
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
42
41
|
}
|
|
43
|
-
|
|
44
42
|
/**
|
|
45
43
|
* Unwrap a parsed JSONL line into its underlying Anthropic SSE event. The CLI's
|
|
46
44
|
* `--include-partial-messages` form nests the real event under `stream_event`;
|
|
47
45
|
* the bare form has it at the top level. Returns the inner event object.
|
|
48
46
|
*/
|
|
49
|
-
/** @param {any} evt */
|
|
50
47
|
function unwrapEvent(evt) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
if (evt && evt.type === 'stream_event' && evt.event && typeof evt.event === 'object') {
|
|
49
|
+
return evt.event;
|
|
50
|
+
}
|
|
51
|
+
return evt;
|
|
55
52
|
}
|
|
56
|
-
|
|
57
53
|
/**
|
|
58
54
|
* Parse a Claude `stream-json` stdout string into a normalized structure of the
|
|
59
55
|
* usage-bearing events. Returns null when the content yields no usable signal
|
|
@@ -68,97 +64,106 @@ function unwrapEvent(evt) {
|
|
|
68
64
|
* resultUsage: { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens } | null
|
|
69
65
|
* }
|
|
70
66
|
*/
|
|
71
|
-
/** @param {string} content */
|
|
72
67
|
function parseClaudeStreamJson(content) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
let sessionId = null;
|
|
76
|
-
let model = null;
|
|
77
|
-
let toolCalls = 0;
|
|
78
|
-
let resultUsage = null;
|
|
79
|
-
let resultCostUsd = null;
|
|
80
|
-
const messageStarts = [];
|
|
81
|
-
const messageDeltas = [];
|
|
82
|
-
|
|
83
|
-
for (const line of String(content).split('\n')) {
|
|
84
|
-
const trimmed = line.trim();
|
|
85
|
-
if (!trimmed.startsWith('{')) {continue;}
|
|
86
|
-
let outer;
|
|
87
|
-
try {
|
|
88
|
-
outer = JSON.parse(trimmed);
|
|
89
|
-
} catch (_) {
|
|
90
|
-
continue;
|
|
68
|
+
if (!content) {
|
|
69
|
+
return null;
|
|
91
70
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
71
|
+
let sessionId = null;
|
|
72
|
+
let model = null;
|
|
73
|
+
let toolCalls = 0;
|
|
74
|
+
let resultUsage = null;
|
|
75
|
+
let resultCostUsd = null;
|
|
76
|
+
const messageStarts = [];
|
|
77
|
+
const messageDeltas = [];
|
|
78
|
+
for (const line of String(content).split('\n')) {
|
|
79
|
+
const trimmed = line.trim();
|
|
80
|
+
if (!trimmed.startsWith('{')) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
let outer;
|
|
84
|
+
try {
|
|
85
|
+
outer = JSON.parse(trimmed);
|
|
86
|
+
}
|
|
87
|
+
catch (_) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!outer || typeof outer !== 'object') {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Top-level CLI envelope fields (present on system/result and sometimes the
|
|
94
|
+
// wrapper) carry session/model metadata.
|
|
95
|
+
if (outer.session_id) {
|
|
96
|
+
sessionId = outer.session_id;
|
|
97
|
+
}
|
|
98
|
+
if (outer.type === 'system' && outer.model) {
|
|
99
|
+
model = outer.model;
|
|
100
|
+
}
|
|
101
|
+
// The final `result` event carries `total_cost_usd` (direct from the CLI)
|
|
102
|
+
// and an aggregate usage object (fallback when partial events were evicted).
|
|
103
|
+
if (outer.type === 'result') {
|
|
104
|
+
if (outer.usage && typeof outer.usage === 'object') {
|
|
105
|
+
resultUsage = {
|
|
106
|
+
inputTokens: num(outer.usage.input_tokens),
|
|
107
|
+
outputTokens: num(outer.usage.output_tokens),
|
|
108
|
+
cacheReadTokens: num(outer.usage.cache_read_input_tokens),
|
|
109
|
+
cacheCreationTokens: num(outer.usage.cache_creation_input_tokens),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (typeof outer.total_cost_usd === 'number' && Number.isFinite(outer.total_cost_usd)) {
|
|
113
|
+
resultCostUsd = outer.total_cost_usd;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const evt = unwrapEvent(outer);
|
|
117
|
+
if (!evt || typeof evt !== 'object') {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
switch (evt.type) {
|
|
121
|
+
case 'message_start': {
|
|
122
|
+
const message = evt.message || {};
|
|
123
|
+
const usage = message.usage || {};
|
|
124
|
+
if (message.model) {
|
|
125
|
+
model = message.model;
|
|
126
|
+
}
|
|
127
|
+
messageStarts.push({
|
|
128
|
+
inputTokens: num(usage.input_tokens),
|
|
129
|
+
outputTokens: num(usage.output_tokens),
|
|
130
|
+
cacheReadTokens: num(usage.cache_read_input_tokens),
|
|
131
|
+
cacheCreationTokens: num(usage.cache_creation_input_tokens),
|
|
132
|
+
model: message.model || null,
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'message_delta': {
|
|
137
|
+
const usage = evt.usage || {};
|
|
138
|
+
messageDeltas.push({ outputTokens: num(usage.output_tokens) });
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case 'content_block_start': {
|
|
142
|
+
const block = evt.content_block || {};
|
|
143
|
+
if (block.type === 'tool_use') {
|
|
144
|
+
toolCalls += 1;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
113
151
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
switch (evt.type) {
|
|
119
|
-
case 'message_start': {
|
|
120
|
-
const message = evt.message || {};
|
|
121
|
-
const usage = message.usage || {};
|
|
122
|
-
if (message.model) {model = message.model;}
|
|
123
|
-
messageStarts.push({
|
|
124
|
-
inputTokens: num(usage.input_tokens),
|
|
125
|
-
outputTokens: num(usage.output_tokens),
|
|
126
|
-
cacheReadTokens: num(usage.cache_read_input_tokens),
|
|
127
|
-
cacheCreationTokens: num(usage.cache_creation_input_tokens),
|
|
128
|
-
model: message.model || null,
|
|
129
|
-
});
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
case 'message_delta': {
|
|
133
|
-
const usage = evt.usage || {};
|
|
134
|
-
messageDeltas.push({ outputTokens: num(usage.output_tokens) });
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
case 'content_block_start': {
|
|
138
|
-
const block = evt.content_block || {};
|
|
139
|
-
if (block.type === 'tool_use') {toolCalls += 1;}
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
default:
|
|
143
|
-
break;
|
|
152
|
+
const hasSignal = messageStarts.length > 0 || messageDeltas.length > 0 || resultUsage || model || sessionId;
|
|
153
|
+
if (!hasSignal) {
|
|
154
|
+
return null;
|
|
144
155
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
messageDeltas,
|
|
156
|
-
toolCalls,
|
|
157
|
-
resultUsage,
|
|
158
|
-
resultCostUsd,
|
|
159
|
-
};
|
|
156
|
+
return {
|
|
157
|
+
sessionId,
|
|
158
|
+
model: model || null,
|
|
159
|
+
provider: PROVIDER,
|
|
160
|
+
messageStarts,
|
|
161
|
+
messageDeltas,
|
|
162
|
+
toolCalls,
|
|
163
|
+
resultUsage,
|
|
164
|
+
resultCostUsd,
|
|
165
|
+
};
|
|
160
166
|
}
|
|
161
|
-
|
|
162
167
|
/**
|
|
163
168
|
* Aggregate token counts from a Claude `stream-json` stdout stream into a
|
|
164
169
|
* telemetry object compatible with `stats.telemetryToStatsFields()`.
|
|
@@ -177,61 +182,51 @@ function parseClaudeStreamJson(content) {
|
|
|
177
182
|
*
|
|
178
183
|
* Returns null when the stream yields no usable token signal.
|
|
179
184
|
*/
|
|
180
|
-
/** @param {string} stdout */
|
|
181
185
|
function extractClaudeTelemetryFromStdout(stdout) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
usagePercent: null,
|
|
229
|
-
// total_cost_usd is emitted directly by the Claude CLI in the result event.
|
|
230
|
-
cost_usd: typeof resultCostUsd === 'number' ? resultCostUsd : 0,
|
|
231
|
-
};
|
|
186
|
+
const parsed = parseClaudeStreamJson(stdout);
|
|
187
|
+
if (!parsed) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const { messageStarts, messageDeltas, resultUsage, resultCostUsd } = parsed;
|
|
191
|
+
let inputTokens = messageStarts.length > 0 ? messageStarts[0].inputTokens : 0;
|
|
192
|
+
let outputTokens = messageDeltas.reduce((sum, d) => sum + d.outputTokens, 0);
|
|
193
|
+
let cachedTokens = messageStarts.reduce((sum, s) => sum + s.cacheReadTokens, 0);
|
|
194
|
+
let cacheCreationTokens = messageStarts.reduce((sum, s) => sum + s.cacheCreationTokens, 0);
|
|
195
|
+
// If partial events were truncated out of the captured tail, fall back to the
|
|
196
|
+
// final `result` event's aggregate usage so we still record real numbers.
|
|
197
|
+
if (inputTokens === 0 && outputTokens === 0 && resultUsage) {
|
|
198
|
+
inputTokens = resultUsage.inputTokens;
|
|
199
|
+
outputTokens = resultUsage.outputTokens;
|
|
200
|
+
cachedTokens = resultUsage.cacheReadTokens;
|
|
201
|
+
cacheCreationTokens = resultUsage.cacheCreationTokens;
|
|
202
|
+
}
|
|
203
|
+
// NOTE: a small `inputTokens` (even 1) alongside a large `cachedTokens` is NOT
|
|
204
|
+
// an artifact — it is normal prompt caching. Anthropic streaming reports only
|
|
205
|
+
// the *uncached* prompt delta as `message_start.usage.input_tokens`; the bulk
|
|
206
|
+
// of the re-sent context is billed under `cache_read_input_tokens`. We keep
|
|
207
|
+
// the parsed value truthful and surface caching honestly in the stats render
|
|
208
|
+
// rather than rewriting telemetry here (see lib/commands/stats.js render).
|
|
209
|
+
// No token signal at all (only metadata events): not worth a telemetry row.
|
|
210
|
+
if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0 && cacheCreationTokens === 0) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
sessionId: parsed.sessionId,
|
|
215
|
+
provider: parsed.provider,
|
|
216
|
+
model: parsed.model,
|
|
217
|
+
effort: null,
|
|
218
|
+
inputTokens,
|
|
219
|
+
outputTokens,
|
|
220
|
+
cachedTokens,
|
|
221
|
+
cacheCreationTokens,
|
|
222
|
+
reasoningTokens: 0,
|
|
223
|
+
totalTokens: inputTokens + outputTokens,
|
|
224
|
+
contextWindow: 0,
|
|
225
|
+
toolCalls: parsed.toolCalls,
|
|
226
|
+
// Claude's CLI exposes no rate-limit percentage (unlike Codex); leave null
|
|
227
|
+
// so telemetryToStatsFields records openai_usage_after as 0.
|
|
228
|
+
usagePercent: null,
|
|
229
|
+
// total_cost_usd is emitted directly by the Claude CLI in the result event.
|
|
230
|
+
cost_usd: typeof resultCostUsd === 'number' ? resultCostUsd : 0,
|
|
231
|
+
};
|
|
232
232
|
}
|
|
233
|
-
|
|
234
|
-
module.exports = {
|
|
235
|
-
parseClaudeStreamJson,
|
|
236
|
-
extractClaudeTelemetryFromStdout,
|
|
237
|
-
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Claude telemetry is read from the Claude CLI's `--output-format stream-json`
|
|
4
|
+
// stdout stream (see claude.js `buildClaudeInvocation`). Unlike Codex — which
|
|
5
|
+
// writes a per-session rollout JSONL to disk — the Claude CLI emits its usage
|
|
6
|
+
// signal inline on stdout as SSE-style JSONL events, so we parse the captured
|
|
7
|
+
// stdout tail directly (no on-disk transcript to read).
|
|
8
|
+
//
|
|
9
|
+
// The CLI runs with `--include-partial-messages`, so the Anthropic SSE events
|
|
10
|
+
// are forwarded as JSONL. Two envelope shapes are handled:
|
|
11
|
+
//
|
|
12
|
+
// 1. Wrapped (real CLI form): each partial event is nested under a
|
|
13
|
+
// `stream_event` wrapper:
|
|
14
|
+
// {"type":"stream_event","event":{"type":"message_start","message":{"usage":{...}}}}
|
|
15
|
+
// {"type":"stream_event","event":{"type":"message_delta","usage":{"output_tokens":N}}}
|
|
16
|
+
// {"type":"stream_event","event":{"type":"content_block_start","content_block":{"type":"tool_use"}}}
|
|
17
|
+
// 2. Raw (bare SSE form): the Anthropic event sits at the top level:
|
|
18
|
+
// {"type":"message_start","message":{"usage":{...}}}
|
|
19
|
+
// {"type":"message_delta","usage":{"output_tokens":N}}
|
|
20
|
+
//
|
|
21
|
+
// The CLI also emits top-level `system` (init), `assistant`, and `result`
|
|
22
|
+
// events; `system`/`result` carry `session_id` and `model`, and `result`
|
|
23
|
+
// carries a final aggregate `usage` used as a fallback when partial events were
|
|
24
|
+
// truncated out of the captured stdout tail.
|
|
25
|
+
//
|
|
26
|
+
// Token semantics (Anthropic streaming):
|
|
27
|
+
// - `message_start.usage.input_tokens` is the prompt size for that turn. A
|
|
28
|
+
// multi-turn (tool-use) invocation re-sends the growing context each turn,
|
|
29
|
+
// so summing every turn's input would multi-count the shared prompt. We take
|
|
30
|
+
// the FIRST message_start's input_tokens as the representative prompt size.
|
|
31
|
+
// - `message_delta.usage.output_tokens` is the FINAL cumulative output count
|
|
32
|
+
// for that one message. Across turns these are independent generations, so
|
|
33
|
+
// the stage's total generated output is the SUM of each turn's final value.
|
|
34
|
+
// - `cache_read_input_tokens` / `cache_creation_input_tokens` appear on
|
|
35
|
+
// message_start.usage when prompt caching is active; summed across turns.
|
|
36
|
+
|
|
37
|
+
const PROVIDER = 'anthropic';
|
|
38
|
+
|
|
39
|
+
function num(value: any) {
|
|
40
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Unwrap a parsed JSONL line into its underlying Anthropic SSE event. The CLI's
|
|
45
|
+
* `--include-partial-messages` form nests the real event under `stream_event`;
|
|
46
|
+
* the bare form has it at the top level. Returns the inner event object.
|
|
47
|
+
*/
|
|
48
|
+
function unwrapEvent(evt: any) {
|
|
49
|
+
if (evt && evt.type === 'stream_event' && evt.event && typeof evt.event === 'object') {
|
|
50
|
+
return evt.event;
|
|
51
|
+
}
|
|
52
|
+
return evt;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse a Claude `stream-json` stdout string into a normalized structure of the
|
|
57
|
+
* usage-bearing events. Returns null when the content yields no usable signal
|
|
58
|
+
* (e.g. empty, garbage, or a stream with no usage events at all).
|
|
59
|
+
*
|
|
60
|
+
* Shape:
|
|
61
|
+
* {
|
|
62
|
+
* sessionId, model, provider: 'anthropic',
|
|
63
|
+
* messageStarts: [{ inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, model }],
|
|
64
|
+
* messageDeltas: [{ outputTokens }],
|
|
65
|
+
* toolCalls,
|
|
66
|
+
* resultUsage: { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens } | null
|
|
67
|
+
* }
|
|
68
|
+
*/
|
|
69
|
+
function parseClaudeStreamJson(content: string) {
|
|
70
|
+
if (!content) {return null;}
|
|
71
|
+
|
|
72
|
+
let sessionId = null;
|
|
73
|
+
let model = null;
|
|
74
|
+
let toolCalls = 0;
|
|
75
|
+
let resultUsage = null;
|
|
76
|
+
let resultCostUsd = null;
|
|
77
|
+
const messageStarts = [];
|
|
78
|
+
const messageDeltas = [];
|
|
79
|
+
|
|
80
|
+
for (const line of String(content).split('\n')) {
|
|
81
|
+
const trimmed = line.trim();
|
|
82
|
+
if (!trimmed.startsWith('{')) {continue;}
|
|
83
|
+
let outer;
|
|
84
|
+
try {
|
|
85
|
+
outer = JSON.parse(trimmed);
|
|
86
|
+
} catch (_) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!outer || typeof outer !== 'object') {continue;}
|
|
90
|
+
|
|
91
|
+
// Top-level CLI envelope fields (present on system/result and sometimes the
|
|
92
|
+
// wrapper) carry session/model metadata.
|
|
93
|
+
if (outer.session_id) {sessionId = outer.session_id;}
|
|
94
|
+
if (outer.type === 'system' && outer.model) {model = outer.model;}
|
|
95
|
+
|
|
96
|
+
// The final `result` event carries `total_cost_usd` (direct from the CLI)
|
|
97
|
+
// and an aggregate usage object (fallback when partial events were evicted).
|
|
98
|
+
if (outer.type === 'result') {
|
|
99
|
+
if (outer.usage && typeof outer.usage === 'object') {
|
|
100
|
+
resultUsage = {
|
|
101
|
+
inputTokens: num(outer.usage.input_tokens),
|
|
102
|
+
outputTokens: num(outer.usage.output_tokens),
|
|
103
|
+
cacheReadTokens: num(outer.usage.cache_read_input_tokens),
|
|
104
|
+
cacheCreationTokens: num(outer.usage.cache_creation_input_tokens),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (typeof outer.total_cost_usd === 'number' && Number.isFinite(outer.total_cost_usd)) {
|
|
108
|
+
resultCostUsd = outer.total_cost_usd;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const evt = unwrapEvent(outer);
|
|
113
|
+
if (!evt || typeof evt !== 'object') {continue;}
|
|
114
|
+
|
|
115
|
+
switch (evt.type) {
|
|
116
|
+
case 'message_start': {
|
|
117
|
+
const message = evt.message || {};
|
|
118
|
+
const usage = message.usage || {};
|
|
119
|
+
if (message.model) {model = message.model;}
|
|
120
|
+
messageStarts.push({
|
|
121
|
+
inputTokens: num(usage.input_tokens),
|
|
122
|
+
outputTokens: num(usage.output_tokens),
|
|
123
|
+
cacheReadTokens: num(usage.cache_read_input_tokens),
|
|
124
|
+
cacheCreationTokens: num(usage.cache_creation_input_tokens),
|
|
125
|
+
model: message.model || null,
|
|
126
|
+
});
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'message_delta': {
|
|
130
|
+
const usage = evt.usage || {};
|
|
131
|
+
messageDeltas.push({ outputTokens: num(usage.output_tokens) });
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case 'content_block_start': {
|
|
135
|
+
const block = evt.content_block || {};
|
|
136
|
+
if (block.type === 'tool_use') {toolCalls += 1;}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
default:
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const hasSignal = messageStarts.length > 0 || messageDeltas.length > 0 || resultUsage || model || sessionId;
|
|
145
|
+
if (!hasSignal) {return null;}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
sessionId,
|
|
149
|
+
model: model || null,
|
|
150
|
+
provider: PROVIDER,
|
|
151
|
+
messageStarts,
|
|
152
|
+
messageDeltas,
|
|
153
|
+
toolCalls,
|
|
154
|
+
resultUsage,
|
|
155
|
+
resultCostUsd,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Aggregate token counts from a Claude `stream-json` stdout stream into a
|
|
161
|
+
* telemetry object compatible with `stats.telemetryToStatsFields()`.
|
|
162
|
+
*
|
|
163
|
+
* Aggregation rules (see token-semantics note at top of file):
|
|
164
|
+
* - inputTokens = first message_start's input_tokens (representative prompt
|
|
165
|
+
* size; avoids multi-counting the re-sent context across
|
|
166
|
+
* tool-use turns).
|
|
167
|
+
* - outputTokens = sum of every message_delta's output_tokens (total
|
|
168
|
+
* generation across all turns).
|
|
169
|
+
* - cachedTokens = sum of message_start cache_read_input_tokens across turns.
|
|
170
|
+
* - totalTokens = inputTokens + outputTokens.
|
|
171
|
+
*
|
|
172
|
+
* When partial events are absent (e.g. truncated stdout tail) but a `result`
|
|
173
|
+
* event survived, its aggregate usage is used as a fallback.
|
|
174
|
+
*
|
|
175
|
+
* Returns null when the stream yields no usable token signal.
|
|
176
|
+
*/
|
|
177
|
+
function extractClaudeTelemetryFromStdout(stdout: string) {
|
|
178
|
+
const parsed = parseClaudeStreamJson(stdout);
|
|
179
|
+
if (!parsed) {return null;}
|
|
180
|
+
|
|
181
|
+
const { messageStarts, messageDeltas, resultUsage, resultCostUsd } = parsed;
|
|
182
|
+
|
|
183
|
+
let inputTokens = messageStarts.length > 0 ? messageStarts[0].inputTokens : 0;
|
|
184
|
+
let outputTokens = messageDeltas.reduce((sum, d) => sum + d.outputTokens, 0);
|
|
185
|
+
let cachedTokens = messageStarts.reduce((sum, s) => sum + s.cacheReadTokens, 0);
|
|
186
|
+
let cacheCreationTokens = messageStarts.reduce((sum, s) => sum + s.cacheCreationTokens, 0);
|
|
187
|
+
|
|
188
|
+
// If partial events were truncated out of the captured tail, fall back to the
|
|
189
|
+
// final `result` event's aggregate usage so we still record real numbers.
|
|
190
|
+
if (inputTokens === 0 && outputTokens === 0 && resultUsage) {
|
|
191
|
+
inputTokens = resultUsage.inputTokens;
|
|
192
|
+
outputTokens = resultUsage.outputTokens;
|
|
193
|
+
cachedTokens = resultUsage.cacheReadTokens;
|
|
194
|
+
cacheCreationTokens = resultUsage.cacheCreationTokens;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// NOTE: a small `inputTokens` (even 1) alongside a large `cachedTokens` is NOT
|
|
198
|
+
// an artifact — it is normal prompt caching. Anthropic streaming reports only
|
|
199
|
+
// the *uncached* prompt delta as `message_start.usage.input_tokens`; the bulk
|
|
200
|
+
// of the re-sent context is billed under `cache_read_input_tokens`. We keep
|
|
201
|
+
// the parsed value truthful and surface caching honestly in the stats render
|
|
202
|
+
// rather than rewriting telemetry here (see lib/commands/stats.js render).
|
|
203
|
+
|
|
204
|
+
// No token signal at all (only metadata events): not worth a telemetry row.
|
|
205
|
+
if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0 && cacheCreationTokens === 0) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
sessionId: parsed.sessionId,
|
|
211
|
+
provider: parsed.provider,
|
|
212
|
+
model: parsed.model,
|
|
213
|
+
effort: null,
|
|
214
|
+
inputTokens,
|
|
215
|
+
outputTokens,
|
|
216
|
+
cachedTokens,
|
|
217
|
+
cacheCreationTokens,
|
|
218
|
+
reasoningTokens: 0,
|
|
219
|
+
totalTokens: inputTokens + outputTokens,
|
|
220
|
+
contextWindow: 0,
|
|
221
|
+
toolCalls: parsed.toolCalls,
|
|
222
|
+
// Claude's CLI exposes no rate-limit percentage (unlike Codex); leave null
|
|
223
|
+
// so telemetryToStatsFields records openai_usage_after as 0.
|
|
224
|
+
usagePercent: null,
|
|
225
|
+
// total_cost_usd is emitted directly by the Claude CLI in the result event.
|
|
226
|
+
cost_usd: typeof resultCostUsd === 'number' ? resultCostUsd : 0,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export {
|
|
231
|
+
parseClaudeStreamJson,
|
|
232
|
+
extractClaudeTelemetryFromStdout,
|
|
233
|
+
};
|