@genesislcap/foundation-ai 14.469.0 → 14.470.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.
@@ -57,6 +57,53 @@ function estimatedAnthropicRatesUsdPerMillion(model) {
57
57
  // Opus 4.7
58
58
  return { promptPerMillion: 5, candidatePerMillion: 25 };
59
59
  }
60
+ /**
61
+ * Prompt-cache pricing multipliers, applied to the model's base input rate
62
+ * (`promptPerMillion`) — https://docs.claude.com/en/docs/build-with-claude/prompt-caching
63
+ * Reads bill at ~0.1× base input; writes bill by TTL — 5-minute at ~1.25× and 1-hour at 2×.
64
+ * Both write TTLs are reachable (`CachePolicy.ttl` is `'5m' | '1h'` and `applyCacheControl`
65
+ * emits either), so the cost path costs each TTL bucket from the response's per-TTL
66
+ * `cache_creation` breakdown rather than assuming a single rate.
67
+ */
68
+ const ANTHROPIC_CACHE_READ_MULTIPLIER = 0.1;
69
+ const ANTHROPIC_CACHE_WRITE_5M_MULTIPLIER = 1.25;
70
+ const ANTHROPIC_CACHE_WRITE_1H_MULTIPLIER = 2;
71
+ /**
72
+ * Thrown when a response stops at `stop_reason: 'max_tokens'` while it still
73
+ * carries tool calls. The model ran out of its output-token budget mid-stream,
74
+ * so the truncated tool call's argument JSON was cut off before it closed — the
75
+ * arguments are incomplete and the call is unusable (e.g. a `vfs_write` whose
76
+ * `content` never finished serializing). Rather than hand a corrupt tool call to
77
+ * the caller — which silently runs with missing args and tends to be retried into
78
+ * an identical wall — the transport raises this so the failure is loud and
79
+ * diagnosable.
80
+ *
81
+ * It is deterministic: re-issuing the same request hits the same cap. The remedy
82
+ * is to raise the provider's `maxTokens` (see {@link AnthropicAIConfig.maxTokens})
83
+ * or to split the work into smaller outputs — not to retry verbatim.
84
+ *
85
+ * @beta
86
+ */
87
+ export class ResponseTruncatedError extends Error {
88
+ constructor(
89
+ /** The model that produced the truncated response. */
90
+ model,
91
+ /** The `max_tokens` cap the request was sent with. */
92
+ maxTokens,
93
+ /** Output tokens generated before truncation, when the usage block is present. */
94
+ outputTokens,
95
+ /** Names of the tool call(s) on the truncated turn (the last is the cut-off one). */
96
+ toolNames) {
97
+ super(`Anthropic response truncated at the max_tokens cap (${maxTokens}) for model ${model}` +
98
+ (toolNames.length > 0 ? ` while emitting tool call(s): ${toolNames.join(', ')}` : '') +
99
+ '. The output exceeds the per-response limit — raise the provider maxTokens or split the work into smaller outputs.');
100
+ this.model = model;
101
+ this.maxTokens = maxTokens;
102
+ this.outputTokens = outputTokens;
103
+ this.toolNames = toolNames;
104
+ this.name = 'ResponseTruncatedError';
105
+ }
106
+ }
60
107
  /**
61
108
  * Transport for Anthropic Claude. Calls the Messages API directly when `apiKey`
62
109
  * is provided, otherwise falls back to a server-proxy endpoint (if `serverEndpoint`
@@ -77,6 +124,12 @@ export class AnthropicTransport {
77
124
  * fields instead so its session total stays attributed to chat turns only.
78
125
  */
79
126
  this.lifetimeCostUsd = 0;
127
+ /**
128
+ * Estimated USD saved by prompt caching vs paying full input price, accumulated across
129
+ * every request. Net of cache-write premiums, so it can be briefly negative before reads
130
+ * accrue. Surfaced alongside `getLifetimeCost`.
131
+ */
132
+ this.lifetimeSavingsUsd = 0;
80
133
  const model = (_a = config.model) !== null && _a !== void 0 ? _a : DEFAULT_MODEL;
81
134
  assertSupportedAnthropicModel(model);
82
135
  this.model = model;
@@ -102,9 +155,14 @@ export class AnthropicTransport {
102
155
  getLifetimeCost() {
103
156
  return this.lifetimeCostUsd;
104
157
  }
105
- /** Reset the lifetime cost counter. Intended for chat-clear / new-session flows. */
158
+ /** Estimated USD saved by prompt caching (net of write premiums) across this instance. */
159
+ getLifetimeSavings() {
160
+ return this.lifetimeSavingsUsd;
161
+ }
162
+ /** Reset the lifetime cost + savings counters. Intended for chat-clear / new-session flows. */
106
163
  resetLifetimeCost() {
107
164
  this.lifetimeCostUsd = 0;
165
+ this.lifetimeSavingsUsd = 0;
108
166
  }
109
167
  // ── AITransport (structured prompt) ────────────────────────────────────
110
168
  sendStructuredPrompt(options) {
@@ -185,26 +243,116 @@ export class AnthropicTransport {
185
243
  maxTemp: ANTHROPIC_MAX_TEMPERATURE,
186
244
  });
187
245
  }
246
+ // Place prompt-cache breakpoints per the resolved policy (no-op for `'default'`/absent).
247
+ if (options === null || options === void 0 ? void 0 : options.cachePolicy) {
248
+ this.applyCacheControl(body, options.cachePolicy);
249
+ }
250
+ // Inject never-stored tail context AFTER the cache breakpoints, so this per-turn volatile
251
+ // block stays outside the cached prefix. The caller has already framed it.
252
+ if (options === null || options === void 0 ? void 0 : options.tailContext) {
253
+ this.appendTailContext(body, options.tailContext);
254
+ }
188
255
  const response = yield this.post(body, options === null || options === void 0 ? void 0 : options.signal);
189
256
  return this.fromAnthropicResponse(response);
190
257
  });
191
258
  }
259
+ /**
260
+ * Place prompt-cache breakpoints on the outgoing request per the resolved {@link CachePolicy}.
261
+ * A breakpoint is `cache_control: {type:'ephemeral'}` on a content block / tool / system block;
262
+ * the API caches the byte-exact prefix up to it. Below a model's minimum cacheable prefix the
263
+ * marker is silently ignored (no error), so placing one is always safe.
264
+ *
265
+ * Render order is tools → system → messages, so a breakpoint's cached prefix covers everything
266
+ * before it:
267
+ * - `'tools'` — last tool (caches the tool block; survives a later system-prompt change).
268
+ * - `'prompt'` — system block (caches tools + system); falls back to the last tool when there
269
+ * is no system prompt.
270
+ * - `'history'` — last stored message block (caches tools + system + the whole stored history),
271
+ * keeping the system-tier breakpoint as a cheaper fallback. The injected, never-stored tail
272
+ * context (if any) sits after this and is intentionally left uncached.
273
+ */
274
+ applyCacheControl(body, policy) {
275
+ var _a, _b;
276
+ if (policy.scope === 'default')
277
+ return;
278
+ const cacheControl = policy.ttl === '1h' ? { type: 'ephemeral', ttl: '1h' } : { type: 'ephemeral' };
279
+ if (policy.scope === 'tools') {
280
+ if ((_a = body.tools) === null || _a === void 0 ? void 0 : _a.length) {
281
+ body.tools[body.tools.length - 1].cache_control = cacheControl;
282
+ }
283
+ return;
284
+ }
285
+ // 'prompt' and 'history' both cache through the system prompt. A breakpoint on the (single)
286
+ // system block caches tools + system together; with no system prompt, fall back to the last
287
+ // tool — the furthest stable prefix available.
288
+ if (typeof body.system === 'string' && body.system.length > 0) {
289
+ body.system = [{ type: 'text', text: body.system, cache_control: cacheControl }];
290
+ }
291
+ else if ((_b = body.tools) === null || _b === void 0 ? void 0 : _b.length) {
292
+ body.tools[body.tools.length - 1].cache_control = cacheControl;
293
+ }
294
+ if (policy.scope === 'history') {
295
+ const lastMessage = body.messages[body.messages.length - 1];
296
+ if (lastMessage && Array.isArray(lastMessage.content) && lastMessage.content.length > 0) {
297
+ lastMessage.content[lastMessage.content.length - 1].cache_control = cacheControl;
298
+ }
299
+ }
300
+ }
301
+ /**
302
+ * Append never-stored tail context (already framed by the caller) as the final block of the
303
+ * outbound messages. Called AFTER `applyCacheControl`, so any `'history'` breakpoint sits
304
+ * on the last *stored* block and this per-turn volatile block stays outside the cached prefix.
305
+ * At a model-call the last message is user-role; if not (defensive), start a fresh user turn.
306
+ */
307
+ appendTailContext(body, tailContext) {
308
+ const block = { type: 'text', text: tailContext };
309
+ const lastMessage = body.messages[body.messages.length - 1];
310
+ if ((lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.role) === 'user' && Array.isArray(lastMessage.content)) {
311
+ lastMessage.content.push(block);
312
+ }
313
+ else {
314
+ body.messages.push({ role: 'user', content: [block] });
315
+ }
316
+ }
192
317
  /**
193
318
  * Logs the per-call cost breakdown, accumulates the lifetime running total,
194
319
  * and returns the per-call total so the caller can attach it to the response
195
320
  * message.
196
321
  */
197
- logTokenUsage(promptTokens, candidateTokens) {
322
+ logTokenUsage(promptTokens, candidateTokens, cacheReadTokens, cacheCreationTokens, cacheCreation1hTokens) {
198
323
  const { promptPerMillion, candidatePerMillion } = estimatedAnthropicRatesUsdPerMillion(this.model);
199
- const promptCost = (promptTokens / AnthropicTransport.TOKENS_PER_MILLION) * promptPerMillion;
200
- const candidateCost = (candidateTokens / AnthropicTransport.TOKENS_PER_MILLION) * candidatePerMillion;
201
- const totalCost = promptCost + candidateCost;
324
+ const m = AnthropicTransport.TOKENS_PER_MILLION;
325
+ // `promptTokens` is `input_tokens` the UNCACHED remainder, billed at the full input
326
+ // rate. Cache reads bill at ~0.1×. Cache writes bill by TTL — 5-min at ~1.25×, 1-hour at
327
+ // 2× — so split the creation total into its 1-hour portion (from the response breakdown)
328
+ // and the 5-min remainder, costing each at its own rate. All buckets are off the same base
329
+ // input rate. Summing them keeps the cost correct whether or not caching was active (all
330
+ // cache fields are 0 when it wasn't).
331
+ const cacheCreation5mTokens = Math.max(0, cacheCreationTokens - cacheCreation1hTokens);
332
+ const promptCost = (promptTokens / m) * promptPerMillion;
333
+ const cacheReadCost = (cacheReadTokens / m) * promptPerMillion * ANTHROPIC_CACHE_READ_MULTIPLIER;
334
+ const cacheWriteCost = (cacheCreation5mTokens / m) * promptPerMillion * ANTHROPIC_CACHE_WRITE_5M_MULTIPLIER +
335
+ (cacheCreation1hTokens / m) * promptPerMillion * ANTHROPIC_CACHE_WRITE_1H_MULTIPLIER;
336
+ const candidateCost = (candidateTokens / m) * candidatePerMillion;
337
+ const totalCost = promptCost + cacheReadCost + cacheWriteCost + candidateCost;
202
338
  this.lifetimeCostUsd += totalCost;
339
+ // Net savings vs no caching: the cache-read tokens would have cost full input price
340
+ // (we paid ~0.1× of that), while the write premium (~0.25× over full) is an upfront cost
341
+ // that pays back on later reads — so the net can dip negative on a write-heavy request and
342
+ // climb positive as reads accrue. Zero when caching was inactive.
343
+ const cacheReadFull = (cacheReadTokens / m) * promptPerMillion;
344
+ const cacheWriteFull = (cacheCreationTokens / m) * promptPerMillion;
345
+ const saved = cacheReadFull - cacheReadCost + (cacheWriteFull - cacheWriteCost);
346
+ this.lifetimeSavingsUsd += saved;
347
+ const dp = AnthropicTransport.COST_DECIMAL_PLACES;
203
348
  console.log(`--- Anthropic Token Usage (${this.model}) ---`);
204
- console.log(`Prompt Tokens: ${promptTokens} ($${promptCost.toFixed(AnthropicTransport.COST_DECIMAL_PLACES)})`);
205
- console.log(`Candidate Tokens: ${candidateTokens} ($${candidateCost.toFixed(AnthropicTransport.COST_DECIMAL_PLACES)})`);
206
- console.log(`Total Cost: $${totalCost.toFixed(AnthropicTransport.COST_DECIMAL_PLACES)}`);
207
- console.log(`Lifetime Cost: $${this.lifetimeCostUsd.toFixed(AnthropicTransport.COST_DECIMAL_PLACES)}`);
349
+ console.log(`Prompt Tokens: ${promptTokens} ($${promptCost.toFixed(dp)})`);
350
+ console.log(`Cache Read: ${cacheReadTokens} ($${cacheReadCost.toFixed(dp)})`);
351
+ console.log(`Cache Write: ${cacheCreationTokens} ($${cacheWriteCost.toFixed(dp)})`);
352
+ console.log(`Candidate Tokens: ${candidateTokens} ($${candidateCost.toFixed(dp)})`);
353
+ console.log(`Total Cost: $${totalCost.toFixed(dp)}`);
354
+ console.log(`Cache Saved: $${saved.toFixed(dp)} (lifetime $${this.lifetimeSavingsUsd.toFixed(dp)})`);
355
+ console.log(`Lifetime Cost: $${this.lifetimeCostUsd.toFixed(dp)}`);
208
356
  console.log('--------------------------');
209
357
  return totalCost;
210
358
  }
@@ -278,20 +426,30 @@ export class AnthropicTransport {
278
426
  return messages;
279
427
  }
280
428
  fromAnthropicResponse(response) {
281
- var _a, _b, _c, _d;
429
+ var _a, _b, _c, _d, _e, _f, _g, _h;
282
430
  let inputTokens;
283
431
  let outputTokens;
284
432
  let cost;
285
433
  if (response.usage) {
286
- cost = this.logTokenUsage((_a = response.usage.input_tokens) !== null && _a !== void 0 ? _a : 0, (_b = response.usage.output_tokens) !== null && _b !== void 0 ? _b : 0);
287
- if (response.usage.input_tokens != null) {
288
- inputTokens = response.usage.input_tokens;
289
- }
434
+ const uncachedInput = (_a = response.usage.input_tokens) !== null && _a !== void 0 ? _a : 0;
435
+ const cacheRead = (_b = response.usage.cache_read_input_tokens) !== null && _b !== void 0 ? _b : 0;
436
+ // 1-hour writes bill at 2× vs 1.25× for 5-min, so read the per-TTL breakdown when present
437
+ // and let the cost path split the buckets. Fall back to the flat total (treated as 5-min)
438
+ // when the breakdown is absent.
439
+ const breakdown = response.usage.cache_creation;
440
+ const cacheCreation1h = (_c = breakdown === null || breakdown === void 0 ? void 0 : breakdown.ephemeral_1h_input_tokens) !== null && _c !== void 0 ? _c : 0;
441
+ 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);
442
+ cost = this.logTokenUsage(uncachedInput, (_f = response.usage.output_tokens) !== null && _f !== void 0 ? _f : 0, cacheRead, cacheCreation, cacheCreation1h);
443
+ // Report TOTAL prompt size (uncached + cache read + cache write) so `inputTokens` means
444
+ // "prompt tokens" consistently whether or not caching was active — matching Gemini's
445
+ // `promptTokenCount`, which already includes cached tokens. Cost (above) reflects the
446
+ // cache discount; this field is the size, not the bill.
447
+ inputTokens = uncachedInput + cacheRead + cacheCreation;
290
448
  if (response.usage.output_tokens != null) {
291
449
  outputTokens = response.usage.output_tokens;
292
450
  }
293
451
  }
294
- const blocks = (_c = response.content) !== null && _c !== void 0 ? _c : [];
452
+ const blocks = (_g = response.content) !== null && _g !== void 0 ? _g : [];
295
453
  const toolCalls = [];
296
454
  const thoughtParts = [];
297
455
  const textParts = [];
@@ -300,7 +458,7 @@ export class AnthropicTransport {
300
458
  toolCalls.push({
301
459
  id: block.id,
302
460
  name: block.name,
303
- args: (_d = block.input) !== null && _d !== void 0 ? _d : {},
461
+ args: (_h = block.input) !== null && _h !== void 0 ? _h : {},
304
462
  });
305
463
  }
306
464
  else if (block.type === 'thinking') {
@@ -310,6 +468,15 @@ export class AnthropicTransport {
310
468
  textParts.push(block.text);
311
469
  }
312
470
  }
471
+ // A `max_tokens` stop means the response was cut off mid-stream. For a turn
472
+ // carrying tool calls this is fatal: the final tool call's argument JSON was
473
+ // truncated before it closed, so its args are incomplete and the call cannot
474
+ // be run. Raise rather than return a corrupt tool call (which would run with
475
+ // missing args and get retried into the same wall). Usage/cost is already
476
+ // logged above, so the spent tokens are still accounted for on this instance.
477
+ if (response.stop_reason === 'max_tokens' && toolCalls.length > 0) {
478
+ throw new ResponseTruncatedError(this.model, this.maxTokens, outputTokens, toolCalls.map((tc) => tc.name));
479
+ }
313
480
  const base = toolCalls.length > 0
314
481
  ? {
315
482
  role: 'assistant',
@@ -323,6 +490,13 @@ export class AnthropicTransport {
323
490
  base.outputTokens = outputTokens;
324
491
  if (cost != null)
325
492
  base.cost = cost;
493
+ // Surface a non-standard stop reason for observability and parity with the
494
+ // Gemini transport's `responseMeta.finishReason`. A text-only `max_tokens`
495
+ // stop yields partial-but-legible content, so it is returned (not raised) with
496
+ // the truncation flagged — the caller can decide what to do with it.
497
+ if (response.stop_reason === 'max_tokens') {
498
+ base.responseMeta = { finishReason: 'max_tokens' };
499
+ }
326
500
  return base;
327
501
  }
328
502
  buildEndpoint() {
@@ -46,6 +46,14 @@ function assertSupportedGeminiModel(model) {
46
46
  * to the whole request based on prompt size — it is not a marginal/blended rate.
47
47
  */
48
48
  const GEMINI_LONG_CONTEXT_THRESHOLD = 200000;
49
+ /**
50
+ * Cached / context-cache input tokens bill at a flat ~10% of the model's normal input
51
+ * rate — consistent across models (flash $0.30→$0.03, flash-lite $0.10→$0.01,
52
+ * pro $1.25→$0.125). Source: https://ai.google.dev/gemini-api/docs/pricing.
53
+ * (Explicit context caching also has an hourly storage fee; implicit caching — what we
54
+ * rely on — has none, so only this read discount applies.)
55
+ */
56
+ const GEMINI_CACHED_INPUT_MULTIPLIER = 0.1;
49
57
  /**
50
58
  * Paid Standard tier rates (USD per million tokens) — https://ai.google.dev/gemini-api/docs/pricing
51
59
  */
@@ -141,6 +149,12 @@ export class GeminiTransport {
141
149
  * fields instead so its session total stays attributed to chat turns only.
142
150
  */
143
151
  this.lifetimeCostUsd = 0;
152
+ /**
153
+ * Estimated USD saved by context caching vs paying full input price, accumulated across
154
+ * every request. Gemini implicit caching has no write premium, so this is always ≥ 0.
155
+ * Surfaced alongside `getLifetimeCost`.
156
+ */
157
+ this.lifetimeSavingsUsd = 0;
144
158
  const model = (_a = config.model) !== null && _a !== void 0 ? _a : DEFAULT_MODEL;
145
159
  assertSupportedGeminiModel(model);
146
160
  this.model = model;
@@ -163,9 +177,14 @@ export class GeminiTransport {
163
177
  getLifetimeCost() {
164
178
  return this.lifetimeCostUsd;
165
179
  }
166
- /** Reset the lifetime cost counter. Intended for chat-clear / new-session flows. */
180
+ /** Estimated USD saved by context caching across this instance (implicit caching → always ≥ 0). */
181
+ getLifetimeSavings() {
182
+ return this.lifetimeSavingsUsd;
183
+ }
184
+ /** Reset the lifetime cost + savings counters. Intended for chat-clear / new-session flows. */
167
185
  resetLifetimeCost() {
168
186
  this.lifetimeCostUsd = 0;
187
+ this.lifetimeSavingsUsd = 0;
169
188
  }
170
189
  // ── AITransport (structured prompt) ────────────────────────────────────
171
190
  sendStructuredPrompt(options) {
@@ -186,7 +205,18 @@ export class GeminiTransport {
186
205
  sendChatMessage(history, userMessage, options) {
187
206
  return __awaiter(this, void 0, void 0, function* () {
188
207
  var _a, _b;
208
+ // NOTE: `options.cachePolicy` is intentionally NOT read here. Gemini 2.5+ does *implicit*
209
+ // caching automatically and always-on — it caches as much of the stable request prefix as it
210
+ // can on every call, regardless of the policy's `scope` (including `'default'`). We issue no
211
+ // explicit `cachedContent`, so there is no request-side field to set; the only lever is
212
+ // keeping the prefix byte-stable (a consumer concern). The cached tokens that come back are
213
+ // priced in `logTokenUsage` via `cachedContentTokenCount`. See `CachePolicy` docs.
189
214
  const contents = this.toGeminiContents(history, userMessage, options === null || options === void 0 ? void 0 : options.attachments);
215
+ // Never-stored tail context as a trailing user content (after the stable prefix). Verified
216
+ // valid even directly after a functionResponse content. The caller has already framed it.
217
+ if (options === null || options === void 0 ? void 0 : options.tailContext) {
218
+ contents.push({ role: 'user', parts: [{ text: options.tailContext }] });
219
+ }
190
220
  const tools = ((_a = options === null || options === void 0 ? void 0 : options.tools) === null || _a === void 0 ? void 0 : _a.length)
191
221
  ? [
192
222
  {
@@ -234,21 +264,33 @@ export class GeminiTransport {
234
264
  * and returns the per-call total so the caller can attach it to the response
235
265
  * message.
236
266
  */
237
- logTokenUsage(promptTokens, candidateTokens, thoughtTokens) {
267
+ logTokenUsage(promptTokens, candidateTokens, thoughtTokens, cachedTokens) {
238
268
  const { promptPerMillion, candidatePerMillion } = estimatedGeminiPaidRatesUsdPerMillion(this.model, promptTokens);
239
- const promptCost = (promptTokens / GeminiTransport.TOKENS_PER_MILLION) * promptPerMillion;
269
+ const m = GeminiTransport.TOKENS_PER_MILLION;
270
+ // `promptTokenCount` INCLUDES the cached portion (opposite of Anthropic's `input_tokens`),
271
+ // so split it: the uncached remainder bills at the full input rate and the cached slice at
272
+ // ~0.1×. The pricing TIER is still chosen on the full prompt size (above). `cachedTokens`
273
+ // is 0 when caching wasn't active, so this is a no-op then.
274
+ const uncachedPromptTokens = Math.max(0, promptTokens - cachedTokens);
275
+ const promptCost = (uncachedPromptTokens / m) * promptPerMillion;
276
+ const cacheReadCost = (cachedTokens / m) * promptPerMillion * GEMINI_CACHED_INPUT_MULTIPLIER;
240
277
  // Thinking tokens are billed at the output (candidate) rate. They are
241
278
  // incurred whenever the model thinks (always, on 2.5 Pro) — counting them
242
279
  // here corrects a long-standing undercount, it does not raise the bill.
243
- const candidateCost = ((candidateTokens + thoughtTokens) / GeminiTransport.TOKENS_PER_MILLION) *
244
- candidatePerMillion;
245
- const totalCost = promptCost + candidateCost;
280
+ const candidateCost = ((candidateTokens + thoughtTokens) / m) * candidatePerMillion;
281
+ const totalCost = promptCost + cacheReadCost + candidateCost;
246
282
  this.lifetimeCostUsd += totalCost;
283
+ // Savings vs no caching: the cached tokens would have cost full input price; we paid ~0.1×.
284
+ // Implicit caching has no write premium, so this is always ≥ 0.
285
+ const saved = (cachedTokens / m) * promptPerMillion - cacheReadCost;
286
+ this.lifetimeSavingsUsd += saved;
287
+ const dp = GeminiTransport.COST_DECIMAL_PLACES;
247
288
  console.log(`--- Gemini Token Usage (${this.model}) ---`);
248
- console.log(`Prompt Tokens: ${promptTokens} ($${promptCost.toFixed(GeminiTransport.COST_DECIMAL_PLACES)})`);
249
- console.log(`Candidate Tokens: ${candidateTokens} (+${thoughtTokens} thinking) ($${candidateCost.toFixed(GeminiTransport.COST_DECIMAL_PLACES)})`);
250
- console.log(`Total Cost: $${totalCost.toFixed(GeminiTransport.COST_DECIMAL_PLACES)}`);
251
- console.log(`Lifetime Cost: $${this.lifetimeCostUsd.toFixed(GeminiTransport.COST_DECIMAL_PLACES)}`);
289
+ console.log(`Prompt Tokens: ${promptTokens} (${cachedTokens} cached) ($${promptCost.toFixed(dp)} + $${cacheReadCost.toFixed(dp)})`);
290
+ console.log(`Candidate Tokens: ${candidateTokens} (+${thoughtTokens} thinking) ($${candidateCost.toFixed(dp)})`);
291
+ console.log(`Total Cost: $${totalCost.toFixed(dp)}`);
292
+ console.log(`Cache Saved: $${saved.toFixed(dp)} (lifetime $${this.lifetimeSavingsUsd.toFixed(dp)})`);
293
+ console.log(`Lifetime Cost: $${this.lifetimeCostUsd.toFixed(dp)}`);
252
294
  console.log('--------------------------');
253
295
  return totalCost;
254
296
  }
@@ -325,14 +367,14 @@ export class GeminiTransport {
325
367
  return contents;
326
368
  }
327
369
  fromGeminiResponse(response, offeredToolNames = new Set()) {
328
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
370
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
329
371
  let inputTokens;
330
372
  let outputTokens;
331
373
  let thoughtsTokens;
332
374
  let cost;
333
375
  if (response.usageMetadata) {
334
376
  const usage = response.usageMetadata;
335
- 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);
377
+ 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);
336
378
  if (usage.promptTokenCount != null) {
337
379
  inputTokens = usage.promptTokenCount;
338
380
  }
@@ -344,12 +386,12 @@ export class GeminiTransport {
344
386
  // reported in a field disjoint from `candidatesTokenCount`. Fold them into
345
387
  // the provider-agnostic `outputTokens` so it reflects the true generated
346
388
  // total — matching Anthropic, whose `output_tokens` already includes them.
347
- outputTokens = ((_d = usage.candidatesTokenCount) !== null && _d !== void 0 ? _d : 0) + ((_e = usage.thoughtsTokenCount) !== null && _e !== void 0 ? _e : 0);
389
+ outputTokens = ((_e = usage.candidatesTokenCount) !== null && _e !== void 0 ? _e : 0) + ((_f = usage.thoughtsTokenCount) !== null && _f !== void 0 ? _f : 0);
348
390
  }
349
391
  }
350
392
  const candidates = response === null || response === void 0 ? void 0 : response.candidates;
351
393
  const firstCandidate = candidates === null || candidates === void 0 ? void 0 : candidates[0];
352
- const parts = (_g = (_f = firstCandidate === null || firstCandidate === void 0 ? void 0 : firstCandidate.content) === null || _f === void 0 ? void 0 : _f.parts) !== null && _g !== void 0 ? _g : [];
394
+ 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 : [];
353
395
  const toolCalls = [];
354
396
  const thoughtParts = [];
355
397
  const textParts = [];
@@ -366,7 +408,7 @@ export class GeminiTransport {
366
408
  toolCalls.push({
367
409
  id: crypto.randomUUID(),
368
410
  name: part.functionCall.name,
369
- args: (_h = part.functionCall.args) !== null && _h !== void 0 ? _h : {},
411
+ args: (_j = part.functionCall.args) !== null && _j !== void 0 ? _j : {},
370
412
  providerMetadata: { [GEMINI_PROVIDER_KEY]: state },
371
413
  });
372
414
  }
@@ -430,7 +472,7 @@ export class GeminiTransport {
430
472
  // Surface the provider diagnostic on the message so the driver can fold it
431
473
  // into the debug-log meta events (the console log above is dev-only). Only
432
474
  // when there's signal — a finish reason, thinking tokens, or a block reason.
433
- const blockReason = (_j = response.promptFeedback) === null || _j === void 0 ? void 0 : _j.blockReason;
475
+ const blockReason = (_k = response.promptFeedback) === null || _k === void 0 ? void 0 : _k.blockReason;
434
476
  if (finishReason != null || thoughtsTokens != null || blockReason != null) {
435
477
  base.responseMeta = {
436
478
  finishReason,