ak-gemini 2.2.1 → 2.4.1

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/base.js CHANGED
@@ -25,26 +25,50 @@ const DEFAULT_THINKING_CONFIG = {
25
25
 
26
26
  const DEFAULT_MAX_OUTPUT_TOKENS = 50_000;
27
27
 
28
- /** Models that support thinking features */
28
+ /** Models that support thinking features. Image / live / tts variants intentionally excluded. */
29
29
  const THINKING_SUPPORTED_MODELS = [
30
- /^gemini-3-flash(-preview)?$/,
31
- /^gemini-3-pro(-preview|-image-preview)?$/,
30
+ /^gemini-3(\.\d+)?-pro(-preview)?$/,
31
+ /^gemini-3(\.\d+)?-flash(-preview)?$/,
32
+ /^gemini-3(\.\d+)?-flash-lite(-preview)?$/,
32
33
  /^gemini-2\.5-pro/,
33
34
  /^gemini-2\.5-flash(-preview)?$/,
34
35
  /^gemini-2\.5-flash-lite(-preview)?$/,
35
36
  /^gemini-2\.0-flash$/
36
37
  ];
37
38
 
38
- /** Model pricing per million tokens (as of Dec 2025) */
39
+ /**
40
+ * Model pricing per million tokens (Paid Tier Standard, base rate, as of July 2026).
41
+ * Source: https://ai.google.dev/gemini-api/docs/pricing
42
+ *
43
+ * NOTES:
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).
47
+ * - Image-output tokens on Nano Banana models bill at $60/M (1.5 Flash Image) or $120/M (3 Pro Image).
48
+ * Only text-input/text-output rates are modelled here; image-output cost is NOT included in estimateCost().
49
+ * - Audio input is more expensive on most models — listed rate covers text/image/video input.
50
+ */
39
51
  const MODEL_PRICING = {
40
- 'gemini-2.5-flash': { input: 0.15, output: 0.60 },
41
- 'gemini-2.5-flash-lite': { input: 0.02, output: 0.10 },
42
- 'gemini-2.5-pro': { input: 2.50, output: 10.00 },
43
- 'gemini-3-pro': { input: 2.00, output: 12.00 },
44
- 'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
52
+ // Gemini 3.x stable
53
+ 'gemini-3.5-flash': { input: 1.50, output: 9.00 },
54
+ 'gemini-3.1-flash-lite': { input: 0.25, output: 1.50 },
55
+ // Gemini 3.x preview
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)
58
+ 'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
59
+ 'gemini-3.1-flash-lite-preview': { input: 0.25, output: 1.50 },
60
+ 'gemini-3.1-flash-image-preview': { input: 0.50, output: 3.00 }, // text-only; image-output is $60/M
61
+ 'gemini-3-pro-image-preview': { input: 2.00, output: 12.00 }, // text-only; image-output is $120/M
62
+ // Gemini 2.5 stable
63
+ 'gemini-2.5-flash': { input: 0.30, output: 2.50 },
64
+ 'gemini-2.5-flash-lite': { input: 0.10, output: 0.40 },
65
+ 'gemini-2.5-pro': { input: 1.25, output: 10.00 }, // ≤200k tier
66
+ 'gemini-2.5-flash-image': { input: 0.30, output: 0 }, // image-output is ~$0.039/image (1290 tokens)
67
+ // Deprecated but kept for back-compat (shut down June 2026)
45
68
  'gemini-2.0-flash': { input: 0.10, output: 0.40 },
46
69
  'gemini-2.0-flash-lite': { input: 0.02, output: 0.10 },
47
- 'gemini-embedding-001': { input: 0.006, output: 0 }
70
+ // Embeddings
71
+ 'gemini-embedding-001': { input: 0.15, output: 0 }
48
72
  };
49
73
 
50
74
  export { DEFAULT_SAFETY_SETTINGS, DEFAULT_THINKING_CONFIG, THINKING_SUPPORTED_MODELS, MODEL_PRICING, DEFAULT_MAX_OUTPUT_TOKENS };
@@ -70,7 +94,7 @@ class BaseGemini {
70
94
  */
71
95
  constructor(options = {}) {
72
96
  // ── Model ──
73
- this.modelName = options.modelName || 'gemini-2.5-flash';
97
+ this.modelName = options.modelName || 'gemini-3-flash-preview';
74
98
 
75
99
  // ── System Prompt ──
76
100
  // Subclasses set their own default if options.systemPrompt is undefined
@@ -114,6 +138,14 @@ class BaseGemini {
114
138
  // ── Caching ──
115
139
  this.cachedContent = options.cachedContent || null;
116
140
 
141
+ // ── Service Tier (Gemini API / Vertex AI 2026+) ──
142
+ // Allowed values: 'STANDARD' | 'FLEX' | 'PRIORITY' — cost vs latency trade.
143
+ this.serviceTier = options.serviceTier || null;
144
+
145
+ // ── Server-Side Tool Invocation Visibility (1.46.0+) ──
146
+ // When grounding is on, surface the server's tool calls (e.g. Google Search) in the response.
147
+ this.includeServerSideToolInvocations = options.includeServerSideToolInvocations ?? false;
148
+
117
149
  // ── Chat Config ──
118
150
  this.chatConfig = {
119
151
  temperature: 0.7,
@@ -123,6 +155,16 @@ class BaseGemini {
123
155
  ...options.chatConfig
124
156
  };
125
157
 
158
+ if (this.serviceTier) this.chatConfig['serviceTier'] = this.serviceTier;
159
+ if (this.includeServerSideToolInvocations) this.chatConfig['includeServerSideToolInvocations'] = true;
160
+
161
+ // ── Sampling Shortcuts ──
162
+ // Top-level sugar for chatConfig sampling knobs; wins over chatConfig
163
+ // (same precedence as the maxOutputTokens promotion below)
164
+ for (const key of ['temperature', 'topP', 'topK']) {
165
+ if (options[key] !== undefined) this.chatConfig[key] = options[key];
166
+ }
167
+
126
168
  // Apply systemPrompt to chatConfig
127
169
  if (this.systemPrompt) {
128
170
  this.chatConfig.systemInstruction = this.systemPrompt;
@@ -289,6 +331,7 @@ class BaseGemini {
289
331
  * @param {string} [opts.contextKey='CONTEXT'] - Key for optional context
290
332
  * @param {string} [opts.explanationKey='EXPLANATION'] - Key for optional explanations
291
333
  * @param {string} [opts.systemPromptKey='SYSTEM'] - Key for system prompt overrides in examples
334
+ * @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)
292
335
  * @returns {Promise<Array>} The updated chat history
293
336
  */
294
337
  async seed(examples, opts = {}) {
@@ -304,6 +347,7 @@ class BaseGemini {
304
347
  const contextKey = opts.contextKey || 'CONTEXT';
305
348
  const explanationKey = opts.explanationKey || 'EXPLANATION';
306
349
  const systemPromptKey = opts.systemPromptKey || 'SYSTEM';
350
+ const format = opts.format || 'json';
307
351
 
308
352
  // Check for system prompt override in examples
309
353
  const instructionExample = examples.find(ex => ex[systemPromptKey]);
@@ -335,9 +379,15 @@ class BaseGemini {
335
379
  userText += promptText;
336
380
  }
337
381
 
338
- if (answerValue) modelResponse.data = answerValue;
339
- if (explanationValue) modelResponse.explanation = explanationValue;
340
- const modelText = JSON.stringify(modelResponse, null, 2);
382
+ let modelText;
383
+ if (format === 'text') {
384
+ modelText = isJSON(answerValue) ? JSON.stringify(answerValue, null, 2) : String(answerValue || '');
385
+ if (explanationValue) log.warn('seed(): EXPLANATION has no representation in text format; ignored.');
386
+ } else {
387
+ if (answerValue) modelResponse.data = answerValue;
388
+ if (explanationValue) modelResponse.explanation = explanationValue;
389
+ modelText = JSON.stringify(modelResponse, null, 2);
390
+ }
341
391
 
342
392
  if (userText.trim().length && modelText.trim().length > 0) {
343
393
  historyToAdd.push({ role: 'user', parts: [{ text: userText.trim() }] });
@@ -365,6 +415,7 @@ class BaseGemini {
365
415
  * @protected
366
416
  */
367
417
  _captureMetadata(response) {
418
+ const modelStatus = response?.modelStatus || null;
368
419
  this.lastResponseMetadata = {
369
420
  modelVersion: response.modelVersion || null,
370
421
  requestedModel: this.modelName,
@@ -372,8 +423,13 @@ class BaseGemini {
372
423
  responseTokens: response.usageMetadata?.candidatesTokenCount || 0,
373
424
  totalTokens: response.usageMetadata?.totalTokenCount || 0,
374
425
  timestamp: Date.now(),
375
- groundingMetadata: response.candidates?.[0]?.groundingMetadata || null
426
+ groundingMetadata: response.candidates?.[0]?.groundingMetadata || null,
427
+ modelStatus
376
428
  };
429
+ if (modelStatus === 'DEPRECATED' && !this._deprecationWarned) {
430
+ log.warn(`Model "${this.modelName}" is marked DEPRECATED by Google. Plan migration.`);
431
+ this._deprecationWarned = true;
432
+ }
377
433
  }
378
434
 
379
435
  /**
@@ -396,7 +452,8 @@ class BaseGemini {
396
452
  modelVersion: meta.modelVersion,
397
453
  requestedModel: meta.requestedModel,
398
454
  timestamp: meta.timestamp,
399
- groundingMetadata: meta.groundingMetadata || null
455
+ groundingMetadata: meta.groundingMetadata || null,
456
+ modelStatus: meta.modelStatus || null
400
457
  };
401
458
  }
402
459
 
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
  *
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @fileoverview ImageGenerator — Generate images via Gemini's Nano Banana models.
3
+ *
4
+ * Extends BaseGemini for auth/client reuse but overrides init() to skip chat session
5
+ * creation (image gen is stateless). Mirrors the Embedding class pattern.
6
+ *
7
+ * @example
8
+ * ```javascript
9
+ * import { ImageGenerator } from 'ak-gemini';
10
+ * import { writeFileSync } from 'node:fs';
11
+ *
12
+ * const gen = new ImageGenerator({ apiKey: 'your-key' });
13
+ * const result = await gen.generate('A cat astronaut on the moon');
14
+ * writeFileSync('cat.png', Buffer.from(result.images[0].data, 'base64'));
15
+ * ```
16
+ */
17
+
18
+ import BaseGemini from './base.js';
19
+ import log from './logger.js';
20
+ import { writeFileSync } from 'node:fs';
21
+
22
+ const DEFAULT_IMAGE_MODEL = 'gemini-3.1-flash-image-preview';
23
+
24
+ export default class ImageGenerator extends BaseGemini {
25
+
26
+ /**
27
+ * @param {import('./types.d.ts').ImageGeneratorOptions} [options={}]
28
+ */
29
+ constructor(options = {}) {
30
+ if (options.modelName === undefined) {
31
+ options = { ...options, modelName: DEFAULT_IMAGE_MODEL };
32
+ }
33
+ if (options.systemPrompt === undefined) {
34
+ options = { ...options, systemPrompt: null };
35
+ }
36
+ super(options);
37
+
38
+ this.aspectRatio = options.aspectRatio || null;
39
+ this.imageSize = options.imageSize || null;
40
+ this.personGeneration = options.personGeneration || null;
41
+ this.includeText = options.includeText ?? false;
42
+
43
+ log.debug(`ImageGenerator created with model: ${this.modelName}`);
44
+ }
45
+
46
+ /**
47
+ * Validate API connection only; no chat session (stateless).
48
+ * @param {boolean} [force=false]
49
+ */
50
+ async init(force = false) {
51
+ if (this._initialized && !force) return;
52
+
53
+ log.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
54
+
55
+ try {
56
+ await this.genAIClient.models.list();
57
+ log.debug(`${this.constructor.name}: API connection successful.`);
58
+ } catch (e) {
59
+ throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
60
+ }
61
+
62
+ this._initialized = true;
63
+ }
64
+
65
+ /**
66
+ * Build a FRESH config — Gemini image models reject safetySettings/temp/topK/topP/thinkingConfig.
67
+ * Do NOT spread this.chatConfig.
68
+ * @private
69
+ */
70
+ _buildConfig(overrides = {}) {
71
+ const includeText = overrides.includeText ?? this.includeText;
72
+ const config = { responseModalities: includeText ? ['IMAGE', 'TEXT'] : ['IMAGE'] };
73
+
74
+ const imageConfig = {};
75
+ const aspectRatio = overrides.aspectRatio || this.aspectRatio;
76
+ const imageSize = overrides.imageSize || this.imageSize;
77
+ const personGeneration = overrides.personGeneration || this.personGeneration;
78
+ if (aspectRatio) imageConfig.aspectRatio = aspectRatio;
79
+ if (imageSize) imageConfig.imageSize = imageSize;
80
+ if (personGeneration) imageConfig.personGeneration = personGeneration;
81
+ if (Object.keys(imageConfig).length > 0) config.imageConfig = imageConfig;
82
+
83
+ return config;
84
+ }
85
+
86
+ /**
87
+ * Generate one or more images from a text prompt.
88
+ * Optionally accepts `inputImages` for image editing / multi-image composition.
89
+ *
90
+ * @param {string} prompt
91
+ * @param {import('./types.d.ts').ImageGenerateOptions} [opts={}]
92
+ * @returns {Promise<import('./types.d.ts').ImageGenerationResult>}
93
+ */
94
+ async generate(prompt, opts = {}) {
95
+ if (!this._initialized) await this.init();
96
+
97
+ /** @type {any[]} */
98
+ const parts = [{ text: prompt }];
99
+ if (Array.isArray(opts.inputImages)) {
100
+ for (const img of opts.inputImages) {
101
+ parts.push({ inlineData: { data: img.data, mimeType: img.mimeType } });
102
+ }
103
+ }
104
+
105
+ const result = await this._withRetry(() => this.genAIClient.models.generateContent({
106
+ model: this.modelName,
107
+ contents: [{ role: 'user', parts }],
108
+ config: this._buildConfig(opts)
109
+ }));
110
+
111
+ this._captureMetadata(result);
112
+ this._cumulativeUsage = {
113
+ promptTokens: this.lastResponseMetadata.promptTokens,
114
+ responseTokens: this.lastResponseMetadata.responseTokens,
115
+ totalTokens: this.lastResponseMetadata.totalTokens,
116
+ attempts: 1
117
+ };
118
+
119
+ const images = [];
120
+ let text = '';
121
+ const responseParts = result.candidates?.[0]?.content?.parts || [];
122
+ for (const part of responseParts) {
123
+ if (part.inlineData?.data) {
124
+ images.push({
125
+ data: part.inlineData.data,
126
+ mimeType: part.inlineData.mimeType || 'image/png'
127
+ });
128
+ } else if (part.text) {
129
+ text += part.text;
130
+ }
131
+ }
132
+
133
+ if (images.length === 0) {
134
+ log.warn('ImageGenerator: no images returned. Check prompt or safety filters.');
135
+ }
136
+
137
+ return { images, text: text || null, usage: this.getLastUsage() };
138
+ }
139
+
140
+ /**
141
+ * Convenience: write one or all images to disk.
142
+ * If multiple images, suffixes with `_N` before extension.
143
+ * @param {import('./types.d.ts').ImageGenerationResult} result
144
+ * @param {string} filePath
145
+ * @returns {string[]} Written file paths
146
+ */
147
+ save(result, filePath) {
148
+ if (!result?.images?.length) {
149
+ log.warn('ImageGenerator.save(): no images to save.');
150
+ return [];
151
+ }
152
+ const paths = [];
153
+ const dot = filePath.lastIndexOf('.');
154
+ const base = dot >= 0 ? filePath.slice(0, dot) : filePath;
155
+ const ext = dot >= 0 ? filePath.slice(dot) : '.png';
156
+ result.images.forEach((img, i) => {
157
+ const out = result.images.length === 1 ? filePath : `${base}_${i}${ext}`;
158
+ writeFileSync(out, Buffer.from(img.data, 'base64'));
159
+ paths.push(out);
160
+ });
161
+ return paths;
162
+ }
163
+
164
+ // ── No-ops (image gen is stateless) ──
165
+
166
+ /** @returns {any[]} Always returns empty array */
167
+ getHistory() { return []; }
168
+
169
+ /** No-op for ImageGenerator */
170
+ async clearHistory() {}
171
+
172
+ /** No-op for ImageGenerator */
173
+ async seed() {
174
+ log.warn('ImageGenerator.seed() is a no-op — image generation does not support few-shot.');
175
+ return [];
176
+ }
177
+
178
+ /**
179
+ * @param {any} _nextPayload
180
+ * @throws {Error} ImageGenerator does not support token estimation
181
+ * @returns {Promise<{ inputTokens: number }>}
182
+ */
183
+ async estimate(_nextPayload) {
184
+ throw new Error('ImageGenerator does not support token estimation. Use generate() directly.');
185
+ }
186
+ }
package/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  Embedding: () => Embedding,
36
36
  HarmBlockThreshold: () => import_genai2.HarmBlockThreshold,
37
37
  HarmCategory: () => import_genai2.HarmCategory,
38
+ ImageGenerator: () => ImageGenerator,
38
39
  Message: () => message_default,
39
40
  RagAgent: () => rag_agent_default,
40
41
  ThinkingLevel: () => import_genai2.ThinkingLevel,
@@ -322,29 +323,48 @@ var DEFAULT_THINKING_CONFIG = {
322
323
  };
323
324
  var DEFAULT_MAX_OUTPUT_TOKENS = 5e4;
324
325
  var THINKING_SUPPORTED_MODELS = [
325
- /^gemini-3-flash(-preview)?$/,
326
- /^gemini-3-pro(-preview|-image-preview)?$/,
326
+ /^gemini-3(\.\d+)?-pro(-preview)?$/,
327
+ /^gemini-3(\.\d+)?-flash(-preview)?$/,
328
+ /^gemini-3(\.\d+)?-flash-lite(-preview)?$/,
327
329
  /^gemini-2\.5-pro/,
328
330
  /^gemini-2\.5-flash(-preview)?$/,
329
331
  /^gemini-2\.5-flash-lite(-preview)?$/,
330
332
  /^gemini-2\.0-flash$/
331
333
  ];
332
334
  var MODEL_PRICING = {
333
- "gemini-2.5-flash": { input: 0.15, output: 0.6 },
334
- "gemini-2.5-flash-lite": { input: 0.02, output: 0.1 },
335
- "gemini-2.5-pro": { input: 2.5, output: 10 },
336
- "gemini-3-pro": { input: 2, output: 12 },
335
+ // Gemini 3.x stable
336
+ "gemini-3.5-flash": { input: 1.5, output: 9 },
337
+ "gemini-3.1-flash-lite": { input: 0.25, output: 1.5 },
338
+ // Gemini 3.x preview
339
+ "gemini-3.1-pro-preview": { input: 2, output: 12 },
340
+ // ≤200k tier
337
341
  "gemini-3-pro-preview": { input: 2, output: 12 },
342
+ // ≤200k tier; launch rate (superseded by 3.1, off the pricing page)
343
+ "gemini-3-flash-preview": { input: 0.5, output: 3 },
344
+ "gemini-3.1-flash-lite-preview": { input: 0.25, output: 1.5 },
345
+ "gemini-3.1-flash-image-preview": { input: 0.5, output: 3 },
346
+ // text-only; image-output is $60/M
347
+ "gemini-3-pro-image-preview": { input: 2, output: 12 },
348
+ // text-only; image-output is $120/M
349
+ // Gemini 2.5 stable
350
+ "gemini-2.5-flash": { input: 0.3, output: 2.5 },
351
+ "gemini-2.5-flash-lite": { input: 0.1, output: 0.4 },
352
+ "gemini-2.5-pro": { input: 1.25, output: 10 },
353
+ // ≤200k tier
354
+ "gemini-2.5-flash-image": { input: 0.3, output: 0 },
355
+ // image-output is ~$0.039/image (1290 tokens)
356
+ // Deprecated but kept for back-compat (shut down June 2026)
338
357
  "gemini-2.0-flash": { input: 0.1, output: 0.4 },
339
358
  "gemini-2.0-flash-lite": { input: 0.02, output: 0.1 },
340
- "gemini-embedding-001": { input: 6e-3, output: 0 }
359
+ // Embeddings
360
+ "gemini-embedding-001": { input: 0.15, output: 0 }
341
361
  };
342
362
  var BaseGemini = class {
343
363
  /**
344
364
  * @param {BaseGeminiOptions} [options={}]
345
365
  */
346
366
  constructor(options = {}) {
347
- this.modelName = options.modelName || "gemini-2.5-flash";
367
+ this.modelName = options.modelName || "gemini-3-flash-preview";
348
368
  if (options.systemPrompt !== void 0) {
349
369
  this.systemPrompt = options.systemPrompt;
350
370
  } else {
@@ -369,6 +389,8 @@ var BaseGemini = class {
369
389
  this.enableGrounding = options.enableGrounding || false;
370
390
  this.groundingConfig = options.groundingConfig || {};
371
391
  this.cachedContent = options.cachedContent || null;
392
+ this.serviceTier = options.serviceTier || null;
393
+ this.includeServerSideToolInvocations = options.includeServerSideToolInvocations ?? false;
372
394
  this.chatConfig = {
373
395
  temperature: 0.7,
374
396
  topP: 0.95,
@@ -376,6 +398,11 @@ var BaseGemini = class {
376
398
  safetySettings: DEFAULT_SAFETY_SETTINGS,
377
399
  ...options.chatConfig
378
400
  };
401
+ if (this.serviceTier) this.chatConfig["serviceTier"] = this.serviceTier;
402
+ if (this.includeServerSideToolInvocations) this.chatConfig["includeServerSideToolInvocations"] = true;
403
+ for (const key of ["temperature", "topP", "topK"]) {
404
+ if (options[key] !== void 0) this.chatConfig[key] = options[key];
405
+ }
379
406
  if (this.systemPrompt) {
380
407
  this.chatConfig.systemInstruction = this.systemPrompt;
381
408
  } else if (this.systemPrompt === null && options.systemPrompt === void 0) {
@@ -509,6 +536,7 @@ var BaseGemini = class {
509
536
  * @param {string} [opts.contextKey='CONTEXT'] - Key for optional context
510
537
  * @param {string} [opts.explanationKey='EXPLANATION'] - Key for optional explanations
511
538
  * @param {string} [opts.systemPromptKey='SYSTEM'] - Key for system prompt overrides in examples
539
+ * @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)
512
540
  * @returns {Promise<Array>} The updated chat history
513
541
  */
514
542
  async seed(examples, opts = {}) {
@@ -522,6 +550,7 @@ var BaseGemini = class {
522
550
  const contextKey = opts.contextKey || "CONTEXT";
523
551
  const explanationKey = opts.explanationKey || "EXPLANATION";
524
552
  const systemPromptKey = opts.systemPromptKey || "SYSTEM";
553
+ const format = opts.format || "json";
525
554
  const instructionExample = examples.find((ex) => ex[systemPromptKey]);
526
555
  if (instructionExample) {
527
556
  logger_default.debug(`Found system prompt in examples; reinitializing chat.`);
@@ -550,9 +579,15 @@ ${contextText}
550
579
  let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
551
580
  userText += promptText;
552
581
  }
553
- if (answerValue) modelResponse.data = answerValue;
554
- if (explanationValue) modelResponse.explanation = explanationValue;
555
- const modelText = JSON.stringify(modelResponse, null, 2);
582
+ let modelText;
583
+ if (format === "text") {
584
+ modelText = isJSON(answerValue) ? JSON.stringify(answerValue, null, 2) : String(answerValue || "");
585
+ if (explanationValue) logger_default.warn("seed(): EXPLANATION has no representation in text format; ignored.");
586
+ } else {
587
+ if (answerValue) modelResponse.data = answerValue;
588
+ if (explanationValue) modelResponse.explanation = explanationValue;
589
+ modelText = JSON.stringify(modelResponse, null, 2);
590
+ }
556
591
  if (userText.trim().length && modelText.trim().length > 0) {
557
592
  historyToAdd.push({ role: "user", parts: [{ text: userText.trim() }] });
558
593
  historyToAdd.push({ role: "model", parts: [{ text: modelText.trim() }] });
@@ -573,6 +608,7 @@ ${contextText}
573
608
  * @protected
574
609
  */
575
610
  _captureMetadata(response) {
611
+ const modelStatus = response?.modelStatus || null;
576
612
  this.lastResponseMetadata = {
577
613
  modelVersion: response.modelVersion || null,
578
614
  requestedModel: this.modelName,
@@ -580,8 +616,13 @@ ${contextText}
580
616
  responseTokens: response.usageMetadata?.candidatesTokenCount || 0,
581
617
  totalTokens: response.usageMetadata?.totalTokenCount || 0,
582
618
  timestamp: Date.now(),
583
- groundingMetadata: response.candidates?.[0]?.groundingMetadata || null
619
+ groundingMetadata: response.candidates?.[0]?.groundingMetadata || null,
620
+ modelStatus
584
621
  };
622
+ if (modelStatus === "DEPRECATED" && !this._deprecationWarned) {
623
+ logger_default.warn(`Model "${this.modelName}" is marked DEPRECATED by Google. Plan migration.`);
624
+ this._deprecationWarned = true;
625
+ }
585
626
  }
586
627
  /**
587
628
  * Returns structured usage data from the last API call for billing verification.
@@ -601,7 +642,8 @@ ${contextText}
601
642
  modelVersion: meta.modelVersion,
602
643
  requestedModel: meta.requestedModel,
603
644
  timestamp: meta.timestamp,
604
- groundingMetadata: meta.groundingMetadata || null
645
+ groundingMetadata: meta.groundingMetadata || null,
646
+ modelStatus: meta.modelStatus || null
605
647
  };
606
648
  }
607
649
  // ── Token Estimation ─────────────────────────────────────────────────────
@@ -909,7 +951,8 @@ var Transformer = class extends base_default {
909
951
  answerKey: this.answerKey,
910
952
  contextKey: this.contextKey,
911
953
  explanationKey: this.explanationKey,
912
- systemPromptKey: this.systemPromptKey
954
+ systemPromptKey: this.systemPromptKey,
955
+ format: "json"
913
956
  });
914
957
  }
915
958
  // ── Primary Send Method ──────────────────────────────────────────────────
@@ -1187,6 +1230,18 @@ var Chat = class extends base_default {
1187
1230
  super(options);
1188
1231
  logger_default.debug(`Chat created with model: ${this.modelName}`);
1189
1232
  }
1233
+ /**
1234
+ * Seeds the conversation with example pairs stored as plain prose turns.
1235
+ * Chat is a prose agent — model turns are stored verbatim, not wrapped in
1236
+ * Transformer's {data} JSON envelope.
1237
+ *
1238
+ * @param {import('./types').TransformationExample[]} [examples]
1239
+ * @param {import('./types').SeedOptions} [opts={}]
1240
+ * @returns {Promise<Array>} The updated chat history
1241
+ */
1242
+ async seed(examples, opts = {}) {
1243
+ return super.seed(examples, { format: "text", ...opts });
1244
+ }
1190
1245
  /**
1191
1246
  * Send a text message and get a response. Adds to conversation history.
1192
1247
  *
@@ -3035,9 +3090,155 @@ var Embedding = class extends base_default {
3035
3090
  }
3036
3091
  };
3037
3092
 
3093
+ // image-generator.js
3094
+ var import_node_fs = require("node:fs");
3095
+ var DEFAULT_IMAGE_MODEL = "gemini-3.1-flash-image-preview";
3096
+ var ImageGenerator = class extends base_default {
3097
+ /**
3098
+ * @param {import('./types.d.ts').ImageGeneratorOptions} [options={}]
3099
+ */
3100
+ constructor(options = {}) {
3101
+ if (options.modelName === void 0) {
3102
+ options = { ...options, modelName: DEFAULT_IMAGE_MODEL };
3103
+ }
3104
+ if (options.systemPrompt === void 0) {
3105
+ options = { ...options, systemPrompt: null };
3106
+ }
3107
+ super(options);
3108
+ this.aspectRatio = options.aspectRatio || null;
3109
+ this.imageSize = options.imageSize || null;
3110
+ this.personGeneration = options.personGeneration || null;
3111
+ this.includeText = options.includeText ?? false;
3112
+ logger_default.debug(`ImageGenerator created with model: ${this.modelName}`);
3113
+ }
3114
+ /**
3115
+ * Validate API connection only; no chat session (stateless).
3116
+ * @param {boolean} [force=false]
3117
+ */
3118
+ async init(force = false) {
3119
+ if (this._initialized && !force) return;
3120
+ logger_default.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
3121
+ try {
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
+ }
3127
+ this._initialized = true;
3128
+ }
3129
+ /**
3130
+ * Build a FRESH config — Gemini image models reject safetySettings/temp/topK/topP/thinkingConfig.
3131
+ * Do NOT spread this.chatConfig.
3132
+ * @private
3133
+ */
3134
+ _buildConfig(overrides = {}) {
3135
+ const includeText = overrides.includeText ?? this.includeText;
3136
+ const config = { responseModalities: includeText ? ["IMAGE", "TEXT"] : ["IMAGE"] };
3137
+ const imageConfig = {};
3138
+ const aspectRatio = overrides.aspectRatio || this.aspectRatio;
3139
+ const imageSize = overrides.imageSize || this.imageSize;
3140
+ const personGeneration = overrides.personGeneration || this.personGeneration;
3141
+ if (aspectRatio) imageConfig.aspectRatio = aspectRatio;
3142
+ if (imageSize) imageConfig.imageSize = imageSize;
3143
+ if (personGeneration) imageConfig.personGeneration = personGeneration;
3144
+ if (Object.keys(imageConfig).length > 0) config.imageConfig = imageConfig;
3145
+ return config;
3146
+ }
3147
+ /**
3148
+ * Generate one or more images from a text prompt.
3149
+ * Optionally accepts `inputImages` for image editing / multi-image composition.
3150
+ *
3151
+ * @param {string} prompt
3152
+ * @param {import('./types.d.ts').ImageGenerateOptions} [opts={}]
3153
+ * @returns {Promise<import('./types.d.ts').ImageGenerationResult>}
3154
+ */
3155
+ async generate(prompt, opts = {}) {
3156
+ if (!this._initialized) await this.init();
3157
+ const parts = [{ text: prompt }];
3158
+ if (Array.isArray(opts.inputImages)) {
3159
+ for (const img of opts.inputImages) {
3160
+ parts.push({ inlineData: { data: img.data, mimeType: img.mimeType } });
3161
+ }
3162
+ }
3163
+ const result = await this._withRetry(() => this.genAIClient.models.generateContent({
3164
+ model: this.modelName,
3165
+ contents: [{ role: "user", parts }],
3166
+ config: this._buildConfig(opts)
3167
+ }));
3168
+ this._captureMetadata(result);
3169
+ this._cumulativeUsage = {
3170
+ promptTokens: this.lastResponseMetadata.promptTokens,
3171
+ responseTokens: this.lastResponseMetadata.responseTokens,
3172
+ totalTokens: this.lastResponseMetadata.totalTokens,
3173
+ attempts: 1
3174
+ };
3175
+ const images = [];
3176
+ let text = "";
3177
+ const responseParts = result.candidates?.[0]?.content?.parts || [];
3178
+ for (const part of responseParts) {
3179
+ if (part.inlineData?.data) {
3180
+ images.push({
3181
+ data: part.inlineData.data,
3182
+ mimeType: part.inlineData.mimeType || "image/png"
3183
+ });
3184
+ } else if (part.text) {
3185
+ text += part.text;
3186
+ }
3187
+ }
3188
+ if (images.length === 0) {
3189
+ logger_default.warn("ImageGenerator: no images returned. Check prompt or safety filters.");
3190
+ }
3191
+ return { images, text: text || null, usage: this.getLastUsage() };
3192
+ }
3193
+ /**
3194
+ * Convenience: write one or all images to disk.
3195
+ * If multiple images, suffixes with `_N` before extension.
3196
+ * @param {import('./types.d.ts').ImageGenerationResult} result
3197
+ * @param {string} filePath
3198
+ * @returns {string[]} Written file paths
3199
+ */
3200
+ save(result, filePath) {
3201
+ if (!result?.images?.length) {
3202
+ logger_default.warn("ImageGenerator.save(): no images to save.");
3203
+ return [];
3204
+ }
3205
+ const paths = [];
3206
+ const dot = filePath.lastIndexOf(".");
3207
+ const base = dot >= 0 ? filePath.slice(0, dot) : filePath;
3208
+ const ext = dot >= 0 ? filePath.slice(dot) : ".png";
3209
+ result.images.forEach((img, i) => {
3210
+ const out = result.images.length === 1 ? filePath : `${base}_${i}${ext}`;
3211
+ (0, import_node_fs.writeFileSync)(out, Buffer.from(img.data, "base64"));
3212
+ paths.push(out);
3213
+ });
3214
+ return paths;
3215
+ }
3216
+ // ── No-ops (image gen is stateless) ──
3217
+ /** @returns {any[]} Always returns empty array */
3218
+ getHistory() {
3219
+ return [];
3220
+ }
3221
+ /** No-op for ImageGenerator */
3222
+ async clearHistory() {
3223
+ }
3224
+ /** No-op for ImageGenerator */
3225
+ async seed() {
3226
+ logger_default.warn("ImageGenerator.seed() is a no-op \u2014 image generation does not support few-shot.");
3227
+ return [];
3228
+ }
3229
+ /**
3230
+ * @param {any} _nextPayload
3231
+ * @throws {Error} ImageGenerator does not support token estimation
3232
+ * @returns {Promise<{ inputTokens: number }>}
3233
+ */
3234
+ async estimate(_nextPayload) {
3235
+ throw new Error("ImageGenerator does not support token estimation. Use generate() directly.");
3236
+ }
3237
+ };
3238
+
3038
3239
  // index.js
3039
3240
  var import_genai2 = require("@google/genai");
3040
- var index_default = { Transformer: transformer_default, Chat: chat_default, Message: message_default, ToolAgent: tool_agent_default, CodeAgent: code_agent_default, RagAgent: rag_agent_default, Embedding };
3241
+ var index_default = { Transformer: transformer_default, Chat: chat_default, Message: message_default, ToolAgent: tool_agent_default, CodeAgent: code_agent_default, RagAgent: rag_agent_default, Embedding, ImageGenerator };
3041
3242
  // Annotate the CommonJS export names for ESM import in node:
3042
3243
  0 && (module.exports = {
3043
3244
  BaseGemini,
@@ -3046,6 +3247,7 @@ var index_default = { Transformer: transformer_default, Chat: chat_default, Mess
3046
3247
  Embedding,
3047
3248
  HarmBlockThreshold,
3048
3249
  HarmCategory,
3250
+ ImageGenerator,
3049
3251
  Message,
3050
3252
  RagAgent,
3051
3253
  ThinkingLevel,
package/index.js CHANGED
@@ -27,6 +27,7 @@ export { default as ToolAgent } from './tool-agent.js';
27
27
  export { default as CodeAgent } from './code-agent.js';
28
28
  export { default as RagAgent } from './rag-agent.js';
29
29
  export { default as Embedding } from './embedding.js';
30
+ export { default as ImageGenerator } from './image-generator.js';
30
31
  export { default as BaseGemini } from './base.js';
31
32
  export { default as log } from './logger.js';
32
33
  export { ThinkingLevel, HarmCategory, HarmBlockThreshold } from '@google/genai';
@@ -41,5 +42,6 @@ import ToolAgent from './tool-agent.js';
41
42
  import CodeAgent from './code-agent.js';
42
43
  import RagAgent from './rag-agent.js';
43
44
  import Embedding from './embedding.js';
45
+ import ImageGenerator from './image-generator.js';
44
46
 
45
- export default { Transformer, Chat, Message, ToolAgent, CodeAgent, RagAgent, Embedding };
47
+ export default { Transformer, Chat, Message, ToolAgent, CodeAgent, RagAgent, Embedding, ImageGenerator };
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.2.1",
5
+ "version": "2.4.1",
6
6
  "main": "index.js",
7
7
  "files": [
8
8
  "index.js",
@@ -15,6 +15,7 @@
15
15
  "code-agent.js",
16
16
  "rag-agent.js",
17
17
  "embedding.js",
18
+ "image-generator.js",
18
19
  "json-helpers.js",
19
20
  "types.d.ts",
20
21
  "logger.js",
@@ -64,7 +65,7 @@
64
65
  ],
65
66
  "license": "ISC",
66
67
  "dependencies": {
67
- "@google/genai": "^1.44.0",
68
+ "@google/genai": "^2.10.0",
68
69
  "dotenv": "^17.3.1",
69
70
  "pino": "^10.3.1",
70
71
  "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
@@ -68,12 +68,14 @@ export interface UsageData {
68
68
  totalTokens: number;
69
69
  /** Number of attempts (1 = first try success, 2+ = retries needed) */
70
70
  attempts: number;
71
- /** Actual model that responded (e.g., 'gemini-2.5-flash-001') */
71
+ /** Actual model that responded (e.g., 'gemini-3-flash-preview-001') */
72
72
  modelVersion: string | null;
73
- /** Model you requested (e.g., 'gemini-2.5-flash') */
73
+ /** Model you requested (e.g., 'gemini-3-flash-preview') */
74
74
  requestedModel: string;
75
75
  timestamp: number;
76
76
  groundingMetadata?: GroundingMetadata | null;
77
+ /** Model lifecycle status from Google (e.g., 'DEPRECATED'). Surfaced from @google/genai 1.47+. */
78
+ modelStatus?: string | null;
77
79
  }
78
80
 
79
81
  export interface TransformationExample {
@@ -130,11 +132,12 @@ export interface CachedContentInfo {
130
132
 
131
133
  export type AsyncValidatorFunction = (payload: Record<string, unknown>) => Promise<unknown>;
132
134
  export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'none';
135
+ export type ServiceTier = 'STANDARD' | 'FLEX' | 'PRIORITY';
133
136
 
134
137
  // ── Constructor Options ──────────────────────────────────────────────────────
135
138
 
136
139
  export interface BaseGeminiOptions {
137
- /** Gemini model to use (default: 'gemini-2.5-flash') */
140
+ /** Gemini model to use (default: 'gemini-3-flash-preview') */
138
141
  modelName?: string;
139
142
  /** System prompt for the model (null or false to disable) */
140
143
  systemPrompt?: string | null | false;
@@ -144,6 +147,12 @@ export interface BaseGeminiOptions {
144
147
  thinkingConfig?: ThinkingConfig | null;
145
148
  /** Maximum output tokens (default: 50000, null removes limit) */
146
149
  maxOutputTokens?: number | null;
150
+ /** Sampling temperature (shorthand for chatConfig.temperature; wins over chatConfig) */
151
+ temperature?: number;
152
+ /** Nucleus sampling (shorthand for chatConfig.topP; wins over chatConfig) */
153
+ topP?: number;
154
+ /** Top-k sampling (shorthand for chatConfig.topK; wins over chatConfig) */
155
+ topK?: number;
147
156
  /** Log level (default: based on NODE_ENV) */
148
157
  logLevel?: LogLevel;
149
158
 
@@ -177,6 +186,12 @@ export interface BaseGeminiOptions {
177
186
 
178
187
  /** Run models.list() health check during init() (default: false) */
179
188
  healthCheck?: boolean;
189
+
190
+ /** Service tier for generateContent (STANDARD | FLEX | PRIORITY). @google/genai 1.47+ */
191
+ serviceTier?: ServiceTier;
192
+
193
+ /** Surface server-side tool invocations (e.g. Google Search) in the response. @google/genai 1.46+ */
194
+ includeServerSideToolInvocations?: boolean;
180
195
  }
181
196
 
182
197
  export interface TransformerOptions extends BaseGeminiOptions {
@@ -254,6 +269,48 @@ export interface EmbeddingResult {
254
269
  statistics?: { tokenCount?: number; truncated?: boolean };
255
270
  }
256
271
 
272
+ // ── ImageGenerator ───────────────────────────────────────────────────────────
273
+
274
+ export type ImageAspectRatio = '1:1' | '2:3' | '3:2' | '3:4' | '4:3' | '9:16' | '16:9' | '21:9';
275
+ export type ImageSize = '1K' | '2K' | '4K';
276
+ export type PersonGeneration = 'ALLOW_ALL' | 'ALLOW_ADULT' | 'ALLOW_NONE';
277
+
278
+ export interface ImageGeneratorOptions extends BaseGeminiOptions {
279
+ /** Default aspect ratio for generated images */
280
+ aspectRatio?: ImageAspectRatio;
281
+ /** Default output resolution (1K/2K/4K) */
282
+ imageSize?: ImageSize;
283
+ /** Default people-generation policy */
284
+ personGeneration?: PersonGeneration;
285
+ /** Include text output alongside images (default: false) */
286
+ includeText?: boolean;
287
+ }
288
+
289
+ export interface ImageGenerateOptions {
290
+ aspectRatio?: ImageAspectRatio;
291
+ imageSize?: ImageSize;
292
+ personGeneration?: PersonGeneration;
293
+ includeText?: boolean;
294
+ /** Reference images for editing / multi-image composition (base64) */
295
+ inputImages?: Array<{ data: string; mimeType: string }>;
296
+ }
297
+
298
+ export interface GeneratedImage {
299
+ /** Base64-encoded image data */
300
+ data: string;
301
+ /** MIME type (e.g. "image/png") */
302
+ mimeType: string;
303
+ }
304
+
305
+ export interface ImageGenerationResult {
306
+ /** One or more generated images */
307
+ images: GeneratedImage[];
308
+ /** Optional text response (only when includeText: true) */
309
+ text: string | null;
310
+ /** Token usage */
311
+ usage: UsageData | null;
312
+ }
313
+
257
314
  /** Tool declaration in @google/genai FunctionDeclaration format */
258
315
  export interface ToolDeclaration {
259
316
  name: string;
@@ -512,6 +569,8 @@ export interface SeedOptions {
512
569
  contextKey?: string;
513
570
  explanationKey?: string;
514
571
  systemPromptKey?: string;
572
+ /** Model-turn format: 'json' wraps answers in a {data} envelope (Transformer protocol); 'text' stores ANSWER verbatim (prose agents like Chat). Default: 'json' */
573
+ format?: 'json' | 'text';
515
574
  }
516
575
 
517
576
  // ── Class Declarations ───────────────────────────────────────────────────────
@@ -531,6 +590,8 @@ export declare class BaseGemini {
531
590
  enableGrounding: boolean;
532
591
  groundingConfig: Record<string, any>;
533
592
  cachedContent: string | null;
593
+ serviceTier: ServiceTier | null;
594
+ includeServerSideToolInvocations: boolean;
534
595
 
535
596
  init(force?: boolean): Promise<void>;
536
597
  seed(examples?: TransformationExample[], opts?: SeedOptions): Promise<any[]>;
@@ -677,6 +738,21 @@ export declare class Embedding extends BaseGemini {
677
738
  similarity(a: number[], b: number[]): number;
678
739
  }
679
740
 
741
+ export declare class ImageGenerator extends BaseGemini {
742
+ constructor(options?: ImageGeneratorOptions);
743
+
744
+ aspectRatio: ImageAspectRatio | null;
745
+ imageSize: ImageSize | null;
746
+ personGeneration: PersonGeneration | null;
747
+ includeText: boolean;
748
+
749
+ init(force?: boolean): Promise<void>;
750
+ /** Generate one or more images from a text prompt. Supports optional reference images for editing. */
751
+ generate(prompt: string, opts?: ImageGenerateOptions): Promise<ImageGenerationResult>;
752
+ /** Write generated images to disk (suffixes `_N` before extension if >1 image) */
753
+ save(result: ImageGenerationResult, filePath: string): string[];
754
+ }
755
+
680
756
  // ── Module Exports ───────────────────────────────────────────────────────────
681
757
 
682
758
  export declare function extractJSON(text: string): any;
@@ -690,6 +766,7 @@ declare const _default: {
690
766
  CodeAgent: typeof CodeAgent;
691
767
  RagAgent: typeof RagAgent;
692
768
  Embedding: typeof Embedding;
769
+ ImageGenerator: typeof ImageGenerator;
693
770
  };
694
771
 
695
772
  export default _default;