sidekick-shared 0.28.3 → 0.29.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 (97) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +149 -19
  3. package/dist/authStatus.d.ts +54 -0
  4. package/dist/authStatus.js +91 -0
  5. package/dist/browser.d.ts +12 -6
  6. package/dist/browser.js +17 -1
  7. package/dist/codex.d.ts +23 -0
  8. package/dist/codex.js +44 -0
  9. package/dist/codexProfiles.d.ts +1 -0
  10. package/dist/codexProfiles.js +8 -11
  11. package/dist/esm/authStatus.js +87 -0
  12. package/dist/esm/browser.js +4 -2
  13. package/dist/esm/codex.js +18 -0
  14. package/dist/esm/codexProfiles.js +8 -11
  15. package/dist/esm/formatting.js +10 -1
  16. package/dist/esm/index.js +9 -6
  17. package/dist/esm/modelCatalog.js +2 -1
  18. package/dist/esm/modelInfo.js +422 -45
  19. package/dist/esm/node.js +9 -3
  20. package/dist/esm/parsers/codexParser.js +175 -54
  21. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  22. package/dist/esm/parsers/openCodeParser.js +83 -21
  23. package/dist/esm/pricing.js +18 -0
  24. package/dist/esm/pricingCatalog.js +14 -7
  25. package/dist/esm/promptHistory.js +66 -4
  26. package/dist/esm/providerFailure.js +124 -3
  27. package/dist/esm/providers/codex.js +2 -24
  28. package/dist/esm/providers/codexDatabase.js +3 -19
  29. package/dist/esm/providers/codexRollouts.js +24 -1
  30. package/dist/esm/providers/openCode.js +9 -17
  31. package/dist/esm/report/htmlReportGenerator.js +42 -5
  32. package/dist/esm/report/transcriptParser.js +13 -11
  33. package/dist/esm/schemas/providerFailure.js +3 -1
  34. package/dist/esm/schemas/transcript.js +4 -0
  35. package/dist/esm/sessionPromptHistory.js +2 -2
  36. package/dist/esm/statusline/payload.js +9 -0
  37. package/dist/esm/testing/credentialGuard.js +87 -5
  38. package/dist/esm/toolCallOrigins.js +310 -0
  39. package/dist/esm/transcript.js +42 -3
  40. package/dist/esm/usage/usageEvents.js +146 -17
  41. package/dist/esm/usage/usageReports.js +39 -6
  42. package/dist/esm/watchers/jsonlTail.js +378 -59
  43. package/dist/formatting.d.ts +26 -1
  44. package/dist/formatting.js +11 -1
  45. package/dist/index.d.ts +16 -10
  46. package/dist/index.js +35 -12
  47. package/dist/modelCatalog.js +2 -1
  48. package/dist/modelInfo.d.ts +185 -16
  49. package/dist/modelInfo.js +431 -47
  50. package/dist/node.d.ts +16 -5
  51. package/dist/node.js +31 -2
  52. package/dist/parsers/codexParser.d.ts +55 -0
  53. package/dist/parsers/codexParser.js +175 -54
  54. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  55. package/dist/parsers/codexRolloutOrigin.js +111 -0
  56. package/dist/parsers/openCodeParser.d.ts +38 -0
  57. package/dist/parsers/openCodeParser.js +86 -21
  58. package/dist/pricing.d.ts +22 -0
  59. package/dist/pricing.js +50 -0
  60. package/dist/pricingCatalog.d.ts +10 -0
  61. package/dist/pricingCatalog.js +13 -6
  62. package/dist/promptHistory.d.ts +65 -4
  63. package/dist/promptHistory.js +68 -5
  64. package/dist/providerFailure.d.ts +44 -0
  65. package/dist/providerFailure.js +126 -4
  66. package/dist/providers/codex.d.ts +3 -22
  67. package/dist/providers/codex.js +3 -26
  68. package/dist/providers/codexDatabase.d.ts +3 -2
  69. package/dist/providers/codexDatabase.js +4 -22
  70. package/dist/providers/codexRollouts.d.ts +22 -0
  71. package/dist/providers/codexRollouts.js +24 -0
  72. package/dist/providers/openCode.js +8 -16
  73. package/dist/report/htmlReportGenerator.js +42 -5
  74. package/dist/report/transcriptParser.d.ts +10 -1
  75. package/dist/report/transcriptParser.js +13 -11
  76. package/dist/schemas/providerFailure.d.ts +39 -0
  77. package/dist/schemas/providerFailure.js +2 -0
  78. package/dist/schemas/transcript.d.ts +2 -0
  79. package/dist/schemas/transcript.js +4 -0
  80. package/dist/sessionPromptHistory.d.ts +7 -1
  81. package/dist/sessionPromptHistory.js +1 -1
  82. package/dist/statusline/payload.d.ts +10 -0
  83. package/dist/statusline/payload.js +14 -0
  84. package/dist/testing/credentialGuard.d.ts +14 -0
  85. package/dist/testing/credentialGuard.js +87 -5
  86. package/dist/toolCallOrigins.d.ts +70 -0
  87. package/dist/toolCallOrigins.js +346 -0
  88. package/dist/transcript.d.ts +12 -0
  89. package/dist/transcript.js +42 -3
  90. package/dist/types/opencode.d.ts +2 -0
  91. package/dist/usage/usageEvents.d.ts +54 -6
  92. package/dist/usage/usageEvents.js +146 -17
  93. package/dist/usage/usageReports.d.ts +35 -4
  94. package/dist/usage/usageReports.js +39 -6
  95. package/dist/watchers/jsonlTail.d.ts +101 -4
  96. package/dist/watchers/jsonlTail.js +380 -60
  97. package/package.json +41 -1
@@ -11,9 +11,19 @@
11
11
  * every hit and pruned least-recently-used, once per collection.
12
12
  *
13
13
  * A Claude Code session's subagents write their own transcripts under
14
- * `<sessionId>/subagents/`; their usage is attributed to the parent session
15
- * and their sizes and mtimes join the fingerprint. (Codex spawned subagents
16
- * are top-level rollouts, already listed as sessions of their own.)
14
+ * `<sessionId>/subagents/`; their usage is attributed to the parent session,
15
+ * tagged with the subagent, and their sizes and mtimes join the fingerprint.
16
+ * (Codex spawned subagents and forks are top-level rollouts, already listed
17
+ * as sessions of their own; their events are tagged with the parent read
18
+ * from the rollout's first line.)
19
+ *
20
+ * Each event also carries the ordinal of the human prompt it answered, from
21
+ * the same scanner as `collectPromptHistory()`, so usage joins prompt history
22
+ * on `(provider, sessionId, ordinal)`.
23
+ *
24
+ * Cache files are named per cache version (`<provider>--<session>.v3.json`)
25
+ * so readers of different versions sharing the directory never overwrite each
26
+ * other's files; pruning ages out every version's files alike.
17
27
  *
18
28
  * Node-only: reads session files and the cache directory.
19
29
  *
@@ -21,7 +31,10 @@
21
31
  */
22
32
  import * as fs from 'fs';
23
33
  import * as path from 'path';
34
+ import { codexRolloutOrigin } from '../parsers/codexRolloutOrigin';
24
35
  import { getConfigDir } from '../paths';
36
+ import { readPromptOrdinalMarks } from '../promptHistory';
37
+ import { readFirstLineAsync } from '../providers/codexRollouts';
25
38
  import { listSessionPreviewsAsync } from '../sessionPreviews';
26
39
  import { calculateNormalizedUsageCost, extractNormalizedUsage } from '../usageNormalization';
27
40
  import { atomicWriteJson } from '../writers/atomic';
@@ -29,9 +42,11 @@ import { resolveWorkspaceRoots } from '../workspaceRoots';
29
42
  /**
30
43
  * Bump when the cached record shape or the counting rules change; older files
31
44
  * are re-read. v2: Claude split lines and duplicate Codex `token_count` events
32
- * are counted once, and Claude subagent transcripts are included.
45
+ * are counted once, and Claude subagent transcripts are included. v3: events
46
+ * keep reasoning tokens, prompt ordinal, message id, and subagent, and files
47
+ * move to a versioned name (`<provider>--<session>.v3.json`).
33
48
  */
34
- export const USAGE_CACHE_VERSION = 2;
49
+ export const USAGE_CACHE_VERSION = 3;
35
50
  /**
36
51
  * Most-recently-used cache files kept on disk. Sized for a busy month of
37
52
  * sessions (files are a few KB each); a collection larger than this still
@@ -42,9 +57,15 @@ export const MAX_USAGE_CACHE_FILES = 5000;
42
57
  export function getUsageCacheDir() {
43
58
  return path.join(getConfigDir(), 'usage-cache');
44
59
  }
60
+ /**
61
+ * Cache file of one session. The name carries the cache version, so a reader
62
+ * of another version sharing the directory (which rejects this version, and
63
+ * versions before 3 use the unversioned `<provider>--<session>.json`) never
64
+ * overwrites it, and this version never overwrites theirs.
65
+ */
45
66
  function cacheFilePath(dir, provider, sessionId) {
46
67
  const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, '_');
47
- return path.join(dir, `${provider}--${safe}.json`);
68
+ return path.join(dir, `${provider}--${safe}.v${USAGE_CACHE_VERSION}.json`);
48
69
  }
49
70
  function toMs(value, label) {
50
71
  if (value === undefined)
@@ -75,7 +96,11 @@ function readCache(filePath) {
75
96
  return null;
76
97
  }
77
98
  }
78
- /** Deletes all but the `keep` most recently used cache files (by mtime). Best-effort. */
99
+ /**
100
+ * Deletes all but the `keep` most recently used cache files (by mtime), of
101
+ * every cache version alike, so files no reader touches any more age out.
102
+ * Best-effort.
103
+ */
79
104
  export function pruneUsageCache(keep = MAX_USAGE_CACHE_FILES, dir = getUsageCacheDir()) {
80
105
  try {
81
106
  const entries = fs
@@ -108,7 +133,7 @@ export function pruneUsageCache(keep = MAX_USAGE_CACHE_FILES, dir = getUsageCach
108
133
  return 0;
109
134
  }
110
135
  }
111
- function cachedEventFromUsage(usage, timestamp, model) {
136
+ function cachedEventFromUsage(usage, timestamp, model, messageId, agentId) {
112
137
  const record = {
113
138
  t: timestamp,
114
139
  m: model,
@@ -121,16 +146,29 @@ function cachedEventFromUsage(usage, timestamp, model) {
121
146
  };
122
147
  if (usage.reportedCostUsd !== undefined)
123
148
  record.rc = usage.reportedCostUsd;
149
+ if (usage.reasoningTokens > 0)
150
+ record.rt = usage.reasoningTokens;
151
+ if (messageId && !messageId.startsWith('token-count-'))
152
+ record.rid = messageId;
153
+ if (agentId)
154
+ record.sa = agentId;
124
155
  return record;
125
156
  }
126
- /** Price a cached event with the current catalog (or its provider-reported cost). */
127
- function priceCachedEvent(cached, provider, sessionId, project, sessionFile) {
157
+ /**
158
+ * Price a cached event with the current catalog (or its provider-reported
159
+ * cost). Output is priced from the billable output (`bo`), which already
160
+ * holds reasoning however the provider reports it, so restoring the reasoning
161
+ * count never changes the cost.
162
+ */
163
+ function priceCachedEvent(cached, provider, sessionId, project, sessionFile, sessionSubagent) {
164
+ const reasoningTokens = typeof cached.rt === 'number' && cached.rt > 0 ? cached.rt : 0;
128
165
  const usage = {
129
166
  uncachedInputTokens: cached.i,
130
167
  cacheReadTokens: cached.cr,
131
168
  cacheWriteTokens: cached.cw,
132
169
  outputTokens: cached.o,
133
- reasoningTokens: 0,
170
+ reasoningTokens,
171
+ // `bo` is already the billable output; nothing is added to it.
134
172
  reasoningIncludedInOutput: true,
135
173
  cacheInclusiveInputTokens: cached.i + cached.cr + cached.cw,
136
174
  billableOutputTokens: cached.bo,
@@ -142,6 +180,9 @@ function priceCachedEvent(cached, provider, sessionId, project, sessionFile) {
142
180
  usage,
143
181
  modelId: cached.m === 'unknown' ? undefined : cached.m,
144
182
  });
183
+ const subagent = typeof cached.sa === 'string' && cached.sa
184
+ ? { agentId: cached.sa, parentSessionId: sessionId }
185
+ : sessionSubagent;
145
186
  return {
146
187
  timestamp: cached.t,
147
188
  provider,
@@ -154,13 +195,19 @@ function priceCachedEvent(cached, provider, sessionId, project, sessionFile) {
154
195
  cacheWriteTokens: cached.cw,
155
196
  cacheReadTokens: cached.cr,
156
197
  totalTokens: cached.tt,
198
+ ...(reasoningTokens > 0 ? { reasoningTokens } : {}),
157
199
  },
158
200
  costUsd: priced.costUsd,
159
201
  costProvenance: priced.source,
160
202
  filePath: sessionFile,
203
+ ...(cached.po === null || (typeof cached.po === 'number' && Number.isInteger(cached.po))
204
+ ? { promptOrdinal: cached.po }
205
+ : {}),
206
+ ...(typeof cached.rid === 'string' ? { requestId: cached.rid } : {}),
207
+ ...(subagent ? { subagent: { ...subagent } } : {}),
161
208
  };
162
209
  }
163
- const CLAUDE_AGENT_FILE = /^agent-.+\.jsonl$/;
210
+ const CLAUDE_AGENT_FILE = /^agent-(.+)\.jsonl$/;
164
211
  /** Claude Code subagent transcripts of a session, sorted by name. */
165
212
  function claudeSubagentFiles(preview) {
166
213
  if (preview.provider !== 'claude-code')
@@ -180,6 +227,7 @@ function claudeSubagentFiles(preview) {
180
227
  files.push({
181
228
  path: path.join(dir, name),
182
229
  name,
230
+ agentId: CLAUDE_AGENT_FILE.exec(name)?.[1] ?? name,
183
231
  sizeBytes: stat.size,
184
232
  modifiedAt: stat.mtimeMs,
185
233
  });
@@ -196,7 +244,7 @@ function sessionFingerprint(preview, subagents) {
196
244
  return base;
197
245
  return `${base}|${subagents.map((f) => `${f.name}:${f.sizeBytes}:${f.modifiedAt}`).join(',')}`;
198
246
  }
199
- function readTranscriptUsage(provider, filePath, cached) {
247
+ function readTranscriptUsage(provider, filePath, cached, agentId) {
200
248
  const reader = provider.createReader(filePath);
201
249
  const events = reader.readAll();
202
250
  reader.flush();
@@ -218,12 +266,14 @@ function readTranscriptUsage(provider, filePath, cached) {
218
266
  target.cr += usage.cacheReadTokens;
219
267
  target.bo += usage.billableOutputTokens;
220
268
  target.tt += usage.totalTokens;
269
+ if (usage.reasoningTokens > 0)
270
+ target.rt = (target.rt ?? 0) + usage.reasoningTokens;
221
271
  continue;
222
272
  }
223
273
  const timestamp = Date.parse(event.timestamp);
224
274
  if (!Number.isFinite(timestamp))
225
275
  continue;
226
- const record = cachedEventFromUsage(usage, timestamp, event.message?.model ?? usage.model ?? 'unknown');
276
+ const record = cachedEventFromUsage(usage, timestamp, event.message?.model ?? usage.model ?? 'unknown', messageId, agentId);
227
277
  cached.push(record);
228
278
  if (messageId)
229
279
  byMessageId.set(messageId, record);
@@ -233,10 +283,75 @@ function readSessionUsage(provider, preview, subagents) {
233
283
  const cached = [];
234
284
  readTranscriptUsage(provider, preview.filePath, cached);
235
285
  for (const file of subagents)
236
- readTranscriptUsage(provider, file.path, cached);
286
+ readTranscriptUsage(provider, file.path, cached, file.agentId);
237
287
  cached.sort((a, b) => a.t - b.t);
238
288
  return cached;
239
289
  }
290
+ /**
291
+ * Sets each event's `po` to the highest ordinal among the prompts timestamped
292
+ * at or before it, or null when none is. With prompts in time order (the
293
+ * usual case) that is the last prompt before the event.
294
+ */
295
+ function assignPromptOrdinals(events, marks) {
296
+ const sorted = [...marks].sort((a, b) => a.timestampMs - b.timestampMs || a.ordinal - b.ordinal);
297
+ const highest = [];
298
+ let max = -1;
299
+ for (const mark of sorted) {
300
+ max = Math.max(max, mark.ordinal);
301
+ highest.push(max);
302
+ }
303
+ for (const event of events) {
304
+ let low = 0;
305
+ let high = sorted.length;
306
+ while (low < high) {
307
+ const middle = (low + high) >>> 1;
308
+ if (sorted[middle].timestampMs <= event.t)
309
+ low = middle + 1;
310
+ else
311
+ high = middle;
312
+ }
313
+ event.po = low === 0 ? null : highest[low - 1];
314
+ }
315
+ }
316
+ /**
317
+ * Prompt ordinals for a freshly read session, from its main transcript.
318
+ * Returns false when the transcript could not be scanned (the events then
319
+ * keep no ordinal and the session is not cached, so the next run retries).
320
+ */
321
+ async function addPromptOrdinals(preview, events) {
322
+ if (preview.provider !== 'claude-code' && preview.provider !== 'codex')
323
+ return true;
324
+ let marks;
325
+ try {
326
+ marks = await readPromptOrdinalMarks(preview.provider, preview.sessionId, preview.filePath);
327
+ }
328
+ catch {
329
+ return false;
330
+ }
331
+ assignPromptOrdinals(events, marks);
332
+ return true;
333
+ }
334
+ /** Parent of a Codex rollout that is a fork or a spawned subagent, from its first line only. */
335
+ async function codexSessionSubagent(preview) {
336
+ if (preview.provider !== 'codex')
337
+ return undefined;
338
+ const { line } = await readFirstLineAsync(preview.filePath);
339
+ if (!line)
340
+ return undefined;
341
+ let first;
342
+ try {
343
+ first = JSON.parse(line);
344
+ }
345
+ catch {
346
+ return undefined;
347
+ }
348
+ const origin = codexRolloutOrigin(first);
349
+ if (!origin.parentId || (!origin.isSubagent && !origin.forkedFromId))
350
+ return undefined;
351
+ if (origin.parentId === preview.sessionId)
352
+ return undefined;
353
+ return { agentId: preview.sessionId, parentSessionId: origin.parentId };
354
+ }
240
355
  function yieldToEventLoop() {
241
356
  return new Promise((resolve) => setImmediate(resolve));
242
357
  }
@@ -276,12 +391,14 @@ export async function collectUsageEvents(options) {
276
391
  const fingerprint = sessionFingerprint(preview, subagentFiles);
277
392
  const filePath = cacheFilePath(cacheDir, preview.provider, preview.sessionId);
278
393
  let cachedEvents = null;
394
+ let sessionSubagent;
279
395
  let project = preview.workspacePath;
280
396
  let fromCache = false;
281
397
  if (useCache) {
282
398
  const cached = readCache(filePath);
283
399
  if (cached && cached.fingerprint === fingerprint && cached.sessionId === preview.sessionId) {
284
400
  cachedEvents = cached.events;
401
+ sessionSubagent = validSubagent(cached.subagent);
285
402
  project = project ?? cached.project;
286
403
  fromCache = true;
287
404
  cacheHits += 1;
@@ -305,7 +422,10 @@ export async function collectUsageEvents(options) {
305
422
  options.onDiagnostic?.(diagnostic);
306
423
  continue;
307
424
  }
308
- if (useCache) {
425
+ // Ordinals and origin are best-effort: a failure costs only the cache write.
426
+ const ordinalsRead = await addPromptOrdinals(preview, cachedEvents);
427
+ sessionSubagent = await codexSessionSubagent(preview).catch(() => undefined);
428
+ if (useCache && ordinalsRead) {
309
429
  const record = {
310
430
  version: USAGE_CACHE_VERSION,
311
431
  provider: preview.provider,
@@ -313,6 +433,7 @@ export async function collectUsageEvents(options) {
313
433
  fingerprint,
314
434
  project,
315
435
  events: cachedEvents,
436
+ ...(sessionSubagent ? { subagent: sessionSubagent } : {}),
316
437
  };
317
438
  try {
318
439
  await atomicWriteJson(filePath, record);
@@ -332,7 +453,7 @@ export async function collectUsageEvents(options) {
332
453
  continue;
333
454
  if (untilMs !== null && cached.t > untilMs)
334
455
  continue;
335
- events.push(priceCachedEvent(cached, preview.provider, preview.sessionId, project, preview.filePath));
456
+ events.push(priceCachedEvent(cached, preview.provider, preview.sessionId, project, preview.filePath, sessionSubagent));
336
457
  first = first === null ? cached.t : Math.min(first, cached.t);
337
458
  last = last === null ? cached.t : Math.max(last, cached.t);
338
459
  count += 1;
@@ -361,6 +482,14 @@ export async function collectUsageEvents(options) {
361
482
  ...(scope ? { roots: scope.roots } : {}),
362
483
  };
363
484
  }
485
+ function validSubagent(value) {
486
+ if (typeof value !== 'object' || value === null)
487
+ return undefined;
488
+ const { agentId, parentSessionId } = value;
489
+ return typeof agentId === 'string' && typeof parentSessionId === 'string'
490
+ ? { agentId, parentSessionId }
491
+ : undefined;
492
+ }
364
493
  /** The roots to list, or undefined for an unscoped (or legacy single-root) call. */
365
494
  function usageScope(options) {
366
495
  if (options.workspacePaths) {
@@ -7,6 +7,8 @@
7
7
  */
8
8
  import { classifyCostProvenance } from '../aggregation/costProvenance';
9
9
  import { formatLocalDateKey } from '../formatting';
10
+ import { canonicalModelId } from '../modelInfo';
11
+ import { SYNTHETIC_MODEL } from './claudeUsageDedupe';
10
12
  function dayParts(ms, utc) {
11
13
  const date = new Date(ms);
12
14
  return utc
@@ -69,7 +71,8 @@ function addTokens(target, source) {
69
71
  }
70
72
  /**
71
73
  * Group usage events into rows. Rows are sorted by key (sessions by first
72
- * event), then provider, project, and model, so tables read oldest first.
74
+ * event), then provider, project, and model, so tables read oldest first;
75
+ * rows grouped by `session` within one of those follow their first event.
73
76
  */
74
77
  export function bucketUsage(events, options) {
75
78
  const groupBy = new Set(options.groupBy ?? ['provider']);
@@ -77,13 +80,21 @@ export function bucketUsage(events, options) {
77
80
  const byProvider = isSession || groupBy.has('provider');
78
81
  const byProject = isSession || groupBy.has('project');
79
82
  const byModel = groupBy.has('model');
83
+ const bySession = isSession || groupBy.has('session');
84
+ const keyModel = options.modelKey === 'canonical'
85
+ ? (model) => (model === SYNTHETIC_MODEL ? model : canonicalModelId(model) || model)
86
+ : (model) => model;
80
87
  const buckets = new Map();
81
88
  for (const event of events) {
89
+ if (options.excludeSynthetic && event.model === SYNTHETIC_MODEL)
90
+ continue;
82
91
  const key = usageBucketKey(event, options);
83
92
  const provider = byProvider ? event.provider : null;
84
93
  const project = byProject ? event.project : null;
85
- const model = byModel ? event.model : null;
86
- const id = [key, provider ?? '', project ?? '', model ?? ''].join('|');
94
+ const eventModel = keyModel(event.model);
95
+ const model = byModel ? eventModel : null;
96
+ const sessionId = bySession ? event.sessionId : null;
97
+ const id = JSON.stringify([key, provider, project, model, sessionId]);
87
98
  let acc = buckets.get(id);
88
99
  if (!acc) {
89
100
  acc = {
@@ -93,15 +104,17 @@ export function bucketUsage(events, options) {
93
104
  provider,
94
105
  project,
95
106
  model,
96
- sessionId: isSession ? event.sessionId : null,
107
+ sessionId,
97
108
  models: [],
98
109
  calls: 0,
99
110
  sessions: 0,
100
111
  tokens: emptyTokens(),
101
112
  totalTokens: 0,
102
113
  costUsd: 0,
114
+ pricedCostUsd: null,
103
115
  costProvenance: 'none',
104
116
  unpricedCalls: 0,
117
+ reasoningTokens: 0,
105
118
  firstTimestamp: event.timestamp,
106
119
  lastTimestamp: event.timestamp,
107
120
  },
@@ -116,12 +129,14 @@ export function bucketUsage(events, options) {
116
129
  row.calls += 1;
117
130
  addTokens(row.tokens, event.tokens);
118
131
  row.totalTokens += event.tokens.totalTokens;
132
+ row.reasoningTokens = (row.reasoningTokens ?? 0) + reasoningOf(event.tokens.reasoningTokens);
119
133
  row.firstTimestamp = Math.min(row.firstTimestamp, event.timestamp);
120
134
  row.lastTimestamp = Math.max(row.lastTimestamp, event.timestamp);
121
135
  acc.sessionIds.add(event.sessionId);
122
- acc.modelTokens.set(event.model, (acc.modelTokens.get(event.model) ?? 0) + event.tokens.totalTokens);
136
+ acc.modelTokens.set(eventModel, (acc.modelTokens.get(eventModel) ?? 0) + event.tokens.totalTokens);
123
137
  if (event.costUsd !== null) {
124
138
  row.costUsd += event.costUsd;
139
+ row.pricedCostUsd = (row.pricedCostUsd ?? 0) + event.costUsd;
125
140
  if (event.costProvenance === 'provider-reported')
126
141
  acc.reportedCalls += 1;
127
142
  else
@@ -147,7 +162,14 @@ export function bucketUsage(events, options) {
147
162
  return rows.sort((a, b) => (isSession ? a.firstTimestamp - b.firstTimestamp : a.key.localeCompare(b.key)) ||
148
163
  (a.provider ?? '').localeCompare(b.provider ?? '') ||
149
164
  (a.project ?? '').localeCompare(b.project ?? '') ||
150
- (a.model ?? '').localeCompare(b.model ?? ''));
165
+ (a.model ?? '').localeCompare(b.model ?? '') ||
166
+ (bySession && !isSession
167
+ ? a.firstTimestamp - b.firstTimestamp ||
168
+ (a.sessionId ?? '').localeCompare(b.sessionId ?? '')
169
+ : 0));
170
+ }
171
+ function reasoningOf(value) {
172
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
151
173
  }
152
174
  /**
153
175
  * Sum rows that do not overlap (for example every row of one `bucketUsage`
@@ -161,8 +183,10 @@ export function summarizeUsageRows(rows, events) {
161
183
  tokens: emptyTokens(),
162
184
  totalTokens: 0,
163
185
  costUsd: 0,
186
+ pricedCostUsd: null,
164
187
  costProvenance: 'none',
165
188
  unpricedCalls: 0,
189
+ reasoningTokens: 0,
166
190
  };
167
191
  let reportedCalls = 0;
168
192
  let estimatedCalls = 0;
@@ -172,6 +196,15 @@ export function summarizeUsageRows(rows, events) {
172
196
  addTokens(totals.tokens, row.tokens);
173
197
  totals.totalTokens += row.totalTokens;
174
198
  totals.costUsd += row.costUsd;
199
+ // Rows from before `pricedCostUsd` existed: any priced call means a priced cost.
200
+ const rowPriced = row.pricedCostUsd !== undefined
201
+ ? row.pricedCostUsd
202
+ : row.calls > row.unpricedCalls
203
+ ? row.costUsd
204
+ : null;
205
+ if (rowPriced !== null)
206
+ totals.pricedCostUsd = (totals.pricedCostUsd ?? 0) + rowPriced;
207
+ totals.reasoningTokens = (totals.reasoningTokens ?? 0) + reasoningOf(row.reasoningTokens);
175
208
  totals.unpricedCalls += row.unpricedCalls;
176
209
  const priced = row.calls - row.unpricedCalls;
177
210
  if (row.costProvenance === 'reported')