@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.
- package/dist/dts/index.d.ts +2 -2
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/transports/anthropic-transport.d.ts +67 -1
- package/dist/dts/transports/anthropic-transport.d.ts.map +1 -1
- package/dist/dts/transports/gemini-transport.d.ts +9 -1
- package/dist/dts/transports/gemini-transport.d.ts.map +1 -1
- package/dist/dts/types/chat.types.d.ts +92 -1
- package/dist/dts/types/chat.types.d.ts.map +1 -1
- package/dist/dts/types/transports.types.d.ts +6 -1
- package/dist/dts/types/transports.types.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/transports/anthropic-transport.js +190 -16
- package/dist/esm/transports/gemini-transport.js +58 -16
- package/dist/foundation-ai.api.json +411 -8
- package/dist/foundation-ai.d.ts +176 -4
- package/package.json +11 -11
|
@@ -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
|
-
/**
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
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(
|
|
205
|
-
console.log(`
|
|
206
|
-
console.log(`
|
|
207
|
-
console.log(`
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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 = (
|
|
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: (
|
|
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
|
-
/**
|
|
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
|
|
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) /
|
|
244
|
-
|
|
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(
|
|
249
|
-
console.log(`Candidate Tokens: ${candidateTokens} (+${thoughtTokens} thinking) ($${candidateCost.toFixed(
|
|
250
|
-
console.log(`Total Cost: $${totalCost.toFixed(
|
|
251
|
-
console.log(`
|
|
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 = ((
|
|
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 = (
|
|
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: (
|
|
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 = (
|
|
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,
|