@outputai/cli 0.7.1-next.d67ad85.0 → 0.7.1-next.db8ddd7.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.
Files changed (34) hide show
  1. package/dist/api/generated/api.d.ts +2 -2
  2. package/dist/api/generated/api.js +2 -2
  3. package/dist/api/workflow_catalog.d.ts +7 -0
  4. package/dist/api/workflow_catalog.js +11 -0
  5. package/dist/api/workflow_catalog.spec.d.ts +1 -0
  6. package/dist/api/workflow_catalog.spec.js +30 -0
  7. package/dist/assets/docker/docker-compose-dev.yml +1 -1
  8. package/dist/commands/workflow/list.d.ts +2 -0
  9. package/dist/commands/workflow/list.js +23 -21
  10. package/dist/commands/workflow/list.spec.js +57 -0
  11. package/dist/commands/workflow/status.js +6 -1
  12. package/dist/generated/framework_version.json +1 -1
  13. package/dist/services/cost_calculator.d.ts +1 -5
  14. package/dist/services/cost_calculator.js +214 -102
  15. package/dist/services/cost_calculator.spec.js +329 -253
  16. package/dist/services/workflow_runs.js +6 -1
  17. package/dist/types/cost.d.ts +64 -23
  18. package/dist/types/cost.js +4 -0
  19. package/dist/utils/cost_formatter.js +65 -43
  20. package/dist/utils/format_workflow_result.js +4 -2
  21. package/dist/utils/format_workflow_result.spec.js +13 -3
  22. package/dist/utils/normalize_workflow_status.d.ts +8 -0
  23. package/dist/utils/normalize_workflow_status.js +8 -0
  24. package/dist/utils/normalize_workflow_status.spec.d.ts +1 -0
  25. package/dist/utils/normalize_workflow_status.spec.js +13 -0
  26. package/dist/utils/scenario_resolver.js +3 -11
  27. package/dist/utils/scenario_resolver.spec.js +5 -9
  28. package/dist/views/dev/components/workflow_status.js +1 -1
  29. package/dist/views/dev/hooks/use_run_detail.js +6 -1
  30. package/dist/views/dev/hooks/use_run_detail.spec.js +1 -1
  31. package/dist/views/dev/hooks/use_workflow_catalog.js +2 -6
  32. package/dist/views/dev/panels/runs_panel.js +1 -1
  33. package/oclif.manifest.json +19 -2
  34. package/package.json +4 -4
@@ -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.output?.usage, (n, stepName) => {
74
- const loadedPrompt = n.input?.loadedPrompt;
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: n.output.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
- stepName: stepName || 'unknown',
92
- url: n.input?.url || '',
93
- method: n.input?.method || 'GET',
94
- input: n.input || {},
95
- output: n.output || {},
96
- status: n.output?.status
97
- }), parentStepName, seenIds);
98
- }
99
- export function calculateLLMCallCost(usage, modelPricing) {
100
- if (!modelPricing) {
101
- return { cost: 0, warning: 'unknown model' };
102
- }
103
- const inputCost = tokenCost(usage.inputTokens ?? 0, modelPricing.input ?? 0);
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 { step: httpCall.stepName, cost: 0, usage: 'no usage data', warning: 'no usage data' };
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
- const inputTokens = usageObj?.[config.input_field] ?? 0;
128
- const outputTokens = usageObj?.[config.output_field] ?? 0;
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 { step: httpCall.stepName, cost: 0, usage: 'no usage data', warning: 'no usage data' };
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 { step: httpCall.stepName, cost, usage: `${tokens.toLocaleString('en-US')} tokens` };
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: 0, endpoint: 'unknown' };
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: 0, endpoint: endpointName };
215
+ return { units: undefined, endpoint: endpointName };
164
216
  }
165
- return { units: 0, endpoint: 'unknown' };
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
- const price = (model && config.models[model]) || config.default_price || 0;
181
- return { step: httpCall.stepName, cost: price, usage: '1 request', model };
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
- const count = Array.isArray(items) ? items.length : 0;
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: count * endpointConfig.price_per_item,
200
- usage: `${count} items`,
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 { step: httpCall.stepName, cost: 0, usage: 'unknown endpoint', warning: 'unknown endpoint' };
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
- if (typeof cost === 'number' && cost > 0) {
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 { step: httpCall.stepName, cost: 0, usage: 'no cost data', warning: 'no cost data' };
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 { step: httpCall.stepName, cost: 0, usage: 'unknown type', warning: 'unknown type' };
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 { pricing: models[model], matchedKey: model };
361
+ return models[model];
268
362
  }
269
- const prefixMatch = Object.entries(models).find(([key]) => model.startsWith(key));
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 = { inputTokens: 0, outputTokens: 0, cachedTokens: 0, reasoningTokens: 0, cost: 0 };
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 { pricing, matchedKey } = findModelPricing(call.model, config.models ?? {});
280
- const { cost, warning } = calculateLLMCallCost(call.usage, pricing);
281
- const prefixWarning = (pricing && matchedKey !== call.model) ?
282
- `priced as ${matchedKey}` :
283
- undefined;
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
- cost,
295
- warning: warning ?? prefixWarning
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.cost += cost;
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
- llmTotalCost: totals.cost,
310
- unknownModels: [...unknownModels]
400
+ llmOriginalCost: totals.originalCost,
401
+ llmAdjustedCost: totals.adjustedCost
311
402
  };
312
403
  }
313
- export function calculateCost(trace, config, traceFile = '') {
314
- const llmCalls = findLLMCalls(trace);
315
- const httpCalls = findHTTPCalls(trace);
316
- const serviceResults = {};
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.status && call.status >= 400) {
435
+ if (call.originalCost === undefined) {
319
436
  continue;
320
437
  }
321
438
  const serviceInfo = identifyService(call, config.services);
322
- if (!serviceInfo) {
323
- continue;
324
- }
325
- if (serviceInfo.config.type === 'response_cost') {
326
- const hasCostData = extractValue(call, serviceInfo.config.cost_path);
327
- const isBillableMethod = serviceInfo.config.billable_method &&
328
- call.method === serviceInfo.config.billable_method;
329
- if (!hasCostData && !isBillableMethod) {
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
- const { results: llmResults, totalInputTokens, totalOutputTokens, totalCachedTokens, totalReasoningTokens, llmTotalCost, unknownModels } = aggregateLLMCosts(llmCalls, config);
345
- const serviceTotalCost = Object.values(serviceResults).reduce((sum, s) => sum + s.totalCost, 0);
346
- const totalCost = llmTotalCost + serviceTotalCost;
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
- llmTotalCost,
463
+ llmOriginalCost,
464
+ llmAdjustedCost,
354
465
  totalInputTokens,
355
466
  totalOutputTokens,
356
467
  totalCachedTokens,
357
468
  totalReasoningTokens,
358
- unknownModels,
359
- services: Object.values(serviceResults),
360
- serviceTotalCost,
361
- totalCost
469
+ httpCosts,
470
+ httpOriginalCost,
471
+ httpAdjustedCost,
472
+ originalTotalCost: llmOriginalCost + httpOriginalCost,
473
+ totalCost: llmAdjustedCost + httpAdjustedCost
362
474
  };
363
475
  }