ak-gemini 2.4.1 → 2.6.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/CHANGELOG.md +111 -0
- package/GUIDE.md +11 -1
- package/README.md +56 -4
- package/UPGRADING.md +111 -0
- package/base.js +292 -34
- package/chat.js +2 -0
- package/code-agent.js +2 -0
- package/embedding.js +4 -6
- package/image-generator.js +10 -7
- package/index.cjs +319 -57
- package/index.js +2 -1
- package/json-helpers.js +105 -0
- package/message.js +20 -7
- package/package.json +5 -3
- package/rag-agent.js +2 -0
- package/tool-agent.js +2 -0
- package/transformer.js +6 -2
- package/types.d.ts +87 -4
package/base.js
CHANGED
|
@@ -25,6 +25,22 @@ const DEFAULT_THINKING_CONFIG = {
|
|
|
25
25
|
|
|
26
26
|
const DEFAULT_MAX_OUTPUT_TOKENS = 50_000;
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Effort levels accepted by the cross-package `effort` option.
|
|
30
|
+
* Gemini's wire field is `thinkingConfig.thinkingLevel`, which accepts
|
|
31
|
+
* minimal/low/medium/high. `xhigh` and `max` exist on ak-claude only and are
|
|
32
|
+
* clamped to `high` so the same config object works against both packages.
|
|
33
|
+
*/
|
|
34
|
+
const EFFORT_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
35
|
+
const EFFORT_TO_THINKING_LEVEL = {
|
|
36
|
+
minimal: 'minimal',
|
|
37
|
+
low: 'low',
|
|
38
|
+
medium: 'medium',
|
|
39
|
+
high: 'high',
|
|
40
|
+
xhigh: 'high',
|
|
41
|
+
max: 'high'
|
|
42
|
+
};
|
|
43
|
+
|
|
28
44
|
/** Models that support thinking features. Image / live / tts variants intentionally excluded. */
|
|
29
45
|
const THINKING_SUPPORTED_MODELS = [
|
|
30
46
|
/^gemini-3(\.\d+)?-pro(-preview)?$/,
|
|
@@ -36,33 +52,56 @@ const THINKING_SUPPORTED_MODELS = [
|
|
|
36
52
|
/^gemini-2\.0-flash$/
|
|
37
53
|
];
|
|
38
54
|
|
|
55
|
+
/** Date the pricing table below was last verified against Google's pricing page. */
|
|
56
|
+
const MODEL_PRICING_AS_OF = '2026-07-28';
|
|
57
|
+
|
|
58
|
+
/** Context-window size at which tiered models switch to their higher rate. */
|
|
59
|
+
const TIER_THRESHOLD = 200_000;
|
|
60
|
+
|
|
39
61
|
/**
|
|
40
|
-
* Model pricing per million tokens (Paid Tier Standard, base rate, as of
|
|
62
|
+
* Model pricing per million tokens (Paid Tier Standard, base rate, as of MODEL_PRICING_AS_OF).
|
|
41
63
|
* Source: https://ai.google.dev/gemini-api/docs/pricing
|
|
42
64
|
*
|
|
65
|
+
* Fields:
|
|
66
|
+
* - `input` / `output` — standard per-1M rates (≤200k tier where a model is tiered).
|
|
67
|
+
* - `cachedInput` — per-1M rate for tokens served from a context cache. Omitted when
|
|
68
|
+
* Google does not publish one; computeCost() then falls back to the full `input`
|
|
69
|
+
* rate, which OVERSTATES rather than understates cost.
|
|
70
|
+
* - `inputAbove200k` / `outputAbove200k` / `cachedInputAbove200k` — >200k context tier.
|
|
71
|
+
*
|
|
43
72
|
* NOTES:
|
|
44
|
-
* - Pro models use tiered pricing (≤200k vs >200k context). Listed rate is ≤200k base tier.
|
|
45
73
|
* - Gemma models (e.g. Gemma 4) are intentionally excluded — they are open models with
|
|
46
74
|
* no paid per-token tier on the Gemini API (Vertex deployments bill by compute).
|
|
47
75
|
* - Image-output tokens on Nano Banana models bill at $60/M (1.5 Flash Image) or $120/M (3 Pro Image).
|
|
48
76
|
* Only text-input/text-output rates are modelled here; image-output cost is NOT included in estimateCost().
|
|
49
77
|
* - Audio input is more expensive on most models — listed rate covers text/image/video input.
|
|
78
|
+
* - Cache STORAGE ($1.00/1M tokens/hour on Flash tiers, $4.50 on Pro) is time-based and
|
|
79
|
+
* NOT modelled here — estimatedCost only covers per-token charges.
|
|
80
|
+
* - A null result from resolvePricing()/computeCost() means UNKNOWN, not free.
|
|
50
81
|
*/
|
|
51
82
|
const MODEL_PRICING = {
|
|
52
83
|
// Gemini 3.x stable
|
|
53
|
-
'gemini-3.
|
|
54
|
-
'gemini-3.
|
|
84
|
+
'gemini-3.6-flash': { input: 1.50, output: 7.50, cachedInput: 0.15 },
|
|
85
|
+
'gemini-3.5-flash': { input: 1.50, output: 9.00, cachedInput: 0.15 },
|
|
86
|
+
'gemini-3.5-flash-lite': { input: 0.30, output: 2.50, cachedInput: 0.03 },
|
|
87
|
+
'gemini-3.1-flash-lite': { input: 0.25, output: 1.50, cachedInput: 0.025 },
|
|
55
88
|
// Gemini 3.x preview
|
|
56
|
-
'gemini-3.1-pro-preview': {
|
|
89
|
+
'gemini-3.1-pro-preview': {
|
|
90
|
+
input: 2.00, output: 12.00, cachedInput: 0.20,
|
|
91
|
+
inputAbove200k: 4.00, outputAbove200k: 18.00, cachedInputAbove200k: 0.40
|
|
92
|
+
},
|
|
57
93
|
'gemini-3-pro-preview': { input: 2.00, output: 12.00 }, // ≤200k tier; launch rate (superseded by 3.1, off the pricing page)
|
|
58
94
|
'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
|
|
59
|
-
'gemini-3.1-flash-lite-preview': { input: 0.25, output: 1.50 },
|
|
95
|
+
'gemini-3.1-flash-lite-preview': { input: 0.25, output: 1.50, cachedInput: 0.025 },
|
|
60
96
|
'gemini-3.1-flash-image-preview': { input: 0.50, output: 3.00 }, // text-only; image-output is $60/M
|
|
61
97
|
'gemini-3-pro-image-preview': { input: 2.00, output: 12.00 }, // text-only; image-output is $120/M
|
|
62
98
|
// Gemini 2.5 stable
|
|
63
|
-
'gemini-2.5-flash': { input: 0.30, output:
|
|
64
|
-
'gemini-2.5-flash-lite': { input: 0.10, output: 0.40 },
|
|
65
|
-
'gemini-2.5-pro': {
|
|
99
|
+
'gemini-2.5-flash': { input: 0.30, output: 3.00, cachedInput: 0.05 },
|
|
100
|
+
'gemini-2.5-flash-lite': { input: 0.10, output: 0.40, cachedInput: 0.01 },
|
|
101
|
+
'gemini-2.5-pro': {
|
|
102
|
+
input: 1.25, output: 10.00, cachedInput: 0.125,
|
|
103
|
+
inputAbove200k: 2.50, outputAbove200k: 15.00, cachedInputAbove200k: 0.25
|
|
104
|
+
},
|
|
66
105
|
'gemini-2.5-flash-image': { input: 0.30, output: 0 }, // image-output is ~$0.039/image (1290 tokens)
|
|
67
106
|
// Deprecated but kept for back-compat (shut down June 2026)
|
|
68
107
|
'gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
@@ -71,7 +110,108 @@ const MODEL_PRICING = {
|
|
|
71
110
|
'gemini-embedding-001': { input: 0.15, output: 0 }
|
|
72
111
|
};
|
|
73
112
|
|
|
74
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Alias → canonical model id map for pricing resolution.
|
|
115
|
+
* Google publishes floating `-latest` aliases that resolve server-side to a
|
|
116
|
+
* concrete model; they never appear in MODEL_PRICING directly. Map them to the
|
|
117
|
+
* canonical id whose rate they currently bill at so estimatedCost can resolve.
|
|
118
|
+
*
|
|
119
|
+
* These go stale whenever Google rotates an alias. Prefer the model id the API
|
|
120
|
+
* ECHOES (`response.modelVersion`) over the alias you requested — `_estimatedCost`
|
|
121
|
+
* already does this, and alias resolution is logged at debug level.
|
|
122
|
+
*/
|
|
123
|
+
const MODEL_ALIASES = {
|
|
124
|
+
'gemini-flash-latest': 'gemini-3.6-flash',
|
|
125
|
+
'gemini-pro-latest': 'gemini-3.1-pro-preview',
|
|
126
|
+
'gemini-flash-lite-latest': 'gemini-3.5-flash-lite'
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Resolves pricing for a model id, following `-latest` aliases.
|
|
131
|
+
* A null result means pricing is UNKNOWN, not free.
|
|
132
|
+
* @param {string|null|undefined} modelId
|
|
133
|
+
* @returns {{ input: number, output: number, cachedInput?: number, inputAbove200k?: number, outputAbove200k?: number, cachedInputAbove200k?: number, tierThreshold: number, asOf: string }|null}
|
|
134
|
+
*/
|
|
135
|
+
function resolvePricing(modelId) {
|
|
136
|
+
const entry = _lookupPricing(modelId);
|
|
137
|
+
if (!entry) return null;
|
|
138
|
+
return { ...entry, tierThreshold: TIER_THRESHOLD, asOf: MODEL_PRICING_AS_OF };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Raw table lookup with alias + pinned-build fallback. No asOf/tier decoration.
|
|
143
|
+
* @param {string|null|undefined} modelId
|
|
144
|
+
* @returns {any|null}
|
|
145
|
+
* @private
|
|
146
|
+
*/
|
|
147
|
+
function _lookupPricing(modelId) {
|
|
148
|
+
if (!modelId) return null;
|
|
149
|
+
const tryKey = (id) => {
|
|
150
|
+
if (MODEL_PRICING[id]) return MODEL_PRICING[id];
|
|
151
|
+
const alias = MODEL_ALIASES[id];
|
|
152
|
+
if (alias && MODEL_PRICING[alias]) {
|
|
153
|
+
log.debug(`Pricing: alias "${id}" resolved to "${alias}". If Google has rotated this alias the rate may be wrong — prefer the echoed modelVersion.`);
|
|
154
|
+
return MODEL_PRICING[alias];
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
};
|
|
158
|
+
let hit = tryKey(modelId);
|
|
159
|
+
if (hit) return hit;
|
|
160
|
+
// The API echoes a pinned build in modelVersion (e.g. 'gemini-2.5-flash-001',
|
|
161
|
+
// 'gemini-3-flash-preview-09-2025') even when you request the bare id. Peel
|
|
162
|
+
// trailing '-<digits>' groups and retry until a priced id matches.
|
|
163
|
+
let stripped = modelId;
|
|
164
|
+
while (true) {
|
|
165
|
+
const next = stripped.replace(/-\d{2,}$/, '');
|
|
166
|
+
if (next === stripped) break;
|
|
167
|
+
stripped = next;
|
|
168
|
+
hit = tryKey(stripped);
|
|
169
|
+
if (hit) return hit;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Computes estimated USD cost from token counts using MODEL_PRICING.
|
|
176
|
+
* Thinking ("thoughts") tokens are billed at the output rate.
|
|
177
|
+
*
|
|
178
|
+
* IMPORTANT: Gemini's `promptTokenCount` INCLUDES `cachedContentTokenCount`, so
|
|
179
|
+
* cached tokens are SUBTRACTED from promptTokens and re-billed at the discounted
|
|
180
|
+
* cached rate. (ak-claude is the opposite — Anthropic's `input_tokens` excludes
|
|
181
|
+
* cache tokens and they are added on top. Do not "unify" these.)
|
|
182
|
+
*
|
|
183
|
+
* When a model has no published `cachedInput` rate, cached tokens bill at the full
|
|
184
|
+
* input rate — an overestimate, never an underestimate.
|
|
185
|
+
*
|
|
186
|
+
* Cache STORAGE (per token-hour) is not modelled.
|
|
187
|
+
*
|
|
188
|
+
* @param {string|null|undefined} modelId
|
|
189
|
+
* @param {number} promptTokens - Total prompt tokens as reported by the API (INCLUDES cached tokens)
|
|
190
|
+
* @param {number} responseTokens
|
|
191
|
+
* @param {number} [thoughtsTokens=0] - Thinking tokens (billed at output rate)
|
|
192
|
+
* @param {number} [cachedTokens=0] - Tokens served from a context cache (subset of promptTokens)
|
|
193
|
+
* @returns {number|null} Cost in USD, or null when pricing is UNKNOWN (not free).
|
|
194
|
+
*/
|
|
195
|
+
function computeCost(modelId, promptTokens, responseTokens, thoughtsTokens = 0, cachedTokens = 0) {
|
|
196
|
+
const pricing = resolvePricing(modelId);
|
|
197
|
+
if (!pricing) return null;
|
|
198
|
+
|
|
199
|
+
const above = (promptTokens || 0) > TIER_THRESHOLD;
|
|
200
|
+
const inputRate = (above && pricing.inputAbove200k) || pricing.input;
|
|
201
|
+
const outputRate = (above && pricing.outputAbove200k) || pricing.output;
|
|
202
|
+
const cachedRate = above
|
|
203
|
+
? (pricing.cachedInputAbove200k ?? pricing.cachedInput ?? inputRate)
|
|
204
|
+
: (pricing.cachedInput ?? inputRate);
|
|
205
|
+
|
|
206
|
+
const cached = Math.max(0, Math.min(cachedTokens || 0, promptTokens || 0));
|
|
207
|
+
const billablePrompt = Math.max(0, (promptTokens || 0) - cached);
|
|
208
|
+
|
|
209
|
+
return (billablePrompt / 1_000_000) * inputRate
|
|
210
|
+
+ (cached / 1_000_000) * cachedRate
|
|
211
|
+
+ (((responseTokens || 0) + (thoughtsTokens || 0)) / 1_000_000) * outputRate;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { DEFAULT_SAFETY_SETTINGS, DEFAULT_THINKING_CONFIG, THINKING_SUPPORTED_MODELS, MODEL_PRICING, MODEL_PRICING_AS_OF, MODEL_ALIASES, DEFAULT_MAX_OUTPUT_TOKENS, EFFORT_LEVELS, resolvePricing, computeCost };
|
|
75
215
|
|
|
76
216
|
// ── BaseGemini Class ─────────────────────────────────────────────────────────
|
|
77
217
|
|
|
@@ -193,6 +333,9 @@ class BaseGemini {
|
|
|
193
333
|
}
|
|
194
334
|
|
|
195
335
|
// ── Thinking Config ──
|
|
336
|
+
// `effort` is the cross-package option (same name in ak-claude); it maps to
|
|
337
|
+
// Gemini's `thinkingConfig.thinkingLevel`.
|
|
338
|
+
this.effort = this._normalizeEffort(options.effort);
|
|
196
339
|
this._configureThinking(options.thinkingConfig);
|
|
197
340
|
|
|
198
341
|
// ── GenAI Client ──
|
|
@@ -214,6 +357,8 @@ class BaseGemini {
|
|
|
214
357
|
this._cumulativeUsage = {
|
|
215
358
|
promptTokens: 0,
|
|
216
359
|
responseTokens: 0,
|
|
360
|
+
thoughtsTokens: 0,
|
|
361
|
+
cachedTokens: 0,
|
|
217
362
|
totalTokens: 0,
|
|
218
363
|
attempts: 0
|
|
219
364
|
};
|
|
@@ -237,18 +382,28 @@ class BaseGemini {
|
|
|
237
382
|
const chatOptions = this._getChatCreateOptions();
|
|
238
383
|
this.chatSession = this.genAIClient.chats.create(chatOptions);
|
|
239
384
|
|
|
240
|
-
|
|
241
|
-
try {
|
|
242
|
-
await this._withRetry(() => this.genAIClient.models.list());
|
|
243
|
-
log.debug(`${this.constructor.name}: API connection successful.`);
|
|
244
|
-
} catch (e) {
|
|
245
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
385
|
+
await this._healthCheckPing();
|
|
248
386
|
|
|
249
387
|
log.debug(`${this.constructor.name}: Chat session initialized.`);
|
|
250
388
|
}
|
|
251
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Opt-in connectivity check via `models.list()` — runs only when
|
|
392
|
+
* `healthCheck: true`. Fails fast (no exponential backoff): a readiness probe
|
|
393
|
+
* should surface a bad config immediately, not after ~30s of 429 retries.
|
|
394
|
+
* @returns {Promise<void>}
|
|
395
|
+
* @protected
|
|
396
|
+
*/
|
|
397
|
+
async _healthCheckPing() {
|
|
398
|
+
if (!this.healthCheck) return;
|
|
399
|
+
try {
|
|
400
|
+
await this.genAIClient.models.list();
|
|
401
|
+
log.debug(`${this.constructor.name}: API connection successful.`);
|
|
402
|
+
} catch (e) {
|
|
403
|
+
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
252
407
|
/**
|
|
253
408
|
* Builds the options object for `genAIClient.chats.create()`.
|
|
254
409
|
* Override in subclasses to add tools, grounding, etc.
|
|
@@ -316,7 +471,7 @@ class BaseGemini {
|
|
|
316
471
|
}
|
|
317
472
|
this.chatSession = this._createChatSession([]);
|
|
318
473
|
this.lastResponseMetadata = null;
|
|
319
|
-
this._cumulativeUsage = { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 0 };
|
|
474
|
+
this._cumulativeUsage = { promptTokens: 0, responseTokens: 0, thoughtsTokens: 0, cachedTokens: 0, totalTokens: 0, attempts: 0 };
|
|
320
475
|
log.debug(`${this.constructor.name}: Conversation history cleared.`);
|
|
321
476
|
}
|
|
322
477
|
|
|
@@ -416,12 +571,20 @@ class BaseGemini {
|
|
|
416
571
|
*/
|
|
417
572
|
_captureMetadata(response) {
|
|
418
573
|
const modelStatus = response?.modelStatus || null;
|
|
574
|
+
const promptTokens = response.usageMetadata?.promptTokenCount || 0;
|
|
575
|
+
const responseTokens = response.usageMetadata?.candidatesTokenCount || 0;
|
|
576
|
+
const thoughtsTokens = response.usageMetadata?.thoughtsTokenCount || 0;
|
|
577
|
+
// NB: promptTokenCount INCLUDES cachedContentTokenCount — see computeCost().
|
|
578
|
+
const cachedTokens = response.usageMetadata?.cachedContentTokenCount || 0;
|
|
419
579
|
this.lastResponseMetadata = {
|
|
420
580
|
modelVersion: response.modelVersion || null,
|
|
421
581
|
requestedModel: this.modelName,
|
|
422
|
-
promptTokens
|
|
423
|
-
responseTokens
|
|
424
|
-
|
|
582
|
+
promptTokens,
|
|
583
|
+
responseTokens,
|
|
584
|
+
thoughtsTokens,
|
|
585
|
+
cachedTokens,
|
|
586
|
+
// totalTokenCount includes thoughts; fall back to summing the parts.
|
|
587
|
+
totalTokens: response.usageMetadata?.totalTokenCount || (promptTokens + responseTokens + thoughtsTokens),
|
|
425
588
|
timestamp: Date.now(),
|
|
426
589
|
groundingMetadata: response.candidates?.[0]?.groundingMetadata || null,
|
|
427
590
|
modelStatus
|
|
@@ -441,19 +604,82 @@ class BaseGemini {
|
|
|
441
604
|
if (!this.lastResponseMetadata) return null;
|
|
442
605
|
|
|
443
606
|
const meta = this.lastResponseMetadata;
|
|
444
|
-
const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 1 };
|
|
607
|
+
const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, thoughtsTokens: 0, cachedTokens: 0, attempts: 1 };
|
|
445
608
|
const useCumulative = cumulative.attempts > 0;
|
|
446
609
|
|
|
610
|
+
const promptTokens = useCumulative ? cumulative.promptTokens : meta.promptTokens;
|
|
611
|
+
const responseTokens = useCumulative ? cumulative.responseTokens : meta.responseTokens;
|
|
612
|
+
const thoughtsTokens = useCumulative ? (cumulative.thoughtsTokens || 0) : (meta.thoughtsTokens || 0);
|
|
613
|
+
const totalTokens = useCumulative ? cumulative.totalTokens : meta.totalTokens;
|
|
614
|
+
// Cached tokens accumulate across retries when tracked; otherwise fall back
|
|
615
|
+
// to the last response's value (single-call classes).
|
|
616
|
+
const cachedTokens = useCumulative && cumulative.cachedTokens !== undefined
|
|
617
|
+
? cumulative.cachedTokens
|
|
618
|
+
: (meta.cachedTokens || 0);
|
|
619
|
+
|
|
447
620
|
return {
|
|
448
|
-
promptTokens
|
|
449
|
-
responseTokens
|
|
450
|
-
|
|
621
|
+
promptTokens,
|
|
622
|
+
responseTokens,
|
|
623
|
+
thoughtsTokens,
|
|
624
|
+
cachedTokens,
|
|
625
|
+
totalTokens,
|
|
451
626
|
attempts: useCumulative ? cumulative.attempts : 1,
|
|
452
627
|
modelVersion: meta.modelVersion,
|
|
453
628
|
requestedModel: meta.requestedModel,
|
|
454
629
|
timestamp: meta.timestamp,
|
|
455
630
|
groundingMetadata: meta.groundingMetadata || null,
|
|
456
|
-
modelStatus: meta.modelStatus || null
|
|
631
|
+
modelStatus: meta.modelStatus || null,
|
|
632
|
+
estimatedCost: this._estimatedCost(meta.modelVersion, promptTokens, responseTokens, thoughtsTokens, cachedTokens)
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Estimated USD cost, preferring the model id the API echoed (`modelVersion`)
|
|
638
|
+
* and falling back to the requested model when that build isn't priced.
|
|
639
|
+
* @param {string|null|undefined} modelVersion
|
|
640
|
+
* @param {number} promptTokens
|
|
641
|
+
* @param {number} responseTokens
|
|
642
|
+
* @param {number} [thoughtsTokens=0]
|
|
643
|
+
* @param {number} [cachedTokens=0] - Cached-content tokens (a SUBSET of promptTokens)
|
|
644
|
+
* @returns {number|null}
|
|
645
|
+
* @protected
|
|
646
|
+
*/
|
|
647
|
+
_estimatedCost(modelVersion, promptTokens, responseTokens, thoughtsTokens = 0, cachedTokens = 0) {
|
|
648
|
+
return computeCost(modelVersion, promptTokens, responseTokens, thoughtsTokens, cachedTokens)
|
|
649
|
+
?? computeCost(this.modelName, promptTokens, responseTokens, thoughtsTokens, cachedTokens);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Builds a usage object directly from a single API response, WITHOUT reading
|
|
654
|
+
* mutable instance state (`lastResponseMetadata`/`_cumulativeUsage`). Safe to
|
|
655
|
+
* call under concurrent send() calls on a shared instance — unlike
|
|
656
|
+
* getLastUsage(), which reflects whichever call most recently mutated the
|
|
657
|
+
* instance and can cross-talk between concurrent sends.
|
|
658
|
+
* @param {Object} response - A single generateContent() response
|
|
659
|
+
* @param {number} [attempts=1] - Attempts this call consumed
|
|
660
|
+
* @returns {UsageData}
|
|
661
|
+
* @protected
|
|
662
|
+
*/
|
|
663
|
+
_usageFromResponse(response, attempts = 1) {
|
|
664
|
+
const promptTokens = response?.usageMetadata?.promptTokenCount || 0;
|
|
665
|
+
const responseTokens = response?.usageMetadata?.candidatesTokenCount || 0;
|
|
666
|
+
const thoughtsTokens = response?.usageMetadata?.thoughtsTokenCount || 0;
|
|
667
|
+
const cachedTokens = response?.usageMetadata?.cachedContentTokenCount || 0;
|
|
668
|
+
const totalTokens = response?.usageMetadata?.totalTokenCount || (promptTokens + responseTokens + thoughtsTokens);
|
|
669
|
+
const modelVersion = response?.modelVersion || null;
|
|
670
|
+
return {
|
|
671
|
+
promptTokens,
|
|
672
|
+
responseTokens,
|
|
673
|
+
thoughtsTokens,
|
|
674
|
+
cachedTokens,
|
|
675
|
+
totalTokens,
|
|
676
|
+
attempts,
|
|
677
|
+
modelVersion,
|
|
678
|
+
requestedModel: this.modelName,
|
|
679
|
+
timestamp: Date.now(),
|
|
680
|
+
groundingMetadata: response?.candidates?.[0]?.groundingMetadata || null,
|
|
681
|
+
modelStatus: response?.modelStatus || null,
|
|
682
|
+
estimatedCost: this._estimatedCost(modelVersion, promptTokens, responseTokens, thoughtsTokens, cachedTokens)
|
|
457
683
|
};
|
|
458
684
|
}
|
|
459
685
|
|
|
@@ -500,14 +726,16 @@ class BaseGemini {
|
|
|
500
726
|
*/
|
|
501
727
|
async estimateCost(nextPayload) {
|
|
502
728
|
const tokenInfo = await this.estimate(nextPayload);
|
|
503
|
-
const pricing =
|
|
729
|
+
const pricing = resolvePricing(this.modelName);
|
|
504
730
|
|
|
505
731
|
return {
|
|
506
732
|
inputTokens: tokenInfo.inputTokens,
|
|
507
733
|
model: this.modelName,
|
|
508
734
|
pricing: pricing,
|
|
509
|
-
estimatedInputCost: (tokenInfo.inputTokens / 1_000_000) * pricing.input,
|
|
510
|
-
note:
|
|
735
|
+
estimatedInputCost: pricing ? (tokenInfo.inputTokens / 1_000_000) * pricing.input : null,
|
|
736
|
+
note: pricing
|
|
737
|
+
? 'Cost is for input tokens only; output cost depends on response length'
|
|
738
|
+
: `No pricing known for model "${this.modelName}"; estimatedInputCost is null`
|
|
511
739
|
};
|
|
512
740
|
}
|
|
513
741
|
|
|
@@ -690,7 +918,7 @@ class BaseGemini {
|
|
|
690
918
|
_configureThinking(thinkingConfig) {
|
|
691
919
|
const modelSupportsThinking = THINKING_SUPPORTED_MODELS.some(p => p.test(this.modelName));
|
|
692
920
|
|
|
693
|
-
if (thinkingConfig === undefined) return;
|
|
921
|
+
if (thinkingConfig === undefined && this.effort === null) return;
|
|
694
922
|
|
|
695
923
|
if (thinkingConfig === null) {
|
|
696
924
|
delete this.chatConfig.thinkingConfig;
|
|
@@ -699,17 +927,47 @@ class BaseGemini {
|
|
|
699
927
|
}
|
|
700
928
|
|
|
701
929
|
if (!modelSupportsThinking) {
|
|
702
|
-
log.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
|
|
930
|
+
log.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig/effort.`);
|
|
703
931
|
return;
|
|
704
932
|
}
|
|
705
933
|
|
|
706
|
-
const config = { ...DEFAULT_THINKING_CONFIG, ...thinkingConfig };
|
|
707
|
-
|
|
934
|
+
const config = { ...DEFAULT_THINKING_CONFIG, ...(thinkingConfig || {}) };
|
|
935
|
+
|
|
936
|
+
// `effort` wins over an explicit thinkingLevel/thinkingBudget — it is the
|
|
937
|
+
// cross-package knob and is set deliberately.
|
|
938
|
+
if (this.effort !== null) {
|
|
939
|
+
const level = EFFORT_TO_THINKING_LEVEL[this.effort];
|
|
940
|
+
if (level !== this.effort) {
|
|
941
|
+
log.debug(`Effort '${this.effort}' has no Gemini equivalent; using thinkingLevel '${level}'.`);
|
|
942
|
+
}
|
|
943
|
+
config.thinkingLevel = level;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// thinkingLevel and thinkingBudget are mutually exclusive on the wire.
|
|
947
|
+
if (config.thinkingLevel !== undefined) {
|
|
708
948
|
delete config.thinkingBudget;
|
|
709
949
|
}
|
|
950
|
+
|
|
710
951
|
this.chatConfig.thinkingConfig = config;
|
|
711
952
|
log.debug(`Thinking config applied: ${JSON.stringify(config)}`);
|
|
712
953
|
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Validates the cross-package `effort` option.
|
|
957
|
+
* Throws rather than warns: this runs in the constructor before log levels are
|
|
958
|
+
* finalized, so a warning could be swallowed.
|
|
959
|
+
* @param {string|null|undefined} effort
|
|
960
|
+
* @returns {'minimal'|'low'|'medium'|'high'|'xhigh'|'max'|null}
|
|
961
|
+
* @protected
|
|
962
|
+
*/
|
|
963
|
+
_normalizeEffort(effort) {
|
|
964
|
+
if (effort === undefined || effort === null) return null;
|
|
965
|
+
const level = String(effort).toLowerCase();
|
|
966
|
+
if (!EFFORT_LEVELS.includes(level)) {
|
|
967
|
+
throw new Error(`Invalid effort "${effort}". Expected one of: ${EFFORT_LEVELS.join(', ')}.`);
|
|
968
|
+
}
|
|
969
|
+
return /** @type {any} */ (level);
|
|
970
|
+
}
|
|
713
971
|
}
|
|
714
972
|
|
|
715
973
|
export default BaseGemini;
|
package/chat.js
CHANGED
|
@@ -87,6 +87,8 @@ class Chat extends BaseGemini {
|
|
|
87
87
|
this._cumulativeUsage = {
|
|
88
88
|
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
89
89
|
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
90
|
+
thoughtsTokens: this.lastResponseMetadata.thoughtsTokens,
|
|
91
|
+
cachedTokens: this.lastResponseMetadata.cachedTokens,
|
|
90
92
|
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
91
93
|
attempts: 1
|
|
92
94
|
};
|
package/code-agent.js
CHANGED
|
@@ -922,6 +922,8 @@ ${lang.codeRules}
|
|
|
922
922
|
this._cumulativeUsage = {
|
|
923
923
|
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
924
924
|
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
925
|
+
thoughtsTokens: this.lastResponseMetadata.thoughtsTokens,
|
|
926
|
+
cachedTokens: this.lastResponseMetadata.cachedTokens,
|
|
925
927
|
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
926
928
|
attempts: 1
|
|
927
929
|
};
|
package/embedding.js
CHANGED
|
@@ -54,12 +54,10 @@ export default class Embedding extends BaseGemini {
|
|
|
54
54
|
|
|
55
55
|
log.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
62
|
-
}
|
|
57
|
+
// Connectivity check is opt-in (healthCheck: true) — avoids an extra
|
|
58
|
+
// round-trip and the models.list() IAM surface. First embed() is a fine
|
|
59
|
+
// error signal on its own.
|
|
60
|
+
await this._healthCheckPing();
|
|
63
61
|
|
|
64
62
|
this._initialized = true;
|
|
65
63
|
log.debug(`${this.constructor.name}: Initialized (stateless mode).`);
|
package/image-generator.js
CHANGED
|
@@ -52,12 +52,10 @@ export default class ImageGenerator extends BaseGemini {
|
|
|
52
52
|
|
|
53
53
|
log.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
60
|
-
}
|
|
55
|
+
// Connectivity check is opt-in (healthCheck: true) — avoids an extra
|
|
56
|
+
// round-trip and the models.list() IAM surface. First generate() is a fine
|
|
57
|
+
// error signal on its own.
|
|
58
|
+
await this._healthCheckPing();
|
|
61
59
|
|
|
62
60
|
this._initialized = true;
|
|
63
61
|
}
|
|
@@ -108,10 +106,15 @@ export default class ImageGenerator extends BaseGemini {
|
|
|
108
106
|
config: this._buildConfig(opts)
|
|
109
107
|
}));
|
|
110
108
|
|
|
109
|
+
// Per-call usage computed synchronously from THIS response (concurrency-safe).
|
|
110
|
+
const usage = this._usageFromResponse(result);
|
|
111
|
+
|
|
111
112
|
this._captureMetadata(result);
|
|
112
113
|
this._cumulativeUsage = {
|
|
113
114
|
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
114
115
|
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
116
|
+
thoughtsTokens: this.lastResponseMetadata.thoughtsTokens,
|
|
117
|
+
cachedTokens: this.lastResponseMetadata.cachedTokens,
|
|
115
118
|
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
116
119
|
attempts: 1
|
|
117
120
|
};
|
|
@@ -134,7 +137,7 @@ export default class ImageGenerator extends BaseGemini {
|
|
|
134
137
|
log.warn('ImageGenerator: no images returned. Check prompt or safety filters.');
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
return { images, text: text || null, usage
|
|
140
|
+
return { images, text: text || null, usage };
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
/**
|