sidekick-shared 0.28.2 → 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 (142) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +176 -30
  3. package/dist/authStatus.d.ts +54 -0
  4. package/dist/authStatus.js +91 -0
  5. package/dist/browser.d.ts +14 -6
  6. package/dist/browser.js +26 -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/codexQuota.js +17 -4
  12. package/dist/esm/authStatus.js +87 -0
  13. package/dist/esm/browser.js +6 -2
  14. package/dist/esm/codex.js +18 -0
  15. package/dist/esm/codexProfiles.js +8 -11
  16. package/dist/esm/codexQuota.js +18 -5
  17. package/dist/esm/formatting.js +10 -1
  18. package/dist/esm/index.js +12 -9
  19. package/dist/esm/modelCatalog.js +2 -1
  20. package/dist/esm/modelInfo.js +422 -45
  21. package/dist/esm/multiProviderQuotaService.js +1 -1
  22. package/dist/esm/node.js +9 -3
  23. package/dist/esm/parsers/codexParser.js +175 -54
  24. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  25. package/dist/esm/parsers/openCodeParser.js +83 -21
  26. package/dist/esm/pricing.js +18 -0
  27. package/dist/esm/pricingCatalog.js +14 -7
  28. package/dist/esm/promptHistory.js +66 -4
  29. package/dist/esm/providerFailure.js +124 -3
  30. package/dist/esm/providers/codex.js +2 -24
  31. package/dist/esm/providers/codexDatabase.js +3 -19
  32. package/dist/esm/providers/codexRollouts.js +24 -1
  33. package/dist/esm/providers/openCode.js +9 -17
  34. package/dist/esm/quota.js +356 -24
  35. package/dist/esm/quotaHistory.js +33 -8
  36. package/dist/esm/quotaPoller.js +5 -2
  37. package/dist/esm/quotaResolver.js +31 -6
  38. package/dist/esm/quotaSnapshots.js +65 -10
  39. package/dist/esm/quotaThresholds.js +7 -2
  40. package/dist/esm/report/htmlReportGenerator.js +42 -5
  41. package/dist/esm/report/transcriptParser.js +13 -11
  42. package/dist/esm/schemas/index.js +2 -2
  43. package/dist/esm/schemas/providerFailure.js +3 -1
  44. package/dist/esm/schemas/quota.js +30 -0
  45. package/dist/esm/schemas/quotaHistory.js +10 -2
  46. package/dist/esm/schemas/stateFile.js +9 -0
  47. package/dist/esm/schemas/transcript.js +4 -0
  48. package/dist/esm/sessionPromptHistory.js +2 -2
  49. package/dist/esm/stateFile.js +18 -3
  50. package/dist/esm/statusline/claudeStatuslinePayload.js +16 -4
  51. package/dist/esm/statusline/formatter.js +57 -18
  52. package/dist/esm/statusline/index.js +2 -2
  53. package/dist/esm/statusline/payload.js +9 -0
  54. package/dist/esm/testing/credentialGuard.js +87 -5
  55. package/dist/esm/toolCallOrigins.js +310 -0
  56. package/dist/esm/transcript.js +42 -3
  57. package/dist/esm/usage/usageEvents.js +146 -17
  58. package/dist/esm/usage/usageReports.js +39 -6
  59. package/dist/esm/watchers/jsonlTail.js +378 -59
  60. package/dist/esm/zaiQuotaApi.js +4 -2
  61. package/dist/formatting.d.ts +26 -1
  62. package/dist/formatting.js +11 -1
  63. package/dist/index.d.ts +22 -16
  64. package/dist/index.js +48 -12
  65. package/dist/modelCatalog.js +2 -1
  66. package/dist/modelInfo.d.ts +185 -16
  67. package/dist/modelInfo.js +431 -47
  68. package/dist/multiProviderQuotaService.js +1 -1
  69. package/dist/node.d.ts +16 -5
  70. package/dist/node.js +31 -2
  71. package/dist/parsers/codexParser.d.ts +55 -0
  72. package/dist/parsers/codexParser.js +175 -54
  73. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  74. package/dist/parsers/codexRolloutOrigin.js +111 -0
  75. package/dist/parsers/openCodeParser.d.ts +38 -0
  76. package/dist/parsers/openCodeParser.js +86 -21
  77. package/dist/pricing.d.ts +22 -0
  78. package/dist/pricing.js +50 -0
  79. package/dist/pricingCatalog.d.ts +10 -0
  80. package/dist/pricingCatalog.js +13 -6
  81. package/dist/promptHistory.d.ts +65 -4
  82. package/dist/promptHistory.js +68 -5
  83. package/dist/providerFailure.d.ts +44 -0
  84. package/dist/providerFailure.js +126 -4
  85. package/dist/providers/codex.d.ts +3 -22
  86. package/dist/providers/codex.js +3 -26
  87. package/dist/providers/codexDatabase.d.ts +3 -2
  88. package/dist/providers/codexDatabase.js +4 -22
  89. package/dist/providers/codexRollouts.d.ts +22 -0
  90. package/dist/providers/codexRollouts.js +24 -0
  91. package/dist/providers/openCode.js +8 -16
  92. package/dist/quota.d.ts +131 -1
  93. package/dist/quota.js +366 -24
  94. package/dist/quotaHistory.d.ts +10 -0
  95. package/dist/quotaHistory.js +34 -8
  96. package/dist/quotaPoller.d.ts +11 -3
  97. package/dist/quotaPoller.js +5 -2
  98. package/dist/quotaResolver.js +30 -5
  99. package/dist/quotaSnapshots.js +64 -9
  100. package/dist/quotaThresholds.js +7 -2
  101. package/dist/report/htmlReportGenerator.js +42 -5
  102. package/dist/report/transcriptParser.d.ts +10 -1
  103. package/dist/report/transcriptParser.js +13 -11
  104. package/dist/schemas/index.d.ts +3 -3
  105. package/dist/schemas/index.js +5 -2
  106. package/dist/schemas/providerFailure.d.ts +39 -0
  107. package/dist/schemas/providerFailure.js +2 -0
  108. package/dist/schemas/quota.d.ts +258 -0
  109. package/dist/schemas/quota.js +31 -1
  110. package/dist/schemas/quotaHistory.d.ts +2 -0
  111. package/dist/schemas/quotaHistory.js +10 -2
  112. package/dist/schemas/stateFile.d.ts +34 -0
  113. package/dist/schemas/stateFile.js +10 -1
  114. package/dist/schemas/transcript.d.ts +2 -0
  115. package/dist/schemas/transcript.js +4 -0
  116. package/dist/sessionPromptHistory.d.ts +7 -1
  117. package/dist/sessionPromptHistory.js +1 -1
  118. package/dist/stateFile.d.ts +18 -0
  119. package/dist/stateFile.js +17 -2
  120. package/dist/statusline/claudeStatuslinePayload.d.ts +4 -3
  121. package/dist/statusline/claudeStatuslinePayload.js +16 -4
  122. package/dist/statusline/formatter.d.ts +5 -0
  123. package/dist/statusline/formatter.js +56 -17
  124. package/dist/statusline/index.d.ts +4 -4
  125. package/dist/statusline/index.js +5 -1
  126. package/dist/statusline/payload.d.ts +10 -0
  127. package/dist/statusline/payload.js +14 -0
  128. package/dist/testing/credentialGuard.d.ts +14 -0
  129. package/dist/testing/credentialGuard.js +87 -5
  130. package/dist/toolCallOrigins.d.ts +70 -0
  131. package/dist/toolCallOrigins.js +346 -0
  132. package/dist/transcript.d.ts +12 -0
  133. package/dist/transcript.js +42 -3
  134. package/dist/types/opencode.d.ts +2 -0
  135. package/dist/usage/usageEvents.d.ts +54 -6
  136. package/dist/usage/usageEvents.js +146 -17
  137. package/dist/usage/usageReports.d.ts +35 -4
  138. package/dist/usage/usageReports.js +39 -6
  139. package/dist/watchers/jsonlTail.d.ts +101 -4
  140. package/dist/watchers/jsonlTail.js +380 -60
  141. package/dist/zaiQuotaApi.js +3 -1
  142. package/package.json +41 -1
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Where a Codex rollout came from, read from its `session_meta` record:
3
+ * a session a person started, a fork of another session, or a subagent.
4
+ *
5
+ * Codex writes `session_meta.source` as a plain string for sessions started
6
+ * from a surface (`cli`, `vscode`, `exec`, …) and as an object for subagents:
7
+ * `{ subagent: { thread_spawn: { parent_thread_id, … } } }` for a spawned
8
+ * agent, `{ subagent: { other: 'guardian' } }` for internal reviewers, or
9
+ * `{ subagent: 'review' }`. The same JSON appears as a string in the state
10
+ * database's `threads.source`. A forked rollout records `forked_from_id`, and
11
+ * embeds its parent's `session_meta` after its own, so only the first counts.
12
+ *
13
+ * Pure: no filesystem access. Browser-safe.
14
+ *
15
+ * @module parsers/codexRolloutOrigin
16
+ */
17
+ export interface CodexRolloutOrigin {
18
+ /** `session_meta.forked_from_id`: the session this rollout was forked from. */
19
+ forkedFromId?: string;
20
+ /**
21
+ * `session_meta.source` when it is a string (`cli`, `vscode`, `exec`, …),
22
+ * or `'subagent'` when it is a subagent object.
23
+ */
24
+ source?: string;
25
+ /** The rollout was written by a subagent (`source: { subagent: … }`), not a person. */
26
+ isSubagent: boolean;
27
+ /**
28
+ * The parent session: `forkedFromId`, else the spawn's `parent_thread_id`,
29
+ * else (subagents only) the top-level `parent_thread_id`.
30
+ */
31
+ parentId?: string;
32
+ /**
33
+ * Subagent variant: `thread_spawn`, `review`, `compact`, or the name of an
34
+ * `other` subagent (for example `guardian`).
35
+ */
36
+ subagentKind?: string;
37
+ }
38
+ /**
39
+ * Parent thread id of a spawned subagent's `source` (object, or JSON text as
40
+ * stored in `threads.source`): `subagent.thread_spawn.parent_thread_id`.
41
+ */
42
+ export declare function codexSpawnParentFromSource(source: unknown): string | undefined;
43
+ /**
44
+ * Origin of a Codex rollout from its `session_meta`. Accepts the parsed
45
+ * `session_meta` record, its payload, or the rollout's records in order (the
46
+ * first `session_meta` wins: a fork embeds its parent's after its own).
47
+ * Anything else yields `{ isSubagent: false }`.
48
+ */
49
+ export declare function codexRolloutOrigin(input: unknown | readonly unknown[]): CodexRolloutOrigin;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * Where a Codex rollout came from, read from its `session_meta` record:
4
+ * a session a person started, a fork of another session, or a subagent.
5
+ *
6
+ * Codex writes `session_meta.source` as a plain string for sessions started
7
+ * from a surface (`cli`, `vscode`, `exec`, …) and as an object for subagents:
8
+ * `{ subagent: { thread_spawn: { parent_thread_id, … } } }` for a spawned
9
+ * agent, `{ subagent: { other: 'guardian' } }` for internal reviewers, or
10
+ * `{ subagent: 'review' }`. The same JSON appears as a string in the state
11
+ * database's `threads.source`. A forked rollout records `forked_from_id`, and
12
+ * embeds its parent's `session_meta` after its own, so only the first counts.
13
+ *
14
+ * Pure: no filesystem access. Browser-safe.
15
+ *
16
+ * @module parsers/codexRolloutOrigin
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.codexSpawnParentFromSource = codexSpawnParentFromSource;
20
+ exports.codexRolloutOrigin = codexRolloutOrigin;
21
+ function record(value) {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
23
+ ? value
24
+ : undefined;
25
+ }
26
+ function nonEmpty(value) {
27
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
28
+ }
29
+ /** A `source` value as an object: rollouts store it parsed, the database as JSON text. */
30
+ function sourceObject(source) {
31
+ if (typeof source !== 'string')
32
+ return record(source);
33
+ if (!source.startsWith('{'))
34
+ return undefined;
35
+ try {
36
+ return record(JSON.parse(source));
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ /**
43
+ * Parent thread id of a spawned subagent's `source` (object, or JSON text as
44
+ * stored in `threads.source`): `subagent.thread_spawn.parent_thread_id`.
45
+ */
46
+ function codexSpawnParentFromSource(source) {
47
+ const spawn = record(record(sourceObject(source)?.subagent)?.thread_spawn);
48
+ const parent = spawn?.parent_thread_id;
49
+ return typeof parent === 'string' ? parent : undefined;
50
+ }
51
+ function subagentKindOf(subagent) {
52
+ if (typeof subagent === 'string')
53
+ return nonEmpty(subagent);
54
+ const variant = record(subagent);
55
+ if (!variant)
56
+ return undefined;
57
+ const other = nonEmpty(variant.other);
58
+ if (other)
59
+ return other;
60
+ return Object.keys(variant)[0];
61
+ }
62
+ /** The `session_meta` payload of a record, of a bare payload, or of the first in a list. */
63
+ function metaPayload(input) {
64
+ if (Array.isArray(input)) {
65
+ for (const item of input) {
66
+ const row = record(item);
67
+ if (row?.type === 'session_meta')
68
+ return record(row.payload);
69
+ }
70
+ return undefined;
71
+ }
72
+ const row = record(input);
73
+ if (!row)
74
+ return undefined;
75
+ if (row.type === 'session_meta')
76
+ return record(row.payload);
77
+ // A payload on its own, as `readSessionMeta()` returns it.
78
+ if (row.type === undefined && row.payload === undefined)
79
+ return row;
80
+ return undefined;
81
+ }
82
+ /**
83
+ * Origin of a Codex rollout from its `session_meta`. Accepts the parsed
84
+ * `session_meta` record, its payload, or the rollout's records in order (the
85
+ * first `session_meta` wins: a fork embeds its parent's after its own).
86
+ * Anything else yields `{ isSubagent: false }`.
87
+ */
88
+ function codexRolloutOrigin(input) {
89
+ const payload = metaPayload(input);
90
+ if (!payload)
91
+ return { isSubagent: false };
92
+ const forkedFromId = nonEmpty(payload.forked_from_id);
93
+ const object = sourceObject(payload.source);
94
+ const isSubagent = object !== undefined && 'subagent' in object;
95
+ const source = typeof payload.source === 'string' && object === undefined
96
+ ? nonEmpty(payload.source)
97
+ : isSubagent
98
+ ? 'subagent'
99
+ : undefined;
100
+ const parentId = forkedFromId ??
101
+ nonEmpty(codexSpawnParentFromSource(object)) ??
102
+ (isSubagent ? nonEmpty(payload.parent_thread_id) : undefined);
103
+ const subagentKind = isSubagent ? subagentKindOf(object?.subagent) : undefined;
104
+ return {
105
+ ...(forkedFromId ? { forkedFromId } : {}),
106
+ ...(source ? { source } : {}),
107
+ isSubagent,
108
+ ...(parentId ? { parentId } : {}),
109
+ ...(subagentKind ? { subagentKind } : {}),
110
+ };
111
+ }
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import type { SessionEvent } from '../types/sessionEvent';
14
14
  import type { OpenCodeMessage, OpenCodePart, DbMessage, DbPart } from '../types/opencode';
15
+ import { type NormalizedUsage } from '../usageNormalization';
15
16
  /**
16
17
  * Normalizes an OpenCode tool name to PascalCase.
17
18
  *
@@ -38,6 +39,43 @@ export declare function detectPlanModeFromText(text: string): 'enter' | 'exit' |
38
39
  * This adds snake_case aliases without removing the originals.
39
40
  */
40
41
  export declare function normalizeToolInput(input: Record<string, unknown>): Record<string, unknown>;
42
+ /** OpenCode token counts as stored on an assistant message or step. */
43
+ export interface OpenCodeTokenCounts {
44
+ input?: number;
45
+ output?: number;
46
+ reasoning?: number;
47
+ cacheRead?: number;
48
+ cacheWrite?: number;
49
+ /** Provider-reported total, stored by OpenCode since late 2025. */
50
+ total?: number;
51
+ }
52
+ /**
53
+ * Token counts from a message in either storage shape: database rows are
54
+ * mapped to flat `cacheRead` / `cacheWrite`, while file-storage messages keep
55
+ * OpenCode's nested `cache: { read, write }`.
56
+ * @internal
57
+ */
58
+ export declare function openCodeTokenCounts(tokens: unknown): OpenCodeTokenCounts;
59
+ /**
60
+ * How a stored OpenCode message splits its tokens. Current OpenCode stores
61
+ * every bucket disjoint for every provider: `input` excludes cache reads and
62
+ * writes (since 2025-11) and `output` excludes `reasoning` (since 2026-04),
63
+ * with `total` equal to their sum. Older messages either counted cached input
64
+ * in `input` (OpenAI-compatible providers) or reasoning in `output`. The
65
+ * stored `total` tells the layouts apart; without it the current layout is
66
+ * assumed.
67
+ * @internal
68
+ */
69
+ export declare function openCodeTokenLayout(tokens: OpenCodeTokenCounts): {
70
+ inputIncludesCache: boolean;
71
+ reasoningInOutput: boolean;
72
+ };
73
+ /** Normalizes one OpenCode message's or step's stored tokens. @internal */
74
+ export declare function normalizeOpenCodeUsage(tokens: OpenCodeTokenCounts, providerID: string | undefined, extra?: {
75
+ source?: string;
76
+ model?: string;
77
+ reportedCostUsd?: number;
78
+ }): NormalizedUsage;
41
79
  /**
42
80
  * Converts an OpenCode message and its parts into SessionEvent array.
43
81
  *
@@ -15,6 +15,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.normalizeToolName = normalizeToolName;
16
16
  exports.detectPlanModeFromText = detectPlanModeFromText;
17
17
  exports.normalizeToolInput = normalizeToolInput;
18
+ exports.openCodeTokenCounts = openCodeTokenCounts;
19
+ exports.openCodeTokenLayout = openCodeTokenLayout;
20
+ exports.normalizeOpenCodeUsage = normalizeOpenCodeUsage;
18
21
  exports.convertOpenCodeMessage = convertOpenCodeMessage;
19
22
  exports.parseDbMessageData = parseDbMessageData;
20
23
  exports.parseDbPartData = parseDbPartData;
@@ -123,6 +126,78 @@ function toISOString(time) {
123
126
  return time;
124
127
  return new Date(time).toISOString();
125
128
  }
129
+ /**
130
+ * Token counts from a message in either storage shape: database rows are
131
+ * mapped to flat `cacheRead` / `cacheWrite`, while file-storage messages keep
132
+ * OpenCode's nested `cache: { read, write }`.
133
+ * @internal
134
+ */
135
+ function openCodeTokenCounts(tokens) {
136
+ const raw = (tokens ?? {});
137
+ const cache = (raw.cache ?? {});
138
+ const num = (value) => (typeof value === 'number' ? value : undefined);
139
+ return {
140
+ input: num(raw.input),
141
+ output: num(raw.output),
142
+ reasoning: num(raw.reasoning),
143
+ cacheRead: num(raw.cacheRead) ?? num(cache.read),
144
+ cacheWrite: num(raw.cacheWrite) ?? num(cache.write),
145
+ total: num(raw.total),
146
+ };
147
+ }
148
+ /**
149
+ * How a stored OpenCode message splits its tokens. Current OpenCode stores
150
+ * every bucket disjoint for every provider: `input` excludes cache reads and
151
+ * writes (since 2025-11) and `output` excludes `reasoning` (since 2026-04),
152
+ * with `total` equal to their sum. Older messages either counted cached input
153
+ * in `input` (OpenAI-compatible providers) or reasoning in `output`. The
154
+ * stored `total` tells the layouts apart; without it the current layout is
155
+ * assumed.
156
+ * @internal
157
+ */
158
+ function openCodeTokenLayout(tokens) {
159
+ const n = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
160
+ const input = n(tokens.input);
161
+ const output = n(tokens.output);
162
+ const reasoning = n(tokens.reasoning);
163
+ const cache = n(tokens.cacheRead) + n(tokens.cacheWrite);
164
+ const total = n(tokens.total);
165
+ if (total > 0 && total !== input + cache + output + reasoning) {
166
+ if (reasoning > 0 && total === input + cache + output) {
167
+ return { inputIncludesCache: false, reasoningInOutput: true };
168
+ }
169
+ if (cache > 0 && total === input + output + reasoning) {
170
+ return { inputIncludesCache: true, reasoningInOutput: false };
171
+ }
172
+ if (cache > 0 && reasoning > 0 && total === input + output) {
173
+ return { inputIncludesCache: true, reasoningInOutput: true };
174
+ }
175
+ }
176
+ return { inputIncludesCache: false, reasoningInOutput: false };
177
+ }
178
+ /** Normalizes one OpenCode message's or step's stored tokens. @internal */
179
+ function normalizeOpenCodeUsage(tokens, providerID, extra = {}) {
180
+ const layout = openCodeTokenLayout(tokens);
181
+ return (0, usageNormalization_1.normalizeProviderUsage)({
182
+ // `openai` semantics subtract cache reads from input; only an old
183
+ // cache-inclusive message needs that.
184
+ semantics: layout.inputIncludesCache
185
+ ? 'openai'
186
+ : providerID === 'anthropic'
187
+ ? 'anthropic'
188
+ : 'sidekick',
189
+ provider: providerID ?? 'opencode',
190
+ ...(extra.source ? { source: extra.source } : {}),
191
+ ...(extra.model ? { model: extra.model } : {}),
192
+ inputTokens: tokens.input,
193
+ outputTokens: tokens.output,
194
+ cacheReadTokens: tokens.cacheRead,
195
+ cacheWriteTokens: tokens.cacheWrite,
196
+ reasoningTokens: tokens.reasoning,
197
+ reasoningIncludedInOutput: layout.reasoningInOutput,
198
+ ...(extra.reportedCostUsd !== undefined ? { reportedCostUsd: extra.reportedCostUsd } : {}),
199
+ });
200
+ }
126
201
  /**
127
202
  * Converts an OpenCode message and its parts into SessionEvent array.
128
203
  *
@@ -328,31 +403,20 @@ function convertAssistantMessage(message, parts) {
328
403
  : stepFinishCost > 0
329
404
  ? stepFinishCost
330
405
  : undefined;
331
- const usage = {
332
- input_tokens: message.tokens.input || 0,
333
- output_tokens: message.tokens.output || 0,
334
- cache_creation_input_tokens: message.tokens.cacheWrite || 0,
335
- cache_read_input_tokens: message.tokens.cacheRead || 0,
336
- reported_cost: reportedCost,
337
- reasoning_tokens: message.tokens.reasoning || 0,
338
- };
339
- const normalizedUsage = (0, usageNormalization_1.normalizeProviderUsage)({
340
- semantics: message.providerID === 'openai'
341
- ? 'openai'
342
- : message.providerID === 'anthropic'
343
- ? 'anthropic'
344
- : 'sidekick',
345
- provider: message.providerID ?? 'opencode',
406
+ const counts = openCodeTokenCounts(message.tokens);
407
+ const normalizedUsage = normalizeOpenCodeUsage(counts, message.providerID, {
346
408
  source: 'opencode-message',
347
409
  model: message.modelID,
348
- inputTokens: message.tokens.input,
349
- outputTokens: message.tokens.output,
350
- cacheReadTokens: message.tokens.cacheRead,
351
- cacheWriteTokens: message.tokens.cacheWrite,
352
- reasoningTokens: message.tokens.reasoning,
353
- reasoningIncludedInOutput: message.providerID === 'openai',
354
410
  reportedCostUsd: reportedCost,
355
411
  });
412
+ const usage = {
413
+ input_tokens: normalizedUsage.uncachedInputTokens,
414
+ output_tokens: counts.output || 0,
415
+ cache_creation_input_tokens: counts.cacheWrite || 0,
416
+ cache_read_input_tokens: counts.cacheRead || 0,
417
+ reported_cost: reportedCost,
418
+ reasoning_tokens: counts.reasoning || 0,
419
+ };
356
420
  const timestamp = toISOString(message.time.completed || message.time.created);
357
421
  const retryAttempts = parts
358
422
  .filter((part) => part.type === 'retry')
@@ -491,6 +555,7 @@ function parseDbMessageData(row) {
491
555
  cacheRead: cache?.read || 0,
492
556
  cacheWrite: cache?.write || 0,
493
557
  reasoning: tokens?.reasoning || 0,
558
+ ...(typeof tokens?.total === 'number' ? { total: tokens.total } : {}),
494
559
  },
495
560
  time: {
496
561
  created: time?.created || row.time_created,
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `sidekick-shared/pricing` — model pricing, model-id identity, usage
3
+ * normalization and cost, cost provenance, cost/token formatting, and text
4
+ * token estimates, as one small pure entry (no filesystem, network, or Zod).
5
+ *
6
+ * For hosts that price usage or format costs without session reading: it
7
+ * loads a handful of modules instead of the full browser surface. LiteLLM
8
+ * catalog hydration stays Node-only in `sidekick-shared/node`
9
+ * (`hydratePricingCatalog`); once hydrated, lookups here see the catalog.
10
+ * Everything here is also exported from the package root and
11
+ * `sidekick-shared/browser`.
12
+ *
13
+ * @module pricing
14
+ */
15
+ export { MODEL_PROVIDER_IDS, canonicalModelId, isMovingModelAlias, parseModelId, compareModelIds, sortModelIds, shortModelName, getModelDisplayInfo, getModelInfo, getModelPricing, resolveModelPricing, registerModelPricing, clearRegisteredModelPricing, getPricingCatalogInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, costSourceFromPricingProvenance, mergeCostComponents, formatCost, } from './modelInfo';
16
+ export type { ModelPricing, ModelPricingSource, ModelPricingMatch, ModelPricingProvenance, ResolvedModelPricing, ResolveModelPricingOptions, RegisterModelPricingOptions, PricingCatalogSource, PricingCatalogInfo, CostComponent, CostTokenUsage, ModelProvider, ParsedModelId, ModelInfo, CostSource, CostProvenanceInput, CostWithProvenance, ModelDisplayInfo, FormatCostOptions, } from './modelInfo';
17
+ export { normalizeProviderUsage, extractNormalizedUsage, calculateNormalizedUsageCost, } from './usageNormalization';
18
+ export type { UsageProvider, UsageSemantics, UsageNormalizationProvenance, AnthropicUsageInput, OpenAIUsageInput, SidekickUsageInput, ProviderUsageInput, NormalizedUsage, PricingProvenance, NormalizedUsageCostInput, NormalizedUsageCost, } from './usageNormalization';
19
+ export { formatTokenCount, formatDurationMs } from './formatting';
20
+ export type { FormatTokenCountOptions, FormatDurationMsOptions } from './formatting';
21
+ export { ASCII_CHARS_PER_TOKEN, estimateTextTokens, estimateSerializedTokens, } from './tokenEstimation';
22
+ export type { TokenEstimationMethod, TokenEstimationConfidence, ExactTokenCounterContext, ExactTokenCounter, TokenEstimationOptions, TokenEstimate, } from './tokenEstimation';
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * `sidekick-shared/pricing` — model pricing, model-id identity, usage
4
+ * normalization and cost, cost provenance, cost/token formatting, and text
5
+ * token estimates, as one small pure entry (no filesystem, network, or Zod).
6
+ *
7
+ * For hosts that price usage or format costs without session reading: it
8
+ * loads a handful of modules instead of the full browser surface. LiteLLM
9
+ * catalog hydration stays Node-only in `sidekick-shared/node`
10
+ * (`hydratePricingCatalog`); once hydrated, lookups here see the catalog.
11
+ * Everything here is also exported from the package root and
12
+ * `sidekick-shared/browser`.
13
+ *
14
+ * @module pricing
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.estimateSerializedTokens = exports.estimateTextTokens = exports.ASCII_CHARS_PER_TOKEN = exports.formatDurationMs = exports.formatTokenCount = exports.calculateNormalizedUsageCost = exports.extractNormalizedUsage = exports.normalizeProviderUsage = exports.formatCost = exports.mergeCostComponents = exports.costSourceFromPricingProvenance = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getPricingCatalogInfo = exports.clearRegisteredModelPricing = exports.registerModelPricing = exports.resolveModelPricing = exports.getModelPricing = exports.getModelInfo = exports.getModelDisplayInfo = exports.shortModelName = exports.sortModelIds = exports.compareModelIds = exports.parseModelId = exports.isMovingModelAlias = exports.canonicalModelId = exports.MODEL_PROVIDER_IDS = void 0;
18
+ var modelInfo_1 = require("./modelInfo");
19
+ Object.defineProperty(exports, "MODEL_PROVIDER_IDS", { enumerable: true, get: function () { return modelInfo_1.MODEL_PROVIDER_IDS; } });
20
+ Object.defineProperty(exports, "canonicalModelId", { enumerable: true, get: function () { return modelInfo_1.canonicalModelId; } });
21
+ Object.defineProperty(exports, "isMovingModelAlias", { enumerable: true, get: function () { return modelInfo_1.isMovingModelAlias; } });
22
+ Object.defineProperty(exports, "parseModelId", { enumerable: true, get: function () { return modelInfo_1.parseModelId; } });
23
+ Object.defineProperty(exports, "compareModelIds", { enumerable: true, get: function () { return modelInfo_1.compareModelIds; } });
24
+ Object.defineProperty(exports, "sortModelIds", { enumerable: true, get: function () { return modelInfo_1.sortModelIds; } });
25
+ Object.defineProperty(exports, "shortModelName", { enumerable: true, get: function () { return modelInfo_1.shortModelName; } });
26
+ Object.defineProperty(exports, "getModelDisplayInfo", { enumerable: true, get: function () { return modelInfo_1.getModelDisplayInfo; } });
27
+ Object.defineProperty(exports, "getModelInfo", { enumerable: true, get: function () { return modelInfo_1.getModelInfo; } });
28
+ Object.defineProperty(exports, "getModelPricing", { enumerable: true, get: function () { return modelInfo_1.getModelPricing; } });
29
+ Object.defineProperty(exports, "resolveModelPricing", { enumerable: true, get: function () { return modelInfo_1.resolveModelPricing; } });
30
+ Object.defineProperty(exports, "registerModelPricing", { enumerable: true, get: function () { return modelInfo_1.registerModelPricing; } });
31
+ Object.defineProperty(exports, "clearRegisteredModelPricing", { enumerable: true, get: function () { return modelInfo_1.clearRegisteredModelPricing; } });
32
+ Object.defineProperty(exports, "getPricingCatalogInfo", { enumerable: true, get: function () { return modelInfo_1.getPricingCatalogInfo; } });
33
+ Object.defineProperty(exports, "calculateCost", { enumerable: true, get: function () { return modelInfo_1.calculateCost; } });
34
+ Object.defineProperty(exports, "calculateCostWithPricing", { enumerable: true, get: function () { return modelInfo_1.calculateCostWithPricing; } });
35
+ Object.defineProperty(exports, "calculateCostWithProvenance", { enumerable: true, get: function () { return modelInfo_1.calculateCostWithProvenance; } });
36
+ Object.defineProperty(exports, "mergeCostSources", { enumerable: true, get: function () { return modelInfo_1.mergeCostSources; } });
37
+ Object.defineProperty(exports, "costSourceFromPricingProvenance", { enumerable: true, get: function () { return modelInfo_1.costSourceFromPricingProvenance; } });
38
+ Object.defineProperty(exports, "mergeCostComponents", { enumerable: true, get: function () { return modelInfo_1.mergeCostComponents; } });
39
+ Object.defineProperty(exports, "formatCost", { enumerable: true, get: function () { return modelInfo_1.formatCost; } });
40
+ var usageNormalization_1 = require("./usageNormalization");
41
+ Object.defineProperty(exports, "normalizeProviderUsage", { enumerable: true, get: function () { return usageNormalization_1.normalizeProviderUsage; } });
42
+ Object.defineProperty(exports, "extractNormalizedUsage", { enumerable: true, get: function () { return usageNormalization_1.extractNormalizedUsage; } });
43
+ Object.defineProperty(exports, "calculateNormalizedUsageCost", { enumerable: true, get: function () { return usageNormalization_1.calculateNormalizedUsageCost; } });
44
+ var formatting_1 = require("./formatting");
45
+ Object.defineProperty(exports, "formatTokenCount", { enumerable: true, get: function () { return formatting_1.formatTokenCount; } });
46
+ Object.defineProperty(exports, "formatDurationMs", { enumerable: true, get: function () { return formatting_1.formatDurationMs; } });
47
+ var tokenEstimation_1 = require("./tokenEstimation");
48
+ Object.defineProperty(exports, "ASCII_CHARS_PER_TOKEN", { enumerable: true, get: function () { return tokenEstimation_1.ASCII_CHARS_PER_TOKEN; } });
49
+ Object.defineProperty(exports, "estimateTextTokens", { enumerable: true, get: function () { return tokenEstimation_1.estimateTextTokens; } });
50
+ Object.defineProperty(exports, "estimateSerializedTokens", { enumerable: true, get: function () { return tokenEstimation_1.estimateSerializedTokens; } });
@@ -51,6 +51,12 @@ export interface HydrateResult {
51
51
  contextWindowEntries: number;
52
52
  /** ISO timestamp of the fetch that produced the cache. */
53
53
  fetchedAt: string;
54
+ /**
55
+ * `getPricingCatalogInfo().digest` after hydration: a stable fingerprint of
56
+ * every active pricing table, so callers can record which prices produced
57
+ * an estimate. See `getPricingCatalogInfo()` for the source and staleness.
58
+ */
59
+ digest: string;
54
60
  }
55
61
  export declare const LITELLM_CATALOG_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
56
62
  /**
@@ -61,6 +67,10 @@ export declare const LITELLM_CATALOG_URL = "https://raw.githubusercontent.com/Be
61
67
  * 2. Otherwise attempt a network fetch; on success, persist + apply.
62
68
  * 3. On any failure, load a stale cache if present; else leave overrides empty.
63
69
  *
70
+ * `getPricingCatalogInfo().source` afterwards reads `litellm` for a fresh fetch
71
+ * or a cache within `ttlMs`, and `stale-cache` for a cache older than `ttlMs`
72
+ * — whether the refresh failed or `offline` skipped it.
73
+ *
64
74
  * Always resolves — never throws. Offline-safe.
65
75
  */
66
76
  export declare function hydratePricingCatalog(options?: HydrateOptions): Promise<HydrateResult>;
@@ -75,6 +75,10 @@ const DEFAULT_TIMEOUT_MS = 3_000;
75
75
  * 2. Otherwise attempt a network fetch; on success, persist + apply.
76
76
  * 3. On any failure, load a stale cache if present; else leave overrides empty.
77
77
  *
78
+ * `getPricingCatalogInfo().source` afterwards reads `litellm` for a fresh fetch
79
+ * or a cache within `ttlMs`, and `stale-cache` for a cache older than `ttlMs`
80
+ * — whether the refresh failed or `offline` skipped it.
81
+ *
78
82
  * Always resolves — never throws. Offline-safe.
79
83
  */
80
84
  async function hydratePricingCatalog(options = {}) {
@@ -83,8 +87,9 @@ async function hydratePricingCatalog(options = {}) {
83
87
  const now = Date.now();
84
88
  // 1. Fresh cache? (Offline callers take any cache, however old.)
85
89
  const cached = await readCache(cachePath);
86
- if (cached && (offline || now - Date.parse(cached.fetchedAt) < ttlMs)) {
87
- return applyCacheFile(cached, 'cache');
90
+ const cacheFresh = cached !== null && now - Date.parse(cached.fetchedAt) < ttlMs;
91
+ if (cached && (offline || cacheFresh)) {
92
+ return applyCacheFile(cached, 'cache', cacheFresh ? 'litellm' : 'stale-cache');
88
93
  }
89
94
  // 2. Try network.
90
95
  if (fetchImpl && !offline) {
@@ -97,12 +102,12 @@ async function hydratePricingCatalog(options = {}) {
97
102
  contextWindows: normalizeLiteLlmContextWindows(fetched),
98
103
  };
99
104
  await writeCache(cachePath, payload, logger);
100
- return applyCacheFile(payload, 'network');
105
+ return applyCacheFile(payload, 'network', 'litellm');
101
106
  }
102
107
  }
103
108
  // 3. Fall back to stale cache if present.
104
109
  if (cached) {
105
- return applyCacheFile(cached, 'cache');
110
+ return applyCacheFile(cached, 'cache', 'stale-cache');
106
111
  }
107
112
  // 4. Nothing we can do — stay on static tables.
108
113
  return {
@@ -110,6 +115,7 @@ async function hydratePricingCatalog(options = {}) {
110
115
  entries: 0,
111
116
  contextWindowEntries: 0,
112
117
  fetchedAt: new Date(now).toISOString(),
118
+ digest: (0, modelInfo_1.getPricingCatalogInfo)().digest,
113
119
  };
114
120
  }
115
121
  /**
@@ -117,9 +123,9 @@ async function hydratePricingCatalog(options = {}) {
117
123
  * `contextWindows` is absent on caches written before context hydration; those
118
124
  * contribute no overrides rather than clearing whatever is already loaded.
119
125
  */
120
- function applyCacheFile(cache, source) {
126
+ function applyCacheFile(cache, source, basis) {
121
127
  const contextWindows = cache.contextWindows ?? {};
122
- (0, modelInfo_1._setPricingOverrides)(cache.overrides);
128
+ (0, modelInfo_1._setPricingOverrides)(cache.overrides, { source: basis, fetchedAt: cache.fetchedAt });
123
129
  if (Object.keys(contextWindows).length > 0) {
124
130
  (0, modelContext_1._setCatalogContextWindows)(contextWindows);
125
131
  }
@@ -128,6 +134,7 @@ function applyCacheFile(cache, source) {
128
134
  entries: Object.keys(cache.overrides).length,
129
135
  contextWindowEntries: Object.keys(contextWindows).length,
130
136
  fetchedAt: cache.fetchedAt,
137
+ digest: (0, modelInfo_1.getPricingCatalogInfo)().digest,
131
138
  };
132
139
  }
133
140
  /**
@@ -14,6 +14,10 @@
14
14
  * record over `maxRecordBytes` is skipped without being buffered and reported
15
15
  * in `exclusions`, the only way a record is permanently left out.
16
16
  *
17
+ * `includeText: false` returns the same entries, ordinals, timestamps,
18
+ * metadata, exclusions, and cursor without prompt text, for consumers that
19
+ * only count or attribute prompts. Cursors are interchangeable between modes.
20
+ *
17
21
  * A prompt can be written before its answer. The cursor then keeps the byte
18
22
  * offset of that prompt's line and its answer-tracking state (never text), and
19
23
  * the prompt is returned again under the same identity once its first
@@ -34,8 +38,14 @@ export interface PromptHistoryBounds {
34
38
  /** Bytes read across all files in this call. Default 256 MiB. */
35
39
  maxTotalBytes?: number;
36
40
  /**
37
- * Largest single log record that is parsed. Default 16 MiB. A longer record
38
- * is skipped without being buffered and reported in `exclusions`.
41
+ * Largest single log record that is parsed. Default 16 MiB
42
+ * (`PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES`). A longer record is skipped
43
+ * without being buffered and reported in `exclusions`.
44
+ *
45
+ * Ordinals depend on this bound: a prompt inside a skipped record is never
46
+ * seen, so later prompts in that file do not count it. Ordinals are only
47
+ * stable across runs and across consumers (for example usage events'
48
+ * `promptOrdinal`) when every reader uses the default.
39
49
  */
40
50
  maxRecordBytes?: number;
41
51
  /** Entries returned by one call. Default 10 000. */
@@ -139,8 +149,10 @@ export interface PromptHistoryEntry {
139
149
  ordinal: number;
140
150
  /** ISO timestamp from the log line; never synthesized. */
141
151
  timestamp: string;
142
- /** Full prompt text, untruncated. */
152
+ /** Full prompt text, untruncated; `''` when the call passed `includeText: false`. */
143
153
  text: string;
154
+ /** Set when `text` was left out by `includeText: false`; the prompt was not empty. */
155
+ textOmitted?: true;
144
156
  /** Realpath of the working directory; always inside an allowed root. */
145
157
  cwd: string;
146
158
  gitBranch?: string;
@@ -170,7 +182,9 @@ export interface PromptHistorySessionRef {
170
182
  /**
171
183
  * A log record permanently left out. Records over `maxRecordBytes` and lines
172
184
  * that are not valid JSON are never parsed, so any prompt inside is lost, and
173
- * the ordinals of later prompts in that file do not count it.
185
+ * the ordinals of later prompts in that file do not count it. Because of
186
+ * that, ordinals are only stable across runs and consumers that all use the
187
+ * default `maxRecordBytes` (`PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES`).
174
188
  */
175
189
  export interface PromptHistoryExclusion {
176
190
  provider: PromptHistoryProvider;
@@ -213,6 +227,14 @@ export interface CollectPromptHistoryOptions {
213
227
  cursor?: PromptHistoryCursor;
214
228
  bounds?: PromptHistoryBounds;
215
229
  signal?: AbortSignal;
230
+ /**
231
+ * Return prompt text. Default true. With `false`, entries keep their
232
+ * ordinal, timestamp, cwd, branch, and answer metadata but carry `text: ''`
233
+ * and `textOmitted: true`, and no prompt text is kept past classifying its
234
+ * line. Ordinals, timestamps, exclusions, and the cursor are identical in
235
+ * both modes, so a cursor written in one mode resumes in the other.
236
+ */
237
+ includeText?: boolean;
216
238
  }
217
239
  export interface PromptHistoryResult {
218
240
  /** Ordered by provider, session, then ordinal. */
@@ -235,6 +257,13 @@ export interface PromptHistoryResult {
235
257
  /** Realpaths of the roots scanned, after worktree expansion. */
236
258
  roots?: string[];
237
259
  }
260
+ /**
261
+ * The default `maxRecordBytes` (16 MiB). Prompt ordinals are only comparable
262
+ * between readers that skip the same records, so every reader that publishes
263
+ * or joins on ordinals (prompt history, session prompt history, usage events'
264
+ * `promptOrdinal`) uses this bound.
265
+ */
266
+ export declare const PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES: number;
238
267
  /** @internal */
239
268
  export declare const DEFAULT_BOUNDS: Required<PromptHistoryBounds>;
240
269
  /** Test hooks; not part of the public entry points. */
@@ -257,6 +286,17 @@ export type RecordObserver = (record: Record<string, unknown>, info: {
257
286
  ordinal: number;
258
287
  lineOffset: number;
259
288
  }) => void;
289
+ /**
290
+ * Sees every human prompt as it is given its ordinal, before the timestamp and
291
+ * scope checks, so prompts that are not returned are seen too. `timestamp` is
292
+ * undefined when the line has no usable one.
293
+ * @internal
294
+ */
295
+ export type PromptObserver = (prompt: {
296
+ ordinal: number;
297
+ timestamp: string | undefined;
298
+ lineOffset: number;
299
+ }) => void;
260
300
  /** @internal */
261
301
  export interface ScanContext {
262
302
  roots: string[];
@@ -269,6 +309,9 @@ export interface ScanContext {
269
309
  entriesSoFar: () => number;
270
310
  exclude: (exclusion: PromptHistoryExclusion) => void;
271
311
  observe?: RecordObserver;
312
+ onPrompt?: PromptObserver;
313
+ /** Keep prompt text on entries. Default true. */
314
+ includeText?: boolean;
272
315
  }
273
316
  /** @internal */
274
317
  export interface FileBudget {
@@ -293,6 +336,24 @@ export interface FileOutcome {
293
336
  }
294
337
  /** Collect human-typed prompts from Claude Code and Codex logs under the given roots. */
295
338
  export declare function collectPromptHistory(options: CollectPromptHistoryOptions): Promise<PromptHistoryResult>;
339
+ /** One human prompt of a session file: its ordinal and parsed timestamp. @internal */
340
+ export interface PromptOrdinalMark {
341
+ ordinal: number;
342
+ /** Prompt line timestamp, ms since epoch. */
343
+ timestampMs: number;
344
+ }
345
+ /**
346
+ * Every human prompt of one session file with its ordinal and timestamp, read
347
+ * whole in one pass with the default record bound, so the ordinals are the
348
+ * ones `collectPromptHistory()` returns with default bounds. No scope check
349
+ * and no text: prompts are counted wherever their cwd points. Prompts whose
350
+ * line has no usable timestamp still take an ordinal but are left out. A
351
+ * non-interactive Codex session (subagent, fork, unknown source) has none.
352
+ * In file order. Rejects when the file cannot be opened or changes while its
353
+ * head is read.
354
+ * @internal
355
+ */
356
+ export declare function readPromptOrdinalMarks(provider: PromptHistoryProvider, sessionId: string, filePath: string): Promise<PromptOrdinalMark[]>;
296
357
  /** @internal */
297
358
  export declare function emptyStats(): PromptHistoryStats;
298
359
  /**