@outputai/cli 0.12.0 → 0.12.1-next.1243f78.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Generated by orval v8.9.0 🍺
2
+ * Generated by orval 🍺
3
3
  * Do not edit manually.
4
4
  * Output.ai API
5
5
  * API for managing and executing Output.ai workflows
@@ -49,6 +49,9 @@ export const WorkflowResultV2ResponseV = {
49
49
  export const getGetHealthUrl = () => {
50
50
  return `/health`;
51
51
  };
52
+ /**
53
+ * @summary Health check the API
54
+ */
52
55
  export const getHealth = async (options) => {
53
56
  return customFetchInstance(getGetHealthUrl(), {
54
57
  ...options,
@@ -58,6 +61,9 @@ export const getHealth = async (options) => {
58
61
  export const getGetReadyUrl = () => {
59
62
  return `/ready`;
60
63
  };
64
+ /**
65
+ * @summary Check if the API is ready to answer requests
66
+ */
61
67
  export const getReady = async (options) => {
62
68
  return customFetchInstance(getGetReadyUrl(), {
63
69
  ...options,
@@ -67,6 +73,10 @@ export const getReady = async (options) => {
67
73
  export const getPostWorkflowRunUrl = () => {
68
74
  return `/workflow/run`;
69
75
  };
76
+ /**
77
+ * Executes a workflow and waits for it to complete before returning the result
78
+ * @summary Execute a workflow synchronously
79
+ */
70
80
  export const postWorkflowRun = async (postWorkflowRunBody, options) => {
71
81
  return customFetchInstance(getPostWorkflowRunUrl(), {
72
82
  ...options,
@@ -78,6 +88,9 @@ export const postWorkflowRun = async (postWorkflowRunBody, options) => {
78
88
  export const getPostWorkflowStartUrl = () => {
79
89
  return `/workflow/start`;
80
90
  };
91
+ /**
92
+ * @summary Start a workflow asynchronously
93
+ */
81
94
  export const postWorkflowStart = async (postWorkflowStartBody, options) => {
82
95
  return customFetchInstance(getPostWorkflowStartUrl(), {
83
96
  ...options,
@@ -89,6 +102,10 @@ export const postWorkflowStart = async (postWorkflowStartBody, options) => {
89
102
  export const getGetWorkflowIdStatusUrl = (id) => {
90
103
  return `/workflow/${id}/status`;
91
104
  };
105
+ /**
106
+ * Returns the status of the latest run for the given workflow. To pin a specific run, use `/workflow/{id}/runs/{rid}/status`.
107
+ * @summary Get workflow execution status (latest run)
108
+ */
92
109
  export const getWorkflowIdStatus = async (id, options) => {
93
110
  return customFetchInstance(getGetWorkflowIdStatusUrl(id), {
94
111
  ...options,
@@ -98,6 +115,9 @@ export const getWorkflowIdStatus = async (id, options) => {
98
115
  export const getGetWorkflowIdRunsRidStatusUrl = (id, rid) => {
99
116
  return `/workflow/${id}/runs/${rid}/status`;
100
117
  };
118
+ /**
119
+ * @summary Get workflow execution status for a specific run
120
+ */
101
121
  export const getWorkflowIdRunsRidStatus = async (id, rid, options) => {
102
122
  return customFetchInstance(getGetWorkflowIdRunsRidStatusUrl(id, rid), {
103
123
  ...options,
@@ -107,6 +127,9 @@ export const getWorkflowIdRunsRidStatus = async (id, rid, options) => {
107
127
  export const getPatchWorkflowIdRunsRidStopUrl = (id, rid) => {
108
128
  return `/workflow/${id}/runs/${rid}/stop`;
109
129
  };
130
+ /**
131
+ * @summary Stop a specific workflow run
132
+ */
110
133
  export const patchWorkflowIdRunsRidStop = async (id, rid, options) => {
111
134
  return customFetchInstance(getPatchWorkflowIdRunsRidStopUrl(id, rid), {
112
135
  ...options,
@@ -116,6 +139,11 @@ export const patchWorkflowIdRunsRidStop = async (id, rid, options) => {
116
139
  export const getPatchWorkflowIdStopUrl = (id) => {
117
140
  return `/workflow/${id}/stop`;
118
141
  };
142
+ /**
143
+ * Stops the latest run of the given workflow. The returned `runId` reflects the run at describe-time and may differ from the cancelled run if a new execution started concurrently. Deprecated; use `PATCH /workflow/{id}/runs/{rid}/stop` to pin a specific run. Scheduled for removal after 2026-07-16.
144
+ * @deprecated
145
+ * @summary [Deprecated] Stop the latest workflow run
146
+ */
119
147
  export const patchWorkflowIdStop = async (id, options) => {
120
148
  return customFetchInstance(getPatchWorkflowIdStopUrl(id), {
121
149
  ...options,
@@ -125,6 +153,10 @@ export const patchWorkflowIdStop = async (id, options) => {
125
153
  export const getPostWorkflowIdRunsRidTerminateUrl = (id, rid) => {
126
154
  return `/workflow/${id}/runs/${rid}/terminate`;
127
155
  };
156
+ /**
157
+ * Force terminates a workflow run. Unlike stop/cancel, terminate immediately stops the run without allowing cleanup.
158
+ * @summary Terminate a specific workflow run (force stop)
159
+ */
128
160
  export const postWorkflowIdRunsRidTerminate = async (id, rid, postWorkflowIdRunsRidTerminateBody, options) => {
129
161
  return customFetchInstance(getPostWorkflowIdRunsRidTerminateUrl(id, rid), {
130
162
  ...options,
@@ -136,6 +168,11 @@ export const postWorkflowIdRunsRidTerminate = async (id, rid, postWorkflowIdRuns
136
168
  export const getPostWorkflowIdTerminateUrl = (id) => {
137
169
  return `/workflow/${id}/terminate`;
138
170
  };
171
+ /**
172
+ * Force terminates the latest run. Deprecated; use `POST /workflow/{id}/runs/{rid}/terminate` to target a specific run. Scheduled for removal after 2026-07-16.
173
+ * @deprecated
174
+ * @summary [Deprecated] Terminate the latest workflow run
175
+ */
139
176
  export const postWorkflowIdTerminate = async (id, postWorkflowIdTerminateBody, options) => {
140
177
  return customFetchInstance(getPostWorkflowIdTerminateUrl(id), {
141
178
  ...options,
@@ -147,6 +184,10 @@ export const postWorkflowIdTerminate = async (id, postWorkflowIdTerminateBody, o
147
184
  export const getPostWorkflowIdRunsRidResetUrl = (id, rid) => {
148
185
  return `/workflow/${id}/runs/${rid}/reset`;
149
186
  };
187
+ /**
188
+ * Resets a pinned workflow run to the point after a completed step, creating a new run that replays from that point. The current execution is terminated.
189
+ * @summary Reset a specific workflow run to re-run from after a completed step
190
+ */
150
191
  export const postWorkflowIdRunsRidReset = async (id, rid, resetWorkflowRequest, options) => {
151
192
  return customFetchInstance(getPostWorkflowIdRunsRidResetUrl(id, rid), {
152
193
  ...options,
@@ -158,6 +199,11 @@ export const postWorkflowIdRunsRidReset = async (id, rid, resetWorkflowRequest,
158
199
  export const getPostWorkflowIdResetUrl = (id) => {
159
200
  return `/workflow/${id}/reset`;
160
201
  };
202
+ /**
203
+ * Resets the latest run. Deprecated; use `POST /workflow/{id}/runs/{rid}/reset` to target a specific run. Scheduled for removal after 2026-07-16.
204
+ * @deprecated
205
+ * @summary [Deprecated] Reset the latest workflow run
206
+ */
161
207
  export const postWorkflowIdReset = async (id, resetWorkflowRequest, options) => {
162
208
  return customFetchInstance(getPostWorkflowIdResetUrl(id), {
163
209
  ...options,
@@ -169,6 +215,10 @@ export const postWorkflowIdReset = async (id, resetWorkflowRequest, options) =>
169
215
  export const getGetWorkflowIdResultUrl = (id) => {
170
216
  return `/workflow/${id}/result`;
171
217
  };
218
+ /**
219
+ * Returns the result of the latest run for the given workflow. To pin a specific run, use `/workflow/{id}/runs/{rid}/result`.
220
+ * @summary Return the result of a workflow (latest run)
221
+ */
172
222
  export const getWorkflowIdResult = async (id, options) => {
173
223
  return customFetchInstance(getGetWorkflowIdResultUrl(id), {
174
224
  ...options,
@@ -178,6 +228,9 @@ export const getWorkflowIdResult = async (id, options) => {
178
228
  export const getGetWorkflowIdRunsRidResultUrl = (id, rid) => {
179
229
  return `/workflow/${id}/runs/${rid}/result`;
180
230
  };
231
+ /**
232
+ * @summary Return the result of a specific workflow run
233
+ */
181
234
  export const getWorkflowIdRunsRidResult = async (id, rid, options) => {
182
235
  return customFetchInstance(getGetWorkflowIdRunsRidResultUrl(id, rid), {
183
236
  ...options,
@@ -187,6 +240,10 @@ export const getWorkflowIdRunsRidResult = async (id, rid, options) => {
187
240
  export const getGetWorkflowIdInputUrl = (id) => {
188
241
  return `/workflow/${id}/input`;
189
242
  };
243
+ /**
244
+ * Returns the original input passed to the latest run of the given workflow. Works for workflows in any state, including running. To pin a specific run, use `/workflow/{id}/runs/{rid}/input`.
245
+ * @summary Return the original input of a workflow (latest run)
246
+ */
190
247
  export const getWorkflowIdInput = async (id, options) => {
191
248
  return customFetchInstance(getGetWorkflowIdInputUrl(id), {
192
249
  ...options,
@@ -196,6 +253,9 @@ export const getWorkflowIdInput = async (id, options) => {
196
253
  export const getGetWorkflowIdRunsRidInputUrl = (id, rid) => {
197
254
  return `/workflow/${id}/runs/${rid}/input`;
198
255
  };
256
+ /**
257
+ * @summary Return the original input of a specific workflow run
258
+ */
199
259
  export const getWorkflowIdRunsRidInput = async (id, rid, options) => {
200
260
  return customFetchInstance(getGetWorkflowIdRunsRidInputUrl(id, rid), {
201
261
  ...options,
@@ -205,6 +265,10 @@ export const getWorkflowIdRunsRidInput = async (id, rid, options) => {
205
265
  export const getGetWorkflowIdTraceLogUrl = (id) => {
206
266
  return `/workflow/${id}/trace-log`;
207
267
  };
268
+ /**
269
+ * Returns trace data for the latest run of the given workflow. If trace is stored remotely (S3), fetches and returns the data inline. If trace is local only, returns the local path. To pin a specific run, use `/workflow/{id}/runs/{rid}/trace-log`.
270
+ * @summary Get workflow trace log data (latest run)
271
+ */
208
272
  export const getWorkflowIdTraceLog = async (id, options) => {
209
273
  return customFetchInstance(getGetWorkflowIdTraceLogUrl(id), {
210
274
  ...options,
@@ -214,6 +278,10 @@ export const getWorkflowIdTraceLog = async (id, options) => {
214
278
  export const getGetWorkflowIdRunsRidTraceLogUrl = (id, rid) => {
215
279
  return `/workflow/${id}/runs/${rid}/trace-log`;
216
280
  };
281
+ /**
282
+ * Returns trace data for a pinned workflow run. If trace is stored remotely (S3), fetches and returns the data inline. If trace is local only, returns the local path.
283
+ * @summary Get workflow trace log data for a specific run
284
+ */
217
285
  export const getWorkflowIdRunsRidTraceLog = async (id, rid, options) => {
218
286
  return customFetchInstance(getGetWorkflowIdRunsRidTraceLogUrl(id, rid), {
219
287
  ...options,
@@ -224,12 +292,16 @@ export const getGetWorkflowIdHistoryUrl = (id, params) => {
224
292
  const normalizedParams = new URLSearchParams();
225
293
  Object.entries(params || {}).forEach(([key, value]) => {
226
294
  if (value !== undefined) {
227
- normalizedParams.append(key, value === null ? 'null' : value.toString());
295
+ normalizedParams.append(key, value === null ? 'null' : String(value));
228
296
  }
229
297
  });
230
298
  const stringifiedParams = normalizedParams.toString();
231
299
  return stringifiedParams.length > 0 ? `/workflow/${id}/history?${stringifiedParams}` : `/workflow/${id}/history`;
232
300
  };
301
+ /**
302
+ * Returns decoded Temporal history events with optional payload inclusion. First page includes workflow metadata; subsequent pages return events only.
303
+ * @summary Get paginated workflow execution history
304
+ */
233
305
  export const getWorkflowIdHistory = async (id, params, options) => {
234
306
  return customFetchInstance(getGetWorkflowIdHistoryUrl(id, params), {
235
307
  ...options,
@@ -240,12 +312,15 @@ export const getGetWorkflowIdRunsRidHistoryUrl = (id, rid, params) => {
240
312
  const normalizedParams = new URLSearchParams();
241
313
  Object.entries(params || {}).forEach(([key, value]) => {
242
314
  if (value !== undefined) {
243
- normalizedParams.append(key, value === null ? 'null' : value.toString());
315
+ normalizedParams.append(key, value === null ? 'null' : String(value));
244
316
  }
245
317
  });
246
318
  const stringifiedParams = normalizedParams.toString();
247
319
  return stringifiedParams.length > 0 ? `/workflow/${id}/runs/${rid}/history?${stringifiedParams}` : `/workflow/${id}/runs/${rid}/history`;
248
320
  };
321
+ /**
322
+ * @summary Get paginated workflow execution history for a specific run
323
+ */
249
324
  export const getWorkflowIdRunsRidHistory = async (id, rid, params, options) => {
250
325
  return customFetchInstance(getGetWorkflowIdRunsRidHistoryUrl(id, rid, params), {
251
326
  ...options,
@@ -256,12 +331,16 @@ export const getGetWorkflowIdHistoryStreamUrl = (id, params) => {
256
331
  const normalizedParams = new URLSearchParams();
257
332
  Object.entries(params || {}).forEach(([key, value]) => {
258
333
  if (value !== undefined) {
259
- normalizedParams.append(key, value === null ? 'null' : value.toString());
334
+ normalizedParams.append(key, value === null ? 'null' : String(value));
260
335
  }
261
336
  });
262
337
  const stringifiedParams = normalizedParams.toString();
263
338
  return stringifiedParams.length > 0 ? `/workflow/${id}/history/stream?${stringifiedParams}` : `/workflow/${id}/history/stream`;
264
339
  };
340
+ /**
341
+ * Opens a persistent SSE connection that delivers Temporal workflow history events in real time. Emits named events: `workflow` (metadata, once), `history` (event batches), `done` (terminal state), `server_error` (post-flush errors). The `done` event carries `{ reason, newRunId? }` where `reason` is the terminal Temporal event type (`WORKFLOW_EXECUTION_COMPLETED`, `WORKFLOW_EXECUTION_FAILED`, `WORKFLOW_EXECUTION_TIMED_OUT`, `WORKFLOW_EXECUTION_CANCELED`, `WORKFLOW_EXECUTION_TERMINATED`, `WORKFLOW_EXECUTION_CONTINUED_AS_NEW`) and `newRunId` is present only when the terminal event chains a follow-on run. `server_error` carries `{ error, message, workflowId, runId }`. Errors before the stream opens are returned as JSON HTTP responses (400/404); once open, failures arrive as a `server_error` event. Supports reconnect via `Last-Event-ID` header or `lastEventId` query param.
342
+ * @summary Stream workflow history events via Server-Sent Events
343
+ */
265
344
  export const getWorkflowIdHistoryStream = async (id, params, options) => {
266
345
  return customFetchInstance(getGetWorkflowIdHistoryStreamUrl(id, params), {
267
346
  ...options,
@@ -272,12 +351,16 @@ export const getGetWorkflowIdRunsRidHistoryStreamUrl = (id, rid, params) => {
272
351
  const normalizedParams = new URLSearchParams();
273
352
  Object.entries(params || {}).forEach(([key, value]) => {
274
353
  if (value !== undefined) {
275
- normalizedParams.append(key, value === null ? 'null' : value.toString());
354
+ normalizedParams.append(key, value === null ? 'null' : String(value));
276
355
  }
277
356
  });
278
357
  const stringifiedParams = normalizedParams.toString();
279
358
  return stringifiedParams.length > 0 ? `/workflow/${id}/runs/${rid}/history/stream?${stringifiedParams}` : `/workflow/${id}/runs/${rid}/history/stream`;
280
359
  };
360
+ /**
361
+ * Same as /workflow/{id}/history/stream but targets a specific run ID.
362
+ * @summary Stream pinned-run workflow history events via Server-Sent Events
363
+ */
281
364
  export const getWorkflowIdRunsRidHistoryStream = async (id, rid, params, options) => {
282
365
  return customFetchInstance(getGetWorkflowIdRunsRidHistoryStreamUrl(id, rid, params), {
283
366
  ...options,
@@ -287,6 +370,9 @@ export const getWorkflowIdRunsRidHistoryStream = async (id, rid, params, options
287
370
  export const getGetWorkflowCatalogIdUrl = (id) => {
288
371
  return `/workflow/catalog/${id}`;
289
372
  };
373
+ /**
374
+ * @summary Get a specific workflow catalog by ID
375
+ */
290
376
  export const getWorkflowCatalogId = async (id, options) => {
291
377
  return customFetchInstance(getGetWorkflowCatalogIdUrl(id), {
292
378
  ...options,
@@ -296,6 +382,9 @@ export const getWorkflowCatalogId = async (id, options) => {
296
382
  export const getGetWorkflowCatalogUrl = () => {
297
383
  return `/workflow/catalog`;
298
384
  };
385
+ /**
386
+ * @summary Get the default workflow catalog
387
+ */
299
388
  export const getWorkflowCatalog = async (options) => {
300
389
  return customFetchInstance(getGetWorkflowCatalogUrl(), {
301
390
  ...options,
@@ -306,12 +395,16 @@ export const getGetWorkflowRunsUrl = (params) => {
306
395
  const normalizedParams = new URLSearchParams();
307
396
  Object.entries(params || {}).forEach(([key, value]) => {
308
397
  if (value !== undefined) {
309
- normalizedParams.append(key, value === null ? 'null' : value.toString());
398
+ normalizedParams.append(key, value === null ? 'null' : String(value));
310
399
  }
311
400
  });
312
401
  const stringifiedParams = normalizedParams.toString();
313
402
  return stringifiedParams.length > 0 ? `/workflow/runs?${stringifiedParams}` : `/workflow/runs`;
314
403
  };
404
+ /**
405
+ * Returns a list of workflow runs with optional filtering by workflow type
406
+ * @summary List workflow runs
407
+ */
315
408
  export const getWorkflowRuns = async (params, options) => {
316
409
  return customFetchInstance(getGetWorkflowRunsUrl(params), {
317
410
  ...options,
@@ -321,6 +414,10 @@ export const getWorkflowRuns = async (params, options) => {
321
414
  export const getPostWorkflowIdFeedbackUrl = (id) => {
322
415
  return `/workflow/${id}/feedback`;
323
416
  };
417
+ /**
418
+ * Always targets the latest run; `runId` cannot be pinned for Temporal signal operations.
419
+ * @summary Send feedback to a workflow
420
+ */
324
421
  export const postWorkflowIdFeedback = async (id, postWorkflowIdFeedbackBody, options) => {
325
422
  return customFetchInstance(getPostWorkflowIdFeedbackUrl(id), {
326
423
  ...options,
@@ -332,6 +429,10 @@ export const postWorkflowIdFeedback = async (id, postWorkflowIdFeedbackBody, opt
332
429
  export const getPostWorkflowIdSignalSignalUrl = (id, signal) => {
333
430
  return `/workflow/${id}/signal/${signal}`;
334
431
  };
432
+ /**
433
+ * Always targets the latest run; `runId` cannot be pinned for Temporal signal operations.
434
+ * @summary Send a signal to an workflow
435
+ */
335
436
  export const postWorkflowIdSignalSignal = async (id, signal, postWorkflowIdSignalSignalBody, options) => {
336
437
  return customFetchInstance(getPostWorkflowIdSignalSignalUrl(id, signal), {
337
438
  ...options,
@@ -343,6 +444,10 @@ export const postWorkflowIdSignalSignal = async (id, signal, postWorkflowIdSigna
343
444
  export const getPostWorkflowIdQueryQueryUrl = (id, query) => {
344
445
  return `/workflow/${id}/query/${query}`;
345
446
  };
447
+ /**
448
+ * Always targets the latest run; `runId` cannot be pinned for Temporal query operations.
449
+ * @summary Send a query to an workflow
450
+ */
346
451
  export const postWorkflowIdQueryQuery = async (id, query, postWorkflowIdQueryQueryBody, options) => {
347
452
  return customFetchInstance(getPostWorkflowIdQueryQueryUrl(id, query), {
348
453
  ...options,
@@ -354,6 +459,10 @@ export const postWorkflowIdQueryQuery = async (id, query, postWorkflowIdQueryQue
354
459
  export const getPostWorkflowIdUpdateUpdateUrl = (id, update) => {
355
460
  return `/workflow/${id}/update/${update}`;
356
461
  };
462
+ /**
463
+ * Always targets the latest run; `runId` cannot be pinned for Temporal update operations.
464
+ * @summary Execute an update on an workflow
465
+ */
357
466
  export const postWorkflowIdUpdateUpdate = async (id, update, postWorkflowIdUpdateUpdateBody, options) => {
358
467
  return customFetchInstance(getPostWorkflowIdUpdateUpdateUrl(id, update), {
359
468
  ...options,
@@ -366,6 +475,9 @@ export const postWorkflowIdUpdateUpdate = async (id, update, postWorkflowIdUpdat
366
475
  export const getPostHeartbeatUrl = () => {
367
476
  return `/heartbeat`;
368
477
  };
478
+ /**
479
+ * @summary A dummy post endpoint for test only
480
+ */
369
481
  export const postHeartbeat = async (options) => {
370
482
  return customFetchInstance(getPostHeartbeatUrl(), {
371
483
  ...options,
@@ -80,7 +80,7 @@ services:
80
80
  condition: service_healthy
81
81
  worker:
82
82
  condition: service_healthy
83
- image: outputai/api:${OUTPUT_API_VERSION:-0.12.0}
83
+ image: outputai/api:${OUTPUT_API_VERSION:-0.12.1-next.1243f78.0}
84
84
  init: true
85
85
  networks:
86
86
  - main
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.12.0"
2
+ "framework": "0.12.1-next.1243f78.0"
3
3
  }
@@ -84,7 +84,8 @@ function parseLegacyLLMUsageEvent(node, stepName, event) {
84
84
  model: event.modelId || 'unknown',
85
85
  usage: eventTokenUsage(event.usage ?? []),
86
86
  originalCost: event.total,
87
- lines: event.usage ?? []
87
+ lines: event.usage ?? [],
88
+ incomplete: false
88
89
  };
89
90
  }
90
91
  function parseNormalizedLLMUsage(node, stepName, event) {
@@ -100,7 +101,11 @@ function parseNormalizedLLMUsage(node, stepName, event) {
100
101
  model: event.modelId || 'unknown',
101
102
  usage: normalizedItemTokenUsage(event.items),
102
103
  originalCost: 0,
103
- lines
104
+ lines,
105
+ // This path runs whenever cost computation produced no llm:generation:cost
106
+ // attribute at all (e.g. pricing config missing, not just unrated grounding),
107
+ // so every line here is unpriced by construction, not only grounding calls.
108
+ incomplete: true
104
109
  };
105
110
  }
106
111
  function parseLLMCostEvent(node, stepName, event) {
@@ -116,7 +121,10 @@ function parseLLMCostEvent(node, stepName, event) {
116
121
  model: event.modelId || 'unknown',
117
122
  usage: normalizedItemTokenUsage(event.items),
118
123
  originalCost: event.total ?? 0,
119
- lines
124
+ lines,
125
+ // An INCOMPLETE event carries a real but unpriced charge (e.g. unrated
126
+ // grounding). Flag it so the report can mark the understated total.
127
+ incomplete: event.status === 'incomplete'
120
128
  };
121
129
  }
122
130
  export function extractValue(obj, path) {
@@ -461,7 +469,8 @@ function aggregateLLMCosts(llmCalls, config) {
461
469
  cached: call.usage.cachedInputTokens ?? 0,
462
470
  reasoning: call.usage.reasoningTokens ?? 0,
463
471
  originalCost,
464
- adjustedCost
472
+ adjustedCost,
473
+ incomplete: call.incomplete
465
474
  });
466
475
  totals.inputTokens += call.usage.inputTokens ?? 0;
467
476
  totals.outputTokens += call.usage.outputTokens ?? 0;
@@ -37,10 +37,11 @@ function llmEventNode(id, model, lines) {
37
37
  }
38
38
  };
39
39
  }
40
- function llmCostNode(id, model, items) {
40
+ function llmCostNode(id, model, items, status = 'precise') {
41
41
  const totalOf = (group) => items.filter(item => item.group === group).reduce((sum, item) => sum + (item.total ?? 0), 0);
42
42
  const input = totalOf('input');
43
43
  const output = totalOf('output');
44
+ const request = totalOf('request');
44
45
  return {
45
46
  id,
46
47
  kind: 'llm',
@@ -52,8 +53,9 @@ function llmCostNode(id, model, items) {
52
53
  modelId: model,
53
54
  input,
54
55
  output,
55
- total: input + output,
56
- status: 'precise',
56
+ request,
57
+ total: input + output + request,
58
+ status,
57
59
  items
58
60
  }
59
61
  }
@@ -280,6 +282,66 @@ describe('findLLMCalls', () => {
280
282
  ]);
281
283
  expect(calls[0].originalCost).toBeCloseTo(0.001965, 8);
282
284
  });
285
+ it('reads a priced grounding request item without counting it as tokens', () => {
286
+ const items = [
287
+ { group: 'input', label: 'no_cache', amount: 1032, ppm: 0.3, total: 0.0003096, status: 'ok' },
288
+ { group: 'output', label: 'text', amount: 793, ppm: 2.5, total: 0.0019825, status: 'ok' },
289
+ { group: 'request', label: 'grounding_prompt', amount: 1, ppm: 35_000, total: 0.035, status: 'ok' }
290
+ ];
291
+ const calls = findLLMCalls({
292
+ kind: 'workflow',
293
+ children: [llmCostNode('grounded', 'gemini-2.5-flash', items)]
294
+ });
295
+ expect(calls[0].incomplete).toBe(false);
296
+ expect(calls[0].originalCost).toBeCloseTo(0.0372921, 8);
297
+ // Grounding bills per request, so it must not inflate token usage.
298
+ expect(calls[0].usage).toEqual({
299
+ inputTokens: 1032,
300
+ cachedInputTokens: 0,
301
+ outputTokens: 793,
302
+ reasoningTokens: 0
303
+ });
304
+ expect(calls[0].lines.find(l => l.type === 'request_grounding_prompt')).toEqual({
305
+ type: 'request_grounding_prompt',
306
+ ppm: 35_000,
307
+ amount: 1,
308
+ total: 0.035
309
+ });
310
+ });
311
+ it('preserves a priced grounding charge as-charged when re-pricing at costs.yml rates', () => {
312
+ const items = [
313
+ { group: 'input', label: 'no_cache', amount: 1032, ppm: 0.3, total: 0.0003096, status: 'ok' },
314
+ { group: 'output', label: 'text', amount: 793, ppm: 2.5, total: 0.0019825, status: 'ok' },
315
+ { group: 'request', label: 'grounding_prompt', amount: 1, ppm: 35_000, total: 0.035, status: 'ok' }
316
+ ];
317
+ const config = {
318
+ models: { 'gemini-2.5-flash': { provider: 'google-vertex', input: 0.3, output: 2.5 } },
319
+ services: {}
320
+ };
321
+ const report = calculateCost({ kind: 'workflow', name: 'w', children: [llmCostNode('grounded', 'gemini-2.5-flash', items)] }, config);
322
+ // grounding has no costs.yml line rate, so it degrades to its as-charged total, not $0.
323
+ expect(report.llmAdjustedCost).toBeCloseTo(0.0372921, 8);
324
+ });
325
+ it('flags an unrated grounding charge as incomplete instead of a silent $0', () => {
326
+ const items = [
327
+ { group: 'input', label: 'no_cache', amount: 1000, ppm: 1, total: 0.001, status: 'ok' },
328
+ { group: 'output', label: null, amount: 500, ppm: 5, total: 0.0025, status: 'ok' },
329
+ { group: 'request', label: 'grounding', amount: 3, ppm: null, total: null, status: 'missing' }
330
+ ];
331
+ const calls = findLLMCalls({
332
+ kind: 'workflow',
333
+ children: [llmCostNode('unrated', 'gemini-9-ultra', items, 'incomplete')]
334
+ });
335
+ expect(calls[0].incomplete).toBe(true);
336
+ // The unrated line still shows up (as $0), but the call carries the signal.
337
+ expect(calls[0].lines.find(l => l.type === 'request_grounding')).toEqual({
338
+ type: 'request_grounding',
339
+ ppm: 0,
340
+ amount: 3,
341
+ total: 0
342
+ });
343
+ expect(calls[0].usage).toMatchObject({ inputTokens: 1000, outputTokens: 500 });
344
+ });
283
345
  it('reads normalized usage without interpreting token totals as cost', () => {
284
346
  const items = [
285
347
  { group: 'input', label: null, amount: 100 },
@@ -303,7 +365,9 @@ describe('findLLMCalls', () => {
303
365
  outputTokens: 110,
304
366
  reasoningTokens: 70
305
367
  },
306
- originalCost: 0
368
+ originalCost: 0,
369
+ // Usage-only means cost computation produced no priced result at all.
370
+ incomplete: true
307
371
  });
308
372
  expect(calls[0].lines).toEqual([
309
373
  { type: 'input', ppm: 0, amount: 100, total: 0 },
@@ -51,6 +51,7 @@ export interface LLMCall {
51
51
  usage: TokenUsage;
52
52
  originalCost: number;
53
53
  lines: LLMUsageLine[];
54
+ incomplete: boolean;
54
55
  }
55
56
  export interface HTTPCall {
56
57
  stepName: string;
@@ -109,6 +110,7 @@ export interface LLMCostResult {
109
110
  reasoning: number;
110
111
  originalCost: number;
111
112
  adjustedCost: number;
113
+ incomplete: boolean;
112
114
  }
113
115
  export interface ServiceCostResult {
114
116
  step: string;
@@ -155,6 +157,7 @@ export interface LLMModelSummary {
155
157
  count: number;
156
158
  originalCost: number;
157
159
  adjustedCost: number;
160
+ incomplete: boolean;
158
161
  }
159
162
  export interface HostSummary {
160
163
  host: string;
@@ -25,15 +25,23 @@ function formatCurrency(amount) {
25
25
  function pluralize(count, singular) {
26
26
  return count === 1 ? `1 ${singular}` : `${count} ${singular}s`;
27
27
  }
28
+ // Marks a figure whose call had a real but unpriced charge (e.g. unrated
29
+ // grounding), so a $0.00 line reads as "unpriced" rather than a clean zero.
30
+ const INCOMPLETE_MARKER = '*';
31
+ const INCOMPLETE_FOOTNOTE = '* cost incomplete (unpriced charge such as grounding, or missing usage); the total understates the bill';
32
+ function markIncomplete(text, incomplete) {
33
+ return incomplete ? `${text} ${INCOMPLETE_MARKER}` : text;
34
+ }
28
35
  export function parseCostData(report) {
29
36
  const byModel = {};
30
37
  for (const r of report.llmCalls) {
31
38
  if (!byModel[r.model]) {
32
- byModel[r.model] = { count: 0, originalCost: 0, adjustedCost: 0 };
39
+ byModel[r.model] = { count: 0, originalCost: 0, adjustedCost: 0, incomplete: false };
33
40
  }
34
41
  byModel[r.model].count++;
35
42
  byModel[r.model].originalCost += r.originalCost;
36
43
  byModel[r.model].adjustedCost += r.adjustedCost;
44
+ byModel[r.model].incomplete ||= r.incomplete;
37
45
  }
38
46
  const llmModels = Object.entries(byModel)
39
47
  .sort((a, b) => b[1].adjustedCost - a[1].adjustedCost)
@@ -82,12 +90,13 @@ function formatSummary(data) {
82
90
  style: { head: ['cyan'] },
83
91
  colAligns: ['left', 'right', 'right', 'right']
84
92
  });
93
+ const anyIncomplete = data.llmModels.some(m => m.incomplete);
85
94
  for (const m of data.llmModels) {
86
95
  table.push([
87
96
  m.model,
88
97
  pluralize(m.count, 'call'),
89
- formatCurrency(m.originalCost),
90
- formatCurrency(m.adjustedCost)
98
+ markIncomplete(formatCurrency(m.originalCost), m.incomplete),
99
+ markIncomplete(formatCurrency(m.adjustedCost), m.incomplete)
91
100
  ]);
92
101
  }
93
102
  table.push([
@@ -98,6 +107,9 @@ function formatSummary(data) {
98
107
  ]);
99
108
  lines.push('LLM Costs:');
100
109
  lines.push(table.toString());
110
+ if (anyIncomplete) {
111
+ lines.push(INCOMPLETE_FOOTNOTE);
112
+ }
101
113
  lines.push('');
102
114
  }
103
115
  if (data.hosts.length > 0) {
@@ -146,6 +158,7 @@ function formatVerbose(data) {
146
158
  style: { head: ['cyan'] },
147
159
  colAligns
148
160
  });
161
+ const anyIncomplete = data.llmCalls.some(r => r.incomplete);
149
162
  for (const r of data.llmCalls) {
150
163
  const row = [
151
164
  r.step,
@@ -159,7 +172,7 @@ function formatVerbose(data) {
159
172
  if (data.verbose.hasReasoning) {
160
173
  row.push(formatNumber(r.reasoning));
161
174
  }
162
- row.push(formatCurrency(r.originalCost), formatCurrency(r.adjustedCost));
175
+ row.push(markIncomplete(formatCurrency(r.originalCost), r.incomplete), markIncomplete(formatCurrency(r.adjustedCost), r.incomplete));
163
176
  table.push(row);
164
177
  }
165
178
  const totalRow = [
@@ -178,6 +191,9 @@ function formatVerbose(data) {
178
191
  table.push(totalRow);
179
192
  lines.push('LLM Calls:');
180
193
  lines.push(table.toString());
194
+ if (anyIncomplete) {
195
+ lines.push(INCOMPLETE_FOOTNOTE);
196
+ }
181
197
  lines.push('');
182
198
  }
183
199
  if (data.httpDetails.length > 0) {
@@ -0,0 +1 @@
1
+ export {};