metergraph-cli 0.2.1 → 0.2.3

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 (43) hide show
  1. package/README.md +60 -11
  2. package/assets/skill/SKILL.md +52 -3
  3. package/assets/skill/manifest.json +3 -3
  4. package/assets/skills/manifest.json +13 -13
  5. package/assets/skills/metergraph-analyze/SKILL.md +62 -4
  6. package/assets/skills/metergraph-investigate/SKILL.md +222 -35
  7. package/assets/skills/metergraph-onboarding/SKILL.md +7 -0
  8. package/assets/skills/metergraph-report/SKILL.md +19 -2
  9. package/bin/metergraph.js +1 -1
  10. package/package.json +1 -1
  11. package/src/agent-key.js +160 -0
  12. package/src/approval-handoff.js +5 -0
  13. package/src/args.js +572 -17
  14. package/src/auth-browser.js +5 -19
  15. package/src/auth-callback.js +24 -15
  16. package/src/auth-login.js +55 -12
  17. package/src/auth-oauth.js +77 -7
  18. package/src/callback-page.js +58 -0
  19. package/src/cli.js +190 -45
  20. package/src/constants.js +73 -3
  21. package/src/environment.js +169 -0
  22. package/src/experiments-contract.js +462 -0
  23. package/src/experiments-output.js +189 -0
  24. package/src/experiments.js +397 -0
  25. package/src/origin.js +6 -0
  26. package/src/output.js +437 -131
  27. package/src/pricing-args.js +163 -0
  28. package/src/pricing-output.js +206 -0
  29. package/src/pricing-reference.js +195 -0
  30. package/src/pricing.js +307 -0
  31. package/src/read-contract.js +976 -16
  32. package/src/read-output.js +449 -30
  33. package/src/read.js +312 -60
  34. package/src/setup-deployment.js +11 -3
  35. package/src/setup-interactive.js +341 -0
  36. package/src/setup-refusal.js +172 -0
  37. package/src/setup-state.js +4 -2
  38. package/src/setup.js +227 -53
  39. package/src/skill-bundle.js +2 -2
  40. package/src/style.js +101 -0
  41. package/src/transport.js +13 -7
  42. package/src/usage-aggregates.js +197 -0
  43. package/src/verify-output.js +21 -11
@@ -1,11 +1,38 @@
1
1
  import { authMessage } from "./output.js";
2
+ import { PLAIN, fields, headline, paragraph, render, steps, toneFor } from "./style.js";
2
3
 
3
4
  // Fixed text for read command reasons and notices. Reasons shared with sign
4
5
  // in and doctor fall back to their wording. Nothing here contains a value from
5
6
  // a server response other than validated metadata printed by readText.
6
7
 
7
8
  const READ_MESSAGES = {
8
- not_signed_in: 'This project is not signed in. Run "metergraph login --runtime local" first.',
9
+ not_signed_in:
10
+ 'This project is not signed in and no Metadata key was given. Run "metergraph login --runtime local" first, ' +
11
+ "or, where browser sign in cannot finish, give the CLI a coding-agent key with Metadata access in " +
12
+ "METERGRAPH_AGENT_TOKEN.",
13
+ agent_key_rejected:
14
+ "The service refused the Metadata key: it is unknown, expired or revoked. Nothing was read.",
15
+ agent_key_insufficient_scope:
16
+ "The key is valid but lacks Metadata access to this workspace, for example because it is an ingest key. " +
17
+ "No broader access was requested. Nothing was read.",
18
+ agent_token_malformed:
19
+ "METERGRAPH_AGENT_TOKEN does not hold exactly one key with no label, spaces or extra lines. Its value is not " +
20
+ "shown. Nothing was sent.",
21
+ agent_token_missing: "No Metadata key was found. Nothing was sent.",
22
+ metergraph_url_invalid:
23
+ "METERGRAPH_URL is not a bare https origin, or an http origin on localhost. Its value is not shown. Nothing was sent.",
24
+ bound_to_other_origin:
25
+ "This project is bound to a different origin than --url or METERGRAPH_URL names. Nothing was sent.",
26
+ bound_to_other_workspace: "This project is bound to a different workspace than --workspace names. Nothing was sent.",
27
+ agent_key_origin_unconfirmed:
28
+ "This project's binding names a non-hosted origin, and a key is never sent to an origin taken only from a " +
29
+ "project file. Set METERGRAPH_URL or pass --url with the same origin to confirm it. Nothing was sent.",
30
+ scope_mismatch:
31
+ "The credential is not limited to exactly the Metadata scope, so it was not used. The CLI never uses broader access.",
32
+ content_access_granted:
33
+ "The credential would allow content, replay or provider access, so it was not used. The CLI never uses broader access.",
34
+ platform_credential_handoff:
35
+ "--agent-token-file cannot be checked for privacy on this platform. Use METERGRAPH_AGENT_TOKEN instead.",
9
36
  capability_unavailable:
10
37
  "The service does not offer this read to the project's Metadata grant. No data was read and no broader access was requested.",
11
38
  exceeds_service_bounds:
@@ -19,9 +46,29 @@ const READ_MESSAGES = {
19
46
  contract_version_unsupported: "The service answered with an agent access contract version this CLI does not support.",
20
47
  content_in_metadata_response:
21
48
  "The service's Metadata response contained a field that could carry captured content or a credential. Nothing from it was printed.",
22
- filter_mismatch: "The service reported different trace filters than the ones requested. Nothing from it was printed.",
49
+ filter_mismatch: "The service reported different filters than the ones requested. Nothing from it was printed.",
23
50
  filter_not_applied:
24
51
  "The service returned traces that do not match the requested filter. Nothing from it was printed.",
52
+ metadata_filter_unsupported:
53
+ "The service did not confirm the metadata filter, so the CLI cannot show that it applied. " +
54
+ "The service may predate metadata filters. Nothing from it was printed.",
55
+ metadata_filter_mismatch:
56
+ "The service reported different metadata filters than the ones requested. Nothing from it was printed.",
57
+ group_by_mismatch:
58
+ "The service did not group by the requested dimensions or metadata key. The service may predate that grouping. " +
59
+ "Nothing from it was printed.",
60
+ coverage_not_reported:
61
+ "The service's grouped usage response carries no coverage evidence, so its groups cannot be checked. " +
62
+ "Nothing from it was printed.",
63
+ window_mismatch:
64
+ "The service read a different UTC window than the one requested. Nothing from it was printed.",
65
+ usage_aggregates_unavailable:
66
+ "This deployment does not offer grouped usage (by provider, model, route or UTC day) to the project's " +
67
+ "Metadata grant. Nothing was read.",
68
+ temporarily_unavailable:
69
+ "The service stopped this read at its time budget and returned nothing partial. Nothing was retried.",
70
+ response_too_large:
71
+ "The response is larger than the read's size budget, so nothing from it was printed.",
25
72
  read_response_invalid: "The service's response is not usable JSON. Nothing from it was printed.",
26
73
  usage_response_invalid: "The service's usage response does not match the contract. Nothing from it was printed.",
27
74
  routes_response_invalid: "The service's routes response does not match the contract. Nothing from it was printed.",
@@ -38,16 +85,33 @@ const READ_MESSAGES = {
38
85
  "The service's Metadata response contained a credential this CLI holds for the project. Nothing from it was printed.",
39
86
  };
40
87
 
88
+ const AGENT_TOKEN_FILE_MESSAGE =
89
+ "The --agent-token-file file is not usable. It must be an existing regular file owned by this user with mode " +
90
+ "0600 and one link, in a directory only this user can write, holding exactly one key. Its path and content are " +
91
+ "not shown. Nothing was sent.";
92
+
41
93
  export function readMessage(outcome, reason) {
94
+ if (typeof reason === "string" && reason.startsWith("agent_token_file_")) return AGENT_TOKEN_FILE_MESSAGE;
42
95
  return READ_MESSAGES[reason] ?? authMessage(outcome, reason);
43
96
  }
44
97
 
45
98
  const NOTICES = {
99
+ workspace_not_pinned:
100
+ "No workspace was pinned, so the key's own workspace was verified and used. Pass --workspace, or bind the " +
101
+ "project, so that a key for another workspace fails instead.",
102
+ agent_key_not_used:
103
+ "METERGRAPH_AGENT_TOKEN is set, but this project's saved sign in was used. Pass --agent-token-file to use a key instead.",
46
104
  application_traffic_not_verified:
47
105
  "Application traffic is not verified. A signed in project, existing data or a configured SDK alone does not prove it.",
48
106
  evidence_incomplete: "The service marked this evidence as incomplete.",
49
107
  rows_truncated: "The service returned fewer rows than exist. Totals cover the returned rows only.",
50
108
  more_pages: "More traces exist. Pass --cursor with next_cursor to read the next page.",
109
+ metadata_groups_truncated:
110
+ "More metadata values exist than --limit. Calls in the window still counts every call.",
111
+ metadata_key_mostly_missing:
112
+ "Most of this window's calls or cost have no value for the grouped key, so the rows with a value explain less than half of it.",
113
+ metadata_values_missing:
114
+ "Some calls have no usable value for the grouped key (missing, not a string or too long). They are shown as (no value) and totalled as not attributed to any value.",
51
115
  routes_truncated_locally:
52
116
  "The service returned more routes than --limit. Only the first rows are shown; the routes endpoint has no server limit or window.",
53
117
  route_details_omitted: "Route descriptions, constraints and evaluation contract bodies are not printed.",
@@ -55,17 +119,136 @@ const NOTICES = {
55
119
  unsafe_text_omitted: "Some names contained control or formatting characters and are shown as null.",
56
120
  unrecognized_capabilities_ignored: "Capabilities this CLI does not recognize are not shown.",
57
121
  privacy_class_details_omitted: "Privacy class descriptions are not printed.",
122
+ coverage_partial:
123
+ "The service reports this read does not cover the whole question. See coverage.reasons before treating any total as complete.",
124
+ coverage_unknown:
125
+ "The service could not establish whether this read is complete, so it is not treated as complete.",
126
+ coverage_not_reported:
127
+ "This service does not report coverage details. Completeness rests on its evidence and truncation flags alone.",
128
+ pricing_lower_bound:
129
+ "Some calls have missing, partial or unknown pricing, so cost is a lower bound.",
130
+ mixed_provider_traces:
131
+ "Some traces used more than one provider or model. A trace cost is the whole-trace total and cannot be split by provider; use usage for per-route cost.",
132
+ unpriced_traces: "Some traces have no priced call, so their cost is unknown (shown as -), not zero.",
133
+ groups_truncated:
134
+ "More groups matched than --limit. Only the highest-cost groups are listed; the all-calls totals still cover every matching call.",
135
+ ranking_partial:
136
+ "The service returns the highest-cost groups first, so this ranking covers only the listed groups, not every group.",
137
+ current_day_partial:
138
+ "The window ends now, part way through the current UTC day, so that day is partial.",
139
+ partial_day_comparison:
140
+ "A partial current UTC day is compared with a whole prior period. The totals are not like for like.",
141
+ comparison_incomplete:
142
+ "One of the compared windows is incomplete, so a change may be missing groups. A group absent from a cut window is unknown, not zero.",
143
+ environment_restricted_key:
144
+ "The grant is restricted to some environments, so totals cover only those environments.",
58
145
  trace_links_unavailable:
59
146
  "The service does not provide workspace-bound trace links yet, so no link is shown.",
60
147
  };
61
148
 
149
+ // Coverage, window and pricing lines shared by usage and traces. Only
150
+ // validated fields and fixed tokens are printed.
151
+ function coverageLines(lines, report) {
152
+ // Service warning codes, once, on one line. Their messages are never printed.
153
+ const codes = [...new Set((report.warnings ?? []).map((entry) => entry.code))];
154
+ if (codes.length > 0) lines.push(`Service warnings: ${codes.join(", ")}`);
155
+ const coverage = report.coverage;
156
+ if (coverage) {
157
+ const requested = coverage.requested_window;
158
+ const effective = coverage.effective_window;
159
+ if (requested && effective && (requested.since !== effective.since || requested.until !== effective.until)) {
160
+ lines.push(
161
+ `Window read: ${effective.since} to ${effective.until} ` +
162
+ `(requested ${requested.since} to ${requested.until})`,
163
+ );
164
+ }
165
+ const matched = coverage.rows.matched === null ? "" : ` of ${coverage.rows.matched}`;
166
+ // Pricing reasons are shown on the Pricing line instead.
167
+ const reasons = coverage.reasons.filter((reason) => !reason.startsWith("pricing_"));
168
+ lines.push(
169
+ `Coverage: ${coverage.completeness}` +
170
+ (reasons.length > 0 ? ` (${reasons.join(", ")})` : "") +
171
+ `; ${coverage.rows.returned}${matched} ${coverage.rows.shape === "page" ? "rows on this page" : "rows returned"}` +
172
+ `; as of ${coverage.as_of}`,
173
+ );
174
+ } else {
175
+ lines.push("Coverage: not reported by this service");
176
+ }
177
+ const pricing = report.pricing;
178
+ if (pricing) {
179
+ lines.push(
180
+ pricing.cost === "exact"
181
+ ? `Pricing: exact (${pricing.priced_calls} priced calls, ${pricing.zero_cost_calls} priced at zero)`
182
+ : `Pricing: lower bound (${pricing.unpriced_calls} unpriced, ${pricing.partial_calls} partially priced, ` +
183
+ `${pricing.unknown_calls} unknown, ${pricing.priced_calls} priced of which ${pricing.zero_cost_calls} at zero)`,
184
+ );
185
+ }
186
+ }
187
+
188
+ // Notices for human output, without lines that repeat what the report
189
+ // already printed. JSON output keeps every notice token.
190
+ // - A Coverage line with completeness and reasons supersedes the generic
191
+ // incomplete-evidence and truncation notices.
192
+ // - A Pricing line supersedes the lower-bound notice.
193
+ // - The "mostly missing" warning supersedes the plain missing-values notice.
194
+ // - Service warning codes are printed once on the "Service warnings" line,
195
+ // so the "messages omitted" note adds nothing in text.
196
+ // - Grouped usage prints the partial UTC day in its Window line, the spend
197
+ // without a key value in its metadata lines, and the comparison's window
198
+ // lengths, prior coverage and completeness in its comparison lines.
199
+ // A metadata grouping where no call has a value: every call is unattributed.
200
+ function noCallHasTheKey(report) {
201
+ const groups = report?.metadata_groups;
202
+ if (!groups || !report.totals) return false;
203
+ return report.totals.calls > 0 && groups.listed_with_value.calls === 0 &&
204
+ groups.unlisted.other_values.calls === 0 && groups.unattributed.calls === report.totals.calls;
205
+ }
206
+
207
+ function textNotices(tokens, report) {
208
+ const shown = new Set(tokens);
209
+ const coverage = report?.coverage ?? null;
210
+ if (coverage && coverage.completeness !== "complete") {
211
+ for (const token of ["evidence_incomplete", "rows_truncated", "groups_truncated"]) shown.delete(token);
212
+ }
213
+ if (report?.pricing) shown.delete("pricing_lower_bound");
214
+ if (report?.source === "usage_aggregates") {
215
+ shown.delete("current_day_partial");
216
+ if (report.metadata_groups) shown.delete("metadata_values_missing");
217
+ if (report.comparison) {
218
+ for (const token of ["partial_day_comparison", "comparison_incomplete"]) shown.delete(token);
219
+ // Only the prior notices the comparison lines state (coverage, pricing,
220
+ // completeness, truncation); any other prior notice is still shown.
221
+ for (const token of [
222
+ "prior_coverage_partial", "prior_coverage_unknown", "prior_coverage_not_reported",
223
+ "prior_pricing_lower_bound", "prior_evidence_incomplete", "prior_groups_truncated",
224
+ "prior_warning_messages_omitted",
225
+ ]) shown.delete(token);
226
+ }
227
+ }
228
+ if (shown.has("metadata_key_mostly_missing")) shown.delete("metadata_values_missing");
229
+ // The grouped body states outright when no call has the key.
230
+ if (noCallHasTheKey(report)) {
231
+ shown.delete("metadata_key_mostly_missing");
232
+ shown.delete("metadata_values_missing");
233
+ }
234
+ shown.delete("warning_messages_omitted");
235
+ return tokens.filter((token) => shown.has(token));
236
+ }
237
+
62
238
  export function noticeText(token) {
239
+ if (token.startsWith("prior_")) {
240
+ const text = NOTICES[token.slice("prior_".length)];
241
+ return text === undefined ? null : `Prior window: ${text}`;
242
+ }
63
243
  return NOTICES[token] ?? null;
64
244
  }
65
245
 
66
246
  const yesNo = (value) => (value === true ? "yes" : value === false ? "no" : "not checked");
67
247
  const show = (value) => (value === null || value === undefined ? "-" : String(value));
68
- const usd = (value) => (value === null ? "-" : `$${value.toFixed(4)}`);
248
+ // Four decimals, or the service's eight for a non-zero amount below $0.0001,
249
+ // so a small cost never prints as zero.
250
+ const usd = (value) =>
251
+ value === null ? "-" : `$${value !== 0 && Math.abs(value) < 0.0001 ? value.toFixed(8) : value.toFixed(4)}`;
69
252
 
70
253
  function table(header, rows) {
71
254
  const widths = header.map((title, column) =>
@@ -75,51 +258,100 @@ function table(header, rows) {
75
258
  return [line(header), ...rows.map(line)];
76
259
  }
77
260
 
78
- export function readText(result, message) {
261
+ const READ_TITLES = {
262
+ status: "Project status",
263
+ context: "Workspace context",
264
+ capabilities: "Agent capabilities",
265
+ usage: "Usage",
266
+ routes: "Routes",
267
+ traces: "Traces",
268
+ };
269
+
270
+ export function readText(result, message, style = PLAIN) {
79
271
  const data = result.data;
80
- const lines = [`Metergraph ${result.command} (Metadata only)`];
81
- if (data.origin !== null) lines.push(`Origin: ${data.origin}`);
272
+ const title = `${READ_TITLES[result.command] ?? result.command}${result.ok ? "" : " could not be read"}`;
273
+ const lines = [
274
+ `${headline(style, result.ok ? "info" : toneFor(result.outcome), title, result.outcome)} ${style.dim("Metadata only")}`,
275
+ "",
276
+ ];
277
+ const rows = [["Origin", data.origin]];
82
278
  if (result.command === "status") {
83
- statusLines(data, lines);
279
+ statusRows(data, rows);
280
+ lines.push(...fields(style, rows));
84
281
  } else {
85
- if (data.workspace !== null) lines.push(`Workspace: ${data.workspace.id}`);
86
- if (result.ok) resultLines(result.command, data.result, lines);
282
+ rows.push(["Workspace", data.workspace?.id ?? null]);
283
+ if (result.ok) {
284
+ const body = [];
285
+ resultLines(result.command, data.result, body);
286
+ // "Label: value" lines before the first blank line join the field block.
287
+ let split = 0;
288
+ while (split < body.length && body[split] !== "" && /^[A-Z][^:]{0,40}: /.test(body[split])) {
289
+ const at = body[split].indexOf(": ");
290
+ rows.push([body[split].slice(0, at), body[split].slice(at + 2)]);
291
+ split += 1;
292
+ }
293
+ lines.push(...fields(style, rows), "");
294
+ for (const line of body.slice(split)) {
295
+ lines.push(/^[A-Z_]+( +[A-Z_]+)*$/.test(line.trim()) && line === line.toUpperCase() && line.trim() !== ""
296
+ ? ` ${style.dim(line)}` : line === "" ? "" : ` ${line}`);
297
+ }
298
+ } else {
299
+ lines.push(...fields(style, rows));
300
+ }
301
+ }
302
+ const notices = textNotices(data.notices, result.ok ? data.result : null).map(noticeText).filter(Boolean);
303
+ if (notices.length > 0) {
304
+ lines.push("");
305
+ for (const text of notices) {
306
+ const [first, ...rest] = paragraph(style, text, " ");
307
+ lines.push(` ${style.yellow("!")} ${first.trimStart()}`, ...rest);
308
+ }
87
309
  }
88
- const notices = data.notices.map(noticeText).filter(Boolean);
89
- if (notices.length > 0) lines.push("", ...notices.map((text) => `Note: ${text}`));
90
310
  if (!result.ok) {
91
- lines.push("", `Result: ${result.outcome} (exit ${result.exit_code})`, message);
311
+ lines.push("", ...paragraph(style, message));
92
312
  if (data.retry_after_seconds !== null && data.retry_after_seconds !== undefined) {
93
- lines.push(`Retry after: ${data.retry_after_seconds} seconds`);
313
+ lines.push(...fields(style, [["Retry after", `${data.retry_after_seconds} seconds`]]));
94
314
  }
95
315
  }
96
- if (data.next_action?.message) lines.push(`Next: ${data.next_action.message}`);
97
- return `${lines.join("\n")}\n`;
316
+ if (data.next_action?.message) {
317
+ const next = data.next_action;
318
+ const handoff = next.kind === "use_agent_key";
319
+ const keysUrl = handoff
320
+ ? next.person_steps.find((step) => step.action === "create_metadata_key")?.url ?? null
321
+ : next.url ?? null;
322
+ lines.push("", ...steps(style, [
323
+ ["", next.message],
324
+ ["Keys page", keysUrl],
325
+ ["Connection guide", handoff ? next.url : null],
326
+ ]));
327
+ }
328
+ return render(lines);
98
329
  }
99
330
 
100
- function statusLines(data, lines) {
101
- lines.push(
102
- `Configured: ${data.configured ? "yes (.metergraph/project.json)" : "no"}`,
103
- `Reachable: ${yesNo(data.reachable)}`,
104
- `Authenticated: ${data.authenticated ? "yes, verified by the service" : "no"}`,
105
- `Workspace: bound ${show(data.workspace.intended)}, verified ${show(data.workspace.actual)}`,
106
- `Deployment profile: bound ${show(data.deployment_profile)}, reported by the service: ${
331
+ function statusRows(data, rows) {
332
+ rows.push(
333
+ ["Configured", data.configured ? "yes" : "no"],
334
+ ["Credential", { oauth_grant: "saved sign in", agent_key: "Metadata key" }[data.credential] ?? "none"],
335
+ ["Reachable", yesNo(data.reachable)],
336
+ ["Authenticated", data.authenticated ? "yes, verified by the service" : "no"],
337
+ ["Workspace", `bound ${show(data.workspace.intended)}, verified ${show(data.workspace.actual)}`],
338
+ ["Deployment profile", `bound ${show(data.deployment_profile)}, reported by the service: ${
107
339
  data.deployment_profile_verified === true
108
340
  ? "same"
109
341
  : data.deployment_profile_verified === false
110
342
  ? "different"
111
343
  : "not checked"
112
- }`,
113
- `Scopes: ${data.scopes.length > 0 ? data.scopes.join(" ") : "none"}`,
344
+ }`],
345
+ ["Scopes", data.scopes.length > 0 ? data.scopes.join(" ") : "none"],
114
346
  );
115
347
  if (data.capabilities !== null) {
116
348
  const flag = (name) => yesNo(data.capabilities[name]);
117
- lines.push(
118
- `Metadata reads: usage ${flag("usage")}, routes ${flag("routes")}, traces ${flag("trace_metadata")}`,
119
- "Content, evidence and replay: not available to this grant",
349
+ rows.push(
350
+ ["Metadata reads", `usage ${flag("usage")}, routes ${flag("routes")}, traces ${flag("trace_metadata")}`],
351
+ ["Content and replay", "not available to this grant"],
120
352
  );
121
353
  }
122
- lines.push(`Application traffic verified: no`);
354
+ rows.push(["Application traffic verified", "no"]);
123
355
  }
124
356
 
125
357
  function resultLines(command, report, lines) {
@@ -149,8 +381,17 @@ function resultLines(command, report, lines) {
149
381
  );
150
382
  return;
151
383
  }
384
+ if (command === "usage" && report.source === "usage_aggregates") {
385
+ aggregateUsageLines(report, lines);
386
+ return;
387
+ }
152
388
  if (command === "usage") {
153
- lines.push(`Window: last ${report.window.days} days (${report.window.since} to ${report.window.until})`, "");
389
+ lines.push(
390
+ `Window: rolling ${report.window.hours} h ending now, ${report.window.since} to ${report.window.until} UTC ` +
391
+ "(not aligned to UTC days: the first and last dates are partial; --today, --since or --group-by use UTC days)",
392
+ );
393
+ if (report.filters.metadata.count > 0) lines.push(metadataFilterLine(report.filters.metadata));
394
+ lines.push("");
154
395
  if (report.empty) {
155
396
  lines.push("No usage rows in this window.");
156
397
  } else {
@@ -173,6 +414,14 @@ function resultLines(command, report, lines) {
173
414
  `${totals.input_tokens} input and ${totals.output_tokens} output tokens.`,
174
415
  );
175
416
  }
417
+ if (report.population_totals) {
418
+ const all = report.population_totals;
419
+ lines.push(
420
+ `All matching calls in the window: ${all.calls} calls, ${all.error_calls} errors, ${usd(all.cost_usd)}, ` +
421
+ `${all.input_tokens} input and ${all.output_tokens} output tokens.`,
422
+ );
423
+ }
424
+ coverageLines(lines, report);
176
425
  lines.push(`Evidence: ${report.complete ? "complete" : "incomplete"}`);
177
426
  return;
178
427
  }
@@ -194,7 +443,9 @@ function resultLines(command, report, lines) {
194
443
  return;
195
444
  }
196
445
  if (command === "traces") {
197
- lines.push(`Window: last ${report.window.days} days`, "");
446
+ lines.push(`Window: last ${report.window.days} days`);
447
+ if (report.filters.metadata.count > 0) lines.push(metadataFilterLine(report.filters.metadata));
448
+ lines.push("");
198
449
  if (report.empty) {
199
450
  lines.push("No traces on this page.");
200
451
  } else {
@@ -211,6 +462,174 @@ function resultLines(command, report, lines) {
211
462
  }
212
463
  lines.push("", `Page: ${report.page.rows} traces, limit ${report.page.limit}`);
213
464
  if (report.next_cursor !== null) lines.push(`Next page: --cursor ${report.next_cursor}`);
465
+ coverageLines(lines, report);
466
+ const mixed = report.traces.filter((trace) => trace.mixed_providers).length;
467
+ const matching = report.attribution?.cost_basis === "matching_calls";
468
+ lines.push(
469
+ (matching ? "Costs: matching calls only (a filter selected calls, not whole traces)" : "Costs: whole-trace totals") +
470
+ (mixed > 0 ? `; ${mixed} trace${mixed === 1 ? "" : "s"} used several providers, not split by provider` : ""),
471
+ );
472
+ lines.push(`Evidence: ${report.complete ? "complete" : "incomplete"}`);
214
473
  lines.push("Trace links: not available from the service yet");
215
474
  }
216
475
  }
476
+
477
+ // Filter values are not printed back; only the keys they applied to.
478
+ function metadataFilterLine(filters) {
479
+ return `Metadata filter: exact match on ${filters.keys.join(", ")}`;
480
+ }
481
+
482
+ const DIMENSION_TITLES = { provider: "PROVIDER", model: "MODEL", route: "ROUTE", day: "DAY_UTC" };
483
+ const titleOf = (dimension) => DIMENSION_TITLES[dimension] ?? "METADATA_VALUE";
484
+ // One group's cell for a dimension. The metadata group without a value is
485
+ // named, not shown as an empty string.
486
+ const cellOf = (item, dimension) => {
487
+ if (dimension === "day") return `${item.day}${item.partial_day ? " (partial)" : ""}`;
488
+ if (dimension.startsWith("metadata.")) return item.has_value ? show(item.metadata_value) : "(no value)";
489
+ return show(item[dimension]);
490
+ };
491
+ // A change in dollars: four decimals, or the service's eight when smaller, so
492
+ // a non-zero change never prints as zero.
493
+ const usdChange = (value) => `$${value !== 0 && value < 0.0001 ? value.toFixed(8) : value.toFixed(4)}`;
494
+ const signed = (value, format) => (value === null ? "-" : `${value > 0 ? "+" : value < 0 ? "-" : ""}${format(Math.abs(value))}`);
495
+ const percent = (value) => (value === null ? "-" : `${value > 0 ? "+" : ""}${value}%`);
496
+
497
+ function windowLine(window) {
498
+ const kind = window.preset === "utc_days"
499
+ ? window.days === 1
500
+ ? "today so far"
501
+ : `the current UTC day so far and the ${window.days - 1} whole UTC days before it`
502
+ : "explicit window";
503
+ const partial = window.partial_days.length > 0 ? `; partial UTC day ${window.partial_days.join(", ")}` : "";
504
+ return `${window.since} to ${window.until} UTC, ${window.hours} h (${kind}${partial})`;
505
+ }
506
+
507
+ // Grouped usage: one row per group in the chosen order, then all-calls
508
+ // totals, coverage, pricing, attribution and the latency definitions.
509
+ function aggregateUsageLines(report, lines) {
510
+ const filters = Object.entries(report.filters)
511
+ .filter(([key, count]) => key !== "metadata" && count > 0)
512
+ .map(([key, count]) => `${key} (${count} value${count === 1 ? "" : "s"})`);
513
+ lines.push(`Window: ${windowLine(report.window)}`, `Grouped by: ${report.group_by.join(", ")}`);
514
+ const metadataDimension = report.group_by.find((dimension) => dimension.startsWith("metadata."));
515
+ if (metadataDimension) {
516
+ lines.push(`Metadata key: ${metadataDimension.slice("metadata.".length)} (a route is not a prompt; one route can carry several values)`);
517
+ }
518
+ if (filters.length > 0) lines.push(`Filters: exact match on ${filters.join(", ")}`);
519
+ if (report.filters.metadata.count > 0) lines.push(metadataFilterLine(report.filters.metadata));
520
+ lines.push(
521
+ `Ranked by: ${report.sort.meaning}${report.sort.complete ? "" : " (listed groups only)"}`,
522
+ "",
523
+ );
524
+ if (report.empty) {
525
+ lines.push("No calls match this window and these filters.");
526
+ } else {
527
+ const rows = report.items.map((item) => [
528
+ String(item.rank),
529
+ ...report.group_by.map((dimension) => cellOf(item, dimension)),
530
+ usd(item.cost_usd),
531
+ String(item.calls),
532
+ usd(item.cost_per_call_usd),
533
+ String(item.error_calls),
534
+ show(item.latency.avg_ms),
535
+ show(item.latency.p95_ms),
536
+ ]);
537
+ lines.push(...table(
538
+ ["#", ...report.group_by.map(titleOf), "COST", "CALLS", "COST/CALL", "ERRORS",
539
+ "AVG_MS", "P95_MS_EST"],
540
+ rows,
541
+ ));
542
+ }
543
+ const all = report.totals;
544
+ lines.push(
545
+ "",
546
+ `Groups: ${report.rows} listed of ${report.matched_groups ?? "an unreported number of"} matched.`,
547
+ `All matching calls: ${all.calls} calls, ${all.error_calls} errors, ${usd(all.cost_usd)}, ` +
548
+ `${usd(all.cost_per_call_usd)} per call, ${all.input_tokens} input and ${all.output_tokens} output tokens.`,
549
+ );
550
+ if (noCallHasTheKey(report)) {
551
+ lines.push(
552
+ `No call in this window has a value for ${report.metadata_groups.key}. ` +
553
+ "Check the key's spelling and that the SDK sets it.",
554
+ );
555
+ }
556
+ if (report.metadata_groups) {
557
+ const groups = report.metadata_groups;
558
+ const { other_values: other, missing_key: missing, ineligible_value: ineligible } = groups.unlisted;
559
+ lines.push(
560
+ `Attributed to a ${groups.key} value: ${groups.listed_with_value.calls} listed calls ` +
561
+ `(${usd(groups.listed_with_value.cost_usd)}); ${other.groups} more values past --limit: ` +
562
+ `${other.calls} calls (${usd(other.cost_usd)}).`,
563
+ `Not attributed to any ${groups.key} value: ${groups.unattributed.calls} calls ` +
564
+ `(${usd(groups.unattributed.cost_usd)}): without the key ${missing.calls} (${usd(missing.cost_usd)}), ` +
565
+ `non-string or oversize value ${ineligible.calls} (${usd(ineligible.cost_usd)}).`,
566
+ );
567
+ }
568
+ coverageLines(lines, report);
569
+ if (report.attribution) {
570
+ lines.push(
571
+ `Attribution: per ${report.attribution.level}; ` +
572
+ (report.attribution.per_provider_exact
573
+ ? "a trace that used several providers adds each call's cost to that call's provider"
574
+ : "cost is not split exactly by provider"),
575
+ );
576
+ }
577
+ lines.push(
578
+ "Latency: AVG_MS is the mean over calls that reported latency; P95_MS_EST is estimated from a fixed latency " +
579
+ "histogram, not an exact percentile.",
580
+ `Evidence: ${report.complete ? "complete" : "incomplete"}`,
581
+ );
582
+ if (report.comparison) comparisonLines(report, lines);
583
+ }
584
+
585
+ function comparisonLines(report, lines) {
586
+ const comparison = report.comparison;
587
+ const { totals } = comparison;
588
+ lines.push(
589
+ "",
590
+ `Compared with ${comparison.mode === "previous" ? "the whole previous period" : "the previous period to the same elapsed time"}: ` +
591
+ windowLine(comparison.prior.window),
592
+ `Total cost: ${usd(totals.current.cost_usd)} now, ${usd(totals.prior.cost_usd)} before, ` +
593
+ (totals.cost_change_known
594
+ ? `${signed(totals.cost_change.delta, usdChange)} (${percent(totals.cost_change.percent)})`
595
+ : "change unknown (a window's coverage is incomplete or its cost is a lower bound)"),
596
+ `Calls: ${totals.current.calls} now, ${totals.prior.calls} before, ` +
597
+ (totals.calls_change_known === false
598
+ ? "change unknown (a window's coverage is incomplete)"
599
+ : `${signed(totals.calls_change.delta, String)} (${percent(totals.calls_change.percent)})`),
600
+ );
601
+ if (comparison.partial_day_compared_with_full) {
602
+ lines.push(
603
+ `Not like for like: ${comparison.hours.current} h now against ${comparison.hours.prior} h before.`,
604
+ );
605
+ }
606
+ if (comparison.items.length > 0) {
607
+ const rows = comparison.items.map((item) => [
608
+ ...report.group_by.map((dimension) => (dimension === "day" ? show(item.day) : cellOf(item, dimension))),
609
+ item.current === null ? "unknown" : usd(item.current.cost_usd),
610
+ item.prior === null ? "unknown" : usd(item.prior.cost_usd),
611
+ signed(item.cost_change.delta, usdChange),
612
+ percent(item.cost_change.percent),
613
+ item.current === null ? "unknown" : String(item.current.calls),
614
+ item.prior === null ? "unknown" : String(item.prior.calls),
615
+ ]);
616
+ lines.push(
617
+ "",
618
+ ...table(
619
+ [...report.group_by.map(titleOf), "COST", "PRIOR_COST", "CHANGE", "CHANGE_%",
620
+ "CALLS", "PRIOR_CALLS"],
621
+ rows,
622
+ ),
623
+ );
624
+ }
625
+ const prior = comparison.prior;
626
+ const priorCodes = [...new Set((prior.warnings ?? []).map((entry) => entry.code))];
627
+ lines.push(
628
+ "",
629
+ ...(priorCodes.length > 0 ? [`Prior service warnings: ${priorCodes.join(", ")}`] : []),
630
+ `Prior coverage: ${prior.coverage?.completeness ?? "not reported"}` +
631
+ (prior.coverage && prior.coverage.reasons.length > 0 ? ` (${prior.coverage.reasons.join(", ")})` : "") +
632
+ `; pricing ${prior.pricing?.cost === "exact" ? "exact" : prior.pricing ? "lower bound" : "not reported"}`,
633
+ `Comparison: ${comparison.complete ? "complete" : "incomplete"}`,
634
+ );
635
+ }