ak-gemini 1.0.7 → 1.0.9

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.
Files changed (4) hide show
  1. package/index.cjs +216 -16
  2. package/index.js +308 -19
  3. package/package.json +2 -1
  4. package/types.d.ts +13 -5
package/index.cjs CHANGED
@@ -29,7 +29,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // index.js
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
+ HarmBlockThreshold: () => import_genai.HarmBlockThreshold,
33
+ HarmCategory: () => import_genai.HarmCategory,
32
34
  ThinkingLevel: () => import_genai.ThinkingLevel,
35
+ attemptJSONRecovery: () => attemptJSONRecovery,
33
36
  default: () => index_default,
34
37
  log: () => logger_default
35
38
  });
@@ -79,6 +82,7 @@ var DEFAULT_THINKING_CONFIG = {
79
82
  thinkingBudget: 0,
80
83
  thinkingLevel: import_genai.ThinkingLevel.MINIMAL
81
84
  };
85
+ var DEFAULT_MAX_OUTPUT_TOKENS = 1e5;
82
86
  var THINKING_SUPPORTED_MODELS = [
83
87
  /^gemini-3-flash(-preview)?$/,
84
88
  /^gemini-3-pro(-preview|-image-preview)?$/,
@@ -166,21 +170,43 @@ function AITransformFactory(options = {}) {
166
170
  ...options.chatConfig,
167
171
  systemInstruction: this.systemInstructions
168
172
  };
173
+ if (options.maxOutputTokens !== void 0) {
174
+ if (options.maxOutputTokens === null) {
175
+ delete this.chatConfig.maxOutputTokens;
176
+ } else {
177
+ this.chatConfig.maxOutputTokens = options.maxOutputTokens;
178
+ }
179
+ } else if (options.chatConfig?.maxOutputTokens !== void 0) {
180
+ if (options.chatConfig.maxOutputTokens === null) {
181
+ delete this.chatConfig.maxOutputTokens;
182
+ } else {
183
+ this.chatConfig.maxOutputTokens = options.chatConfig.maxOutputTokens;
184
+ }
185
+ } else {
186
+ this.chatConfig.maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
187
+ }
169
188
  const modelSupportsThinking = THINKING_SUPPORTED_MODELS.some(
170
189
  (pattern) => pattern.test(this.modelName)
171
190
  );
172
- if (modelSupportsThinking && options.thinkingConfig) {
173
- const thinkingConfig = {
174
- ...DEFAULT_THINKING_CONFIG,
175
- ...options.thinkingConfig
176
- };
177
- this.chatConfig.thinkingConfig = thinkingConfig;
178
- if (logger_default.level !== "silent") {
179
- logger_default.debug(`Model ${this.modelName} supports thinking. Applied thinkingConfig:`, thinkingConfig);
180
- }
181
- } else if (options.thinkingConfig && !modelSupportsThinking) {
182
- if (logger_default.level !== "silent") {
183
- logger_default.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
191
+ if (options.thinkingConfig !== void 0) {
192
+ if (options.thinkingConfig === null) {
193
+ delete this.chatConfig.thinkingConfig;
194
+ if (logger_default.level !== "silent") {
195
+ logger_default.debug(`thinkingConfig set to null - removed from configuration`);
196
+ }
197
+ } else if (modelSupportsThinking) {
198
+ const thinkingConfig = {
199
+ ...DEFAULT_THINKING_CONFIG,
200
+ ...options.thinkingConfig
201
+ };
202
+ this.chatConfig.thinkingConfig = thinkingConfig;
203
+ if (logger_default.level !== "silent") {
204
+ logger_default.debug(`Model ${this.modelName} supports thinking. Applied thinkingConfig:`, thinkingConfig);
205
+ }
206
+ } else {
207
+ if (logger_default.level !== "silent") {
208
+ logger_default.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
209
+ }
184
210
  }
185
211
  }
186
212
  if (options.responseSchema) {
@@ -197,12 +223,17 @@ function AITransformFactory(options = {}) {
197
223
  this.retryDelay = options.retryDelay || 1e3;
198
224
  this.asyncValidator = options.asyncValidator || null;
199
225
  this.onlyJSON = options.onlyJSON !== void 0 ? options.onlyJSON : true;
226
+ this.enableGrounding = options.enableGrounding || false;
227
+ this.groundingConfig = options.groundingConfig || {};
200
228
  if (this.promptKey === this.answerKey) {
201
229
  throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
202
230
  }
203
231
  if (logger_default.level !== "silent") {
204
232
  logger_default.debug(`Creating AI Transformer with model: ${this.modelName}`);
205
233
  logger_default.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
234
+ logger_default.debug(`Max output tokens set to: ${this.chatConfig.maxOutputTokens}`);
235
+ logger_default.debug(`Using API key: ${this.apiKey.substring(0, 10)}...`);
236
+ logger_default.debug(`Grounding ${this.enableGrounding ? "ENABLED" : "DISABLED"} (costs $35/1k queries)`);
206
237
  }
207
238
  const ai = new import_genai.GoogleGenAI({ apiKey: this.apiKey });
208
239
  this.genAIClient = ai;
@@ -211,12 +242,19 @@ function AITransformFactory(options = {}) {
211
242
  async function initChat(force = false) {
212
243
  if (this.chat && !force) return;
213
244
  logger_default.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
214
- this.chat = await this.genAIClient.chats.create({
245
+ const chatOptions = {
215
246
  model: this.modelName,
216
247
  // @ts-ignore
217
248
  config: this.chatConfig,
218
249
  history: []
219
- });
250
+ };
251
+ if (this.enableGrounding) {
252
+ chatOptions.config.tools = [{
253
+ googleSearch: this.groundingConfig
254
+ }];
255
+ logger_default.debug(`Search grounding ENABLED for this session (WARNING: costs $35/1k queries)`);
256
+ }
257
+ this.chat = await this.genAIClient.chats.create(chatOptions);
220
258
  try {
221
259
  await this.genAIClient.models.list();
222
260
  logger_default.debug("Gemini API connection successful.");
@@ -320,6 +358,32 @@ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorF
320
358
  }
321
359
  const maxRetries = options.maxRetries ?? this.maxRetries;
322
360
  const retryDelay = options.retryDelay ?? this.retryDelay;
361
+ const enableGroundingForMessage = options.enableGrounding ?? this.enableGrounding;
362
+ const groundingConfigForMessage = options.groundingConfig ?? this.groundingConfig;
363
+ if (enableGroundingForMessage !== this.enableGrounding) {
364
+ const originalGrounding = this.enableGrounding;
365
+ const originalConfig = this.groundingConfig;
366
+ try {
367
+ this.enableGrounding = enableGroundingForMessage;
368
+ this.groundingConfig = groundingConfigForMessage;
369
+ await this.init(true);
370
+ if (enableGroundingForMessage) {
371
+ logger_default.warn(`Search grounding ENABLED for this message (WARNING: costs $35/1k queries)`);
372
+ } else {
373
+ logger_default.debug(`Search grounding DISABLED for this message`);
374
+ }
375
+ } catch (error) {
376
+ this.enableGrounding = originalGrounding;
377
+ this.groundingConfig = originalConfig;
378
+ throw error;
379
+ }
380
+ const restoreGrounding = async () => {
381
+ this.enableGrounding = originalGrounding;
382
+ this.groundingConfig = originalConfig;
383
+ await this.init(true);
384
+ };
385
+ options._restoreGrounding = restoreGrounding;
386
+ }
323
387
  let lastError = null;
324
388
  let lastPayload = null;
325
389
  if (sourcePayload && isJSON(sourcePayload)) {
@@ -341,12 +405,18 @@ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorF
341
405
  await validatorFn(transformedPayload);
342
406
  }
343
407
  logger_default.debug(`Transformation succeeded on attempt ${attempt + 1}`);
408
+ if (options._restoreGrounding) {
409
+ await options._restoreGrounding();
410
+ }
344
411
  return transformedPayload;
345
412
  } catch (error) {
346
413
  lastError = error;
347
414
  logger_default.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
348
415
  if (attempt >= maxRetries) {
349
416
  logger_default.error(`All ${maxRetries + 1} attempts failed.`);
417
+ if (options._restoreGrounding) {
418
+ await options._restoreGrounding();
419
+ }
350
420
  throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
351
421
  }
352
422
  const delay = retryDelay * Math.pow(2, attempt);
@@ -405,12 +475,19 @@ async function estimateTokenUsage(nextPayload) {
405
475
  async function resetChat() {
406
476
  if (this.chat) {
407
477
  logger_default.debug("Resetting Gemini chat session...");
408
- this.chat = await this.genAIClient.chats.create({
478
+ const chatOptions = {
409
479
  model: this.modelName,
410
480
  // @ts-ignore
411
481
  config: this.chatConfig,
412
482
  history: []
413
- });
483
+ };
484
+ if (this.enableGrounding) {
485
+ chatOptions.config.tools = [{
486
+ googleSearch: this.groundingConfig
487
+ }];
488
+ logger_default.debug(`Search grounding preserved during reset (WARNING: costs $35/1k queries)`);
489
+ }
490
+ this.chat = await this.genAIClient.chats.create(chatOptions);
414
491
  logger_default.debug("Chat session reset.");
415
492
  } else {
416
493
  logger_default.warn("Cannot reset chat session: chat not yet initialized.");
@@ -423,6 +500,122 @@ function getChatHistory() {
423
500
  }
424
501
  return this.chat.getHistory();
425
502
  }
503
+ function attemptJSONRecovery(text, maxAttempts = 100) {
504
+ if (!text || typeof text !== "string") return null;
505
+ try {
506
+ return JSON.parse(text);
507
+ } catch (e) {
508
+ }
509
+ let workingText = text.trim();
510
+ let braces = 0;
511
+ let brackets = 0;
512
+ let inString = false;
513
+ let escapeNext = false;
514
+ for (let j = 0; j < workingText.length; j++) {
515
+ const char = workingText[j];
516
+ if (escapeNext) {
517
+ escapeNext = false;
518
+ continue;
519
+ }
520
+ if (char === "\\") {
521
+ escapeNext = true;
522
+ continue;
523
+ }
524
+ if (char === '"') {
525
+ inString = !inString;
526
+ continue;
527
+ }
528
+ if (!inString) {
529
+ if (char === "{") braces++;
530
+ else if (char === "}") braces--;
531
+ else if (char === "[") brackets++;
532
+ else if (char === "]") brackets--;
533
+ }
534
+ }
535
+ if ((braces > 0 || brackets > 0 || inString) && workingText.length > 2) {
536
+ let fixedText = workingText;
537
+ if (inString) {
538
+ fixedText += '"';
539
+ }
540
+ while (braces > 0) {
541
+ fixedText += "}";
542
+ braces--;
543
+ }
544
+ while (brackets > 0) {
545
+ fixedText += "]";
546
+ brackets--;
547
+ }
548
+ try {
549
+ const result = JSON.parse(fixedText);
550
+ if (logger_default.level !== "silent") {
551
+ logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
552
+ }
553
+ return result;
554
+ } catch (e) {
555
+ }
556
+ }
557
+ for (let i = 0; i < maxAttempts && workingText.length > 2; i++) {
558
+ workingText = workingText.slice(0, -1);
559
+ let braces2 = 0;
560
+ let brackets2 = 0;
561
+ let inString2 = false;
562
+ let escapeNext2 = false;
563
+ for (let j = 0; j < workingText.length; j++) {
564
+ const char = workingText[j];
565
+ if (escapeNext2) {
566
+ escapeNext2 = false;
567
+ continue;
568
+ }
569
+ if (char === "\\") {
570
+ escapeNext2 = true;
571
+ continue;
572
+ }
573
+ if (char === '"') {
574
+ inString2 = !inString2;
575
+ continue;
576
+ }
577
+ if (!inString2) {
578
+ if (char === "{") braces2++;
579
+ else if (char === "}") braces2--;
580
+ else if (char === "[") brackets2++;
581
+ else if (char === "]") brackets2--;
582
+ }
583
+ }
584
+ if (braces2 === 0 && brackets2 === 0 && !inString2) {
585
+ try {
586
+ const result = JSON.parse(workingText);
587
+ if (logger_default.level !== "silent") {
588
+ logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by removing ${i + 1} characters from the end.`);
589
+ }
590
+ return result;
591
+ } catch (e) {
592
+ }
593
+ }
594
+ if (i > 5) {
595
+ let fixedText = workingText;
596
+ if (inString2) {
597
+ fixedText += '"';
598
+ }
599
+ while (braces2 > 0) {
600
+ fixedText += "}";
601
+ braces2--;
602
+ }
603
+ while (brackets2 > 0) {
604
+ fixedText += "]";
605
+ brackets2--;
606
+ }
607
+ try {
608
+ const result = JSON.parse(fixedText);
609
+ if (logger_default.level !== "silent") {
610
+ logger_default.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
611
+ }
612
+ return result;
613
+ } catch (e) {
614
+ }
615
+ }
616
+ }
617
+ return null;
618
+ }
426
619
  function isJSON(data) {
427
620
  try {
428
621
  const attempt = JSON.stringify(data);
@@ -497,6 +690,10 @@ function extractJSON(text) {
497
690
  if (isJSONStr(cleanedText)) {
498
691
  return JSON.parse(cleanedText);
499
692
  }
693
+ const recoveredJSON = attemptJSONRecovery(text);
694
+ if (recoveredJSON !== null) {
695
+ return recoveredJSON;
696
+ }
500
697
  throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
501
698
  }
502
699
  function findCompleteJSONStructures(text) {
@@ -603,6 +800,9 @@ if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
603
800
  }
604
801
  // Annotate the CommonJS export names for ESM import in node:
605
802
  0 && (module.exports = {
803
+ HarmBlockThreshold,
804
+ HarmCategory,
606
805
  ThinkingLevel,
806
+ attemptJSONRecovery,
607
807
  log
608
808
  });
package/index.js CHANGED
@@ -28,7 +28,7 @@ import u from 'ak-tools';
28
28
  import path from 'path';
29
29
  import log from './logger.js';
30
30
  export { log };
31
- export { ThinkingLevel };
31
+ export { ThinkingLevel, HarmCategory, HarmBlockThreshold };
32
32
 
33
33
 
34
34
 
@@ -57,6 +57,8 @@ const DEFAULT_THINKING_CONFIG = {
57
57
  thinkingLevel: ThinkingLevel.MINIMAL
58
58
  };
59
59
 
60
+ const DEFAULT_MAX_OUTPUT_TOKENS = 100000; // Default ceiling for output tokens
61
+
60
62
  // Models that support thinking features (as of Dec 2024)
61
63
  // Using regex patterns for more precise matching
62
64
  const THINKING_SUPPORTED_MODELS = [
@@ -136,6 +138,7 @@ class AITransformer {
136
138
  }
137
139
 
138
140
  export default AITransformer;
141
+ export { attemptJSONRecovery }; // Export for testing
139
142
 
140
143
  /**
141
144
  * factory function to create an AI Transformer instance
@@ -186,25 +189,53 @@ function AITransformFactory(options = {}) {
186
189
  systemInstruction: this.systemInstructions
187
190
  };
188
191
 
192
+ // Handle maxOutputTokens with explicit null check
193
+ // Priority: options.maxOutputTokens > options.chatConfig.maxOutputTokens > DEFAULT
194
+ // Setting to null explicitly removes the limit
195
+ if (options.maxOutputTokens !== undefined) {
196
+ if (options.maxOutputTokens === null) {
197
+ delete this.chatConfig.maxOutputTokens;
198
+ } else {
199
+ this.chatConfig.maxOutputTokens = options.maxOutputTokens;
200
+ }
201
+ } else if (options.chatConfig?.maxOutputTokens !== undefined) {
202
+ if (options.chatConfig.maxOutputTokens === null) {
203
+ delete this.chatConfig.maxOutputTokens;
204
+ } else {
205
+ this.chatConfig.maxOutputTokens = options.chatConfig.maxOutputTokens;
206
+ }
207
+ } else {
208
+ this.chatConfig.maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
209
+ }
210
+
189
211
  // Only add thinkingConfig if the model supports it
190
212
  const modelSupportsThinking = THINKING_SUPPORTED_MODELS.some(pattern =>
191
213
  pattern.test(this.modelName)
192
214
  );
193
215
 
194
- if (modelSupportsThinking && options.thinkingConfig) {
195
- // Handle thinkingConfig - merge with defaults
196
- const thinkingConfig = {
197
- ...DEFAULT_THINKING_CONFIG,
198
- ...options.thinkingConfig
199
- };
200
- this.chatConfig.thinkingConfig = thinkingConfig;
201
-
202
- if (log.level !== 'silent') {
203
- log.debug(`Model ${this.modelName} supports thinking. Applied thinkingConfig:`, thinkingConfig);
204
- }
205
- } else if (options.thinkingConfig && !modelSupportsThinking) {
206
- if (log.level !== 'silent') {
207
- log.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
216
+ // Handle thinkingConfig - null explicitly removes it, undefined means not specified
217
+ if (options.thinkingConfig !== undefined) {
218
+ if (options.thinkingConfig === null) {
219
+ // Explicitly remove thinkingConfig if set to null
220
+ delete this.chatConfig.thinkingConfig;
221
+ if (log.level !== 'silent') {
222
+ log.debug(`thinkingConfig set to null - removed from configuration`);
223
+ }
224
+ } else if (modelSupportsThinking) {
225
+ // Handle thinkingConfig - merge with defaults
226
+ const thinkingConfig = {
227
+ ...DEFAULT_THINKING_CONFIG,
228
+ ...options.thinkingConfig
229
+ };
230
+ this.chatConfig.thinkingConfig = thinkingConfig;
231
+
232
+ if (log.level !== 'silent') {
233
+ log.debug(`Model ${this.modelName} supports thinking. Applied thinkingConfig:`, thinkingConfig);
234
+ }
235
+ } else {
236
+ if (log.level !== 'silent') {
237
+ log.warn(`Model ${this.modelName} does not support thinking features. Ignoring thinkingConfig.`);
238
+ }
208
239
  }
209
240
  }
210
241
 
@@ -234,6 +265,10 @@ function AITransformFactory(options = {}) {
234
265
  //are we forcing json responses only?
235
266
  this.onlyJSON = options.onlyJSON !== undefined ? options.onlyJSON : true; // If true, only return JSON responses
236
267
 
268
+ // Grounding configuration (disabled by default to avoid costs)
269
+ this.enableGrounding = options.enableGrounding || false;
270
+ this.groundingConfig = options.groundingConfig || {};
271
+
237
272
  if (this.promptKey === this.answerKey) {
238
273
  throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
239
274
  }
@@ -241,6 +276,10 @@ function AITransformFactory(options = {}) {
241
276
  if (log.level !== 'silent') {
242
277
  log.debug(`Creating AI Transformer with model: ${this.modelName}`);
243
278
  log.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
279
+ log.debug(`Max output tokens set to: ${this.chatConfig.maxOutputTokens}`);
280
+ // Log API key prefix for tracking (first 10 chars only for security)
281
+ log.debug(`Using API key: ${this.apiKey.substring(0, 10)}...`);
282
+ log.debug(`Grounding ${this.enableGrounding ? 'ENABLED' : 'DISABLED'} (costs $35/1k queries)`);
244
283
  }
245
284
 
246
285
  const ai = new GoogleGenAI({ apiKey: this.apiKey });
@@ -259,12 +298,23 @@ async function initChat(force = false) {
259
298
 
260
299
  log.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
261
300
 
262
- this.chat = await this.genAIClient.chats.create({
301
+ // Add grounding tools if enabled
302
+ const chatOptions = {
263
303
  model: this.modelName,
264
304
  // @ts-ignore
265
305
  config: this.chatConfig,
266
306
  history: [],
267
- });
307
+ };
308
+
309
+ // Only add tools if grounding is explicitly enabled
310
+ if (this.enableGrounding) {
311
+ chatOptions.config.tools = [{
312
+ googleSearch: this.groundingConfig
313
+ }];
314
+ log.debug(`Search grounding ENABLED for this session (WARNING: costs $35/1k queries)`);
315
+ }
316
+
317
+ this.chat = await this.genAIClient.chats.create(chatOptions);
268
318
 
269
319
  try {
270
320
  await this.genAIClient.models.list();
@@ -431,6 +481,47 @@ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorF
431
481
  const maxRetries = options.maxRetries ?? this.maxRetries;
432
482
  const retryDelay = options.retryDelay ?? this.retryDelay;
433
483
 
484
+ // Check if grounding should be enabled for this specific message
485
+ const enableGroundingForMessage = options.enableGrounding ?? this.enableGrounding;
486
+ const groundingConfigForMessage = options.groundingConfig ?? this.groundingConfig;
487
+
488
+ // Reinitialize chat if grounding settings changed for this message
489
+ if (enableGroundingForMessage !== this.enableGrounding) {
490
+ const originalGrounding = this.enableGrounding;
491
+ const originalConfig = this.groundingConfig;
492
+
493
+ try {
494
+ // Temporarily change grounding settings
495
+ this.enableGrounding = enableGroundingForMessage;
496
+ this.groundingConfig = groundingConfigForMessage;
497
+
498
+ // Force reinit with new settings
499
+ await this.init(true);
500
+
501
+ // Log the change
502
+ if (enableGroundingForMessage) {
503
+ log.warn(`Search grounding ENABLED for this message (WARNING: costs $35/1k queries)`);
504
+ } else {
505
+ log.debug(`Search grounding DISABLED for this message`);
506
+ }
507
+ } catch (error) {
508
+ // Restore original settings on error
509
+ this.enableGrounding = originalGrounding;
510
+ this.groundingConfig = originalConfig;
511
+ throw error;
512
+ }
513
+
514
+ // Schedule restoration after message completes
515
+ const restoreGrounding = async () => {
516
+ this.enableGrounding = originalGrounding;
517
+ this.groundingConfig = originalConfig;
518
+ await this.init(true);
519
+ };
520
+
521
+ // Store restoration function to call after message completes
522
+ options._restoreGrounding = restoreGrounding;
523
+ }
524
+
434
525
  let lastError = null;
435
526
  let lastPayload = null; // Store the payload that caused the validation error
436
527
 
@@ -466,6 +557,12 @@ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorF
466
557
 
467
558
  // Step 3: Success!
468
559
  log.debug(`Transformation succeeded on attempt ${attempt + 1}`);
560
+
561
+ // Restore original grounding settings if they were changed
562
+ if (options._restoreGrounding) {
563
+ await options._restoreGrounding();
564
+ }
565
+
469
566
  return transformedPayload;
470
567
 
471
568
  } catch (error) {
@@ -474,6 +571,12 @@ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorF
474
571
 
475
572
  if (attempt >= maxRetries) {
476
573
  log.error(`All ${maxRetries + 1} attempts failed.`);
574
+
575
+ // Restore original grounding settings even on failure
576
+ if (options._restoreGrounding) {
577
+ await options._restoreGrounding();
578
+ }
579
+
477
580
  throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
478
581
  }
479
582
 
@@ -575,12 +678,24 @@ async function estimateTokenUsage(nextPayload) {
575
678
  async function resetChat() {
576
679
  if (this.chat) {
577
680
  log.debug("Resetting Gemini chat session...");
578
- this.chat = await this.genAIClient.chats.create({
681
+
682
+ // Prepare chat options with grounding if enabled
683
+ const chatOptions = {
579
684
  model: this.modelName,
580
685
  // @ts-ignore
581
686
  config: this.chatConfig,
582
687
  history: [],
583
- });
688
+ };
689
+
690
+ // Only add tools if grounding is explicitly enabled
691
+ if (this.enableGrounding) {
692
+ chatOptions.config.tools = [{
693
+ googleSearch: this.groundingConfig
694
+ }];
695
+ log.debug(`Search grounding preserved during reset (WARNING: costs $35/1k queries)`);
696
+ }
697
+
698
+ this.chat = await this.genAIClient.chats.create(chatOptions);
584
699
  log.debug("Chat session reset.");
585
700
  } else {
586
701
  log.warn("Cannot reset chat session: chat not yet initialized.");
@@ -606,6 +721,173 @@ HELPERS
606
721
  ----
607
722
  */
608
723
 
724
+ /**
725
+ * Attempts to recover truncated JSON by progressively removing characters from the end
726
+ * until valid JSON is found or recovery fails
727
+ * @param {string} text - The potentially truncated JSON string
728
+ * @param {number} maxAttempts - Maximum number of characters to remove
729
+ * @returns {Object|null} - Parsed JSON object or null if recovery fails
730
+ */
731
+ function attemptJSONRecovery(text, maxAttempts = 100) {
732
+ if (!text || typeof text !== 'string') return null;
733
+
734
+ // First, try parsing as-is
735
+ try {
736
+ return JSON.parse(text);
737
+ } catch (e) {
738
+ // Continue with recovery
739
+ }
740
+
741
+ let workingText = text.trim();
742
+
743
+ // First attempt: try to close unclosed structures without removing characters
744
+ // Count open/close braces and brackets in the original text
745
+ let braces = 0;
746
+ let brackets = 0;
747
+ let inString = false;
748
+ let escapeNext = false;
749
+
750
+ for (let j = 0; j < workingText.length; j++) {
751
+ const char = workingText[j];
752
+
753
+ if (escapeNext) {
754
+ escapeNext = false;
755
+ continue;
756
+ }
757
+
758
+ if (char === '\\') {
759
+ escapeNext = true;
760
+ continue;
761
+ }
762
+
763
+ if (char === '"') {
764
+ inString = !inString;
765
+ continue;
766
+ }
767
+
768
+ if (!inString) {
769
+ if (char === '{') braces++;
770
+ else if (char === '}') braces--;
771
+ else if (char === '[') brackets++;
772
+ else if (char === ']') brackets--;
773
+ }
774
+ }
775
+
776
+ // Try to fix by just adding closing characters
777
+ if ((braces > 0 || brackets > 0 || inString) && workingText.length > 2) {
778
+ let fixedText = workingText;
779
+
780
+ // Close any open strings first
781
+ if (inString) {
782
+ fixedText += '"';
783
+ }
784
+
785
+ // Add missing closing characters
786
+ while (braces > 0) {
787
+ fixedText += '}';
788
+ braces--;
789
+ }
790
+ while (brackets > 0) {
791
+ fixedText += ']';
792
+ brackets--;
793
+ }
794
+
795
+ try {
796
+ const result = JSON.parse(fixedText);
797
+ if (log.level !== 'silent') {
798
+ log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
799
+ }
800
+ return result;
801
+ } catch (e) {
802
+ // Simple fix didn't work, continue with more aggressive recovery
803
+ }
804
+ }
805
+
806
+ // Second attempt: progressively remove characters from the end
807
+
808
+ for (let i = 0; i < maxAttempts && workingText.length > 2; i++) {
809
+ // Remove one character from the end
810
+ workingText = workingText.slice(0, -1);
811
+
812
+ // Count open/close braces and brackets
813
+ let braces = 0;
814
+ let brackets = 0;
815
+ let inString = false;
816
+ let escapeNext = false;
817
+
818
+ for (let j = 0; j < workingText.length; j++) {
819
+ const char = workingText[j];
820
+
821
+ if (escapeNext) {
822
+ escapeNext = false;
823
+ continue;
824
+ }
825
+
826
+ if (char === '\\') {
827
+ escapeNext = true;
828
+ continue;
829
+ }
830
+
831
+ if (char === '"') {
832
+ inString = !inString;
833
+ continue;
834
+ }
835
+
836
+ if (!inString) {
837
+ if (char === '{') braces++;
838
+ else if (char === '}') braces--;
839
+ else if (char === '[') brackets++;
840
+ else if (char === ']') brackets--;
841
+ }
842
+ }
843
+
844
+ // If we have balanced braces/brackets, try parsing
845
+ if (braces === 0 && brackets === 0 && !inString) {
846
+ try {
847
+ const result = JSON.parse(workingText);
848
+ if (log.level !== 'silent') {
849
+ log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by removing ${i + 1} characters from the end.`);
850
+ }
851
+ return result;
852
+ } catch (e) {
853
+ // Continue trying
854
+ }
855
+ }
856
+
857
+ // After a few attempts, try adding closing characters
858
+ if (i > 5) {
859
+ let fixedText = workingText;
860
+
861
+ // Close any open strings first
862
+ if (inString) {
863
+ fixedText += '"';
864
+ }
865
+
866
+ // Add missing closing characters
867
+ while (braces > 0) {
868
+ fixedText += '}';
869
+ braces--;
870
+ }
871
+ while (brackets > 0) {
872
+ fixedText += ']';
873
+ brackets--;
874
+ }
875
+
876
+ try {
877
+ const result = JSON.parse(fixedText);
878
+ if (log.level !== 'silent') {
879
+ log.warn(`JSON response appears truncated (possibly hit maxOutputTokens limit). Recovered by adding closing characters.`);
880
+ }
881
+ return result;
882
+ } catch (e) {
883
+ // Recovery failed, continue trying
884
+ }
885
+ }
886
+ }
887
+
888
+ return null;
889
+ }
890
+
609
891
  function isJSON(data) {
610
892
  try {
611
893
  const attempt = JSON.stringify(data);
@@ -703,6 +985,13 @@ function extractJSON(text) {
703
985
  return JSON.parse(cleanedText);
704
986
  }
705
987
 
988
+ // Strategy 6: Last resort - attempt recovery for potentially truncated JSON
989
+ // This is especially useful when maxOutputTokens might have cut off the response
990
+ const recoveredJSON = attemptJSONRecovery(text);
991
+ if (recoveredJSON !== null) {
992
+ return recoveredJSON;
993
+ }
994
+
706
995
  // If all else fails, throw an error with helpful information
707
996
  throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
708
997
  }
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... transforms",
5
- "version": "1.0.7",
5
+ "version": "1.0.9",
6
6
  "main": "index.js",
7
7
  "files": [
8
8
  "index.js",
@@ -39,6 +39,7 @@
39
39
  "prune": "rm -rf tmp/*",
40
40
  "test": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js",
41
41
  "test:unit": "npm test -- tests/module.test.js",
42
+ "test:fixed": "npm test -- --testNamePattern=\"should use context in the prompt and transform accordingly|should augment the payload as instructed by system instructions|should succeed on the first try if validation passes|should handle invalid model names|should handle multiple concurrent messages|should use the constructor-provided asyncValidator|should override system instructions from the file\"",
42
43
  "build:cjs": "esbuild index.js --bundle --platform=node --format=cjs --outfile=index.cjs --external:@google/genai --external:ak-tools --external:dotenv --external:pino-pretty --external:pino",
43
44
  "coverage": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
44
45
  "typecheck": "tsc --noEmit",
package/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { GoogleGenAI, ThinkingLevel } from '@google/genai';
1
+ import type { GoogleGenAI, ThinkingLevel, HarmCategory, HarmBlockThreshold } from '@google/genai';
2
2
 
3
- export { ThinkingLevel };
3
+ export { ThinkingLevel, HarmCategory, HarmBlockThreshold };
4
4
 
5
5
  export interface ThinkingConfig {
6
6
  /** Indicates whether to include thoughts in the response. If true, thoughts are returned only if the model supports thought and thoughts are available. */
@@ -12,8 +12,8 @@ export interface ThinkingConfig {
12
12
  }
13
13
 
14
14
  export interface SafetySetting {
15
- category: string; // The harm category
16
- threshold: string; // The blocking threshold
15
+ category: HarmCategory; // The harm category
16
+ threshold: HarmBlockThreshold; // The blocking threshold
17
17
  }
18
18
 
19
19
  export interface ChatConfig {
@@ -21,6 +21,7 @@ export interface ChatConfig {
21
21
  temperature?: number; // Controls randomness (0.0 to 1.0)
22
22
  topP?: number; // Controls diversity via nucleus sampling
23
23
  topK?: number; // Controls diversity by limiting top-k tokens
24
+ maxOutputTokens?: number; // Maximum number of tokens that can be generated in the response
24
25
  systemInstruction?: string; // System instruction for the model
25
26
  safetySettings?: SafetySetting[]; // Safety settings array
26
27
  responseSchema?: Object; // Schema for validating model responses
@@ -50,7 +51,9 @@ export interface AITransformerContext {
50
51
  rawMessage?: (payload: Record<string, unknown> | string) => Promise<Record<string, unknown>>; // Function to send raw messages to the model
51
52
  genAIClient?: GoogleGenAI; // Google GenAI client instance
52
53
  onlyJSON?: boolean; // If true, only JSON responses are allowed
53
-
54
+ enableGrounding?: boolean; // Enable Google Search grounding (default: false, WARNING: costs $35/1k queries)
55
+ groundingConfig?: Record<string, any>; // Additional grounding configuration options
56
+
54
57
  }
55
58
 
56
59
  export interface TransformationExample {
@@ -74,6 +77,7 @@ export interface AITransformerOptions {
74
77
  systemInstructions?: string; // Custom system instructions for the model
75
78
  chatConfig?: ChatConfig; // Configuration object for the chat session
76
79
  thinkingConfig?: ThinkingConfig; // Thinking features configuration (defaults to thinkingBudget: 0, thinkingLevel: "MINIMAL")
80
+ maxOutputTokens?: number; // Maximum number of tokens that can be generated in the response (defaults to 100000)
77
81
  examplesFile?: string; // Path to JSON file containing transformation examples
78
82
  exampleData?: TransformationExample[]; // Inline examples to seed the transformer
79
83
  sourceKey?: string; // Key name for source data in examples (alias for promptKey)
@@ -91,6 +95,8 @@ export interface AITransformerOptions {
91
95
  onlyJSON?: boolean; // If true, only JSON responses are allowed
92
96
  asyncValidator?: AsyncValidatorFunction; // Optional async validator function for response validation
93
97
  logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'none'; // Log level for the logger (defaults to 'info', 'none' disables logging)
98
+ enableGrounding?: boolean; // Enable Google Search grounding (default: false, WARNING: costs $35/1k queries)
99
+ groundingConfig?: Record<string, any>; // Additional grounding configuration options
94
100
  }
95
101
 
96
102
  // Async validator function type
@@ -118,6 +124,8 @@ export declare class AITransformer {
118
124
  genAIClient: any;
119
125
  chat: any;
120
126
  logLevel: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'none';
127
+ enableGrounding: boolean;
128
+ groundingConfig: Record<string, any>;
121
129
 
122
130
  // Methods
123
131
  init(force?: boolean): Promise<void>;