ak-gemini 2.4.1 → 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 +149 -20
- package/embedding.js +4 -6
- package/image-generator.js +9 -7
- package/index.cjs +210 -41
- package/index.js +2 -1
- package/json-helpers.js +105 -0
- package/message.js +19 -7
- package/package.json +4 -3
- package/types.d.ts +18 -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
|
@@ -71,7 +71,65 @@ const MODEL_PRICING = {
|
|
|
71
71
|
'gemini-embedding-001': { input: 0.15, output: 0 }
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
|
|
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 };
|
|
75
133
|
|
|
76
134
|
// ── BaseGemini Class ─────────────────────────────────────────────────────────
|
|
77
135
|
|
|
@@ -237,18 +295,28 @@ class BaseGemini {
|
|
|
237
295
|
const chatOptions = this._getChatCreateOptions();
|
|
238
296
|
this.chatSession = this.genAIClient.chats.create(chatOptions);
|
|
239
297
|
|
|
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
|
-
}
|
|
298
|
+
await this._healthCheckPing();
|
|
248
299
|
|
|
249
300
|
log.debug(`${this.constructor.name}: Chat session initialized.`);
|
|
250
301
|
}
|
|
251
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
|
+
|
|
252
320
|
/**
|
|
253
321
|
* Builds the options object for `genAIClient.chats.create()`.
|
|
254
322
|
* Override in subclasses to add tools, grounding, etc.
|
|
@@ -416,12 +484,17 @@ class BaseGemini {
|
|
|
416
484
|
*/
|
|
417
485
|
_captureMetadata(response) {
|
|
418
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;
|
|
419
490
|
this.lastResponseMetadata = {
|
|
420
491
|
modelVersion: response.modelVersion || null,
|
|
421
492
|
requestedModel: this.modelName,
|
|
422
|
-
promptTokens
|
|
423
|
-
responseTokens
|
|
424
|
-
|
|
493
|
+
promptTokens,
|
|
494
|
+
responseTokens,
|
|
495
|
+
thoughtsTokens,
|
|
496
|
+
// totalTokenCount includes thoughts; fall back to summing the parts.
|
|
497
|
+
totalTokens: response.usageMetadata?.totalTokenCount || (promptTokens + responseTokens + thoughtsTokens),
|
|
425
498
|
timestamp: Date.now(),
|
|
426
499
|
groundingMetadata: response.candidates?.[0]?.groundingMetadata || null,
|
|
427
500
|
modelStatus
|
|
@@ -441,19 +514,73 @@ class BaseGemini {
|
|
|
441
514
|
if (!this.lastResponseMetadata) return null;
|
|
442
515
|
|
|
443
516
|
const meta = this.lastResponseMetadata;
|
|
444
|
-
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 };
|
|
445
518
|
const useCumulative = cumulative.attempts > 0;
|
|
446
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
|
+
|
|
447
525
|
return {
|
|
448
|
-
promptTokens
|
|
449
|
-
responseTokens
|
|
450
|
-
|
|
526
|
+
promptTokens,
|
|
527
|
+
responseTokens,
|
|
528
|
+
thoughtsTokens,
|
|
529
|
+
totalTokens,
|
|
451
530
|
attempts: useCumulative ? cumulative.attempts : 1,
|
|
452
531
|
modelVersion: meta.modelVersion,
|
|
453
532
|
requestedModel: meta.requestedModel,
|
|
454
533
|
timestamp: meta.timestamp,
|
|
455
534
|
groundingMetadata: meta.groundingMetadata || null,
|
|
456
|
-
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)
|
|
457
584
|
};
|
|
458
585
|
}
|
|
459
586
|
|
|
@@ -500,14 +627,16 @@ class BaseGemini {
|
|
|
500
627
|
*/
|
|
501
628
|
async estimateCost(nextPayload) {
|
|
502
629
|
const tokenInfo = await this.estimate(nextPayload);
|
|
503
|
-
const pricing =
|
|
630
|
+
const pricing = resolvePricing(this.modelName);
|
|
504
631
|
|
|
505
632
|
return {
|
|
506
633
|
inputTokens: tokenInfo.inputTokens,
|
|
507
634
|
model: this.modelName,
|
|
508
635
|
pricing: pricing,
|
|
509
|
-
estimatedInputCost: (tokenInfo.inputTokens / 1_000_000) * pricing.input,
|
|
510
|
-
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`
|
|
511
640
|
};
|
|
512
641
|
}
|
|
513
642
|
|
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");
|
|
@@ -359,6 +437,36 @@ var MODEL_PRICING = {
|
|
|
359
437
|
// Embeddings
|
|
360
438
|
"gemini-embedding-001": { input: 0.15, output: 0 }
|
|
361
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
|
+
}
|
|
362
470
|
var BaseGemini = class {
|
|
363
471
|
/**
|
|
364
472
|
* @param {BaseGeminiOptions} [options={}]
|
|
@@ -453,16 +561,25 @@ var BaseGemini = class {
|
|
|
453
561
|
logger_default.debug(`Initializing ${this.constructor.name} chat session with model: ${this.modelName}...`);
|
|
454
562
|
const chatOptions = this._getChatCreateOptions();
|
|
455
563
|
this.chatSession = this.genAIClient.chats.create(chatOptions);
|
|
456
|
-
|
|
457
|
-
try {
|
|
458
|
-
await this._withRetry(() => this.genAIClient.models.list());
|
|
459
|
-
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
460
|
-
} catch (e) {
|
|
461
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
564
|
+
await this._healthCheckPing();
|
|
464
565
|
logger_default.debug(`${this.constructor.name}: Chat session initialized.`);
|
|
465
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
|
+
}
|
|
466
583
|
/**
|
|
467
584
|
* Builds the options object for `genAIClient.chats.create()`.
|
|
468
585
|
* Override in subclasses to add tools, grounding, etc.
|
|
@@ -609,12 +726,17 @@ ${contextText}
|
|
|
609
726
|
*/
|
|
610
727
|
_captureMetadata(response) {
|
|
611
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;
|
|
612
732
|
this.lastResponseMetadata = {
|
|
613
733
|
modelVersion: response.modelVersion || null,
|
|
614
734
|
requestedModel: this.modelName,
|
|
615
|
-
promptTokens
|
|
616
|
-
responseTokens
|
|
617
|
-
|
|
735
|
+
promptTokens,
|
|
736
|
+
responseTokens,
|
|
737
|
+
thoughtsTokens,
|
|
738
|
+
// totalTokenCount includes thoughts; fall back to summing the parts.
|
|
739
|
+
totalTokens: response.usageMetadata?.totalTokenCount || promptTokens + responseTokens + thoughtsTokens,
|
|
618
740
|
timestamp: Date.now(),
|
|
619
741
|
groundingMetadata: response.candidates?.[0]?.groundingMetadata || null,
|
|
620
742
|
modelStatus
|
|
@@ -632,18 +754,68 @@ ${contextText}
|
|
|
632
754
|
getLastUsage() {
|
|
633
755
|
if (!this.lastResponseMetadata) return null;
|
|
634
756
|
const meta = this.lastResponseMetadata;
|
|
635
|
-
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 };
|
|
636
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;
|
|
637
763
|
return {
|
|
638
|
-
promptTokens
|
|
639
|
-
responseTokens
|
|
640
|
-
|
|
764
|
+
promptTokens,
|
|
765
|
+
responseTokens,
|
|
766
|
+
thoughtsTokens,
|
|
767
|
+
totalTokens,
|
|
641
768
|
attempts: useCumulative ? cumulative.attempts : 1,
|
|
642
769
|
modelVersion: meta.modelVersion,
|
|
643
770
|
requestedModel: meta.requestedModel,
|
|
644
771
|
timestamp: meta.timestamp,
|
|
645
772
|
groundingMetadata: meta.groundingMetadata || null,
|
|
646
|
-
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)
|
|
647
819
|
};
|
|
648
820
|
}
|
|
649
821
|
// ── Token Estimation ─────────────────────────────────────────────────────
|
|
@@ -679,13 +851,13 @@ ${contextText}
|
|
|
679
851
|
*/
|
|
680
852
|
async estimateCost(nextPayload) {
|
|
681
853
|
const tokenInfo = await this.estimate(nextPayload);
|
|
682
|
-
const pricing =
|
|
854
|
+
const pricing = resolvePricing(this.modelName);
|
|
683
855
|
return {
|
|
684
856
|
inputTokens: tokenInfo.inputTokens,
|
|
685
857
|
model: this.modelName,
|
|
686
858
|
pricing,
|
|
687
|
-
estimatedInputCost: tokenInfo.inputTokens / 1e6 * pricing.input,
|
|
688
|
-
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`
|
|
689
861
|
};
|
|
690
862
|
}
|
|
691
863
|
// ── Context Caching ─────────────────────────────────────────────────────
|
|
@@ -1310,6 +1482,9 @@ var Message = class extends base_default {
|
|
|
1310
1482
|
}
|
|
1311
1483
|
if (options.responseMimeType) {
|
|
1312
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'.");
|
|
1313
1488
|
}
|
|
1314
1489
|
this._isStructured = !!(options.responseSchema || options.responseMimeType === "application/json");
|
|
1315
1490
|
logger_default.debug(`Message created (structured=${this._isStructured})`);
|
|
@@ -1323,12 +1498,7 @@ var Message = class extends base_default {
|
|
|
1323
1498
|
async init(force = false) {
|
|
1324
1499
|
if (this._initialized && !force) return;
|
|
1325
1500
|
logger_default.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
1326
|
-
|
|
1327
|
-
await this.genAIClient.models.list();
|
|
1328
|
-
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
1329
|
-
} catch (e) {
|
|
1330
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
1331
|
-
}
|
|
1501
|
+
await this._healthCheckPing();
|
|
1332
1502
|
this._initialized = true;
|
|
1333
1503
|
logger_default.debug(`${this.constructor.name}: Initialized (stateless mode).`);
|
|
1334
1504
|
}
|
|
@@ -1354,10 +1524,12 @@ var Message = class extends base_default {
|
|
|
1354
1524
|
...this.vertexai && Object.keys(mergedLabels).length > 0 && { labels: mergedLabels }
|
|
1355
1525
|
}
|
|
1356
1526
|
}));
|
|
1527
|
+
const usage = this._usageFromResponse(result);
|
|
1357
1528
|
this._captureMetadata(result);
|
|
1358
1529
|
this._cumulativeUsage = {
|
|
1359
1530
|
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
1360
1531
|
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
1532
|
+
thoughtsTokens: this.lastResponseMetadata.thoughtsTokens,
|
|
1361
1533
|
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
1362
1534
|
attempts: 1
|
|
1363
1535
|
};
|
|
@@ -1367,7 +1539,7 @@ var Message = class extends base_default {
|
|
|
1367
1539
|
const text = result.text || "";
|
|
1368
1540
|
const response = {
|
|
1369
1541
|
text,
|
|
1370
|
-
usage
|
|
1542
|
+
usage
|
|
1371
1543
|
};
|
|
1372
1544
|
if (this._isStructured) {
|
|
1373
1545
|
try {
|
|
@@ -2981,12 +3153,7 @@ var Embedding = class extends base_default {
|
|
|
2981
3153
|
async init(force = false) {
|
|
2982
3154
|
if (this._initialized && !force) return;
|
|
2983
3155
|
logger_default.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
2984
|
-
|
|
2985
|
-
await this.genAIClient.models.list();
|
|
2986
|
-
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
2987
|
-
} catch (e) {
|
|
2988
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
2989
|
-
}
|
|
3156
|
+
await this._healthCheckPing();
|
|
2990
3157
|
this._initialized = true;
|
|
2991
3158
|
logger_default.debug(`${this.constructor.name}: Initialized (stateless mode).`);
|
|
2992
3159
|
}
|
|
@@ -3118,12 +3285,7 @@ var ImageGenerator = class extends base_default {
|
|
|
3118
3285
|
async init(force = false) {
|
|
3119
3286
|
if (this._initialized && !force) return;
|
|
3120
3287
|
logger_default.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
3121
|
-
|
|
3122
|
-
await this.genAIClient.models.list();
|
|
3123
|
-
logger_default.debug(`${this.constructor.name}: API connection successful.`);
|
|
3124
|
-
} catch (e) {
|
|
3125
|
-
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
3126
|
-
}
|
|
3288
|
+
await this._healthCheckPing();
|
|
3127
3289
|
this._initialized = true;
|
|
3128
3290
|
}
|
|
3129
3291
|
/**
|
|
@@ -3165,10 +3327,12 @@ var ImageGenerator = class extends base_default {
|
|
|
3165
3327
|
contents: [{ role: "user", parts }],
|
|
3166
3328
|
config: this._buildConfig(opts)
|
|
3167
3329
|
}));
|
|
3330
|
+
const usage = this._usageFromResponse(result);
|
|
3168
3331
|
this._captureMetadata(result);
|
|
3169
3332
|
this._cumulativeUsage = {
|
|
3170
3333
|
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
3171
3334
|
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
3335
|
+
thoughtsTokens: this.lastResponseMetadata.thoughtsTokens,
|
|
3172
3336
|
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
3173
3337
|
attempts: 1
|
|
3174
3338
|
};
|
|
@@ -3188,7 +3352,7 @@ var ImageGenerator = class extends base_default {
|
|
|
3188
3352
|
if (images.length === 0) {
|
|
3189
3353
|
logger_default.warn("ImageGenerator: no images returned. Check prompt or safety filters.");
|
|
3190
3354
|
}
|
|
3191
|
-
return { images, text: text || null, usage
|
|
3355
|
+
return { images, text: text || null, usage };
|
|
3192
3356
|
}
|
|
3193
3357
|
/**
|
|
3194
3358
|
* Convenience: write one or all images to disk.
|
|
@@ -3248,12 +3412,17 @@ var index_default = { Transformer: transformer_default, Chat: chat_default, Mess
|
|
|
3248
3412
|
HarmBlockThreshold,
|
|
3249
3413
|
HarmCategory,
|
|
3250
3414
|
ImageGenerator,
|
|
3415
|
+
MODEL_ALIASES,
|
|
3416
|
+
MODEL_PRICING,
|
|
3251
3417
|
Message,
|
|
3252
3418
|
RagAgent,
|
|
3253
3419
|
ThinkingLevel,
|
|
3254
3420
|
ToolAgent,
|
|
3255
3421
|
Transformer,
|
|
3256
3422
|
attemptJSONRecovery,
|
|
3423
|
+
computeCost,
|
|
3257
3424
|
extractJSON,
|
|
3258
|
-
log
|
|
3425
|
+
log,
|
|
3426
|
+
resolvePricing,
|
|
3427
|
+
validateSchema
|
|
3259
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/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 {
|
|
@@ -602,8 +606,8 @@ export declare class BaseGemini {
|
|
|
602
606
|
estimateCost(nextPayload: Record<string, unknown> | string): Promise<{
|
|
603
607
|
inputTokens: number;
|
|
604
608
|
model: string;
|
|
605
|
-
pricing: { input: number; output: number };
|
|
606
|
-
estimatedInputCost: number;
|
|
609
|
+
pricing: { input: number; output: number } | null;
|
|
610
|
+
estimatedInputCost: number | null;
|
|
607
611
|
note: string;
|
|
608
612
|
}>;
|
|
609
613
|
|
|
@@ -757,6 +761,17 @@ export declare class ImageGenerator extends BaseGemini {
|
|
|
757
761
|
|
|
758
762
|
export declare function extractJSON(text: string): any;
|
|
759
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;
|
|
760
775
|
|
|
761
776
|
declare const _default: {
|
|
762
777
|
Transformer: typeof Transformer;
|