ak-gemini 2.3.0 → 2.5.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 +46 -0
- package/GUIDE.md +11 -1
- package/README.md +7 -0
- package/base.js +171 -24
- package/chat.js +13 -0
- package/embedding.js +4 -6
- package/image-generator.js +9 -7
- package/index.cjs +240 -45
- package/index.js +2 -1
- package/json-helpers.js +105 -0
- package/message.js +19 -7
- package/package.json +4 -3
- package/transformer.js +2 -1
- package/types.d.ts +26 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.5.0
|
|
4
|
+
|
|
5
|
+
Fixes from a downstream consumer's adversarial review. This is a **minor** bump —
|
|
6
|
+
it includes small but real behavior changes (see "Behavior changes" below).
|
|
7
|
+
|
|
8
|
+
### Behavior changes (read before upgrading)
|
|
9
|
+
- **`Message` structured output:** passing `responseSchema` without
|
|
10
|
+
`responseMimeType` now auto-defaults `responseMimeType: 'application/json'`
|
|
11
|
+
(the SDK requires it and does not add it). Previously such a call returned a
|
|
12
|
+
400 or unparseable prose. Explicit `responseMimeType` is still honored.
|
|
13
|
+
- **`init()` no longer validates connectivity by default.** `Message`,
|
|
14
|
+
`Embedding`, and `ImageGenerator` previously called `models.list()`
|
|
15
|
+
unconditionally during `init()`. This is now gated behind `healthCheck: true`
|
|
16
|
+
(default `false`), matching `BaseGemini`. If you relied on `init()` as a
|
|
17
|
+
startup credentials gate, pass `healthCheck: true`. `models.list()` is a
|
|
18
|
+
different IAM surface than `generateContent` and added a per-instance
|
|
19
|
+
round-trip.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **`usage.estimatedCost`** — every `send()`/`generate()` result and
|
|
23
|
+
`getLastUsage()` now include an estimated USD cost from `MODEL_PRICING`
|
|
24
|
+
(`null` when the model is unpriced). Thinking ("thoughts") tokens are billed at
|
|
25
|
+
the output rate and are now included in cost and exposed as
|
|
26
|
+
`usage.thoughtsTokens`.
|
|
27
|
+
- **Concurrency-safe per-call usage.** `Message.send()` and
|
|
28
|
+
`ImageGenerator.generate()` return `result.usage` computed synchronously from
|
|
29
|
+
that call's own response. `getLastUsage()` still reflects the instance's last
|
|
30
|
+
call and is unsafe to read across concurrent sends on a shared instance — use
|
|
31
|
+
`result.usage`.
|
|
32
|
+
- **Pricing helpers exported:** `MODEL_PRICING`, `MODEL_ALIASES`,
|
|
33
|
+
`resolvePricing()`, `computeCost()`. `resolvePricing` follows `-latest`
|
|
34
|
+
aliases and peels version-build suffixes (e.g. `gemini-2.5-flash-001` →
|
|
35
|
+
`gemini-2.5-flash`).
|
|
36
|
+
- **`validateSchema()`** JSON-Schema validator exported (for cross-package
|
|
37
|
+
symmetry; Gemini enforces schemas natively).
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- `estimateCost()` now uses `resolvePricing()` and returns `null` cost fields for
|
|
41
|
+
unknown models (was `0`), consistent with `usage.estimatedCost`.
|
|
42
|
+
- `estimatedCost` no longer returns `null` for a priced model when the API echoes
|
|
43
|
+
a version-suffixed build id in `modelVersion`.
|
|
44
|
+
|
|
45
|
+
### Dependencies
|
|
46
|
+
- `@google/genai` `^2.10.0` → `^2.12.0` (no breaking changes to the used surface).
|
package/GUIDE.md
CHANGED
|
@@ -147,6 +147,8 @@ console.log(result.data);
|
|
|
147
147
|
|
|
148
148
|
Key difference from `Chat`: `result.data` contains the parsed JSON object. `result.text` contains the raw string.
|
|
149
149
|
|
|
150
|
+
> **Note:** `responseSchema` requires `responseMimeType: 'application/json'`. If you pass `responseSchema` without a `responseMimeType`, ak-gemini now auto-defaults it to `'application/json'` (logged at debug). Passing an explicit `responseMimeType` is still honored.
|
|
151
|
+
|
|
150
152
|
### When to Use Message
|
|
151
153
|
|
|
152
154
|
- Classification, tagging, or labeling
|
|
@@ -1037,10 +1039,18 @@ const usage = instance.getLastUsage();
|
|
|
1037
1039
|
// attempts: 1, // 1 = first try, 2+ = retries needed
|
|
1038
1040
|
// modelVersion: 'gemini-2.5-flash-001', // actual model that responded
|
|
1039
1041
|
// requestedModel: 'gemini-2.5-flash', // model you requested
|
|
1040
|
-
// timestamp: 1710000000000
|
|
1042
|
+
// timestamp: 1710000000000,
|
|
1043
|
+
// thoughtsTokens: 0, // thinking tokens, billed at output rate (in totalTokens + cost)
|
|
1044
|
+
// estimatedCost: 0.00123 // USD from MODEL_PRICING; null if model unpriced
|
|
1041
1045
|
// }
|
|
1042
1046
|
```
|
|
1043
1047
|
|
|
1048
|
+
> **Thinking tokens:** for thinking-enabled models, `thoughtsTokens` are billed at the output rate and are included in both `totalTokens` and `estimatedCost`. `responseTokens` is candidate (visible) output only.
|
|
1049
|
+
|
|
1050
|
+
> **Concurrency:** `getLastUsage()` reflects the **instance's last completed call** and mutates on every `send()` — it is **not** safe to read across concurrent sends on a shared instance. The **stateless** classes (`Message`, `ImageGenerator`) return a per-call **`result.usage`** computed synchronously from that call's own response, which *is* concurrency-safe — use it when sharing one instance across concurrent calls. The stateful classes (`Chat`, `Transformer`, `RagAgent`, `ToolAgent`, `CodeAgent`) maintain history/rounds and are not designed to be shared across concurrent calls; their `result.usage` is derived from instance state.
|
|
1051
|
+
|
|
1052
|
+
> **`estimatedCost` / `MODEL_ALIASES` caveat:** `estimatedCost` resolves `-latest` aliases and version-suffixed builds via `MODEL_PRICING`/`MODEL_ALIASES`. Alias→price mappings are pinned as of 2026-07 (e.g. `gemini-flash-latest` → `gemini-3.5-flash`); when Google rotates what a `-latest` alias points to, `estimatedCost` for that alias may be wrong until the table is updated. Pin explicit model ids for billing-grade accuracy.
|
|
1053
|
+
|
|
1044
1054
|
### Cost Estimation
|
|
1045
1055
|
|
|
1046
1056
|
Estimate cost *before* sending:
|
package/README.md
CHANGED
|
@@ -86,8 +86,15 @@ const msg = new Message({
|
|
|
86
86
|
|
|
87
87
|
const result = await msg.send('Alice works at Acme in New York.');
|
|
88
88
|
// result.data → { entities: ['Alice', 'Acme', 'New York'] }
|
|
89
|
+
// result.usage → { promptTokens, responseTokens, thoughtsTokens, totalTokens, estimatedCost, ... }
|
|
89
90
|
```
|
|
90
91
|
|
|
92
|
+
> **`responseSchema` requires `responseMimeType: 'application/json'`.** As of 2.5.0, passing `responseSchema` without a `responseMimeType` auto-defaults it to `'application/json'` — you can omit the line above. An explicit `responseMimeType` is still honored.
|
|
93
|
+
|
|
94
|
+
> **Connectivity check is opt-in.** `Message`, `Embedding`, and `ImageGenerator` do **not** ping the API during `init()` unless you pass `healthCheck: true` (default `false`, as of 2.5.0). The first `send()`/`embed()`/`generate()` surfaces auth/connectivity errors on its own.
|
|
95
|
+
|
|
96
|
+
> **Per-call usage under concurrency.** Read `result.usage` (computed from that call's own response) rather than `getLastUsage()` when sharing a `Message`/`ImageGenerator` instance across concurrent `send()`s — `getLastUsage()` reflects only the instance's last completed call.
|
|
97
|
+
|
|
91
98
|
### ToolAgent — Agent with User-Provided Tools
|
|
92
99
|
|
|
93
100
|
Provide tool declarations and an executor function. The agent manages the tool-use loop automatically.
|
package/base.js
CHANGED
|
@@ -37,11 +37,13 @@ const THINKING_SUPPORTED_MODELS = [
|
|
|
37
37
|
];
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Model pricing per million tokens (Paid Tier Standard, base rate, as of
|
|
40
|
+
* Model pricing per million tokens (Paid Tier Standard, base rate, as of July 2026).
|
|
41
41
|
* Source: https://ai.google.dev/gemini-api/docs/pricing
|
|
42
42
|
*
|
|
43
43
|
* NOTES:
|
|
44
44
|
* - Pro models use tiered pricing (≤200k vs >200k context). Listed rate is ≤200k base tier.
|
|
45
|
+
* - Gemma models (e.g. Gemma 4) are intentionally excluded — they are open models with
|
|
46
|
+
* no paid per-token tier on the Gemini API (Vertex deployments bill by compute).
|
|
45
47
|
* - Image-output tokens on Nano Banana models bill at $60/M (1.5 Flash Image) or $120/M (3 Pro Image).
|
|
46
48
|
* Only text-input/text-output rates are modelled here; image-output cost is NOT included in estimateCost().
|
|
47
49
|
* - Audio input is more expensive on most models — listed rate covers text/image/video input.
|
|
@@ -52,6 +54,7 @@ const MODEL_PRICING = {
|
|
|
52
54
|
'gemini-3.1-flash-lite': { input: 0.25, output: 1.50 },
|
|
53
55
|
// Gemini 3.x preview
|
|
54
56
|
'gemini-3.1-pro-preview': { input: 2.00, output: 12.00 }, // ≤200k tier
|
|
57
|
+
'gemini-3-pro-preview': { input: 2.00, output: 12.00 }, // ≤200k tier; launch rate (superseded by 3.1, off the pricing page)
|
|
55
58
|
'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
|
|
56
59
|
'gemini-3.1-flash-lite-preview': { input: 0.25, output: 1.50 },
|
|
57
60
|
'gemini-3.1-flash-image-preview': { input: 0.50, output: 3.00 }, // text-only; image-output is $60/M
|
|
@@ -68,7 +71,65 @@ const MODEL_PRICING = {
|
|
|
68
71
|
'gemini-embedding-001': { input: 0.15, output: 0 }
|
|
69
72
|
};
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Alias → canonical model id map for pricing resolution.
|
|
76
|
+
* Google publishes floating `-latest` aliases that resolve server-side to a
|
|
77
|
+
* concrete model; they never appear in MODEL_PRICING directly. Map them to the
|
|
78
|
+
* canonical id whose rate they currently bill at so estimatedCost can resolve.
|
|
79
|
+
* Revisit when Google rotates what an alias points to.
|
|
80
|
+
*/
|
|
81
|
+
const MODEL_ALIASES = {
|
|
82
|
+
'gemini-flash-latest': 'gemini-3.5-flash',
|
|
83
|
+
'gemini-pro-latest': 'gemini-3.1-pro-preview',
|
|
84
|
+
'gemini-flash-lite-latest': 'gemini-3.1-flash-lite'
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolves pricing for a model id, following `-latest` aliases.
|
|
89
|
+
* @param {string|null|undefined} modelId
|
|
90
|
+
* @returns {{ input: number, output: number }|null} Pricing, or null if unknown.
|
|
91
|
+
*/
|
|
92
|
+
function resolvePricing(modelId) {
|
|
93
|
+
if (!modelId) return null;
|
|
94
|
+
const tryKey = (id) => {
|
|
95
|
+
if (MODEL_PRICING[id]) return MODEL_PRICING[id];
|
|
96
|
+
const alias = MODEL_ALIASES[id];
|
|
97
|
+
if (alias && MODEL_PRICING[alias]) return MODEL_PRICING[alias];
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
let hit = tryKey(modelId);
|
|
101
|
+
if (hit) return hit;
|
|
102
|
+
// The API echoes a pinned build in modelVersion (e.g. 'gemini-2.5-flash-001',
|
|
103
|
+
// 'gemini-3-flash-preview-09-2025') even when you request the bare id. Peel
|
|
104
|
+
// trailing '-<digits>' groups and retry until a priced id matches.
|
|
105
|
+
let stripped = modelId;
|
|
106
|
+
while (true) {
|
|
107
|
+
const next = stripped.replace(/-\d{2,}$/, '');
|
|
108
|
+
if (next === stripped) break;
|
|
109
|
+
stripped = next;
|
|
110
|
+
hit = tryKey(stripped);
|
|
111
|
+
if (hit) return hit;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Computes estimated USD cost from token counts using MODEL_PRICING.
|
|
118
|
+
* Thinking ("thoughts") tokens are billed at the output rate.
|
|
119
|
+
* @param {string|null|undefined} modelId
|
|
120
|
+
* @param {number} promptTokens
|
|
121
|
+
* @param {number} responseTokens
|
|
122
|
+
* @param {number} [thoughtsTokens=0] - Thinking tokens (billed at output rate)
|
|
123
|
+
* @returns {number|null} Cost in USD, or null when pricing is unknown.
|
|
124
|
+
*/
|
|
125
|
+
function computeCost(modelId, promptTokens, responseTokens, thoughtsTokens = 0) {
|
|
126
|
+
const pricing = resolvePricing(modelId);
|
|
127
|
+
if (!pricing) return null;
|
|
128
|
+
return (promptTokens / 1_000_000) * pricing.input
|
|
129
|
+
+ ((responseTokens + thoughtsTokens) / 1_000_000) * pricing.output;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { DEFAULT_SAFETY_SETTINGS, DEFAULT_THINKING_CONFIG, THINKING_SUPPORTED_MODELS, MODEL_PRICING, MODEL_ALIASES, DEFAULT_MAX_OUTPUT_TOKENS, resolvePricing, computeCost };
|
|
72
133
|
|
|
73
134
|
// ── BaseGemini Class ─────────────────────────────────────────────────────────
|
|
74
135
|
|
|
@@ -155,6 +216,13 @@ class BaseGemini {
|
|
|
155
216
|
if (this.serviceTier) this.chatConfig['serviceTier'] = this.serviceTier;
|
|
156
217
|
if (this.includeServerSideToolInvocations) this.chatConfig['includeServerSideToolInvocations'] = true;
|
|
157
218
|
|
|
219
|
+
// ── Sampling Shortcuts ──
|
|
220
|
+
// Top-level sugar for chatConfig sampling knobs; wins over chatConfig
|
|
221
|
+
// (same precedence as the maxOutputTokens promotion below)
|
|
222
|
+
for (const key of ['temperature', 'topP', 'topK']) {
|
|
223
|
+
if (options[key] !== undefined) this.chatConfig[key] = options[key];
|
|
224
|
+
}
|
|
225
|
+
|
|
158
226
|
// Apply systemPrompt to chatConfig
|
|
159
227
|
if (this.systemPrompt) {
|
|
160
228
|
this.chatConfig.systemInstruction = this.systemPrompt;
|
|
@@ -227,18 +295,28 @@ class BaseGemini {
|
|
|
227
295
|
const chatOptions = this._getChatCreateOptions();
|
|
228
296
|
this.chatSession = this.genAIClient.chats.create(chatOptions);
|
|
229
297
|
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
await this._withRetry(() => this.genAIClient.models.list());
|
|
233
|
-
log.debug(`${this.constructor.name}: API connection successful.`);
|
|
234
|
-
} catch (e) {
|
|
235
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
298
|
+
await this._healthCheckPing();
|
|
238
299
|
|
|
239
300
|
log.debug(`${this.constructor.name}: Chat session initialized.`);
|
|
240
301
|
}
|
|
241
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Opt-in connectivity check via `models.list()` — runs only when
|
|
305
|
+
* `healthCheck: true`. Fails fast (no exponential backoff): a readiness probe
|
|
306
|
+
* should surface a bad config immediately, not after ~30s of 429 retries.
|
|
307
|
+
* @returns {Promise<void>}
|
|
308
|
+
* @protected
|
|
309
|
+
*/
|
|
310
|
+
async _healthCheckPing() {
|
|
311
|
+
if (!this.healthCheck) return;
|
|
312
|
+
try {
|
|
313
|
+
await this.genAIClient.models.list();
|
|
314
|
+
log.debug(`${this.constructor.name}: API connection successful.`);
|
|
315
|
+
} catch (e) {
|
|
316
|
+
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
242
320
|
/**
|
|
243
321
|
* Builds the options object for `genAIClient.chats.create()`.
|
|
244
322
|
* Override in subclasses to add tools, grounding, etc.
|
|
@@ -321,6 +399,7 @@ class BaseGemini {
|
|
|
321
399
|
* @param {string} [opts.contextKey='CONTEXT'] - Key for optional context
|
|
322
400
|
* @param {string} [opts.explanationKey='EXPLANATION'] - Key for optional explanations
|
|
323
401
|
* @param {string} [opts.systemPromptKey='SYSTEM'] - Key for system prompt overrides in examples
|
|
402
|
+
* @param {'json'|'text'} [opts.format='json'] - Model-turn format: 'json' wraps answers in a {data} envelope (Transformer protocol); 'text' stores ANSWER verbatim (prose agents like Chat)
|
|
324
403
|
* @returns {Promise<Array>} The updated chat history
|
|
325
404
|
*/
|
|
326
405
|
async seed(examples, opts = {}) {
|
|
@@ -336,6 +415,7 @@ class BaseGemini {
|
|
|
336
415
|
const contextKey = opts.contextKey || 'CONTEXT';
|
|
337
416
|
const explanationKey = opts.explanationKey || 'EXPLANATION';
|
|
338
417
|
const systemPromptKey = opts.systemPromptKey || 'SYSTEM';
|
|
418
|
+
const format = opts.format || 'json';
|
|
339
419
|
|
|
340
420
|
// Check for system prompt override in examples
|
|
341
421
|
const instructionExample = examples.find(ex => ex[systemPromptKey]);
|
|
@@ -367,9 +447,15 @@ class BaseGemini {
|
|
|
367
447
|
userText += promptText;
|
|
368
448
|
}
|
|
369
449
|
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
450
|
+
let modelText;
|
|
451
|
+
if (format === 'text') {
|
|
452
|
+
modelText = isJSON(answerValue) ? JSON.stringify(answerValue, null, 2) : String(answerValue || '');
|
|
453
|
+
if (explanationValue) log.warn('seed(): EXPLANATION has no representation in text format; ignored.');
|
|
454
|
+
} else {
|
|
455
|
+
if (answerValue) modelResponse.data = answerValue;
|
|
456
|
+
if (explanationValue) modelResponse.explanation = explanationValue;
|
|
457
|
+
modelText = JSON.stringify(modelResponse, null, 2);
|
|
458
|
+
}
|
|
373
459
|
|
|
374
460
|
if (userText.trim().length && modelText.trim().length > 0) {
|
|
375
461
|
historyToAdd.push({ role: 'user', parts: [{ text: userText.trim() }] });
|
|
@@ -398,12 +484,17 @@ class BaseGemini {
|
|
|
398
484
|
*/
|
|
399
485
|
_captureMetadata(response) {
|
|
400
486
|
const modelStatus = response?.modelStatus || null;
|
|
487
|
+
const promptTokens = response.usageMetadata?.promptTokenCount || 0;
|
|
488
|
+
const responseTokens = response.usageMetadata?.candidatesTokenCount || 0;
|
|
489
|
+
const thoughtsTokens = response.usageMetadata?.thoughtsTokenCount || 0;
|
|
401
490
|
this.lastResponseMetadata = {
|
|
402
491
|
modelVersion: response.modelVersion || null,
|
|
403
492
|
requestedModel: this.modelName,
|
|
404
|
-
promptTokens
|
|
405
|
-
responseTokens
|
|
406
|
-
|
|
493
|
+
promptTokens,
|
|
494
|
+
responseTokens,
|
|
495
|
+
thoughtsTokens,
|
|
496
|
+
// totalTokenCount includes thoughts; fall back to summing the parts.
|
|
497
|
+
totalTokens: response.usageMetadata?.totalTokenCount || (promptTokens + responseTokens + thoughtsTokens),
|
|
407
498
|
timestamp: Date.now(),
|
|
408
499
|
groundingMetadata: response.candidates?.[0]?.groundingMetadata || null,
|
|
409
500
|
modelStatus
|
|
@@ -423,19 +514,73 @@ class BaseGemini {
|
|
|
423
514
|
if (!this.lastResponseMetadata) return null;
|
|
424
515
|
|
|
425
516
|
const meta = this.lastResponseMetadata;
|
|
426
|
-
const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 1 };
|
|
517
|
+
const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, thoughtsTokens: 0, attempts: 1 };
|
|
427
518
|
const useCumulative = cumulative.attempts > 0;
|
|
428
519
|
|
|
520
|
+
const promptTokens = useCumulative ? cumulative.promptTokens : meta.promptTokens;
|
|
521
|
+
const responseTokens = useCumulative ? cumulative.responseTokens : meta.responseTokens;
|
|
522
|
+
const thoughtsTokens = useCumulative ? (cumulative.thoughtsTokens || 0) : (meta.thoughtsTokens || 0);
|
|
523
|
+
const totalTokens = useCumulative ? cumulative.totalTokens : meta.totalTokens;
|
|
524
|
+
|
|
429
525
|
return {
|
|
430
|
-
promptTokens
|
|
431
|
-
responseTokens
|
|
432
|
-
|
|
526
|
+
promptTokens,
|
|
527
|
+
responseTokens,
|
|
528
|
+
thoughtsTokens,
|
|
529
|
+
totalTokens,
|
|
433
530
|
attempts: useCumulative ? cumulative.attempts : 1,
|
|
434
531
|
modelVersion: meta.modelVersion,
|
|
435
532
|
requestedModel: meta.requestedModel,
|
|
436
533
|
timestamp: meta.timestamp,
|
|
437
534
|
groundingMetadata: meta.groundingMetadata || null,
|
|
438
|
-
modelStatus: meta.modelStatus || null
|
|
535
|
+
modelStatus: meta.modelStatus || null,
|
|
536
|
+
estimatedCost: this._estimatedCost(meta.modelVersion, promptTokens, responseTokens, thoughtsTokens)
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Estimated USD cost, preferring the model id the API echoed (`modelVersion`)
|
|
542
|
+
* and falling back to the requested model when that build isn't priced.
|
|
543
|
+
* @param {string|null|undefined} modelVersion
|
|
544
|
+
* @param {number} promptTokens
|
|
545
|
+
* @param {number} responseTokens
|
|
546
|
+
* @param {number} [thoughtsTokens=0]
|
|
547
|
+
* @returns {number|null}
|
|
548
|
+
* @protected
|
|
549
|
+
*/
|
|
550
|
+
_estimatedCost(modelVersion, promptTokens, responseTokens, thoughtsTokens = 0) {
|
|
551
|
+
return computeCost(modelVersion, promptTokens, responseTokens, thoughtsTokens)
|
|
552
|
+
?? computeCost(this.modelName, promptTokens, responseTokens, thoughtsTokens);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Builds a usage object directly from a single API response, WITHOUT reading
|
|
557
|
+
* mutable instance state (`lastResponseMetadata`/`_cumulativeUsage`). Safe to
|
|
558
|
+
* call under concurrent send() calls on a shared instance — unlike
|
|
559
|
+
* getLastUsage(), which reflects whichever call most recently mutated the
|
|
560
|
+
* instance and can cross-talk between concurrent sends.
|
|
561
|
+
* @param {Object} response - A single generateContent() response
|
|
562
|
+
* @param {number} [attempts=1] - Attempts this call consumed
|
|
563
|
+
* @returns {UsageData}
|
|
564
|
+
* @protected
|
|
565
|
+
*/
|
|
566
|
+
_usageFromResponse(response, attempts = 1) {
|
|
567
|
+
const promptTokens = response?.usageMetadata?.promptTokenCount || 0;
|
|
568
|
+
const responseTokens = response?.usageMetadata?.candidatesTokenCount || 0;
|
|
569
|
+
const thoughtsTokens = response?.usageMetadata?.thoughtsTokenCount || 0;
|
|
570
|
+
const totalTokens = response?.usageMetadata?.totalTokenCount || (promptTokens + responseTokens + thoughtsTokens);
|
|
571
|
+
const modelVersion = response?.modelVersion || null;
|
|
572
|
+
return {
|
|
573
|
+
promptTokens,
|
|
574
|
+
responseTokens,
|
|
575
|
+
thoughtsTokens,
|
|
576
|
+
totalTokens,
|
|
577
|
+
attempts,
|
|
578
|
+
modelVersion,
|
|
579
|
+
requestedModel: this.modelName,
|
|
580
|
+
timestamp: Date.now(),
|
|
581
|
+
groundingMetadata: response?.candidates?.[0]?.groundingMetadata || null,
|
|
582
|
+
modelStatus: response?.modelStatus || null,
|
|
583
|
+
estimatedCost: this._estimatedCost(modelVersion, promptTokens, responseTokens, thoughtsTokens)
|
|
439
584
|
};
|
|
440
585
|
}
|
|
441
586
|
|
|
@@ -482,14 +627,16 @@ class BaseGemini {
|
|
|
482
627
|
*/
|
|
483
628
|
async estimateCost(nextPayload) {
|
|
484
629
|
const tokenInfo = await this.estimate(nextPayload);
|
|
485
|
-
const pricing =
|
|
630
|
+
const pricing = resolvePricing(this.modelName);
|
|
486
631
|
|
|
487
632
|
return {
|
|
488
633
|
inputTokens: tokenInfo.inputTokens,
|
|
489
634
|
model: this.modelName,
|
|
490
635
|
pricing: pricing,
|
|
491
|
-
estimatedInputCost: (tokenInfo.inputTokens / 1_000_000) * pricing.input,
|
|
492
|
-
note:
|
|
636
|
+
estimatedInputCost: pricing ? (tokenInfo.inputTokens / 1_000_000) * pricing.input : null,
|
|
637
|
+
note: pricing
|
|
638
|
+
? 'Cost is for input tokens only; output cost depends on response length'
|
|
639
|
+
: `No pricing known for model "${this.modelName}"; estimatedInputCost is null`
|
|
493
640
|
};
|
|
494
641
|
}
|
|
495
642
|
|
package/chat.js
CHANGED
|
@@ -47,6 +47,19 @@ class Chat extends BaseGemini {
|
|
|
47
47
|
log.debug(`Chat created with model: ${this.modelName}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Seeds the conversation with example pairs stored as plain prose turns.
|
|
52
|
+
* Chat is a prose agent — model turns are stored verbatim, not wrapped in
|
|
53
|
+
* Transformer's {data} JSON envelope.
|
|
54
|
+
*
|
|
55
|
+
* @param {import('./types').TransformationExample[]} [examples]
|
|
56
|
+
* @param {import('./types').SeedOptions} [opts={}]
|
|
57
|
+
* @returns {Promise<Array>} The updated chat history
|
|
58
|
+
*/
|
|
59
|
+
async seed(examples, opts = {}) {
|
|
60
|
+
return super.seed(examples, { format: 'text', ...opts });
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
/**
|
|
51
64
|
* Send a text message and get a response. Adds to conversation history.
|
|
52
65
|
*
|
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,14 @@ 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,
|
|
115
117
|
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
116
118
|
attempts: 1
|
|
117
119
|
};
|
|
@@ -134,7 +136,7 @@ export default class ImageGenerator extends BaseGemini {
|
|
|
134
136
|
log.warn('ImageGenerator: no images returned. Check prompt or safety filters.');
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
return { images, text: text || null, usage
|
|
139
|
+
return { images, text: text || null, usage };
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
/**
|
package/index.cjs
CHANGED
|
@@ -36,15 +36,20 @@ __export(index_exports, {
|
|
|
36
36
|
HarmBlockThreshold: () => import_genai2.HarmBlockThreshold,
|
|
37
37
|
HarmCategory: () => import_genai2.HarmCategory,
|
|
38
38
|
ImageGenerator: () => ImageGenerator,
|
|
39
|
+
MODEL_ALIASES: () => MODEL_ALIASES,
|
|
40
|
+
MODEL_PRICING: () => MODEL_PRICING,
|
|
39
41
|
Message: () => message_default,
|
|
40
42
|
RagAgent: () => rag_agent_default,
|
|
41
43
|
ThinkingLevel: () => import_genai2.ThinkingLevel,
|
|
42
44
|
ToolAgent: () => tool_agent_default,
|
|
43
45
|
Transformer: () => transformer_default,
|
|
44
46
|
attemptJSONRecovery: () => attemptJSONRecovery,
|
|
47
|
+
computeCost: () => computeCost,
|
|
45
48
|
default: () => index_default,
|
|
46
49
|
extractJSON: () => extractJSON,
|
|
47
|
-
log: () => logger_default
|
|
50
|
+
log: () => logger_default,
|
|
51
|
+
resolvePricing: () => resolvePricing,
|
|
52
|
+
validateSchema: () => validateSchema
|
|
48
53
|
});
|
|
49
54
|
module.exports = __toCommonJS(index_exports);
|
|
50
55
|
|
|
@@ -255,6 +260,79 @@ function findCompleteJSONStructures(text) {
|
|
|
255
260
|
}
|
|
256
261
|
return results;
|
|
257
262
|
}
|
|
263
|
+
function validateSchema(data, schema, path2 = "$") {
|
|
264
|
+
const errors = [];
|
|
265
|
+
if (!schema || typeof schema !== "object") return errors;
|
|
266
|
+
if (data === null && schema.nullable === true) return errors;
|
|
267
|
+
if (Array.isArray(schema.enum)) {
|
|
268
|
+
const target = JSON.stringify(data);
|
|
269
|
+
const ok = schema.enum.some((v) => v === data || JSON.stringify(v) === target);
|
|
270
|
+
if (!ok) errors.push(`${path2}: value ${JSON.stringify(data)} is not one of allowed enum values ${JSON.stringify(schema.enum)}`);
|
|
271
|
+
}
|
|
272
|
+
if (schema.type !== void 0) {
|
|
273
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
274
|
+
if (schema.nullable === true) types.push("null");
|
|
275
|
+
if (!types.some((t) => matchesType(data, t))) {
|
|
276
|
+
errors.push(`${path2}: expected type ${types.join("|")} but got ${describeType(data)}`);
|
|
277
|
+
return errors;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const isObject = data !== null && typeof data === "object" && !Array.isArray(data);
|
|
281
|
+
if (isObject && (schema.properties || schema.required || schema.additionalProperties === false)) {
|
|
282
|
+
const props = schema.properties || {};
|
|
283
|
+
if (Array.isArray(schema.required)) {
|
|
284
|
+
for (const key of schema.required) {
|
|
285
|
+
if (!Object.hasOwn(data, key)) errors.push(`${path2}: missing required property "${key}"`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (schema.additionalProperties === false) {
|
|
289
|
+
for (const key of Object.keys(data)) {
|
|
290
|
+
if (!Object.hasOwn(props, key)) errors.push(`${path2}: unexpected property "${key}" (additionalProperties is false)`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const [key, subSchema] of Object.entries(props)) {
|
|
294
|
+
if (Object.hasOwn(data, key)) {
|
|
295
|
+
errors.push(...validateSchema(
|
|
296
|
+
data[key],
|
|
297
|
+
/** @type {any} */
|
|
298
|
+
subSchema,
|
|
299
|
+
`${path2}.${key}`
|
|
300
|
+
));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (Array.isArray(data) && schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
|
|
305
|
+
data.forEach((item, i) => {
|
|
306
|
+
errors.push(...validateSchema(item, schema.items, `${path2}[${i}]`));
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return errors;
|
|
310
|
+
}
|
|
311
|
+
function matchesType(value, type) {
|
|
312
|
+
switch (type) {
|
|
313
|
+
case "string":
|
|
314
|
+
return typeof value === "string";
|
|
315
|
+
case "number":
|
|
316
|
+
return typeof value === "number" && !Number.isNaN(value);
|
|
317
|
+
case "integer":
|
|
318
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
319
|
+
case "boolean":
|
|
320
|
+
return typeof value === "boolean";
|
|
321
|
+
case "object":
|
|
322
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
323
|
+
case "array":
|
|
324
|
+
return Array.isArray(value);
|
|
325
|
+
case "null":
|
|
326
|
+
return value === null;
|
|
327
|
+
default:
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function describeType(value) {
|
|
332
|
+
if (value === null) return "null";
|
|
333
|
+
if (Array.isArray(value)) return "array";
|
|
334
|
+
return typeof value;
|
|
335
|
+
}
|
|
258
336
|
function extractJSON(text) {
|
|
259
337
|
if (!text || typeof text !== "string") {
|
|
260
338
|
throw new Error("No text provided for JSON extraction");
|
|
@@ -338,6 +416,8 @@ var MODEL_PRICING = {
|
|
|
338
416
|
// Gemini 3.x preview
|
|
339
417
|
"gemini-3.1-pro-preview": { input: 2, output: 12 },
|
|
340
418
|
// ≤200k tier
|
|
419
|
+
"gemini-3-pro-preview": { input: 2, output: 12 },
|
|
420
|
+
// ≤200k tier; launch rate (superseded by 3.1, off the pricing page)
|
|
341
421
|
"gemini-3-flash-preview": { input: 0.5, output: 3 },
|
|
342
422
|
"gemini-3.1-flash-lite-preview": { input: 0.25, output: 1.5 },
|
|
343
423
|
"gemini-3.1-flash-image-preview": { input: 0.5, output: 3 },
|
|
@@ -357,6 +437,36 @@ var MODEL_PRICING = {
|
|
|
357
437
|
// Embeddings
|
|
358
438
|
"gemini-embedding-001": { input: 0.15, output: 0 }
|
|
359
439
|
};
|
|
440
|
+
var MODEL_ALIASES = {
|
|
441
|
+
"gemini-flash-latest": "gemini-3.5-flash",
|
|
442
|
+
"gemini-pro-latest": "gemini-3.1-pro-preview",
|
|
443
|
+
"gemini-flash-lite-latest": "gemini-3.1-flash-lite"
|
|
444
|
+
};
|
|
445
|
+
function resolvePricing(modelId) {
|
|
446
|
+
if (!modelId) return null;
|
|
447
|
+
const tryKey = (id) => {
|
|
448
|
+
if (MODEL_PRICING[id]) return MODEL_PRICING[id];
|
|
449
|
+
const alias = MODEL_ALIASES[id];
|
|
450
|
+
if (alias && MODEL_PRICING[alias]) return MODEL_PRICING[alias];
|
|
451
|
+
return null;
|
|
452
|
+
};
|
|
453
|
+
let hit = tryKey(modelId);
|
|
454
|
+
if (hit) return hit;
|
|
455
|
+
let stripped = modelId;
|
|
456
|
+
while (true) {
|
|
457
|
+
const next = stripped.replace(/-\d{2,}$/, "");
|
|
458
|
+
if (next === stripped) break;
|
|
459
|
+
stripped = next;
|
|
460
|
+
hit = tryKey(stripped);
|
|
461
|
+
if (hit) return hit;
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
function computeCost(modelId, promptTokens, responseTokens, thoughtsTokens = 0) {
|
|
466
|
+
const pricing = resolvePricing(modelId);
|
|
467
|
+
if (!pricing) return null;
|
|
468
|
+
return promptTokens / 1e6 * pricing.input + (responseTokens + thoughtsTokens) / 1e6 * pricing.output;
|
|
469
|
+
}
|
|
360
470
|
var BaseGemini = class {
|
|
361
471
|
/**
|
|
362
472
|
* @param {BaseGeminiOptions} [options={}]
|
|
@@ -398,6 +508,9 @@ var BaseGemini = class {
|
|
|
398
508
|
};
|
|
399
509
|
if (this.serviceTier) this.chatConfig["serviceTier"] = this.serviceTier;
|
|
400
510
|
if (this.includeServerSideToolInvocations) this.chatConfig["includeServerSideToolInvocations"] = true;
|
|
511
|
+
for (const key of ["temperature", "topP", "topK"]) {
|
|
512
|
+
if (options[key] !== void 0) this.chatConfig[key] = options[key];
|
|
513
|
+
}
|
|
401
514
|
if (this.systemPrompt) {
|
|
402
515
|
this.chatConfig.systemInstruction = this.systemPrompt;
|
|
403
516
|
} else if (this.systemPrompt === null && options.systemPrompt === void 0) {
|
|
@@ -448,16 +561,25 @@ var BaseGemini = class {
|
|
|
448
561
|
logger_default.debug(`Initializing ${this.constructor.name} chat session with model: ${this.modelName}...`);
|
|
449
562
|
const chatOptions = this._getChatCreateOptions();
|
|
450
563
|
this.chatSession = this.genAIClient.chats.create(chatOptions);
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
await this._withRetry(() => this.genAIClient.models.list());
|
|
454
|
-
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
455
|
-
} catch (e) {
|
|
456
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
564
|
+
await this._healthCheckPing();
|
|
459
565
|
logger_default.debug(`${this.constructor.name}: Chat session initialized.`);
|
|
460
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* Opt-in connectivity check via `models.list()` — runs only when
|
|
569
|
+
* `healthCheck: true`. Fails fast (no exponential backoff): a readiness probe
|
|
570
|
+
* should surface a bad config immediately, not after ~30s of 429 retries.
|
|
571
|
+
* @returns {Promise<void>}
|
|
572
|
+
* @protected
|
|
573
|
+
*/
|
|
574
|
+
async _healthCheckPing() {
|
|
575
|
+
if (!this.healthCheck) return;
|
|
576
|
+
try {
|
|
577
|
+
await this.genAIClient.models.list();
|
|
578
|
+
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
579
|
+
} catch (e) {
|
|
580
|
+
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
461
583
|
/**
|
|
462
584
|
* Builds the options object for `genAIClient.chats.create()`.
|
|
463
585
|
* Override in subclasses to add tools, grounding, etc.
|
|
@@ -531,6 +653,7 @@ var BaseGemini = class {
|
|
|
531
653
|
* @param {string} [opts.contextKey='CONTEXT'] - Key for optional context
|
|
532
654
|
* @param {string} [opts.explanationKey='EXPLANATION'] - Key for optional explanations
|
|
533
655
|
* @param {string} [opts.systemPromptKey='SYSTEM'] - Key for system prompt overrides in examples
|
|
656
|
+
* @param {'json'|'text'} [opts.format='json'] - Model-turn format: 'json' wraps answers in a {data} envelope (Transformer protocol); 'text' stores ANSWER verbatim (prose agents like Chat)
|
|
534
657
|
* @returns {Promise<Array>} The updated chat history
|
|
535
658
|
*/
|
|
536
659
|
async seed(examples, opts = {}) {
|
|
@@ -544,6 +667,7 @@ var BaseGemini = class {
|
|
|
544
667
|
const contextKey = opts.contextKey || "CONTEXT";
|
|
545
668
|
const explanationKey = opts.explanationKey || "EXPLANATION";
|
|
546
669
|
const systemPromptKey = opts.systemPromptKey || "SYSTEM";
|
|
670
|
+
const format = opts.format || "json";
|
|
547
671
|
const instructionExample = examples.find((ex) => ex[systemPromptKey]);
|
|
548
672
|
if (instructionExample) {
|
|
549
673
|
logger_default.debug(`Found system prompt in examples; reinitializing chat.`);
|
|
@@ -572,9 +696,15 @@ ${contextText}
|
|
|
572
696
|
let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
|
|
573
697
|
userText += promptText;
|
|
574
698
|
}
|
|
575
|
-
|
|
576
|
-
if (
|
|
577
|
-
|
|
699
|
+
let modelText;
|
|
700
|
+
if (format === "text") {
|
|
701
|
+
modelText = isJSON(answerValue) ? JSON.stringify(answerValue, null, 2) : String(answerValue || "");
|
|
702
|
+
if (explanationValue) logger_default.warn("seed(): EXPLANATION has no representation in text format; ignored.");
|
|
703
|
+
} else {
|
|
704
|
+
if (answerValue) modelResponse.data = answerValue;
|
|
705
|
+
if (explanationValue) modelResponse.explanation = explanationValue;
|
|
706
|
+
modelText = JSON.stringify(modelResponse, null, 2);
|
|
707
|
+
}
|
|
578
708
|
if (userText.trim().length && modelText.trim().length > 0) {
|
|
579
709
|
historyToAdd.push({ role: "user", parts: [{ text: userText.trim() }] });
|
|
580
710
|
historyToAdd.push({ role: "model", parts: [{ text: modelText.trim() }] });
|
|
@@ -596,12 +726,17 @@ ${contextText}
|
|
|
596
726
|
*/
|
|
597
727
|
_captureMetadata(response) {
|
|
598
728
|
const modelStatus = response?.modelStatus || null;
|
|
729
|
+
const promptTokens = response.usageMetadata?.promptTokenCount || 0;
|
|
730
|
+
const responseTokens = response.usageMetadata?.candidatesTokenCount || 0;
|
|
731
|
+
const thoughtsTokens = response.usageMetadata?.thoughtsTokenCount || 0;
|
|
599
732
|
this.lastResponseMetadata = {
|
|
600
733
|
modelVersion: response.modelVersion || null,
|
|
601
734
|
requestedModel: this.modelName,
|
|
602
|
-
promptTokens
|
|
603
|
-
responseTokens
|
|
604
|
-
|
|
735
|
+
promptTokens,
|
|
736
|
+
responseTokens,
|
|
737
|
+
thoughtsTokens,
|
|
738
|
+
// totalTokenCount includes thoughts; fall back to summing the parts.
|
|
739
|
+
totalTokens: response.usageMetadata?.totalTokenCount || promptTokens + responseTokens + thoughtsTokens,
|
|
605
740
|
timestamp: Date.now(),
|
|
606
741
|
groundingMetadata: response.candidates?.[0]?.groundingMetadata || null,
|
|
607
742
|
modelStatus
|
|
@@ -619,18 +754,68 @@ ${contextText}
|
|
|
619
754
|
getLastUsage() {
|
|
620
755
|
if (!this.lastResponseMetadata) return null;
|
|
621
756
|
const meta = this.lastResponseMetadata;
|
|
622
|
-
const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, attempts: 1 };
|
|
757
|
+
const cumulative = this._cumulativeUsage || { promptTokens: 0, responseTokens: 0, totalTokens: 0, thoughtsTokens: 0, attempts: 1 };
|
|
623
758
|
const useCumulative = cumulative.attempts > 0;
|
|
759
|
+
const promptTokens = useCumulative ? cumulative.promptTokens : meta.promptTokens;
|
|
760
|
+
const responseTokens = useCumulative ? cumulative.responseTokens : meta.responseTokens;
|
|
761
|
+
const thoughtsTokens = useCumulative ? cumulative.thoughtsTokens || 0 : meta.thoughtsTokens || 0;
|
|
762
|
+
const totalTokens = useCumulative ? cumulative.totalTokens : meta.totalTokens;
|
|
624
763
|
return {
|
|
625
|
-
promptTokens
|
|
626
|
-
responseTokens
|
|
627
|
-
|
|
764
|
+
promptTokens,
|
|
765
|
+
responseTokens,
|
|
766
|
+
thoughtsTokens,
|
|
767
|
+
totalTokens,
|
|
628
768
|
attempts: useCumulative ? cumulative.attempts : 1,
|
|
629
769
|
modelVersion: meta.modelVersion,
|
|
630
770
|
requestedModel: meta.requestedModel,
|
|
631
771
|
timestamp: meta.timestamp,
|
|
632
772
|
groundingMetadata: meta.groundingMetadata || null,
|
|
633
|
-
modelStatus: meta.modelStatus || null
|
|
773
|
+
modelStatus: meta.modelStatus || null,
|
|
774
|
+
estimatedCost: this._estimatedCost(meta.modelVersion, promptTokens, responseTokens, thoughtsTokens)
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Estimated USD cost, preferring the model id the API echoed (`modelVersion`)
|
|
779
|
+
* and falling back to the requested model when that build isn't priced.
|
|
780
|
+
* @param {string|null|undefined} modelVersion
|
|
781
|
+
* @param {number} promptTokens
|
|
782
|
+
* @param {number} responseTokens
|
|
783
|
+
* @param {number} [thoughtsTokens=0]
|
|
784
|
+
* @returns {number|null}
|
|
785
|
+
* @protected
|
|
786
|
+
*/
|
|
787
|
+
_estimatedCost(modelVersion, promptTokens, responseTokens, thoughtsTokens = 0) {
|
|
788
|
+
return computeCost(modelVersion, promptTokens, responseTokens, thoughtsTokens) ?? computeCost(this.modelName, promptTokens, responseTokens, thoughtsTokens);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Builds a usage object directly from a single API response, WITHOUT reading
|
|
792
|
+
* mutable instance state (`lastResponseMetadata`/`_cumulativeUsage`). Safe to
|
|
793
|
+
* call under concurrent send() calls on a shared instance — unlike
|
|
794
|
+
* getLastUsage(), which reflects whichever call most recently mutated the
|
|
795
|
+
* instance and can cross-talk between concurrent sends.
|
|
796
|
+
* @param {Object} response - A single generateContent() response
|
|
797
|
+
* @param {number} [attempts=1] - Attempts this call consumed
|
|
798
|
+
* @returns {UsageData}
|
|
799
|
+
* @protected
|
|
800
|
+
*/
|
|
801
|
+
_usageFromResponse(response, attempts = 1) {
|
|
802
|
+
const promptTokens = response?.usageMetadata?.promptTokenCount || 0;
|
|
803
|
+
const responseTokens = response?.usageMetadata?.candidatesTokenCount || 0;
|
|
804
|
+
const thoughtsTokens = response?.usageMetadata?.thoughtsTokenCount || 0;
|
|
805
|
+
const totalTokens = response?.usageMetadata?.totalTokenCount || promptTokens + responseTokens + thoughtsTokens;
|
|
806
|
+
const modelVersion = response?.modelVersion || null;
|
|
807
|
+
return {
|
|
808
|
+
promptTokens,
|
|
809
|
+
responseTokens,
|
|
810
|
+
thoughtsTokens,
|
|
811
|
+
totalTokens,
|
|
812
|
+
attempts,
|
|
813
|
+
modelVersion,
|
|
814
|
+
requestedModel: this.modelName,
|
|
815
|
+
timestamp: Date.now(),
|
|
816
|
+
groundingMetadata: response?.candidates?.[0]?.groundingMetadata || null,
|
|
817
|
+
modelStatus: response?.modelStatus || null,
|
|
818
|
+
estimatedCost: this._estimatedCost(modelVersion, promptTokens, responseTokens, thoughtsTokens)
|
|
634
819
|
};
|
|
635
820
|
}
|
|
636
821
|
// ── Token Estimation ─────────────────────────────────────────────────────
|
|
@@ -666,13 +851,13 @@ ${contextText}
|
|
|
666
851
|
*/
|
|
667
852
|
async estimateCost(nextPayload) {
|
|
668
853
|
const tokenInfo = await this.estimate(nextPayload);
|
|
669
|
-
const pricing =
|
|
854
|
+
const pricing = resolvePricing(this.modelName);
|
|
670
855
|
return {
|
|
671
856
|
inputTokens: tokenInfo.inputTokens,
|
|
672
857
|
model: this.modelName,
|
|
673
858
|
pricing,
|
|
674
|
-
estimatedInputCost: tokenInfo.inputTokens / 1e6 * pricing.input,
|
|
675
|
-
note: "Cost is for input tokens only; output cost depends on response length"
|
|
859
|
+
estimatedInputCost: pricing ? tokenInfo.inputTokens / 1e6 * pricing.input : null,
|
|
860
|
+
note: pricing ? "Cost is for input tokens only; output cost depends on response length" : `No pricing known for model "${this.modelName}"; estimatedInputCost is null`
|
|
676
861
|
};
|
|
677
862
|
}
|
|
678
863
|
// ── Context Caching ─────────────────────────────────────────────────────
|
|
@@ -938,7 +1123,8 @@ var Transformer = class extends base_default {
|
|
|
938
1123
|
answerKey: this.answerKey,
|
|
939
1124
|
contextKey: this.contextKey,
|
|
940
1125
|
explanationKey: this.explanationKey,
|
|
941
|
-
systemPromptKey: this.systemPromptKey
|
|
1126
|
+
systemPromptKey: this.systemPromptKey,
|
|
1127
|
+
format: "json"
|
|
942
1128
|
});
|
|
943
1129
|
}
|
|
944
1130
|
// ── Primary Send Method ──────────────────────────────────────────────────
|
|
@@ -1216,6 +1402,18 @@ var Chat = class extends base_default {
|
|
|
1216
1402
|
super(options);
|
|
1217
1403
|
logger_default.debug(`Chat created with model: ${this.modelName}`);
|
|
1218
1404
|
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Seeds the conversation with example pairs stored as plain prose turns.
|
|
1407
|
+
* Chat is a prose agent — model turns are stored verbatim, not wrapped in
|
|
1408
|
+
* Transformer's {data} JSON envelope.
|
|
1409
|
+
*
|
|
1410
|
+
* @param {import('./types').TransformationExample[]} [examples]
|
|
1411
|
+
* @param {import('./types').SeedOptions} [opts={}]
|
|
1412
|
+
* @returns {Promise<Array>} The updated chat history
|
|
1413
|
+
*/
|
|
1414
|
+
async seed(examples, opts = {}) {
|
|
1415
|
+
return super.seed(examples, { format: "text", ...opts });
|
|
1416
|
+
}
|
|
1219
1417
|
/**
|
|
1220
1418
|
* Send a text message and get a response. Adds to conversation history.
|
|
1221
1419
|
*
|
|
@@ -1284,6 +1482,9 @@ var Message = class extends base_default {
|
|
|
1284
1482
|
}
|
|
1285
1483
|
if (options.responseMimeType) {
|
|
1286
1484
|
this.chatConfig.responseMimeType = options.responseMimeType;
|
|
1485
|
+
} else if (options.responseSchema) {
|
|
1486
|
+
this.chatConfig.responseMimeType = "application/json";
|
|
1487
|
+
logger_default.debug("responseSchema set without responseMimeType \u2014 defaulting responseMimeType to 'application/json'.");
|
|
1287
1488
|
}
|
|
1288
1489
|
this._isStructured = !!(options.responseSchema || options.responseMimeType === "application/json");
|
|
1289
1490
|
logger_default.debug(`Message created (structured=${this._isStructured})`);
|
|
@@ -1297,12 +1498,7 @@ var Message = class extends base_default {
|
|
|
1297
1498
|
async init(force = false) {
|
|
1298
1499
|
if (this._initialized && !force) return;
|
|
1299
1500
|
logger_default.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
1300
|
-
|
|
1301
|
-
await this.genAIClient.models.list();
|
|
1302
|
-
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
1303
|
-
} catch (e) {
|
|
1304
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
1305
|
-
}
|
|
1501
|
+
await this._healthCheckPing();
|
|
1306
1502
|
this._initialized = true;
|
|
1307
1503
|
logger_default.debug(`${this.constructor.name}: Initialized (stateless mode).`);
|
|
1308
1504
|
}
|
|
@@ -1328,10 +1524,12 @@ var Message = class extends base_default {
|
|
|
1328
1524
|
...this.vertexai && Object.keys(mergedLabels).length > 0 && { labels: mergedLabels }
|
|
1329
1525
|
}
|
|
1330
1526
|
}));
|
|
1527
|
+
const usage = this._usageFromResponse(result);
|
|
1331
1528
|
this._captureMetadata(result);
|
|
1332
1529
|
this._cumulativeUsage = {
|
|
1333
1530
|
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
1334
1531
|
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
1532
|
+
thoughtsTokens: this.lastResponseMetadata.thoughtsTokens,
|
|
1335
1533
|
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
1336
1534
|
attempts: 1
|
|
1337
1535
|
};
|
|
@@ -1341,7 +1539,7 @@ var Message = class extends base_default {
|
|
|
1341
1539
|
const text = result.text || "";
|
|
1342
1540
|
const response = {
|
|
1343
1541
|
text,
|
|
1344
|
-
usage
|
|
1542
|
+
usage
|
|
1345
1543
|
};
|
|
1346
1544
|
if (this._isStructured) {
|
|
1347
1545
|
try {
|
|
@@ -2955,12 +3153,7 @@ var Embedding = class extends base_default {
|
|
|
2955
3153
|
async init(force = false) {
|
|
2956
3154
|
if (this._initialized && !force) return;
|
|
2957
3155
|
logger_default.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
2958
|
-
|
|
2959
|
-
await this.genAIClient.models.list();
|
|
2960
|
-
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
2961
|
-
} catch (e) {
|
|
2962
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
2963
|
-
}
|
|
3156
|
+
await this._healthCheckPing();
|
|
2964
3157
|
this._initialized = true;
|
|
2965
3158
|
logger_default.debug(`${this.constructor.name}: Initialized (stateless mode).`);
|
|
2966
3159
|
}
|
|
@@ -3092,12 +3285,7 @@ var ImageGenerator = class extends base_default {
|
|
|
3092
3285
|
async init(force = false) {
|
|
3093
3286
|
if (this._initialized && !force) return;
|
|
3094
3287
|
logger_default.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
3095
|
-
|
|
3096
|
-
await this.genAIClient.models.list();
|
|
3097
|
-
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
3098
|
-
} catch (e) {
|
|
3099
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
3100
|
-
}
|
|
3288
|
+
await this._healthCheckPing();
|
|
3101
3289
|
this._initialized = true;
|
|
3102
3290
|
}
|
|
3103
3291
|
/**
|
|
@@ -3139,10 +3327,12 @@ var ImageGenerator = class extends base_default {
|
|
|
3139
3327
|
contents: [{ role: "user", parts }],
|
|
3140
3328
|
config: this._buildConfig(opts)
|
|
3141
3329
|
}));
|
|
3330
|
+
const usage = this._usageFromResponse(result);
|
|
3142
3331
|
this._captureMetadata(result);
|
|
3143
3332
|
this._cumulativeUsage = {
|
|
3144
3333
|
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
3145
3334
|
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
3335
|
+
thoughtsTokens: this.lastResponseMetadata.thoughtsTokens,
|
|
3146
3336
|
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
3147
3337
|
attempts: 1
|
|
3148
3338
|
};
|
|
@@ -3162,7 +3352,7 @@ var ImageGenerator = class extends base_default {
|
|
|
3162
3352
|
if (images.length === 0) {
|
|
3163
3353
|
logger_default.warn("ImageGenerator: no images returned. Check prompt or safety filters.");
|
|
3164
3354
|
}
|
|
3165
|
-
return { images, text: text || null, usage
|
|
3355
|
+
return { images, text: text || null, usage };
|
|
3166
3356
|
}
|
|
3167
3357
|
/**
|
|
3168
3358
|
* Convenience: write one or all images to disk.
|
|
@@ -3222,12 +3412,17 @@ var index_default = { Transformer: transformer_default, Chat: chat_default, Mess
|
|
|
3222
3412
|
HarmBlockThreshold,
|
|
3223
3413
|
HarmCategory,
|
|
3224
3414
|
ImageGenerator,
|
|
3415
|
+
MODEL_ALIASES,
|
|
3416
|
+
MODEL_PRICING,
|
|
3225
3417
|
Message,
|
|
3226
3418
|
RagAgent,
|
|
3227
3419
|
ThinkingLevel,
|
|
3228
3420
|
ToolAgent,
|
|
3229
3421
|
Transformer,
|
|
3230
3422
|
attemptJSONRecovery,
|
|
3423
|
+
computeCost,
|
|
3231
3424
|
extractJSON,
|
|
3232
|
-
log
|
|
3425
|
+
log,
|
|
3426
|
+
resolvePricing,
|
|
3427
|
+
validateSchema
|
|
3233
3428
|
});
|
package/index.js
CHANGED
|
@@ -29,9 +29,10 @@ export { default as RagAgent } from './rag-agent.js';
|
|
|
29
29
|
export { default as Embedding } from './embedding.js';
|
|
30
30
|
export { default as ImageGenerator } from './image-generator.js';
|
|
31
31
|
export { default as BaseGemini } from './base.js';
|
|
32
|
+
export { MODEL_PRICING, MODEL_ALIASES, resolvePricing, computeCost } from './base.js';
|
|
32
33
|
export { default as log } from './logger.js';
|
|
33
34
|
export { ThinkingLevel, HarmCategory, HarmBlockThreshold } from '@google/genai';
|
|
34
|
-
export { extractJSON, attemptJSONRecovery } from './json-helpers.js';
|
|
35
|
+
export { extractJSON, attemptJSONRecovery, validateSchema } from './json-helpers.js';
|
|
35
36
|
|
|
36
37
|
// ── Default Export (namespace object) ──
|
|
37
38
|
|
package/json-helpers.js
CHANGED
|
@@ -267,6 +267,111 @@ function findCompleteJSONStructures(text) {
|
|
|
267
267
|
return results;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Validates a parsed value against a (subset of) JSON Schema. Dependency-light —
|
|
272
|
+
* intended to guard fallback structured-output paths where the model isn't forced
|
|
273
|
+
* to emit schema-valid JSON.
|
|
274
|
+
*
|
|
275
|
+
* Supported keywords: `type` (string or array of strings; 'integer' checks for
|
|
276
|
+
* whole numbers), `required`, `properties`, `additionalProperties: false`,
|
|
277
|
+
* `items` (single schema), `enum`. Unknown keywords are ignored (lenient).
|
|
278
|
+
*
|
|
279
|
+
* @param {*} data - The parsed value to validate
|
|
280
|
+
* @param {Object} schema - JSON Schema object
|
|
281
|
+
* @param {string} [path='$'] - Internal: JSON path prefix for error messages
|
|
282
|
+
* @returns {string[]} Array of human-readable error strings; empty means valid.
|
|
283
|
+
*/
|
|
284
|
+
export function validateSchema(data, schema, path = '$') {
|
|
285
|
+
// NOTE: ak-gemini enforces structured output natively (responseSchema), so this
|
|
286
|
+
// validator has no runtime call site here — it exists for cross-package API
|
|
287
|
+
// symmetry with ak-claude (whose Vertex fallback path relies on it). Keep this
|
|
288
|
+
// implementation byte-identical to ak-claude/json-helpers.js; apply fixes to both.
|
|
289
|
+
const errors = [];
|
|
290
|
+
if (!schema || typeof schema !== 'object') return errors;
|
|
291
|
+
|
|
292
|
+
// ── nullable (OpenAPI-style) ── a null value is valid on a nullable node.
|
|
293
|
+
if (data === null && schema.nullable === true) return errors;
|
|
294
|
+
|
|
295
|
+
// ── enum ── strict equality first, then deep-equal for object/array members.
|
|
296
|
+
if (Array.isArray(schema.enum)) {
|
|
297
|
+
const target = JSON.stringify(data);
|
|
298
|
+
const ok = schema.enum.some(v => v === data || JSON.stringify(v) === target);
|
|
299
|
+
if (!ok) errors.push(`${path}: value ${JSON.stringify(data)} is not one of allowed enum values ${JSON.stringify(schema.enum)}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── type ──
|
|
303
|
+
if (schema.type !== undefined) {
|
|
304
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
305
|
+
if (schema.nullable === true) types.push('null');
|
|
306
|
+
if (!types.some(t => matchesType(data, t))) {
|
|
307
|
+
errors.push(`${path}: expected type ${types.join('|')} but got ${describeType(data)}`);
|
|
308
|
+
// If the type is wrong, deeper checks are meaningless — stop here for this node.
|
|
309
|
+
return errors;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── object ── (Object.hasOwn avoids prototype-chain keys like 'toString')
|
|
314
|
+
const isObject = data !== null && typeof data === 'object' && !Array.isArray(data);
|
|
315
|
+
if (isObject && (schema.properties || schema.required || schema.additionalProperties === false)) {
|
|
316
|
+
const props = schema.properties || {};
|
|
317
|
+
|
|
318
|
+
if (Array.isArray(schema.required)) {
|
|
319
|
+
for (const key of schema.required) {
|
|
320
|
+
if (!Object.hasOwn(data, key)) errors.push(`${path}: missing required property "${key}"`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (schema.additionalProperties === false) {
|
|
325
|
+
for (const key of Object.keys(data)) {
|
|
326
|
+
if (!Object.hasOwn(props, key)) errors.push(`${path}: unexpected property "${key}" (additionalProperties is false)`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const [key, subSchema] of Object.entries(props)) {
|
|
331
|
+
if (Object.hasOwn(data, key)) {
|
|
332
|
+
errors.push(...validateSchema(data[key], /** @type {any} */ (subSchema), `${path}.${key}`));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── array ──
|
|
338
|
+
if (Array.isArray(data) && schema.items && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
|
|
339
|
+
data.forEach((item, i) => {
|
|
340
|
+
errors.push(...validateSchema(item, schema.items, `${path}[${i}]`));
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return errors;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @param {*} value
|
|
349
|
+
* @param {string} type - JSON Schema type name
|
|
350
|
+
* @returns {boolean}
|
|
351
|
+
*/
|
|
352
|
+
function matchesType(value, type) {
|
|
353
|
+
switch (type) {
|
|
354
|
+
case 'string': return typeof value === 'string';
|
|
355
|
+
case 'number': return typeof value === 'number' && !Number.isNaN(value);
|
|
356
|
+
case 'integer': return typeof value === 'number' && Number.isInteger(value);
|
|
357
|
+
case 'boolean': return typeof value === 'boolean';
|
|
358
|
+
case 'object': return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
359
|
+
case 'array': return Array.isArray(value);
|
|
360
|
+
case 'null': return value === null;
|
|
361
|
+
default: return true; // unknown type keyword — be lenient
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @param {*} value
|
|
367
|
+
* @returns {string}
|
|
368
|
+
*/
|
|
369
|
+
function describeType(value) {
|
|
370
|
+
if (value === null) return 'null';
|
|
371
|
+
if (Array.isArray(value)) return 'array';
|
|
372
|
+
return typeof value;
|
|
373
|
+
}
|
|
374
|
+
|
|
270
375
|
/**
|
|
271
376
|
* Extracts valid JSON from model response text using multiple strategies.
|
|
272
377
|
* @param {string} text - The model response text
|
package/message.js
CHANGED
|
@@ -53,6 +53,13 @@ class Message extends BaseGemini {
|
|
|
53
53
|
}
|
|
54
54
|
if (options.responseMimeType) {
|
|
55
55
|
this.chatConfig.responseMimeType = options.responseMimeType;
|
|
56
|
+
} else if (options.responseSchema) {
|
|
57
|
+
// Google's @google/genai requires response_mime_type: 'application/json'
|
|
58
|
+
// whenever a responseSchema is set — it does NOT add it automatically.
|
|
59
|
+
// Passing a schema without the mime type yields a 400 INVALID_ARGUMENT
|
|
60
|
+
// (Vertex) or prose output that fails JSON extraction. Auto-pair them.
|
|
61
|
+
this.chatConfig.responseMimeType = 'application/json';
|
|
62
|
+
log.debug("responseSchema set without responseMimeType — defaulting responseMimeType to 'application/json'.");
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
this._isStructured = !!(options.responseSchema || options.responseMimeType === 'application/json');
|
|
@@ -71,12 +78,11 @@ class Message extends BaseGemini {
|
|
|
71
78
|
|
|
72
79
|
log.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
73
80
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
81
|
+
// Connectivity check is opt-in (healthCheck: true). models.list() is a
|
|
82
|
+
// different IAM surface than generateContent — a service account allowed to
|
|
83
|
+
// generate but not list publisher models would fail here misleadingly — and
|
|
84
|
+
// it adds a round-trip per instance. The first send() is a fine error signal.
|
|
85
|
+
await this._healthCheckPing();
|
|
80
86
|
|
|
81
87
|
this._initialized = true;
|
|
82
88
|
log.debug(`${this.constructor.name}: Initialized (stateless mode).`);
|
|
@@ -111,11 +117,17 @@ class Message extends BaseGemini {
|
|
|
111
117
|
}
|
|
112
118
|
}));
|
|
113
119
|
|
|
120
|
+
// Compute per-call usage synchronously from THIS response before any other
|
|
121
|
+
// concurrent send() can mutate instance state. result.usage is safe under
|
|
122
|
+
// concurrency; getLastUsage() (instance state, updated below) is not.
|
|
123
|
+
const usage = this._usageFromResponse(result);
|
|
124
|
+
|
|
114
125
|
this._captureMetadata(result);
|
|
115
126
|
|
|
116
127
|
this._cumulativeUsage = {
|
|
117
128
|
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
118
129
|
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
130
|
+
thoughtsTokens: this.lastResponseMetadata.thoughtsTokens,
|
|
119
131
|
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
120
132
|
attempts: 1
|
|
121
133
|
};
|
|
@@ -127,7 +139,7 @@ class Message extends BaseGemini {
|
|
|
127
139
|
const text = result.text || '';
|
|
128
140
|
const response = {
|
|
129
141
|
text,
|
|
130
|
-
usage
|
|
142
|
+
usage
|
|
131
143
|
};
|
|
132
144
|
|
|
133
145
|
// Parse structured data if configured
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "ak-gemini",
|
|
3
3
|
"author": "ak@mixpanel.com",
|
|
4
4
|
"description": "AK's Generative AI Helper for doing... everything",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.5.0",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.js",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"json-helpers.js",
|
|
20
20
|
"types.d.ts",
|
|
21
21
|
"logger.js",
|
|
22
|
-
"GUIDE.md"
|
|
22
|
+
"GUIDE.md",
|
|
23
|
+
"CHANGELOG.md"
|
|
23
24
|
],
|
|
24
25
|
"types": "types.d.ts",
|
|
25
26
|
"exports": {
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
],
|
|
66
67
|
"license": "ISC",
|
|
67
68
|
"dependencies": {
|
|
68
|
-
"@google/genai": "^2.
|
|
69
|
+
"@google/genai": "^2.12.0",
|
|
69
70
|
"dotenv": "^17.3.1",
|
|
70
71
|
"pino": "^10.3.1",
|
|
71
72
|
"pino-pretty": "^13.1.3"
|
package/transformer.js
CHANGED
|
@@ -141,7 +141,8 @@ class Transformer extends BaseGemini {
|
|
|
141
141
|
answerKey: this.answerKey,
|
|
142
142
|
contextKey: this.contextKey,
|
|
143
143
|
explanationKey: this.explanationKey,
|
|
144
|
-
systemPromptKey: this.systemPromptKey
|
|
144
|
+
systemPromptKey: this.systemPromptKey,
|
|
145
|
+
format: 'json'
|
|
145
146
|
});
|
|
146
147
|
}
|
|
147
148
|
|
package/types.d.ts
CHANGED
|
@@ -64,7 +64,9 @@ export interface UsageData {
|
|
|
64
64
|
promptTokens: number;
|
|
65
65
|
/** CUMULATIVE output tokens across all retry attempts */
|
|
66
66
|
responseTokens: number;
|
|
67
|
-
/** CUMULATIVE
|
|
67
|
+
/** CUMULATIVE thinking ("thoughts") tokens; billed at the output rate. Included in totalTokens and estimatedCost. */
|
|
68
|
+
thoughtsTokens?: number;
|
|
69
|
+
/** CUMULATIVE total tokens across all retry attempts (includes thoughtsTokens) */
|
|
68
70
|
totalTokens: number;
|
|
69
71
|
/** Number of attempts (1 = first try success, 2+ = retries needed) */
|
|
70
72
|
attempts: number;
|
|
@@ -76,6 +78,8 @@ export interface UsageData {
|
|
|
76
78
|
groundingMetadata?: GroundingMetadata | null;
|
|
77
79
|
/** Model lifecycle status from Google (e.g., 'DEPRECATED'). Surfaced from @google/genai 1.47+. */
|
|
78
80
|
modelStatus?: string | null;
|
|
81
|
+
/** Estimated USD cost from MODEL_PRICING (input+output). null when the model's pricing is unknown. */
|
|
82
|
+
estimatedCost?: number | null;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
export interface TransformationExample {
|
|
@@ -147,6 +151,12 @@ export interface BaseGeminiOptions {
|
|
|
147
151
|
thinkingConfig?: ThinkingConfig | null;
|
|
148
152
|
/** Maximum output tokens (default: 50000, null removes limit) */
|
|
149
153
|
maxOutputTokens?: number | null;
|
|
154
|
+
/** Sampling temperature (shorthand for chatConfig.temperature; wins over chatConfig) */
|
|
155
|
+
temperature?: number;
|
|
156
|
+
/** Nucleus sampling (shorthand for chatConfig.topP; wins over chatConfig) */
|
|
157
|
+
topP?: number;
|
|
158
|
+
/** Top-k sampling (shorthand for chatConfig.topK; wins over chatConfig) */
|
|
159
|
+
topK?: number;
|
|
150
160
|
/** Log level (default: based on NODE_ENV) */
|
|
151
161
|
logLevel?: LogLevel;
|
|
152
162
|
|
|
@@ -563,6 +573,8 @@ export interface SeedOptions {
|
|
|
563
573
|
contextKey?: string;
|
|
564
574
|
explanationKey?: string;
|
|
565
575
|
systemPromptKey?: string;
|
|
576
|
+
/** Model-turn format: 'json' wraps answers in a {data} envelope (Transformer protocol); 'text' stores ANSWER verbatim (prose agents like Chat). Default: 'json' */
|
|
577
|
+
format?: 'json' | 'text';
|
|
566
578
|
}
|
|
567
579
|
|
|
568
580
|
// ── Class Declarations ───────────────────────────────────────────────────────
|
|
@@ -594,8 +606,8 @@ export declare class BaseGemini {
|
|
|
594
606
|
estimateCost(nextPayload: Record<string, unknown> | string): Promise<{
|
|
595
607
|
inputTokens: number;
|
|
596
608
|
model: string;
|
|
597
|
-
pricing: { input: number; output: number };
|
|
598
|
-
estimatedInputCost: number;
|
|
609
|
+
pricing: { input: number; output: number } | null;
|
|
610
|
+
estimatedInputCost: number | null;
|
|
599
611
|
note: string;
|
|
600
612
|
}>;
|
|
601
613
|
|
|
@@ -749,6 +761,17 @@ export declare class ImageGenerator extends BaseGemini {
|
|
|
749
761
|
|
|
750
762
|
export declare function extractJSON(text: string): any;
|
|
751
763
|
export declare function attemptJSONRecovery(text: string, maxAttempts?: number): any | null;
|
|
764
|
+
/** Validates a parsed value against a subset of JSON Schema. Returns error strings ([] means valid). */
|
|
765
|
+
export declare function validateSchema(data: any, schema: Record<string, any>, path?: string): string[];
|
|
766
|
+
|
|
767
|
+
/** Per-million-token pricing keyed by model id. */
|
|
768
|
+
export declare const MODEL_PRICING: Record<string, { input: number; output: number }>;
|
|
769
|
+
/** Floating `-latest` alias → canonical model id, for pricing resolution. */
|
|
770
|
+
export declare const MODEL_ALIASES: Record<string, string>;
|
|
771
|
+
/** Resolves pricing for a model id (follows -latest aliases). null when unknown. */
|
|
772
|
+
export declare function resolvePricing(modelId: string | null | undefined): { input: number; output: number } | null;
|
|
773
|
+
/** Estimated USD cost from token counts. null when the model's pricing is unknown. */
|
|
774
|
+
export declare function computeCost(modelId: string | null | undefined, promptTokens: number, responseTokens: number): number | null;
|
|
752
775
|
|
|
753
776
|
declare const _default: {
|
|
754
777
|
Transformer: typeof Transformer;
|