@outputai/cli 0.7.1-next.ae5bab4.0 → 0.7.1-next.bd6bd49.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/bin/run.js +1 -1
- package/dist/api/generated/api.d.ts +38 -0
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/generated/framework_version.json +1 -1
- package/dist/hooks/init.js +12 -3
- package/dist/hooks/init.spec.js +18 -8
- package/dist/scripts/refresh_version_check.d.ts +1 -0
- package/dist/scripts/refresh_version_check.js +9 -0
- package/dist/services/cost_calculator.d.ts +1 -5
- package/dist/services/cost_calculator.js +214 -102
- package/dist/services/cost_calculator.spec.js +329 -253
- package/dist/services/npm_update_service.js +11 -3
- package/dist/services/npm_update_service.spec.js +20 -7
- package/dist/services/version_check.d.ts +19 -1
- package/dist/services/version_check.js +53 -17
- package/dist/services/version_check.spec.js +88 -58
- package/dist/types/cost.d.ts +64 -23
- package/dist/types/cost.js +4 -0
- package/dist/utils/cost_formatter.js +65 -43
- package/dist/utils/proxy.d.ts +3 -2
- package/dist/utils/proxy.js +4 -3
- package/dist/utils/proxy.spec.js +4 -4
- package/oclif.manifest.json +1428 -0
- package/package.json +6 -5
|
@@ -5,6 +5,43 @@ const ARRAY_ACCESS_PATTERN = /^(\w+)\[(\d+)\]$/;
|
|
|
5
5
|
function tokenCost(tokens, pricePerMillion) {
|
|
6
6
|
return (tokens / 1_000_000) * pricePerMillion;
|
|
7
7
|
}
|
|
8
|
+
function hostFromUrl(url) {
|
|
9
|
+
if (!url) {
|
|
10
|
+
return 'unknown';
|
|
11
|
+
}
|
|
12
|
+
return url.replace(/^https?:\/\//, '').split('/')[0] || 'unknown';
|
|
13
|
+
}
|
|
14
|
+
function lineRate(type, pricing) {
|
|
15
|
+
const rates = {
|
|
16
|
+
input: pricing.input ?? 0,
|
|
17
|
+
input_cached: pricing.cached_input ?? 0,
|
|
18
|
+
output: pricing.output ?? 0,
|
|
19
|
+
reasoning: pricing.reasoning ?? pricing.output ?? 0
|
|
20
|
+
};
|
|
21
|
+
return rates[type];
|
|
22
|
+
}
|
|
23
|
+
// Re-prices the event's usage lines at costs.yml rates. A line type without a
|
|
24
|
+
// configured mapping degrades to its as-charged total rather than $0, so new
|
|
25
|
+
// producer line types never silently vanish from the adjusted figure.
|
|
26
|
+
function priceLines(lines, pricing) {
|
|
27
|
+
return lines.reduce((sum, line) => {
|
|
28
|
+
const rate = lineRate(line.type, pricing);
|
|
29
|
+
return sum + (rate === undefined ? line.total : tokenCost(line.amount, rate));
|
|
30
|
+
}, 0);
|
|
31
|
+
}
|
|
32
|
+
// Token counts for display. The producer's 'input' line excludes cached tokens
|
|
33
|
+
// (sdk/llm emits input − cached), while AI SDK reports inputTokens as the
|
|
34
|
+
// total — add cached back so the columns match what providers report.
|
|
35
|
+
function eventTokenUsage(lines) {
|
|
36
|
+
const sumOf = (type) => lines.filter(l => l.type === type).reduce((s, l) => s + l.amount, 0);
|
|
37
|
+
const cached = sumOf('input_cached');
|
|
38
|
+
return {
|
|
39
|
+
inputTokens: sumOf('input') + cached,
|
|
40
|
+
cachedInputTokens: cached,
|
|
41
|
+
outputTokens: sumOf('output'),
|
|
42
|
+
reasoningTokens: sumOf('reasoning')
|
|
43
|
+
};
|
|
44
|
+
}
|
|
8
45
|
export function extractValue(obj, path) {
|
|
9
46
|
if (!path || !obj) {
|
|
10
47
|
return obj;
|
|
@@ -69,42 +106,37 @@ function findCalls(node, match, extract, parentStepName = null, seenIds = new Se
|
|
|
69
106
|
}
|
|
70
107
|
return calls;
|
|
71
108
|
}
|
|
109
|
+
// Only nodes carrying an llm:usage event are priced — the event holds the
|
|
110
|
+
// as-charged cost and the per-token-type amounts. Traces from SDKs that
|
|
111
|
+
// predate cost attributes report no LLM costs.
|
|
72
112
|
export function findLLMCalls(node, parentStepName = null, seenIds = new Set()) {
|
|
73
|
-
return findCalls(node, n => n.kind === 'llm' && !!n.
|
|
74
|
-
const
|
|
75
|
-
const outputRecord = n.output;
|
|
76
|
-
const inputRecord = n.input;
|
|
77
|
-
const model = loadedPrompt?.config?.model ||
|
|
78
|
-
outputRecord?.model ||
|
|
79
|
-
inputRecord?.model ||
|
|
80
|
-
'unknown';
|
|
113
|
+
return findCalls(node, n => n.kind === 'llm' && !!n.attributes?.['llm:usage'], (n, stepName) => {
|
|
114
|
+
const event = n.attributes['llm:usage'];
|
|
81
115
|
return {
|
|
82
116
|
stepName: stepName || n.name || 'unknown',
|
|
83
117
|
llmName: n.name || 'llm',
|
|
84
|
-
model,
|
|
85
|
-
usage:
|
|
118
|
+
model: event.modelId || 'unknown',
|
|
119
|
+
usage: eventTokenUsage(event.usage ?? []),
|
|
120
|
+
originalCost: event.total,
|
|
121
|
+
lines: event.usage ?? []
|
|
86
122
|
};
|
|
87
123
|
}, parentStepName, seenIds);
|
|
88
124
|
}
|
|
89
125
|
export function findHTTPCalls(node, parentStepName = null, seenIds = new Set()) {
|
|
90
|
-
return findCalls(node, n => n.kind === 'http', (n, stepName) =>
|
|
91
|
-
|
|
92
|
-
url
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const outputCost = tokenCost(usage.outputTokens ?? 0, modelPricing.output ?? 0);
|
|
105
|
-
const cachedCost = tokenCost(usage.cachedInputTokens ?? 0, modelPricing.cached_input ?? 0);
|
|
106
|
-
const reasoningCost = tokenCost(usage.reasoningTokens ?? 0, modelPricing.reasoning || modelPricing.output || 0);
|
|
107
|
-
return { cost: inputCost + outputCost + cachedCost + reasoningCost };
|
|
126
|
+
return findCalls(node, n => n.kind === 'http', (n, stepName) => {
|
|
127
|
+
const costEvent = n.attributes?.['http:request:cost'];
|
|
128
|
+
const url = costEvent?.url || n.input?.url || '';
|
|
129
|
+
return {
|
|
130
|
+
stepName: stepName || 'unknown',
|
|
131
|
+
url,
|
|
132
|
+
method: n.input?.method || 'GET',
|
|
133
|
+
input: n.input || {},
|
|
134
|
+
output: n.output || {},
|
|
135
|
+
status: n.output?.status,
|
|
136
|
+
host: hostFromUrl(url),
|
|
137
|
+
originalCost: costEvent?.total
|
|
138
|
+
};
|
|
139
|
+
}, parentStepName, seenIds);
|
|
108
140
|
}
|
|
109
141
|
export function identifyService(httpCall, services) {
|
|
110
142
|
if (!services) {
|
|
@@ -119,31 +151,51 @@ export function identifyService(httpCall, services) {
|
|
|
119
151
|
}
|
|
120
152
|
function calculateTokenServiceCost(httpCall, config) {
|
|
121
153
|
if (!config.usage_path) {
|
|
122
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
step: httpCall.stepName, cost: 0, usage: 'no usage data',
|
|
156
|
+
kind: 'failed', warning: 'no usage data'
|
|
157
|
+
};
|
|
123
158
|
}
|
|
124
159
|
const usage = extractValue(httpCall.output, config.usage_path);
|
|
125
160
|
if (config.input_field && config.output_field) {
|
|
126
161
|
const usageObj = usage;
|
|
127
|
-
|
|
128
|
-
|
|
162
|
+
if (!usageObj) {
|
|
163
|
+
return {
|
|
164
|
+
step: httpCall.stepName, cost: 0, usage: 'no usage data',
|
|
165
|
+
kind: 'failed', warning: 'no usage data'
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const inputTokens = usageObj[config.input_field] ?? 0;
|
|
169
|
+
const outputTokens = usageObj[config.output_field] ?? 0;
|
|
129
170
|
const inputCost = tokenCost(inputTokens, config.input_per_million ?? 0);
|
|
130
171
|
const outputCost = tokenCost(outputTokens, config.output_per_million ?? 0);
|
|
131
172
|
return {
|
|
132
173
|
step: httpCall.stepName,
|
|
133
174
|
cost: inputCost + outputCost,
|
|
134
|
-
usage: `${(inputTokens + outputTokens).toLocaleString('en-US')} tokens
|
|
175
|
+
usage: `${(inputTokens + outputTokens).toLocaleString('en-US')} tokens`,
|
|
176
|
+
kind: 'computed'
|
|
135
177
|
};
|
|
136
178
|
}
|
|
137
179
|
const tokens = typeof usage === 'number' ? usage : 0;
|
|
138
180
|
if (tokens === 0) {
|
|
139
|
-
return {
|
|
181
|
+
return {
|
|
182
|
+
step: httpCall.stepName, cost: 0, usage: 'no usage data',
|
|
183
|
+
kind: 'failed', warning: 'no usage data'
|
|
184
|
+
};
|
|
140
185
|
}
|
|
141
186
|
const cost = tokenCost(tokens, config.per_million ?? 0);
|
|
142
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
step: httpCall.stepName,
|
|
189
|
+
cost,
|
|
190
|
+
usage: `${tokens.toLocaleString('en-US')} tokens`,
|
|
191
|
+
kind: 'computed'
|
|
192
|
+
};
|
|
143
193
|
}
|
|
194
|
+
// units: undefined means the call couldn't be measured (no endpoint match, or
|
|
195
|
+
// a units_per_line endpoint without a string body) — distinct from a measured 0.
|
|
144
196
|
function resolveUnitEndpoint(url, httpCall, config) {
|
|
145
197
|
if (!config.endpoints) {
|
|
146
|
-
return { units:
|
|
198
|
+
return { units: undefined, endpoint: 'unknown' };
|
|
147
199
|
}
|
|
148
200
|
for (const [endpointName, endpointConfig] of Object.entries(config.endpoints)) {
|
|
149
201
|
if (!url.includes(endpointConfig.pattern)) {
|
|
@@ -160,25 +212,40 @@ function resolveUnitEndpoint(url, httpCall, config) {
|
|
|
160
212
|
return { units, endpoint: endpointName };
|
|
161
213
|
}
|
|
162
214
|
}
|
|
163
|
-
return { units:
|
|
215
|
+
return { units: undefined, endpoint: endpointName };
|
|
164
216
|
}
|
|
165
|
-
return { units:
|
|
217
|
+
return { units: undefined, endpoint: 'unknown' };
|
|
166
218
|
}
|
|
167
219
|
function calculateUnitServiceCost(httpCall, config) {
|
|
168
220
|
const { units, endpoint } = resolveUnitEndpoint(httpCall.url, httpCall, config);
|
|
221
|
+
if (units === undefined) {
|
|
222
|
+
return {
|
|
223
|
+
step: httpCall.stepName, cost: 0, usage: '0 units',
|
|
224
|
+
kind: 'failed', warning: 'unknown endpoint', endpoint
|
|
225
|
+
};
|
|
226
|
+
}
|
|
169
227
|
const cost = units * (config.price_per_unit || 0);
|
|
170
228
|
return {
|
|
171
229
|
step: httpCall.stepName,
|
|
172
230
|
cost,
|
|
173
231
|
usage: `${units.toLocaleString('en-US')} units`,
|
|
232
|
+
kind: 'computed',
|
|
174
233
|
endpoint
|
|
175
234
|
};
|
|
176
235
|
}
|
|
177
236
|
function calculateRequestServiceCost(httpCall, config) {
|
|
178
237
|
if (config.models && config.model_path) {
|
|
179
238
|
const model = extractValue(httpCall.input, config.model_path);
|
|
180
|
-
|
|
181
|
-
|
|
239
|
+
// ?? rather than || so a configured price of 0 (free tier) is honored.
|
|
240
|
+
const price = (model !== undefined ? config.models[model] : undefined) ??
|
|
241
|
+
config.default_price;
|
|
242
|
+
if (price === undefined) {
|
|
243
|
+
return {
|
|
244
|
+
step: httpCall.stepName, cost: 0, usage: '1 request',
|
|
245
|
+
kind: 'failed', warning: 'unknown model price', model
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return { step: httpCall.stepName, cost: price, usage: '1 request', kind: 'computed', model };
|
|
182
249
|
}
|
|
183
250
|
if (config.endpoints) {
|
|
184
251
|
for (const [endpointName, endpointConfig] of Object.entries(config.endpoints)) {
|
|
@@ -188,27 +255,40 @@ function calculateRequestServiceCost(httpCall, config) {
|
|
|
188
255
|
step: httpCall.stepName,
|
|
189
256
|
cost: endpointConfig.price,
|
|
190
257
|
usage: '1 request',
|
|
258
|
+
kind: 'computed',
|
|
191
259
|
endpoint: endpointName
|
|
192
260
|
};
|
|
193
261
|
}
|
|
194
262
|
if (endpointConfig.price_per_item && endpointConfig.items_path) {
|
|
195
263
|
const items = extractValue(httpCall.input, endpointConfig.items_path);
|
|
196
|
-
|
|
264
|
+
// Missing/un-captured request body is not the same as zero items —
|
|
265
|
+
// only a real array is a measured count.
|
|
266
|
+
if (!Array.isArray(items)) {
|
|
267
|
+
return {
|
|
268
|
+
step: httpCall.stepName, cost: 0, usage: 'items not captured',
|
|
269
|
+
kind: 'failed', warning: 'items not captured', endpoint: endpointName
|
|
270
|
+
};
|
|
271
|
+
}
|
|
197
272
|
return {
|
|
198
273
|
step: httpCall.stepName,
|
|
199
|
-
cost:
|
|
200
|
-
usage: `${
|
|
274
|
+
cost: items.length * endpointConfig.price_per_item,
|
|
275
|
+
usage: `${items.length} items`,
|
|
276
|
+
kind: 'computed',
|
|
201
277
|
endpoint: endpointName
|
|
202
278
|
};
|
|
203
279
|
}
|
|
204
280
|
}
|
|
205
281
|
}
|
|
206
282
|
}
|
|
207
|
-
return {
|
|
283
|
+
return {
|
|
284
|
+
step: httpCall.stepName, cost: 0, usage: 'unknown endpoint',
|
|
285
|
+
kind: 'failed', warning: 'unknown endpoint'
|
|
286
|
+
};
|
|
208
287
|
}
|
|
209
288
|
function calculateResponseCostService(httpCall, config) {
|
|
210
289
|
const cost = extractValue(httpCall, config.cost_path);
|
|
211
|
-
|
|
290
|
+
// A provider-reported cost — including a legitimate $0 — is an exact figure.
|
|
291
|
+
if (typeof cost === 'number') {
|
|
212
292
|
const costDollars = extractValue(httpCall, 'output.body.costDollars');
|
|
213
293
|
const model = extractValue(httpCall, 'output.body.model');
|
|
214
294
|
const numSearches = costDollars?.numSearches ?? 0;
|
|
@@ -217,6 +297,7 @@ function calculateResponseCostService(httpCall, config) {
|
|
|
217
297
|
step: httpCall.stepName,
|
|
218
298
|
cost,
|
|
219
299
|
usage: `${numSearches} searches, ${Math.round(numPages)} pages`,
|
|
300
|
+
kind: 'computed',
|
|
220
301
|
model: model || 'unknown',
|
|
221
302
|
details: costDollars
|
|
222
303
|
};
|
|
@@ -226,27 +307,37 @@ function calculateResponseCostService(httpCall, config) {
|
|
|
226
307
|
extractValue(httpCall, 'output.body.model') ||
|
|
227
308
|
'unknown';
|
|
228
309
|
const fallbackPrice = config.fallback_models[model];
|
|
229
|
-
if (fallbackPrice) {
|
|
310
|
+
if (fallbackPrice !== undefined) {
|
|
230
311
|
return {
|
|
231
312
|
step: httpCall.stepName,
|
|
232
313
|
cost: fallbackPrice,
|
|
233
314
|
usage: '1 request (estimated)',
|
|
315
|
+
kind: 'estimated',
|
|
234
316
|
model,
|
|
235
317
|
warning: 'using fallback estimate'
|
|
236
318
|
};
|
|
237
319
|
}
|
|
238
|
-
if (config.default_fallback) {
|
|
320
|
+
if (config.default_fallback !== undefined) {
|
|
239
321
|
return {
|
|
240
322
|
step: httpCall.stepName,
|
|
241
323
|
cost: config.default_fallback,
|
|
242
324
|
usage: '1 request (estimated)',
|
|
325
|
+
kind: 'estimated',
|
|
243
326
|
model: 'unknown',
|
|
244
327
|
warning: 'using default estimate'
|
|
245
328
|
};
|
|
246
329
|
}
|
|
247
330
|
}
|
|
248
|
-
return {
|
|
331
|
+
return {
|
|
332
|
+
step: httpCall.stepName, cost: 0, usage: 'no cost data',
|
|
333
|
+
kind: 'failed', warning: 'no cost data'
|
|
334
|
+
};
|
|
249
335
|
}
|
|
336
|
+
// Body-dependent service rules (token usage paths, response_cost, per-item
|
|
337
|
+
// counts, units_per_line) only produce a 'computed' result on traces recorded
|
|
338
|
+
// with OUTPUT_TRACE_HTTP_VERBOSE=true (the docker-compose-dev default), since
|
|
339
|
+
// production traces omit HTTP bodies — there they fall back to the as-charged
|
|
340
|
+
// event cost.
|
|
250
341
|
export function calculateServiceCost(httpCall, serviceInfo) {
|
|
251
342
|
const { config } = serviceInfo;
|
|
252
343
|
switch (config.type) {
|
|
@@ -259,31 +350,30 @@ export function calculateServiceCost(httpCall, serviceInfo) {
|
|
|
259
350
|
case 'response_cost':
|
|
260
351
|
return calculateResponseCostService(httpCall, config);
|
|
261
352
|
default:
|
|
262
|
-
return {
|
|
353
|
+
return {
|
|
354
|
+
step: httpCall.stepName, cost: 0, usage: 'unknown type',
|
|
355
|
+
kind: 'failed', warning: 'unknown type'
|
|
356
|
+
};
|
|
263
357
|
}
|
|
264
358
|
}
|
|
265
359
|
function findModelPricing(model, models) {
|
|
266
360
|
if (models[model]) {
|
|
267
|
-
return
|
|
361
|
+
return models[model];
|
|
268
362
|
}
|
|
269
|
-
|
|
270
|
-
return prefixMatch ?
|
|
271
|
-
{ pricing: prefixMatch[1], matchedKey: prefixMatch[0] } :
|
|
272
|
-
{ pricing: undefined, matchedKey: undefined };
|
|
363
|
+
return Object.entries(models).find(([key]) => model.startsWith(key))?.[1];
|
|
273
364
|
}
|
|
274
365
|
function aggregateLLMCosts(llmCalls, config) {
|
|
275
|
-
const unknownModels = new Set();
|
|
276
366
|
const results = [];
|
|
277
|
-
const totals = {
|
|
367
|
+
const totals = {
|
|
368
|
+
inputTokens: 0, outputTokens: 0, cachedTokens: 0, reasoningTokens: 0,
|
|
369
|
+
originalCost: 0, adjustedCost: 0
|
|
370
|
+
};
|
|
278
371
|
for (const call of llmCalls) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (!pricing) {
|
|
285
|
-
unknownModels.add(call.model);
|
|
286
|
-
}
|
|
372
|
+
const pricing = findModelPricing(call.model, config.models ?? {});
|
|
373
|
+
// Original = as-charged from the trace event. Adjusted = the event lines
|
|
374
|
+
// re-priced at costs.yml rates, when the model is configured.
|
|
375
|
+
const originalCost = call.originalCost;
|
|
376
|
+
const adjustedCost = pricing ? priceLines(call.lines, pricing) : originalCost;
|
|
287
377
|
results.push({
|
|
288
378
|
step: call.stepName,
|
|
289
379
|
model: call.model,
|
|
@@ -291,14 +381,15 @@ function aggregateLLMCosts(llmCalls, config) {
|
|
|
291
381
|
output: call.usage.outputTokens ?? 0,
|
|
292
382
|
cached: call.usage.cachedInputTokens ?? 0,
|
|
293
383
|
reasoning: call.usage.reasoningTokens ?? 0,
|
|
294
|
-
|
|
295
|
-
|
|
384
|
+
originalCost,
|
|
385
|
+
adjustedCost
|
|
296
386
|
});
|
|
297
387
|
totals.inputTokens += call.usage.inputTokens ?? 0;
|
|
298
388
|
totals.outputTokens += call.usage.outputTokens ?? 0;
|
|
299
389
|
totals.cachedTokens += call.usage.cachedInputTokens ?? 0;
|
|
300
390
|
totals.reasoningTokens += call.usage.reasoningTokens ?? 0;
|
|
301
|
-
totals.
|
|
391
|
+
totals.originalCost += originalCost;
|
|
392
|
+
totals.adjustedCost += adjustedCost;
|
|
302
393
|
}
|
|
303
394
|
return {
|
|
304
395
|
results,
|
|
@@ -306,58 +397,79 @@ function aggregateLLMCosts(llmCalls, config) {
|
|
|
306
397
|
totalOutputTokens: totals.outputTokens,
|
|
307
398
|
totalCachedTokens: totals.cachedTokens,
|
|
308
399
|
totalReasoningTokens: totals.reasoningTokens,
|
|
309
|
-
|
|
310
|
-
|
|
400
|
+
llmOriginalCost: totals.originalCost,
|
|
401
|
+
llmAdjustedCost: totals.adjustedCost
|
|
311
402
|
};
|
|
312
403
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
404
|
+
function pushHTTPResult(acc, result) {
|
|
405
|
+
if (!acc[result.host]) {
|
|
406
|
+
acc[result.host] = {
|
|
407
|
+
host: result.host, calls: [], originalTotalCost: 0, adjustedTotalCost: 0
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
acc[result.host].calls.push(result);
|
|
411
|
+
acc[result.host].originalTotalCost += result.originalCost;
|
|
412
|
+
acc[result.host].adjustedTotalCost += result.adjustedCost;
|
|
413
|
+
}
|
|
414
|
+
// For an event-bearing (billable) request, decide the adjusted cost: apply the
|
|
415
|
+
// costs.yml recompute only when it produced an exact figure ('computed' —
|
|
416
|
+
// which includes a legitimate $0). Estimates and failed recomputes never
|
|
417
|
+
// replace the as-charged cost, and an errored response can't be re-priced
|
|
418
|
+
// from service rules even though its event proves it was charged.
|
|
419
|
+
function resolveHTTPOverride(call, serviceInfo, originalCost) {
|
|
420
|
+
if (!serviceInfo || (call.status && call.status >= 400)) {
|
|
421
|
+
return { adjustedCost: originalCost, usage: 'as-charged' };
|
|
422
|
+
}
|
|
423
|
+
const recompute = calculateServiceCost(call, serviceInfo);
|
|
424
|
+
return recompute.kind === 'computed' ?
|
|
425
|
+
{ adjustedCost: recompute.cost, usage: recompute.usage } :
|
|
426
|
+
{ adjustedCost: originalCost, usage: 'as-charged' };
|
|
427
|
+
}
|
|
428
|
+
// Only calls carrying an http:request:cost event are billable — the event is
|
|
429
|
+
// proof of a charge (counted regardless of HTTP status). Calls without one
|
|
430
|
+
// (count-only webhooks, polling requests, uninstrumented clients) are not
|
|
431
|
+
// priced.
|
|
432
|
+
function aggregateHTTPCosts(httpCalls, config) {
|
|
433
|
+
const hosts = {};
|
|
317
434
|
for (const call of httpCalls) {
|
|
318
|
-
if (call.
|
|
435
|
+
if (call.originalCost === undefined) {
|
|
319
436
|
continue;
|
|
320
437
|
}
|
|
321
438
|
const serviceInfo = identifyService(call, config.services);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
const result = calculateServiceCost(call, serviceInfo);
|
|
334
|
-
if (!serviceResults[serviceInfo.serviceName]) {
|
|
335
|
-
serviceResults[serviceInfo.serviceName] = {
|
|
336
|
-
serviceName: serviceInfo.serviceName,
|
|
337
|
-
calls: [],
|
|
338
|
-
totalCost: 0
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
serviceResults[serviceInfo.serviceName].calls.push(result);
|
|
342
|
-
serviceResults[serviceInfo.serviceName].totalCost += result.cost;
|
|
439
|
+
const { adjustedCost, usage } = resolveHTTPOverride(call, serviceInfo, call.originalCost);
|
|
440
|
+
pushHTTPResult(hosts, {
|
|
441
|
+
step: call.stepName,
|
|
442
|
+
host: call.host,
|
|
443
|
+
usage,
|
|
444
|
+
originalCost: call.originalCost,
|
|
445
|
+
adjustedCost
|
|
446
|
+
});
|
|
343
447
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
448
|
+
return hosts;
|
|
449
|
+
}
|
|
450
|
+
export function calculateCost(trace, config, traceFile = '') {
|
|
451
|
+
const llmCalls = findLLMCalls(trace);
|
|
452
|
+
const httpCalls = findHTTPCalls(trace);
|
|
453
|
+
const { results: llmResults, totalInputTokens, totalOutputTokens, totalCachedTokens, totalReasoningTokens, llmOriginalCost, llmAdjustedCost } = aggregateLLMCosts(llmCalls, config);
|
|
454
|
+
const httpCosts = Object.values(aggregateHTTPCosts(httpCalls, config));
|
|
455
|
+
const httpOriginalCost = httpCosts.reduce((sum, h) => sum + h.originalTotalCost, 0);
|
|
456
|
+
const httpAdjustedCost = httpCosts.reduce((sum, h) => sum + h.adjustedTotalCost, 0);
|
|
347
457
|
const durationMs = trace.endedAt && trace.startedAt ? trace.endedAt - trace.startedAt : null;
|
|
348
458
|
return {
|
|
349
459
|
traceFile,
|
|
350
460
|
workflowName: trace.name || 'unknown',
|
|
351
461
|
durationMs,
|
|
352
462
|
llmCalls: llmResults,
|
|
353
|
-
|
|
463
|
+
llmOriginalCost,
|
|
464
|
+
llmAdjustedCost,
|
|
354
465
|
totalInputTokens,
|
|
355
466
|
totalOutputTokens,
|
|
356
467
|
totalCachedTokens,
|
|
357
468
|
totalReasoningTokens,
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
469
|
+
httpCosts,
|
|
470
|
+
httpOriginalCost,
|
|
471
|
+
httpAdjustedCost,
|
|
472
|
+
originalTotalCost: llmOriginalCost + httpOriginalCost,
|
|
473
|
+
totalCost: llmAdjustedCost + httpAdjustedCost
|
|
362
474
|
};
|
|
363
475
|
}
|