@genesislcap/ai-assistant 15.3.3 → 15.3.4-GENC-1475.2

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 (79) hide show
  1. package/dist/ai-assistant.api.json +333 -43
  2. package/dist/ai-assistant.d.ts +185 -19
  3. package/dist/chat-driver.cjs +94 -22
  4. package/dist/chat-driver.cjs.map +3 -3
  5. package/dist/chat-driver.mjs +94 -22
  6. package/dist/chat-driver.mjs.map +3 -3
  7. package/dist/custom-elements.json +139 -23
  8. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  9. package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
  10. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  11. package/dist/dts/main/cost-session-banking.test.d.ts +2 -0
  12. package/dist/dts/main/cost-session-banking.test.d.ts.map +1 -0
  13. package/dist/dts/main/main.d.ts +114 -14
  14. package/dist/dts/main/main.d.ts.map +1 -1
  15. package/dist/dts/state/ai-assistant-slice.d.ts +11 -8
  16. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  17. package/dist/dts/state/persistence/session-persistence-provider.d.ts +23 -0
  18. package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
  19. package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
  20. package/dist/dts/state/session-store.d.ts +1 -2
  21. package/dist/dts/state/session-store.d.ts.map +1 -1
  22. package/dist/dts/utils/cost-session-history.d.ts +76 -11
  23. package/dist/dts/utils/cost-session-history.d.ts.map +1 -1
  24. package/dist/dts/utils/resolve-cost-history-config.d.ts +6 -0
  25. package/dist/dts/utils/resolve-cost-history-config.d.ts.map +1 -1
  26. package/dist/dts/utils/sum-costs.d.ts.map +1 -1
  27. package/dist/dts/utils/sum-tokens.d.ts +8 -8
  28. package/dist/dts/utils/sum-tokens.d.ts.map +1 -1
  29. package/dist/dts/utils/sum-usage.d.ts +59 -0
  30. package/dist/dts/utils/sum-usage.d.ts.map +1 -0
  31. package/dist/dts/utils/sum-usage.test.d.ts +2 -0
  32. package/dist/dts/utils/sum-usage.test.d.ts.map +1 -0
  33. package/dist/esm/components/chat-driver/chat-driver.js +6 -0
  34. package/dist/esm/components/settings-modal/settings-modal.styles.js +84 -2
  35. package/dist/esm/components/settings-modal/settings-modal.template.js +130 -32
  36. package/dist/esm/main/cost-session-banking.test.js +216 -0
  37. package/dist/esm/main/main.js +271 -57
  38. package/dist/esm/state/ai-assistant-slice.js +11 -8
  39. package/dist/esm/state/ai-assistant-slice.test.js +12 -5
  40. package/dist/esm/state/debug-event-log.js +2 -2
  41. package/dist/esm/state/persistence/session-persistence.integration.test.js +5 -1
  42. package/dist/esm/state/persistence/session-persister.js +2 -2
  43. package/dist/esm/state/persistence/session-persister.test.js +10 -1
  44. package/dist/esm/state/persistence/session-snapshot.js +6 -2
  45. package/dist/esm/state/persistence/session-snapshot.test.js +4 -1
  46. package/dist/esm/state/persistence/stateful-restore.e2e.test.js +10 -1
  47. package/dist/esm/utils/cost-session-history.js +63 -13
  48. package/dist/esm/utils/cost-session-history.test.js +132 -13
  49. package/dist/esm/utils/resolve-cost-history-config.js +2 -1
  50. package/dist/esm/utils/sum-costs.js +2 -13
  51. package/dist/esm/utils/sum-tokens.js +10 -27
  52. package/dist/esm/utils/sum-tokens.test.js +1 -5
  53. package/dist/esm/utils/sum-usage.js +123 -0
  54. package/dist/esm/utils/sum-usage.test.js +120 -0
  55. package/dist/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +17 -17
  57. package/src/components/chat-driver/chat-driver.ts +6 -0
  58. package/src/components/settings-modal/settings-modal.styles.ts +84 -2
  59. package/src/components/settings-modal/settings-modal.template.ts +144 -33
  60. package/src/main/cost-session-banking.test.ts +270 -0
  61. package/src/main/main.ts +279 -56
  62. package/src/state/ai-assistant-slice.test.ts +12 -5
  63. package/src/state/ai-assistant-slice.ts +21 -13
  64. package/src/state/debug-event-log.ts +2 -2
  65. package/src/state/persistence/session-persistence-provider.ts +24 -0
  66. package/src/state/persistence/session-persistence.integration.test.ts +8 -1
  67. package/src/state/persistence/session-persister.test.ts +10 -1
  68. package/src/state/persistence/session-persister.ts +2 -2
  69. package/src/state/persistence/session-snapshot.test.ts +4 -1
  70. package/src/state/persistence/session-snapshot.ts +5 -1
  71. package/src/state/persistence/stateful-restore.e2e.test.ts +9 -1
  72. package/src/utils/cost-session-history.test.ts +146 -16
  73. package/src/utils/cost-session-history.ts +112 -21
  74. package/src/utils/resolve-cost-history-config.ts +7 -0
  75. package/src/utils/sum-costs.ts +2 -9
  76. package/src/utils/sum-tokens.test.ts +1 -11
  77. package/src/utils/sum-tokens.ts +10 -26
  78. package/src/utils/sum-usage.test.ts +140 -0
  79. package/src/utils/sum-usage.ts +130 -0
@@ -1,4 +1,5 @@
1
1
  import { AgentPickerMode } from '@genesislcap/foundation-ai';
2
+ import type { AggregateUsage } from '@genesislcap/foundation-ai';
2
3
  import { AIProviderRegistry } from '@genesislcap/foundation-ai';
3
4
  import type { AIProviderRegistryStatusEntry } from '@genesislcap/foundation-ai';
4
5
  import type { AIProviderType } from '@genesislcap/foundation-ai';
@@ -1698,21 +1699,58 @@ declare type ChatInteractionEventsMap = {
1698
1699
 
1699
1700
  export { ChatToolChoice }
1700
1701
 
1701
- /** A model used during a finalized cost session. */
1702
+ /** A model used during a recorded cost session. */
1702
1703
  export declare interface CostSessionModelEntry {
1703
1704
  model: string;
1704
1705
  /** Vendor id from `getStatus()` (e.g. `'anthropic'`, `'gemini'`). */
1705
1706
  provider?: AIProviderType;
1706
1707
  }
1707
1708
 
1708
- /** Persisted snapshot of a completed assistant cost session. */
1709
+ /** Persisted usage record for one project's assistant work. */
1709
1710
  export declare interface CostSessionRecord {
1710
1711
  id: string;
1712
+ /**
1713
+ * Stable identity of the work this row stands for — the assistant's session key
1714
+ * (a project id, for hosts that key sessions per project). The upsert key: a row
1715
+ * is one project's lifetime usage, so re-reporting the same key REPLACES the row
1716
+ * rather than adding another.
1717
+ */
1718
+ projectKey: string;
1711
1719
  title: string;
1712
- /** ISO-8601 timestamp when the session was finalized. */
1713
- endedAt: string;
1714
- costUsd: number;
1715
- tokensConsumed: number;
1720
+ /**
1721
+ * ISO-8601 timestamp of the most recent usage recorded for this row.
1722
+ *
1723
+ * Named for what it is: the row is upserted as spend accrues, so this moves. It is
1724
+ * NOT an end time — a row has no terminal state, because a project can always be
1725
+ * worked on again.
1726
+ */
1727
+ updatedAt: string;
1728
+ /**
1729
+ * Cost plus the four token buckets — the same shape the live session tile renders, so
1730
+ * a history row can show the identical breakdown instead of a lone merged total that
1731
+ * cannot explain a large token count at a small cost.
1732
+ *
1733
+ * This is the project's **lifetime** usage: `banked` plus whatever the live transcript
1734
+ * currently proves.
1735
+ */
1736
+ usage: AggregateUsage;
1737
+ /**
1738
+ * Spend that predates the current transcript, and so cannot be re-derived from it.
1739
+ *
1740
+ * Without this a row could only ever report what the open transcript proves. That is
1741
+ * fine when the transcript is restored on load (it *is* the project's history), but
1742
+ * where it starts empty every refresh the row would be rewritten downwards on the next
1743
+ * turn and the project's earlier spend lost. Banking it keeps `usage` a lifetime figure
1744
+ * in both cases.
1745
+ *
1746
+ * Deliberately part of the row rather than element state: two assistant instances (a
1747
+ * docked bubble and a popped-out panel) share one session, and both write this row. A
1748
+ * value derived from the row keeps every write idempotent, whereas an element-held
1749
+ * accumulator would be applied once per instance.
1750
+ *
1751
+ * Optional: rows written before this existed have none, which reads as "nothing banked".
1752
+ */
1753
+ banked?: AggregateUsage;
1716
1754
  models: CostSessionModelEntry[];
1717
1755
  }
1718
1756
 
@@ -1905,10 +1943,13 @@ export declare interface FallbackAgentConfig extends BaseAgentConfig {
1905
1943
  *
1906
1944
  * Popout/collapse coordination uses `agenticActivityBus` topics `chat-popout` and `chat-popin` — not DOM `CustomEvent`s on this element.
1907
1945
  *
1908
- * **Cost build history:** opt-in via `chatConfig.costHistory.enabled`. When enabled,
1909
- * finalized sessions persist in `localStorage` (scoped by `costHistory.scope`, the
1910
- * element `id`, or `'default'`). Snapshots run on tab close and when the session
1911
- * title changes; hosts may call {@link FoundationAiAssistant.finalizeCostSession}.
1946
+ * **Cost build history:** opt-in via `chatConfig.costHistory.enabled`. When enabled, each
1947
+ * session's usage is kept as exactly ONE row per session key in `localStorage` (scoped by
1948
+ * `costHistory.scope`, the element `id`, or `'default'`), upserted as spend accrues. So a
1949
+ * row is a project's lifetime usage rather than a visit to it, and the Cost tab's total is
1950
+ * the sum of the rows alone — the live session is never added on top, because the active
1951
+ * project's own row already reflects it. Hosts may call
1952
+ * {@link FoundationAiAssistant.finalizeCostSession} to flush early; it is idempotent.
1912
1953
  *
1913
1954
  * @fires chat-header-mousedown - Fired when the user presses the chat header in `popout-mode="expand"` (for drag-to-popout hosts). Bubbles and composed. detail: `ChatHeaderMouseDownDetail`
1914
1955
  * @fires session-cleared - Fired after the session is wiped (the lifecycle menu's Clear). The payload is captured before teardown so the host can clean up any external state the active agent was working on. Bubbles and composed. detail: `SessionClearedDetail`
@@ -2141,25 +2182,91 @@ export declare class FoundationAiAssistant extends GenesisElement {
2141
2182
  /** Context window size for the active model, if known. */
2142
2183
  get contextLimit(): number | undefined;
2143
2184
  set contextLimit(value: number | undefined);
2144
- /** Aggregated USD cost across every chat turn in this session. */
2185
+ /**
2186
+ * Cost plus the four token buckets for this session — derived from the transcript
2187
+ * on every change, so it includes sub-agent turns and spend banked by a compaction.
2188
+ */
2189
+ get sessionUsage(): AggregateUsage;
2190
+ set sessionUsage(value: AggregateUsage);
2191
+ /**
2192
+ * Aggregated USD cost across every chat turn in this session.
2193
+ *
2194
+ * Read-only (unlike previously): it is a projection of `sessionUsage`, and a
2195
+ * host assigning it would have set a figure the next transcript change overwrites.
2196
+ */
2145
2197
  get sessionCostUsd(): number;
2146
- set sessionCostUsd(value: number);
2147
- /** Saved build history plus the in-flight session — shown on the Cost tab summary. */
2198
+ /**
2199
+ * Lifetime usage across every project in the history scope — cost and all four token
2200
+ * buckets. What the Cost tab's lifetime section renders.
2201
+ *
2202
+ * Rows ONLY — deliberately not rows + live session. Each row holds a project's lifetime
2203
+ * usage and the active project's row is kept current on every transcript change, so
2204
+ * adding the live total would count this project's spend twice. That double-count is what
2205
+ * made the figure grow on every refresh: a page-hide banked a row while the restored
2206
+ * session still carried the same spend.
2207
+ */
2208
+ get cumulativeUsage(): AggregateUsage;
2209
+ /** Lifetime USD across every project in the history scope. */
2148
2210
  get cumulativeCostUsd(): number;
2211
+ /**
2212
+ * Whether the live session figures amount to this project's whole history, which is true
2213
+ * exactly when the transcript is restored on load. Drives the Cost tab's live labels
2214
+ * ("Project cost" vs "Session cost") so they state the scope rather than imply one.
2215
+ *
2216
+ * Note this is NOT what makes the *rows* lifetime — those bank their pre-transcript spend
2217
+ * (see `CostSessionRecord.banked`) and are lifetime either way.
2218
+ */
2219
+ get liveUsageIsProjectLifetime(): boolean;
2149
2220
  /** Resolved cost tab and build-history settings from `chatConfig.costHistory`. */
2150
2221
  get resolvedCostHistory(): ResolvedCostHistoryConfig;
2151
- /** Cumulative input + output tokens across every chat turn in this session. */
2222
+ /**
2223
+ * Total tokens across every chat turn in this session — all four buckets added up.
2224
+ * Read-only; the per-bucket split is on `sessionUsage`.
2225
+ */
2152
2226
  get sessionTokensConsumed(): number;
2153
- set sessionTokensConsumed(value: number);
2154
2227
  /** Label shown on the Cost tab for the in-flight session (app title or first user phrase). */
2155
2228
  get liveCostSessionTitle(): string | undefined;
2156
2229
  /**
2157
- * Snapshot the running session into persisted build history (`localStorage`).
2158
- * No-op unless `chatConfig.costHistory.enabled` is `true`.
2230
+ * Write this session's usage into persisted build history (`localStorage`) as a single
2231
+ * row keyed by the session key, replacing any existing row for that key.
2232
+ *
2233
+ * **Idempotent** — safe to call per turn, on tab close, and on a title change alike.
2234
+ * The name is historical: it does NOT end the session or reset the running totals.
2235
+ * Resetting them is what used to double-count spend, since the row was banked while the
2236
+ * persisted transcript still carried the same usage, so a reload restored it and the
2237
+ * Cost tab counted both.
2238
+ *
2239
+ * No-op unless `chatConfig.costHistory.enabled` is `true`, when the live session has no
2240
+ * usage yet, or when the session has no key to upsert against. Gating on the *live*
2241
+ * figures (not the banked total) means a reload that restores nothing rewrites nothing.
2159
2242
  */
2160
2243
  finalizeCostSession(options?: {
2161
2244
  title?: string;
2162
2245
  }): void;
2246
+ /**
2247
+ * Spend for `projectKey` that predates the current transcript, resolved once per project
2248
+ * and memoised (`_bankedFor`) so it cannot drift mid-session as the row it came from is
2249
+ * rewritten by our own upserts.
2250
+ *
2251
+ * The two cases turn on whether the transcript is authoritative for this project:
2252
+ *
2253
+ * - **Transcript restored** (`persistenceActive`): it already accounts for everything after
2254
+ * the previous row's own banked figure, so carry that figure forward unchanged. Re-banking
2255
+ * `usage` here would add the restored history to itself on every reload.
2256
+ * - **Transcript starts empty**: nothing on screen accounts for the previous total, so the
2257
+ * whole of it becomes banked. Without this the next turn would rewrite the row downwards
2258
+ * to just that turn, losing the project's earlier spend.
2259
+ */
2260
+ private bankedUsageFor;
2261
+ /**
2262
+ * Discard the banked figure — for when the spend it stands for has been deliberately
2263
+ * thrown away (the user clearing history, or deleting this project's row).
2264
+ *
2265
+ * NOT for reloads. A reload re-reads rows that already include our own upserts, so
2266
+ * re-deriving from them would fold the live session into its own baseline; only a genuine
2267
+ * project change should re-derive, which `bankedUsageFor`'s per-project memo handles.
2268
+ */
2269
+ private resetBankedUsage;
2163
2270
  showConfirmationDialog(confirmationMessage: string, modalParent: HTMLElement, onConfirm: () => void, onCancel?: () => void): void;
2164
2271
  /** Remove one entry from persisted build history. */
2165
2272
  removeCostSessionFromHistory(id: string): void;
@@ -2167,6 +2274,22 @@ export declare class FoundationAiAssistant extends GenesisElement {
2167
2274
  clearCostSessionHistory(): void;
2168
2275
  private getCostHistoryScope;
2169
2276
  private reloadCostSessionHistory;
2277
+ /**
2278
+ * The persistence provider, but only when it implements BOTH cost-history methods.
2279
+ *
2280
+ * Both or neither: a provider that can save but not load would silently accumulate a
2281
+ * ledger nobody ever reads back, so a partial implementation is treated as absent and
2282
+ * falls back to `localStorage`.
2283
+ */
2284
+ private costHistoryProvider;
2285
+ /**
2286
+ * Adopt `records` as the live list and write them through to whichever backend owns
2287
+ * cost history — the provider when the host implements it, else `localStorage`.
2288
+ *
2289
+ * Provider writes are fire-and-forget and best-effort: the ledger is accounting, not
2290
+ * conversation content, so a failed write is logged and never surfaced into the chat.
2291
+ */
2292
+ private persistCostHistory;
2170
2293
  /** Finalize the previous session when the host-provided title changes. */
2171
2294
  private syncActiveCostSessionTitle;
2172
2295
  /** Active model id resolved from the AIProvider, if exposed. */
@@ -2222,8 +2345,15 @@ export declare class FoundationAiAssistant extends GenesisElement {
2222
2345
  private unsubAppSettingsProvider?;
2223
2346
  /** Title tracked for the in-flight cost session (used when `header-title` changes). */
2224
2347
  private _activeCostSessionTitle;
2225
- /** Prevents double-finalize for the same session instance (pagehide + title change). */
2226
- private _costSessionFinalizedForInstance;
2348
+ /** Memoised pre-transcript spend for `_bankedFor` — see `bankedUsageFor`. */
2349
+ private _bankedUsage;
2350
+ /** Project key `_bankedUsage` was resolved for; `undefined` means "not yet resolved". */
2351
+ private _bankedFor;
2352
+ /**
2353
+ * Whether the row list reflects storage yet. Gates cost writes so a project's banked
2354
+ * spend is never derived from a ledger that simply hasn't arrived.
2355
+ */
2356
+ private _costHistoryLoaded;
2227
2357
  private _handlePageHide;
2228
2358
  private haloStartPublished;
2229
2359
  /** Fingerprint of the agents array used to build the current driver. Used by agentsChanged to skip spurious rebuilds. */
@@ -2612,6 +2742,14 @@ export declare class FoundationAiAssistant extends GenesisElement {
2612
2742
  get settingsCostHistorySectionVisible(): boolean;
2613
2743
  /** Whether the built-in Cost tab summary block has anything to show. */
2614
2744
  get settingsCostSummaryVisible(): boolean;
2745
+ /**
2746
+ * Whether the lifetime-usage section has anything to say.
2747
+ *
2748
+ * Requires build history: with it off there are no rows, so `cumulativeUsage` falls back to
2749
+ * the live session and the section would restate the block directly below it under a
2750
+ * "lifetime" heading it hasn't earned.
2751
+ */
2752
+ get settingsCostLifetimeVisible(): boolean;
2615
2753
  /** Whether the Cost tab should appear in the settings modal. */
2616
2754
  get settingsCostTabVisible(): boolean;
2617
2755
  /** Whether session token usage should render on the Cost tab. */
@@ -3268,6 +3406,12 @@ declare interface ResolvedCostHistoryConfig {
3268
3406
  showTokens: boolean;
3269
3407
  /** Live-session badge label on the Cost tab summary. */
3270
3408
  badgeLabel: string;
3409
+ /**
3410
+ * Host-supplied disclosure line for the Cost tab. Stays optional — unlike
3411
+ * `badgeLabel` there is no default, because only the host knows what its `scope`
3412
+ * spans, and the library must not assert it.
3413
+ */
3414
+ summaryHint?: string;
3271
3415
  }
3272
3416
 
3273
3417
  /**
@@ -3385,6 +3529,28 @@ export declare interface SessionPersistenceProvider {
3385
3529
  loadPreferences?(sessionKey: string): Promise<SessionPreferences | undefined>;
3386
3530
  /** Persist the user's UI preferences for `sessionKey` (kept across `clear`). */
3387
3531
  savePreferences?(sessionKey: string, preferences: SessionPreferences): Promise<void>;
3532
+ /**
3533
+ * Scoped cost/build history — the per-project usage rows behind the Cost tab.
3534
+ *
3535
+ * ‼️ **Keyed by SCOPE, not `sessionKey`**, unlike every other method on this interface.
3536
+ * A row holds one project's lifetime usage and the list spans projects, so keying it
3537
+ * per session would defeat the model. The scope comes from
3538
+ * `chatConfig.costHistory.scope`, falling back to the element `id`, then `'default'`.
3539
+ *
3540
+ * Optional: when a provider omits these, the assistant falls back to `localStorage`, so
3541
+ * hosts that don't implement them are unaffected. Implementing them moves the ledger to
3542
+ * the host's own backend, which is what stops it being stranded in one browser.
3543
+ *
3544
+ * Read/written **independently of the `enabled` toggle** (as with preferences and
3545
+ * diagnostics): usage accounting is not conversation content, so a user turning off
3546
+ * chat retention should not lose their cost ledger. Best-effort — a failed write must
3547
+ * never affect the chat.
3548
+ *
3549
+ * `undefined` from `loadCostHistory` means "nothing stored", the same as `[]`.
3550
+ */
3551
+ loadCostHistory?(scope: string): Promise<CostSessionRecord[] | undefined>;
3552
+ /** Replace the stored row list for `scope`. See {@link SessionPersistenceProvider.loadCostHistory}. */
3553
+ saveCostHistory?(scope: string, records: CostSessionRecord[]): Promise<void>;
3388
3554
  /**
3389
3555
  * Server-saved diagnostics (GENC-1351 §5.8) — a **forward-only append** stream
3390
3556
  * that lets the downloadable debug log span the whole session lifetime, not just
@@ -1565,6 +1565,8 @@ ${att.content}` });
1565
1565
  var _a, _b, _c, _d, _e, _f, _g, _h;
1566
1566
  let inputTokens;
1567
1567
  let outputTokens;
1568
+ let cacheReadTokens;
1569
+ let cacheWriteTokens;
1568
1570
  let cost;
1569
1571
  if (response.usage) {
1570
1572
  const uncachedInput = (_a = response.usage.input_tokens) !== null && _a !== void 0 ? _a : 0;
@@ -1574,6 +1576,8 @@ ${att.content}` });
1574
1576
  const cacheCreation = (_d = response.usage.cache_creation_input_tokens) !== null && _d !== void 0 ? _d : breakdown ? ((_e = breakdown.ephemeral_5m_input_tokens) !== null && _e !== void 0 ? _e : 0) + cacheCreation1h : 0;
1575
1577
  cost = this.logTokenUsage(uncachedInput, (_f = response.usage.output_tokens) !== null && _f !== void 0 ? _f : 0, cacheRead, cacheCreation, cacheCreation1h);
1576
1578
  inputTokens = uncachedInput + cacheRead + cacheCreation;
1579
+ cacheReadTokens = cacheRead;
1580
+ cacheWriteTokens = cacheCreation;
1577
1581
  if (response.usage.output_tokens != null) {
1578
1582
  outputTokens = response.usage.output_tokens;
1579
1583
  }
@@ -1604,6 +1608,10 @@ ${att.content}` });
1604
1608
  base.inputTokens = inputTokens;
1605
1609
  if (outputTokens != null)
1606
1610
  base.outputTokens = outputTokens;
1611
+ if (cacheReadTokens != null)
1612
+ base.cacheReadTokens = cacheReadTokens;
1613
+ if (cacheWriteTokens != null)
1614
+ base.cacheWriteTokens = cacheWriteTokens;
1607
1615
  if (cost != null)
1608
1616
  base.cost = cost;
1609
1617
  if (response.stop_reason === "max_tokens") {
@@ -2130,28 +2138,45 @@ ${att.content}`
2130
2138
  }
2131
2139
  return contents;
2132
2140
  }
2141
+ /**
2142
+ * Map Gemini's `usageMetadata` onto the provider-agnostic usage fields, pricing
2143
+ * the call as a side effect (`logTokenUsage`). Returns an empty object when the
2144
+ * response carries no usage — every field is optional, so an absent count stays
2145
+ * `undefined` rather than being reported as a measured 0.
2146
+ *
2147
+ * Split out of `fromGeminiResponse` because it is self-contained bookkeeping
2148
+ * that reads better alone — and because folding it inline pushed that method
2149
+ * past the complexity ceiling.
2150
+ */
2151
+ usageFromGemini(usageMetadata) {
2152
+ var _a, _b, _c, _d, _e, _f;
2153
+ if (!usageMetadata)
2154
+ return {};
2155
+ const usage = usageMetadata;
2156
+ const cost = this.logTokenUsage((_a = usage.promptTokenCount) !== null && _a !== void 0 ? _a : 0, (_b = usage.candidatesTokenCount) !== null && _b !== void 0 ? _b : 0, (_c = usage.thoughtsTokenCount) !== null && _c !== void 0 ? _c : 0, (_d = usage.cachedContentTokenCount) !== null && _d !== void 0 ? _d : 0);
2157
+ return {
2158
+ inputTokens: usage.promptTokenCount,
2159
+ // Thinking tokens are generated output, billed at the candidate rate, but
2160
+ // reported in a field disjoint from `candidatesTokenCount`. Fold them into the
2161
+ // provider-agnostic `outputTokens` so it reflects the true generated total —
2162
+ // matching Anthropic, whose `output_tokens` already includes them. Left
2163
+ // `undefined` only when the provider reported neither count.
2164
+ outputTokens: usage.candidatesTokenCount != null || usage.thoughtsTokenCount != null ? ((_e = usage.candidatesTokenCount) !== null && _e !== void 0 ? _e : 0) + ((_f = usage.thoughtsTokenCount) !== null && _f !== void 0 ? _f : 0) : void 0,
2165
+ thoughtsTokens: usage.thoughtsTokenCount,
2166
+ // Gemini reports cache HITS only — implicit caching bills no separate write, so
2167
+ // `cacheWriteTokens` is left unset rather than 0 (there is no write bucket to
2168
+ // measure, which is not the same as measuring 0). Already counted inside
2169
+ // `promptTokenCount`, so this breaks that total down rather than adding to it.
2170
+ cacheReadTokens: usage.cachedContentTokenCount,
2171
+ cost
2172
+ };
2173
+ }
2133
2174
  fromGeminiResponse(response, offeredToolNames = /* @__PURE__ */ new Set()) {
2134
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
2135
- let inputTokens;
2136
- let outputTokens;
2137
- let thoughtsTokens;
2138
- let cost;
2139
- if (response.usageMetadata) {
2140
- const usage = response.usageMetadata;
2141
- cost = this.logTokenUsage((_a = usage.promptTokenCount) !== null && _a !== void 0 ? _a : 0, (_b = usage.candidatesTokenCount) !== null && _b !== void 0 ? _b : 0, (_c = usage.thoughtsTokenCount) !== null && _c !== void 0 ? _c : 0, (_d = usage.cachedContentTokenCount) !== null && _d !== void 0 ? _d : 0);
2142
- if (usage.promptTokenCount != null) {
2143
- inputTokens = usage.promptTokenCount;
2144
- }
2145
- if (usage.thoughtsTokenCount != null) {
2146
- thoughtsTokens = usage.thoughtsTokenCount;
2147
- }
2148
- if (usage.candidatesTokenCount != null || usage.thoughtsTokenCount != null) {
2149
- outputTokens = ((_e = usage.candidatesTokenCount) !== null && _e !== void 0 ? _e : 0) + ((_f = usage.thoughtsTokenCount) !== null && _f !== void 0 ? _f : 0);
2150
- }
2151
- }
2175
+ var _a, _b, _c, _d;
2176
+ const { inputTokens, outputTokens, thoughtsTokens, cacheReadTokens, cost } = this.usageFromGemini(response.usageMetadata);
2152
2177
  const candidates = response === null || response === void 0 ? void 0 : response.candidates;
2153
2178
  const firstCandidate = candidates === null || candidates === void 0 ? void 0 : candidates[0];
2154
- const parts = (_h = (_g = firstCandidate === null || firstCandidate === void 0 ? void 0 : firstCandidate.content) === null || _g === void 0 ? void 0 : _g.parts) !== null && _h !== void 0 ? _h : [];
2179
+ const parts = (_b = (_a = firstCandidate === null || firstCandidate === void 0 ? void 0 : firstCandidate.content) === null || _a === void 0 ? void 0 : _a.parts) !== null && _b !== void 0 ? _b : [];
2155
2180
  const toolCalls = [];
2156
2181
  const thoughtParts = [];
2157
2182
  const textParts = [];
@@ -2164,7 +2189,7 @@ ${att.content}`
2164
2189
  toolCalls.push({
2165
2190
  id: crypto.randomUUID(),
2166
2191
  name: part.functionCall.name,
2167
- args: (_j = part.functionCall.args) !== null && _j !== void 0 ? _j : {},
2192
+ args: (_c = part.functionCall.args) !== null && _c !== void 0 ? _c : {},
2168
2193
  providerMetadata: { [GEMINI_PROVIDER_KEY]: state }
2169
2194
  });
2170
2195
  } else if (part.thought && part.text) {
@@ -2198,9 +2223,11 @@ ${att.content}`
2198
2223
  base.inputTokens = inputTokens;
2199
2224
  if (outputTokens != null)
2200
2225
  base.outputTokens = outputTokens;
2226
+ if (cacheReadTokens != null)
2227
+ base.cacheReadTokens = cacheReadTokens;
2201
2228
  if (cost != null)
2202
2229
  base.cost = cost;
2203
- const blockReason = (_k = response.promptFeedback) === null || _k === void 0 ? void 0 : _k.blockReason;
2230
+ const blockReason = (_d = response.promptFeedback) === null || _d === void 0 ? void 0 : _d.blockReason;
2204
2231
  if (finishReason != null || thoughtsTokens != null || blockReason != null) {
2205
2232
  base.responseMeta = {
2206
2233
  finishReason,
@@ -2997,6 +3024,46 @@ The above summarizes earlier parts of this conversation that were compacted. Con
2997
3024
  });
2998
3025
  }
2999
3026
 
3027
+ // src/utils/sum-usage.ts
3028
+ function emptyUsage() {
3029
+ return {
3030
+ costUsd: 0,
3031
+ uncachedInputTokens: 0,
3032
+ cacheReadTokens: 0,
3033
+ cacheWriteTokens: 0,
3034
+ outputTokens: 0
3035
+ };
3036
+ }
3037
+ function sumUsage(messages) {
3038
+ const total = emptyUsage();
3039
+ accumulate(messages, total);
3040
+ return total;
3041
+ }
3042
+ function addInto(into, from) {
3043
+ into.costUsd += from.costUsd;
3044
+ into.uncachedInputTokens += from.uncachedInputTokens;
3045
+ into.cacheReadTokens += from.cacheReadTokens;
3046
+ into.cacheWriteTokens += from.cacheWriteTokens;
3047
+ into.outputTokens += from.outputTokens;
3048
+ }
3049
+ function accumulate(messages, into) {
3050
+ for (const m of messages) {
3051
+ if (m.cost != null) into.costUsd += m.cost;
3052
+ if (m.externalCostUsd != null) into.costUsd += m.externalCostUsd;
3053
+ const cacheRead = m.cacheReadTokens ?? 0;
3054
+ const cacheWrite = m.cacheWriteTokens ?? 0;
3055
+ into.cacheReadTokens += cacheRead;
3056
+ into.cacheWriteTokens += cacheWrite;
3057
+ into.uncachedInputTokens += Math.max(0, (m.inputTokens ?? 0) - cacheRead - cacheWrite);
3058
+ into.outputTokens += m.outputTokens ?? 0;
3059
+ const rolled = m.compaction?.rolledUpUsage;
3060
+ if (rolled) addInto(into, rolled);
3061
+ for (const tc of m.toolCalls ?? []) {
3062
+ if (tc.subAgentTrace) accumulate(tc.subAgentTrace, into);
3063
+ }
3064
+ }
3065
+ }
3066
+
3000
3067
  // src/utils/tool-fold.ts
3001
3068
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
3002
3069
 
@@ -3744,7 +3811,12 @@ Output format (strict):
3744
3811
  coveredThroughTimestamp: toCompact[toCompact.length - 1]?.timestamp,
3745
3812
  tokensBefore,
3746
3813
  model: status?.model,
3747
- createdAt
3814
+ createdAt,
3815
+ // Bank what the replaced turns spent. `toCompact` is dropped from history
3816
+ // below, taking its per-message cost/token fields with it — so a host total
3817
+ // re-derived from the transcript would otherwise fall by exactly this much
3818
+ // on every compaction. Summarising a turn does not refund what it cost.
3819
+ rolledUpUsage: sumUsage(toCompact)
3748
3820
  }
3749
3821
  };
3750
3822
  this.loadHistory([summaryMessage, ...tail]);