@outputai/cli 0.7.1-next.5a29fff.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.
@@ -29,76 +29,96 @@ export function parseCostData(report) {
29
29
  const byModel = {};
30
30
  for (const r of report.llmCalls) {
31
31
  if (!byModel[r.model]) {
32
- byModel[r.model] = { count: 0, cost: 0 };
32
+ byModel[r.model] = { count: 0, originalCost: 0, adjustedCost: 0 };
33
33
  }
34
34
  byModel[r.model].count++;
35
- byModel[r.model].cost += r.cost;
35
+ byModel[r.model].originalCost += r.originalCost;
36
+ byModel[r.model].adjustedCost += r.adjustedCost;
36
37
  }
37
38
  const llmModels = Object.entries(byModel)
38
- .sort((a, b) => b[1].cost - a[1].cost)
39
- .map(([model, stats]) => ({ model, count: stats.count, cost: stats.cost }));
40
- const services = [...report.services]
41
- .sort((a, b) => b.totalCost - a.totalCost)
42
- .map(s => ({ serviceName: s.serviceName, callCount: s.calls.length, cost: s.totalCost }));
43
- const serviceTotalCalls = services.reduce((sum, s) => sum + s.callCount, 0);
39
+ .sort((a, b) => b[1].adjustedCost - a[1].adjustedCost)
40
+ .map(([model, s]) => ({ model, ...s }));
41
+ const hosts = [...report.httpCosts]
42
+ .sort((a, b) => b.adjustedTotalCost - a.adjustedTotalCost)
43
+ .map(h => ({
44
+ host: h.host,
45
+ callCount: h.calls.length,
46
+ originalCost: h.originalTotalCost,
47
+ adjustedCost: h.adjustedTotalCost
48
+ }));
49
+ const httpTotalCalls = hosts.reduce((sum, h) => sum + h.callCount, 0);
44
50
  return {
45
51
  traceFile: report.traceFile,
46
52
  workflowName: report.workflowName,
47
53
  duration: report.durationMs ? `${(report.durationMs / 1000).toFixed(1)}s` : 'N/A',
48
54
  llmModels,
49
55
  llmTotalCalls: report.llmCalls.length,
50
- llmTotalCost: report.llmTotalCost,
51
- services,
52
- serviceTotalCalls,
53
- serviceTotalCost: report.serviceTotalCost,
56
+ llmOriginalCost: report.llmOriginalCost,
57
+ llmAdjustedCost: report.llmAdjustedCost,
58
+ hosts,
59
+ httpTotalCalls,
60
+ httpOriginalCost: report.httpOriginalCost,
61
+ httpAdjustedCost: report.httpAdjustedCost,
54
62
  verbose: {
55
63
  hasReasoning: report.totalReasoningTokens > 0,
56
64
  hasCached: report.totalCachedTokens > 0
57
65
  },
58
66
  llmCalls: report.llmCalls,
59
- serviceDetails: report.services,
67
+ httpDetails: report.httpCosts,
60
68
  totalInputTokens: report.totalInputTokens,
61
69
  totalOutputTokens: report.totalOutputTokens,
62
70
  totalCachedTokens: report.totalCachedTokens,
63
71
  totalReasoningTokens: report.totalReasoningTokens,
72
+ originalTotalCost: report.originalTotalCost,
64
73
  totalCost: report.totalCost,
65
- unknownModels: report.unknownModels,
66
- isEmpty: report.llmCalls.length === 0 && report.services.length === 0
74
+ isEmpty: report.llmCalls.length === 0 && report.httpCosts.length === 0
67
75
  };
68
76
  }
69
77
  function formatSummary(data) {
70
78
  const lines = [];
71
79
  if (data.llmModels.length > 0) {
72
80
  const table = new Table({
73
- head: ['Model', 'Calls', 'Cost'],
81
+ head: ['Model', 'Calls', 'Original', 'Adjusted'],
74
82
  style: { head: ['cyan'] },
75
- colAligns: ['left', 'right', 'right']
83
+ colAligns: ['left', 'right', 'right', 'right']
76
84
  });
77
85
  for (const m of data.llmModels) {
78
- table.push([m.model, pluralize(m.count, 'call'), formatCurrency(m.cost)]);
86
+ table.push([
87
+ m.model,
88
+ pluralize(m.count, 'call'),
89
+ formatCurrency(m.originalCost),
90
+ formatCurrency(m.adjustedCost)
91
+ ]);
79
92
  }
80
93
  table.push([
81
94
  'Subtotal',
82
95
  pluralize(data.llmTotalCalls, 'call'),
83
- formatCurrency(data.llmTotalCost)
96
+ formatCurrency(data.llmOriginalCost),
97
+ formatCurrency(data.llmAdjustedCost)
84
98
  ]);
85
99
  lines.push('LLM Costs:');
86
100
  lines.push(table.toString());
87
101
  lines.push('');
88
102
  }
89
- if (data.services.length > 0) {
103
+ if (data.hosts.length > 0) {
90
104
  const table = new Table({
91
- head: ['Service', 'Calls', 'Cost'],
105
+ head: ['Host', 'Calls', 'Original', 'Adjusted'],
92
106
  style: { head: ['cyan'] },
93
- colAligns: ['left', 'right', 'right']
107
+ colAligns: ['left', 'right', 'right', 'right']
94
108
  });
95
- for (const s of data.services) {
96
- table.push([s.serviceName, pluralize(s.callCount, 'call'), formatCurrency(s.cost)]);
109
+ for (const h of data.hosts) {
110
+ table.push([
111
+ h.host,
112
+ pluralize(h.callCount, 'call'),
113
+ formatCurrency(h.originalCost),
114
+ formatCurrency(h.adjustedCost)
115
+ ]);
97
116
  }
98
117
  table.push([
99
118
  'Subtotal',
100
- pluralize(data.serviceTotalCalls, 'call'),
101
- formatCurrency(data.serviceTotalCost)
119
+ pluralize(data.httpTotalCalls, 'call'),
120
+ formatCurrency(data.httpOriginalCost),
121
+ formatCurrency(data.httpAdjustedCost)
102
122
  ]);
103
123
  lines.push('API Costs:');
104
124
  lines.push(table.toString());
@@ -119,8 +139,8 @@ function formatVerbose(data) {
119
139
  head.push('Reasoning');
120
140
  colAligns.push('right');
121
141
  }
122
- head.push('Cost');
123
- colAligns.push('right');
142
+ head.push('Original', 'Adjusted');
143
+ colAligns.push('right', 'right');
124
144
  const table = new Table({
125
145
  head,
126
146
  style: { head: ['cyan'] },
@@ -139,7 +159,7 @@ function formatVerbose(data) {
139
159
  if (data.verbose.hasReasoning) {
140
160
  row.push(formatNumber(r.reasoning));
141
161
  }
142
- row.push(formatCurrency(r.cost) + (r.warning ? ` (${r.warning})` : ''));
162
+ row.push(formatCurrency(r.originalCost), formatCurrency(r.adjustedCost));
143
163
  table.push(row);
144
164
  }
145
165
  const totalRow = [
@@ -154,29 +174,34 @@ function formatVerbose(data) {
154
174
  if (data.verbose.hasReasoning) {
155
175
  totalRow.push(formatNumber(data.totalReasoningTokens));
156
176
  }
157
- totalRow.push(formatCurrency(data.llmTotalCost));
177
+ totalRow.push(formatCurrency(data.llmOriginalCost), formatCurrency(data.llmAdjustedCost));
158
178
  table.push(totalRow);
159
179
  lines.push('LLM Calls:');
160
180
  lines.push(table.toString());
161
181
  lines.push('');
162
182
  }
163
- if (data.serviceDetails.length > 0) {
183
+ if (data.httpDetails.length > 0) {
164
184
  const table = new Table({
165
- head: ['Service', 'Step', 'Usage', 'Cost'],
185
+ head: ['Host', 'Step', 'Usage', 'Original', 'Adjusted'],
166
186
  style: { head: ['cyan'] },
167
- colAligns: ['left', 'left', 'right', 'right']
187
+ colAligns: ['left', 'left', 'right', 'right', 'right']
168
188
  });
169
- for (const service of data.serviceDetails) {
170
- for (const call of service.calls) {
189
+ for (const host of data.httpDetails) {
190
+ for (const call of host.calls) {
171
191
  table.push([
172
- service.serviceName,
192
+ host.host,
173
193
  call.step,
174
194
  call.usage,
175
- formatCurrency(call.cost)
195
+ formatCurrency(call.originalCost),
196
+ formatCurrency(call.adjustedCost)
176
197
  ]);
177
198
  }
178
199
  }
179
- table.push(['Subtotal', '', '', formatCurrency(data.serviceTotalCost)]);
200
+ table.push([
201
+ 'Subtotal', '', '',
202
+ formatCurrency(data.httpOriginalCost),
203
+ formatCurrency(data.httpAdjustedCost)
204
+ ]);
180
205
  lines.push('API Calls:');
181
206
  lines.push(table.toString());
182
207
  lines.push('');
@@ -203,13 +228,10 @@ export function formatCostReport(report, options = {}) {
203
228
  colAligns: ['left', 'right'],
204
229
  colWidths: [36, 12]
205
230
  });
206
- totalTable.push(['TOTAL ESTIMATED COST', formatCurrency(data.totalCost)]);
231
+ totalTable.push(['TOTAL ESTIMATED COST (adjusted)', formatCurrency(data.totalCost)]);
232
+ totalTable.push(['As-charged (from trace)', formatCurrency(data.originalTotalCost)]);
207
233
  lines.push(totalTable.toString());
208
234
  }
209
- if (data.unknownModels.length > 0) {
210
- lines.push('');
211
- lines.push(`Warning: Unknown models (add to config/costs.yml): ${data.unknownModels.join(', ')}`);
212
- }
213
235
  if (data.isEmpty) {
214
236
  lines.push('No billable calls found in trace.');
215
237
  }
@@ -4,6 +4,7 @@
4
4
  * `http_proxy`). No-op when none are set. Invalid URLs are logged and
5
5
  * skipped so the CLI keeps running.
6
6
  *
7
- * Call once at CLI startup, before any network activity.
7
+ * Call once at CLI startup, before any network activity. `undici` is
8
+ * imported lazily so invocations without a proxy skip loading it.
8
9
  */
9
- export declare const bootstrapProxy: () => void;
10
+ export declare const bootstrapProxy: () => Promise<void>;
@@ -1,13 +1,13 @@
1
- import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
2
1
  /**
3
2
  * Routes all `fetch()` calls through an HTTP/HTTPS proxy when standard
4
3
  * proxy env vars are set (`HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`,
5
4
  * `http_proxy`). No-op when none are set. Invalid URLs are logged and
6
5
  * skipped so the CLI keeps running.
7
6
  *
8
- * Call once at CLI startup, before any network activity.
7
+ * Call once at CLI startup, before any network activity. `undici` is
8
+ * imported lazily so invocations without a proxy skip loading it.
9
9
  */
10
- export const bootstrapProxy = () => {
10
+ export const bootstrapProxy = async () => {
11
11
  const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy ||
12
12
  process.env.HTTP_PROXY || process.env.http_proxy;
13
13
  if (!proxyUrl) {
@@ -20,5 +20,6 @@ export const bootstrapProxy = () => {
20
20
  console.warn(`[proxy] Ignoring invalid proxy URL: ${proxyUrl}`);
21
21
  return;
22
22
  }
23
+ const { EnvHttpProxyAgent, setGlobalDispatcher } = await import('undici');
23
24
  setGlobalDispatcher(new EnvHttpProxyAgent());
24
25
  };
@@ -19,20 +19,20 @@ describe('proxy bootstrap', () => {
19
19
  });
20
20
  it('does nothing when no proxy env vars are set', async () => {
21
21
  const { bootstrapProxy } = await import('./proxy.js');
22
- bootstrapProxy();
22
+ await bootstrapProxy();
23
23
  expect(mockSetGlobalDispatcher).not.toHaveBeenCalled();
24
24
  });
25
25
  it('sets global dispatcher when HTTPS_PROXY is set', async () => {
26
26
  process.env.HTTPS_PROXY = 'http://proxy:8080';
27
27
  const { bootstrapProxy } = await import('./proxy.js');
28
- bootstrapProxy();
28
+ await bootstrapProxy();
29
29
  expect(MockEnvHttpProxyAgent).toHaveBeenCalled();
30
30
  expect(mockSetGlobalDispatcher).toHaveBeenCalledTimes(1);
31
31
  });
32
32
  it('sets global dispatcher when HTTP_PROXY is set', async () => {
33
33
  process.env.HTTP_PROXY = 'http://proxy:8080';
34
34
  const { bootstrapProxy } = await import('./proxy.js');
35
- bootstrapProxy();
35
+ await bootstrapProxy();
36
36
  expect(MockEnvHttpProxyAgent).toHaveBeenCalled();
37
37
  expect(mockSetGlobalDispatcher).toHaveBeenCalledTimes(1);
38
38
  });
@@ -40,7 +40,7 @@ describe('proxy bootstrap', () => {
40
40
  process.env.HTTPS_PROXY = 'not a url';
41
41
  const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
42
42
  const { bootstrapProxy } = await import('./proxy.js');
43
- bootstrapProxy();
43
+ await bootstrapProxy();
44
44
  expect(mockSetGlobalDispatcher).not.toHaveBeenCalled();
45
45
  expect(warnSpy).toHaveBeenCalled();
46
46
  warnSpy.mockRestore();