ak-gemini 1.0.4 → 1.0.6

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 (6) hide show
  1. package/README.md +43 -22
  2. package/index.cjs +288 -69
  3. package/index.js +424 -89
  4. package/package.json +21 -16
  5. package/types.d.ts +125 -0
  6. package/types.ts +0 -65
package/README.md CHANGED
@@ -7,12 +7,13 @@ Use this to power LLM-driven data pipelines, JSON mapping, or any automated AI t
7
7
 
8
8
  ## Features
9
9
 
10
- * **Model-Agnostic**: Configure for any Gemini model (`gemini-2.0-flash` by default)
11
- * **Declarative Examples**: Seed transformations using example mappings, with support for custom keys (`PROMPT`, `ANSWER`, `CONTEXT`, or your own)
12
- * **Automatic Validation & Repair**: Validate outputs with your own async function; auto-repair failed payloads with LLM feedback loop (exponential backoff, fully configurable)
13
- * **Strong TypeScript/JSDoc Typings**: All public APIs fully typed (see `/types`)
14
- * **Minimal API Surface**: Dead simple, no ceremony—init, seed, transform, validate.
15
- * **Robust Logging**: Pluggable logger for all steps, easy debugging
10
+ * **Model-Agnostic:** Use any Gemini model (`gemini-2.5-flash` by default)
11
+ * **Declarative Few-shot Examples:** Seed transformations using example mappings, with support for custom keys (`PROMPT`, `ANSWER`, `CONTEXT`, or your own)
12
+ * **Automatic Validation & Repair:** Validate outputs with your own async function; auto-repair failed payloads with LLM feedback loop (exponential backoff, fully configurable)
13
+ * **Token Counting & Safety:** Preview the *exact* Gemini token consumption for any operation—including all examples, instructions, and your input—before sending, so you can avoid window errors and manage costs.
14
+ * **Strong TypeScript/JSDoc Typings:** All public APIs fully typed (see `/types`)
15
+ * **Minimal API Surface:** Dead simple, no ceremony—init, seed, transform, validate.
16
+ * **Robust Logging:** Pluggable logger for all steps, easy debugging
16
17
 
17
18
  ---
18
19
 
@@ -43,10 +44,10 @@ or pass it directly in the constructor options.
43
44
  ### 2. **Basic Example**
44
45
 
45
46
  ```js
46
- import AITransformer from 'ai-transformer';
47
+ import AITransformer from 'ak-gemini';
47
48
 
48
49
  const transformer = new AITransformer({
49
- modelName: 'gemini-2.0-flash', // or your preferred Gemini model
50
+ modelName: 'gemini-2.5-flash', // or your preferred Gemini model
50
51
  sourceKey: 'INPUT', // Custom prompt key (default: 'PROMPT')
51
52
  targetKey: 'OUTPUT', // Custom answer key (default: 'ANSWER')
52
53
  contextKey: 'CONTEXT', // Optional, for per-example context
@@ -72,7 +73,22 @@ console.log(result);
72
73
 
73
74
  ---
74
75
 
75
- ### 3. **Automatic Validation & Self-Healing**
76
+ ### 3. **Token Window Safety/Preview**
77
+
78
+ Before calling `.message()` or `.seed()`, you can preview the exact token usage that will be sent to Gemini—*including* your system instructions, examples, and user input. This is vital for avoiding window errors and managing context size:
79
+
80
+ ```js
81
+ const { totalTokens, breakdown } = await transformer.estimateTokenUsage({ name: "Bob" });
82
+ console.log(`Total tokens: ${totalTokens}`);
83
+ console.log(breakdown); // See per-section token counts
84
+
85
+ // Optional: abort or trim if over limit
86
+ if (totalTokens > 32000) throw new Error("Request too large for selected Gemini model");
87
+ ```
88
+
89
+ ---
90
+
91
+ ### 4. **Automatic Validation & Self-Healing**
76
92
 
77
93
  You can pass a custom async validator—if it fails, the transformer will attempt to self-correct using LLM feedback, retrying up to `maxRetries` times:
78
94
 
@@ -100,7 +116,7 @@ new AITransformer(options)
100
116
 
101
117
  | Option | Type | Default | Description |
102
118
  | ------------------ | ------ | ------------------ | ------------------------------------------------- |
103
- | modelName | string | 'gemini-2.0-flash' | Gemini model to use |
119
+ | modelName | string | 'gemini-2.5-flash' | Gemini model to use |
104
120
  | sourceKey | string | 'PROMPT' | Key for prompt/example input |
105
121
  | targetKey | string | 'ANSWER' | Key for expected output in examples |
106
122
  | contextKey | string | 'CONTEXT' | Key for per-example context (optional) |
@@ -109,6 +125,7 @@ new AITransformer(options)
109
125
  | responseSchema | object | null | Optional JSON schema for strict output validation |
110
126
  | maxRetries | number | 3 | Retries for validation+rebuild loop |
111
127
  | retryDelay | number | 1000 | Initial retry delay in ms (exponential backoff) |
128
+ | logLevel | string | 'info' | Log level: 'trace', 'debug', 'info', 'warn', 'error', 'fatal', or 'none' |
112
129
  | chatConfig | object | ... | Gemini chat config overrides |
113
130
  | systemInstructions | string | ... | System prompt for Gemini |
114
131
 
@@ -127,7 +144,12 @@ You can omit `examples` to use the `examplesFile` (if provided).
127
144
 
128
145
  #### `await transformer.message(sourcePayload)`
129
146
 
130
- Transforms input JSON to output JSON using the seeded examples and system instructions.
147
+ Transforms input JSON to output JSON using the seeded examples and system instructions. Throws if estimated token window would be exceeded.
148
+
149
+ #### `await transformer.estimateTokenUsage(sourcePayload)`
150
+
151
+ Returns `{ totalTokens, breakdown }` for the *full request* that would be sent to Gemini (system instructions + all examples + your sourcePayload as the new prompt).
152
+ Lets you preview token window safety and abort/trim as needed.
131
153
 
132
154
  #### `await transformer.transformWithValidation(sourcePayload, validatorFn, options?)`
133
155
 
@@ -187,10 +209,19 @@ const result = await transformer.transformWithValidation(
187
209
 
188
210
  ---
189
211
 
212
+ ## Token Window Management & Error Handling
213
+
214
+ * Throws on missing `GEMINI_API_KEY`
215
+ * `.message()` and `.seed()` will *estimate* and prevent calls that would exceed Gemini's model window
216
+ * All API and parsing errors surfaced as `Error` with context
217
+ * Validator and retry failures include the number of attempts and last error
218
+
219
+ ---
220
+
190
221
  ## Testing
191
222
 
192
223
  * **Jest test suite included**
193
- * Mocks Google Gemini, logger, ak-tools
224
+ * Real API integration tests as well as local unit tests
194
225
  * 100% coverage for all error cases, configuration options, edge cases
195
226
 
196
227
  Run tests with:
@@ -200,13 +231,3 @@ npm test
200
231
  ```
201
232
 
202
233
  ---
203
-
204
- ## Error Handling
205
-
206
- * Throws on missing `GEMINI_API_KEY`
207
- * All API and parsing errors surfaced as `Error` with context
208
- * Validator and retry failures include the number of attempts and last error
209
-
210
- ---
211
-
212
-
package/index.cjs CHANGED
@@ -29,7 +29,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // index.js
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
- default: () => AITransformer,
32
+ default: () => index_default,
33
33
  log: () => logger_default
34
34
  });
35
35
  module.exports = __toCommonJS(index_exports);
@@ -56,10 +56,7 @@ var logger_default = logger;
56
56
  // index.js
57
57
  var import_meta = {};
58
58
  import_dotenv.default.config();
59
- var { NODE_ENV = "unknown", GEMINI_API_KEY } = process.env;
60
- if (NODE_ENV === "dev") logger_default.level = "debug";
61
- if (NODE_ENV === "test") logger_default.level = "warn";
62
- if (NODE_ENV.startsWith("prod")) logger_default.level = "error";
59
+ var { NODE_ENV = "unknown", GEMINI_API_KEY, LOG_LEVEL = "" } = process.env;
63
60
  var DEFAULT_SAFETY_SETTINGS = [
64
61
  { category: import_genai.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: import_genai.HarmBlockThreshold.BLOCK_NONE },
65
62
  { category: import_genai.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: import_genai.HarmBlockThreshold.BLOCK_NONE }
@@ -75,7 +72,7 @@ When presented with new Source JSON, apply the learned transformation rules to p
75
72
 
76
73
  Always respond ONLY with a valid JSON object that strictly adheres to the expected output format.
77
74
 
78
- Do not include any additional text, explanations, or formatting before or after the JSON object.
75
+ Do not include any additional text, explanations, or formatting before or after the JSON object.
79
76
  `;
80
77
  var DEFAULT_CHAT_CONFIG = {
81
78
  responseMimeType: "application/json",
@@ -95,25 +92,60 @@ var AITransformer = class {
95
92
  this.promptKey = "";
96
93
  this.answerKey = "";
97
94
  this.contextKey = "";
95
+ this.explanationKey = "";
96
+ this.systemInstructionKey = "";
98
97
  this.maxRetries = 3;
99
98
  this.retryDelay = 1e3;
100
99
  this.systemInstructions = "";
101
100
  this.chatConfig = {};
102
101
  this.apiKey = GEMINI_API_KEY;
102
+ this.onlyJSON = true;
103
+ this.asyncValidator = null;
104
+ this.logLevel = "info";
103
105
  AITransformFactory.call(this, options);
104
106
  this.init = initChat.bind(this);
105
107
  this.seed = seedWithExamples.bind(this);
106
- this.message = transformJSON.bind(this);
108
+ this.rawMessage = rawMessage.bind(this);
109
+ this.message = (payload, opts = {}, validatorFn = null) => {
110
+ return prepareAndValidateMessage.call(this, payload, opts, validatorFn || this.asyncValidator);
111
+ };
107
112
  this.rebuild = rebuildPayload.bind(this);
108
113
  this.reset = resetChat.bind(this);
109
114
  this.getHistory = getChatHistory.bind(this);
110
- this.transformWithValidation = transformWithValidation.bind(this);
115
+ this.messageAndValidate = prepareAndValidateMessage.bind(this);
116
+ this.transformWithValidation = prepareAndValidateMessage.bind(this);
117
+ this.estimate = estimateTokenUsage.bind(this);
118
+ this.estimateTokenUsage = estimateTokenUsage.bind(this);
111
119
  }
112
120
  };
121
+ var index_default = AITransformer;
113
122
  function AITransformFactory(options = {}) {
114
- this.modelName = options.modelName || "gemini-2.0-flash";
123
+ this.modelName = options.modelName || "gemini-2.5-flash";
115
124
  this.systemInstructions = options.systemInstructions || DEFAULT_SYSTEM_INSTRUCTIONS;
116
- this.apiKey = options.apiKey || GEMINI_API_KEY;
125
+ if (options.logLevel) {
126
+ this.logLevel = options.logLevel;
127
+ if (this.logLevel === "none") {
128
+ logger_default.level = "silent";
129
+ } else {
130
+ logger_default.level = this.logLevel;
131
+ }
132
+ } else if (LOG_LEVEL) {
133
+ this.logLevel = LOG_LEVEL;
134
+ logger_default.level = LOG_LEVEL;
135
+ } else if (NODE_ENV === "dev") {
136
+ this.logLevel = "debug";
137
+ logger_default.level = "debug";
138
+ } else if (NODE_ENV === "test") {
139
+ this.logLevel = "warn";
140
+ logger_default.level = "warn";
141
+ } else if (NODE_ENV.startsWith("prod")) {
142
+ this.logLevel = "error";
143
+ logger_default.level = "error";
144
+ } else {
145
+ this.logLevel = "info";
146
+ logger_default.level = "info";
147
+ }
148
+ this.apiKey = options.apiKey !== void 0 && options.apiKey !== null ? options.apiKey : GEMINI_API_KEY;
117
149
  if (!this.apiKey) throw new Error("Missing Gemini API key. Provide via options.apiKey or GEMINI_API_KEY env var.");
118
150
  this.chatConfig = {
119
151
  ...DEFAULT_CHAT_CONFIG,
@@ -125,21 +157,28 @@ function AITransformFactory(options = {}) {
125
157
  }
126
158
  this.examplesFile = options.examplesFile || null;
127
159
  this.exampleData = options.exampleData || null;
128
- this.promptKey = options.sourceKey || "PROMPT";
129
- this.answerKey = options.targetKey || "ANSWER";
160
+ this.promptKey = options.promptKey || options.sourceKey || "PROMPT";
161
+ this.answerKey = options.answerKey || options.targetKey || "ANSWER";
130
162
  this.contextKey = options.contextKey || "CONTEXT";
163
+ this.explanationKey = options.explanationKey || "EXPLANATION";
164
+ this.systemInstructionsKey = options.systemInstructionsKey || "SYSTEM";
131
165
  this.maxRetries = options.maxRetries || 3;
132
166
  this.retryDelay = options.retryDelay || 1e3;
167
+ this.asyncValidator = options.asyncValidator || null;
168
+ this.onlyJSON = options.onlyJSON !== void 0 ? options.onlyJSON : true;
133
169
  if (this.promptKey === this.answerKey) {
134
170
  throw new Error("Source and target keys cannot be the same. Please provide distinct keys.");
135
171
  }
136
- logger_default.debug(`Creating AI Transformer with model: ${this.modelName}`);
137
- logger_default.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
138
- this.genAIClient = new import_genai.GoogleGenAI({ apiKey: this.apiKey });
172
+ if (logger_default.level !== "silent") {
173
+ logger_default.debug(`Creating AI Transformer with model: ${this.modelName}`);
174
+ logger_default.debug(`Using keys - Source: "${this.promptKey}", Target: "${this.answerKey}", Context: "${this.contextKey}"`);
175
+ }
176
+ const ai = new import_genai.GoogleGenAI({ apiKey: this.apiKey });
177
+ this.genAIClient = ai;
139
178
  this.chat = null;
140
179
  }
141
- async function initChat() {
142
- if (this.chat) return;
180
+ async function initChat(force = false) {
181
+ if (this.chat && !force) return;
143
182
  logger_default.debug(`Initializing Gemini chat session with model: ${this.modelName}...`);
144
183
  this.chat = await this.genAIClient.chats.create({
145
184
  model: this.modelName,
@@ -147,6 +186,12 @@ async function initChat() {
147
186
  config: this.chatConfig,
148
187
  history: []
149
188
  });
189
+ try {
190
+ await this.genAIClient.models.list();
191
+ logger_default.debug("Gemini API connection successful.");
192
+ } catch (e) {
193
+ throw new Error(`Gemini chat initialization failed: ${e.message}`);
194
+ }
150
195
  logger_default.debug("Gemini chat session initialized.");
151
196
  }
152
197
  async function seedWithExamples(examples) {
@@ -154,95 +199,127 @@ async function seedWithExamples(examples) {
154
199
  if (!examples || !Array.isArray(examples) || examples.length === 0) {
155
200
  if (this.examplesFile) {
156
201
  logger_default.debug(`No examples provided, loading from file: ${this.examplesFile}`);
157
- examples = await import_ak_tools.default.load(import_path.default.resolve(this.examplesFile), true);
202
+ try {
203
+ examples = await import_ak_tools.default.load(import_path.default.resolve(this.examplesFile), true);
204
+ } catch (err) {
205
+ throw new Error(`Could not load examples from file: ${this.examplesFile}. Please check the file path and format.`);
206
+ }
207
+ } else if (this.exampleData) {
208
+ logger_default.debug(`Using example data provided in options.`);
209
+ if (Array.isArray(this.exampleData)) {
210
+ examples = this.exampleData;
211
+ } else {
212
+ throw new Error(`Invalid example data provided. Expected an array of examples.`);
213
+ }
158
214
  } else {
159
215
  logger_default.debug("No examples provided and no examples file specified. Skipping seeding.");
160
216
  return;
161
217
  }
162
218
  }
219
+ const instructionExample = examples.find((ex) => ex[this.systemInstructionsKey]);
220
+ if (instructionExample) {
221
+ logger_default.debug(`Found system instructions in examples; reinitializing chat with new instructions.`);
222
+ this.systemInstructions = instructionExample[this.systemInstructionsKey];
223
+ this.chatConfig.systemInstruction = this.systemInstructions;
224
+ await this.init(true);
225
+ }
163
226
  logger_default.debug(`Seeding chat with ${examples.length} transformation examples...`);
164
227
  const historyToAdd = [];
165
228
  for (const example of examples) {
166
229
  const contextValue = example[this.contextKey] || "";
167
230
  const promptValue = example[this.promptKey] || "";
168
231
  const answerValue = example[this.answerKey] || "";
232
+ const explanationValue = example[this.explanationKey] || "";
233
+ let userText = "";
234
+ let modelResponse = {};
169
235
  if (contextValue) {
170
- let contextText = import_ak_tools.default.isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
171
- historyToAdd.push({
172
- role: "user",
173
- parts: [{ text: `Context: ${contextText}` }]
174
- });
175
- historyToAdd.push({
176
- role: "model",
177
- parts: [{ text: "I understand the context." }]
178
- });
236
+ let contextText = isJSON(contextValue) ? JSON.stringify(contextValue, null, 2) : contextValue;
237
+ userText += `CONTEXT:
238
+ ${contextText}
239
+
240
+ `;
179
241
  }
180
242
  if (promptValue) {
181
- let promptText = import_ak_tools.default.isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
182
- historyToAdd.push({ role: "user", parts: [{ text: promptText }] });
243
+ let promptText = isJSON(promptValue) ? JSON.stringify(promptValue, null, 2) : promptValue;
244
+ userText += promptText;
183
245
  }
184
- if (answerValue) {
185
- let answerText = import_ak_tools.default.isJSON(answerValue) ? JSON.stringify(answerValue, null, 2) : answerValue;
186
- historyToAdd.push({ role: "model", parts: [{ text: answerText }] });
246
+ if (answerValue) modelResponse.data = answerValue;
247
+ if (explanationValue) modelResponse.explanation = explanationValue;
248
+ const modelText = JSON.stringify(modelResponse, null, 2);
249
+ if (userText.trim().length && modelText.trim().length > 0) {
250
+ historyToAdd.push({ role: "user", parts: [{ text: userText.trim() }] });
251
+ historyToAdd.push({ role: "model", parts: [{ text: modelText.trim() }] });
187
252
  }
188
253
  }
189
- const currentHistory = this.chat.getHistory();
254
+ const currentHistory = this?.chat?.getHistory() || [];
255
+ logger_default.debug(`Adding ${historyToAdd.length} examples to chat history (${currentHistory.length} current examples)...`);
190
256
  this.chat = await this.genAIClient.chats.create({
191
257
  model: this.modelName,
192
258
  // @ts-ignore
193
259
  config: this.chatConfig,
194
260
  history: [...currentHistory, ...historyToAdd]
195
261
  });
196
- logger_default.debug("Transformation examples seeded successfully.");
262
+ const newHistory = this.chat.getHistory();
263
+ logger_default.debug(`Created new chat session with ${newHistory.length} examples.`);
264
+ return newHistory;
197
265
  }
198
- async function transformJSON(sourcePayload) {
266
+ async function rawMessage(sourcePayload) {
199
267
  if (!this.chat) {
200
- throw new Error("Chat session not initialized. Call initChat() or seedWithExamples() first.");
268
+ throw new Error("Chat session not initialized.");
201
269
  }
202
- let result;
203
- let actualPayload;
204
- if (sourcePayload && import_ak_tools.default.isJSON(sourcePayload)) actualPayload = JSON.stringify(sourcePayload, null, 2);
205
- else if (typeof sourcePayload === "string") actualPayload = sourcePayload;
206
- else throw new Error("Invalid source payload. Must be a JSON object or a valid JSON string.");
270
+ const actualPayload = typeof sourcePayload === "string" ? sourcePayload : JSON.stringify(sourcePayload, null, 2);
207
271
  try {
208
- result = await this.chat.sendMessage({ message: actualPayload });
272
+ const result = await this.chat.sendMessage({ message: actualPayload });
273
+ const modelResponse = result.text;
274
+ const extractedJSON = extractJSON(modelResponse);
275
+ if (extractedJSON?.data) {
276
+ return extractedJSON.data;
277
+ }
278
+ return extractedJSON;
209
279
  } catch (error) {
210
- logger_default.error("Error with Gemini API:", error);
280
+ if (this.onlyJSON && error.message.includes("Could not extract valid JSON")) {
281
+ throw new Error(`Invalid JSON response from Gemini: ${error.message}`);
282
+ }
211
283
  throw new Error(`Transformation failed: ${error.message}`);
212
284
  }
213
- try {
214
- const modelResponse = result.text;
215
- const parsedResponse = JSON.parse(modelResponse);
216
- return parsedResponse;
217
- } catch (parseError) {
218
- logger_default.error("Error parsing Gemini response:", parseError);
219
- throw new Error(`Invalid JSON response from Gemini: ${parseError.message}`);
220
- }
221
285
  }
222
- async function transformWithValidation(sourcePayload, validatorFn, options = {}) {
286
+ async function prepareAndValidateMessage(sourcePayload, options = {}, validatorFn = null) {
287
+ if (!this.chat) {
288
+ throw new Error("Chat session not initialized. Please call init() first.");
289
+ }
223
290
  const maxRetries = options.maxRetries ?? this.maxRetries;
224
291
  const retryDelay = options.retryDelay ?? this.retryDelay;
225
- let lastPayload = null;
226
292
  let lastError = null;
293
+ let lastPayload = null;
294
+ if (sourcePayload && isJSON(sourcePayload)) {
295
+ lastPayload = JSON.stringify(sourcePayload, null, 2);
296
+ } else if (typeof sourcePayload === "string") {
297
+ lastPayload = sourcePayload;
298
+ } else if (typeof sourcePayload === "boolean" || typeof sourcePayload === "number") {
299
+ lastPayload = sourcePayload.toString();
300
+ } else if (sourcePayload === null || sourcePayload === void 0) {
301
+ lastPayload = JSON.stringify({});
302
+ } else {
303
+ throw new Error("Invalid source payload. Must be a JSON object or string.");
304
+ }
227
305
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
228
306
  try {
229
- const transformedPayload = attempt === 0 ? await this.message(sourcePayload) : await this.rebuild(lastPayload, lastError.message);
230
- const validatedPayload = await validatorFn(transformedPayload);
231
- logger_default.debug(`Transformation and validation succeeded on attempt ${attempt + 1}`);
232
- return validatedPayload;
307
+ const transformedPayload = attempt === 0 ? await this.rawMessage(lastPayload) : await this.rebuild(lastPayload, lastError.message);
308
+ lastPayload = transformedPayload;
309
+ if (validatorFn) {
310
+ await validatorFn(transformedPayload);
311
+ }
312
+ logger_default.debug(`Transformation succeeded on attempt ${attempt + 1}`);
313
+ return transformedPayload;
233
314
  } catch (error) {
234
315
  lastError = error;
235
- if (attempt === 0) {
236
- lastPayload = await this.message(sourcePayload).catch(() => null);
237
- }
238
- if (attempt < maxRetries) {
239
- const delay = retryDelay * Math.pow(2, attempt);
240
- logger_default.warn(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, error.message);
241
- await new Promise((res) => setTimeout(res, delay));
242
- } else {
243
- logger_default.error(`All ${maxRetries + 1} attempts failed`);
244
- throw new Error(`Transformation with validation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
316
+ logger_default.warn(`Attempt ${attempt + 1} failed: ${error.message}`);
317
+ if (attempt >= maxRetries) {
318
+ logger_default.error(`All ${maxRetries + 1} attempts failed.`);
319
+ throw new Error(`Transformation failed after ${maxRetries + 1} attempts. Last error: ${error.message}`);
245
320
  }
321
+ const delay = retryDelay * Math.pow(2, attempt);
322
+ await new Promise((res) => setTimeout(res, delay));
246
323
  }
247
324
  }
248
325
  }
@@ -255,6 +332,7 @@ The server's error message is quoted afterward.
255
332
  ---------------- BAD PAYLOAD ----------------
256
333
  ${JSON.stringify(lastPayload, null, 2)}
257
334
 
335
+
258
336
  ---------------- SERVER ERROR ----------------
259
337
  ${serverError}
260
338
 
@@ -274,6 +352,25 @@ Respond with JSON only \u2013 no comments or explanations.
274
352
  throw new Error(`Gemini returned non-JSON while repairing payload: ${parseErr.message}`);
275
353
  }
276
354
  }
355
+ async function estimateTokenUsage(nextPayload) {
356
+ const contents = [];
357
+ if (this.systemInstructions) {
358
+ contents.push({ parts: [{ text: this.systemInstructions }] });
359
+ }
360
+ if (this.chat && typeof this.chat.getHistory === "function") {
361
+ const history = this.chat.getHistory();
362
+ if (Array.isArray(history) && history.length > 0) {
363
+ contents.push(...history);
364
+ }
365
+ }
366
+ const nextMessage = typeof nextPayload === "string" ? nextPayload : JSON.stringify(nextPayload, null, 2);
367
+ contents.push({ parts: [{ text: nextMessage }] });
368
+ const resp = await this.genAIClient.models.countTokens({
369
+ model: this.modelName,
370
+ contents
371
+ });
372
+ return resp;
373
+ }
277
374
  async function resetChat() {
278
375
  if (this.chat) {
279
376
  logger_default.debug("Resetting Gemini chat session...");
@@ -295,13 +392,135 @@ function getChatHistory() {
295
392
  }
296
393
  return this.chat.getHistory();
297
394
  }
395
+ function isJSON(data) {
396
+ try {
397
+ const attempt = JSON.stringify(data);
398
+ if (attempt?.startsWith("{") || attempt?.startsWith("[")) {
399
+ if (attempt?.endsWith("}") || attempt?.endsWith("]")) {
400
+ return true;
401
+ }
402
+ }
403
+ return false;
404
+ } catch (e) {
405
+ return false;
406
+ }
407
+ }
408
+ function isJSONStr(string) {
409
+ if (typeof string !== "string") return false;
410
+ try {
411
+ const result = JSON.parse(string);
412
+ const type = Object.prototype.toString.call(result);
413
+ return type === "[object Object]" || type === "[object Array]";
414
+ } catch (err) {
415
+ return false;
416
+ }
417
+ }
418
+ function extractJSON(text) {
419
+ if (!text || typeof text !== "string") {
420
+ throw new Error("No text provided for JSON extraction");
421
+ }
422
+ if (isJSONStr(text.trim())) {
423
+ return JSON.parse(text.trim());
424
+ }
425
+ const codeBlockPatterns = [
426
+ /```json\s*\n?([\s\S]*?)\n?\s*```/gi,
427
+ /```\s*\n?([\s\S]*?)\n?\s*```/gi
428
+ ];
429
+ for (const pattern of codeBlockPatterns) {
430
+ const matches = text.match(pattern);
431
+ if (matches) {
432
+ for (const match of matches) {
433
+ const jsonContent = match.replace(/```json\s*\n?/gi, "").replace(/```\s*\n?/gi, "").trim();
434
+ if (isJSONStr(jsonContent)) {
435
+ return JSON.parse(jsonContent);
436
+ }
437
+ }
438
+ }
439
+ }
440
+ const jsonPatterns = [
441
+ // Match complete JSON objects
442
+ /\{[\s\S]*\}/g,
443
+ // Match complete JSON arrays
444
+ /\[[\s\S]*\]/g
445
+ ];
446
+ for (const pattern of jsonPatterns) {
447
+ const matches = text.match(pattern);
448
+ if (matches) {
449
+ for (const match of matches) {
450
+ const candidate = match.trim();
451
+ if (isJSONStr(candidate)) {
452
+ return JSON.parse(candidate);
453
+ }
454
+ }
455
+ }
456
+ }
457
+ const advancedExtract = findCompleteJSONStructures(text);
458
+ if (advancedExtract.length > 0) {
459
+ for (const candidate of advancedExtract) {
460
+ if (isJSONStr(candidate)) {
461
+ return JSON.parse(candidate);
462
+ }
463
+ }
464
+ }
465
+ const cleanedText = text.replace(/^\s*Sure,?\s*here\s+is\s+your?\s+.*?[:\n]/gi, "").replace(/^\s*Here\s+is\s+the\s+.*?[:\n]/gi, "").replace(/^\s*The\s+.*?is\s*[:\n]/gi, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").trim();
466
+ if (isJSONStr(cleanedText)) {
467
+ return JSON.parse(cleanedText);
468
+ }
469
+ throw new Error(`Could not extract valid JSON from model response. Response preview: ${text.substring(0, 200)}...`);
470
+ }
471
+ function findCompleteJSONStructures(text) {
472
+ const results = [];
473
+ const startChars = ["{", "["];
474
+ for (let i = 0; i < text.length; i++) {
475
+ if (startChars.includes(text[i])) {
476
+ const extracted = extractCompleteStructure(text, i);
477
+ if (extracted) {
478
+ results.push(extracted);
479
+ }
480
+ }
481
+ }
482
+ return results;
483
+ }
484
+ function extractCompleteStructure(text, startPos) {
485
+ const startChar = text[startPos];
486
+ const endChar = startChar === "{" ? "}" : "]";
487
+ let depth = 0;
488
+ let inString = false;
489
+ let escaped = false;
490
+ for (let i = startPos; i < text.length; i++) {
491
+ const char = text[i];
492
+ if (escaped) {
493
+ escaped = false;
494
+ continue;
495
+ }
496
+ if (char === "\\" && inString) {
497
+ escaped = true;
498
+ continue;
499
+ }
500
+ if (char === '"' && !escaped) {
501
+ inString = !inString;
502
+ continue;
503
+ }
504
+ if (!inString) {
505
+ if (char === startChar) {
506
+ depth++;
507
+ } else if (char === endChar) {
508
+ depth--;
509
+ if (depth === 0) {
510
+ return text.substring(startPos, i + 1);
511
+ }
512
+ }
513
+ }
514
+ }
515
+ return null;
516
+ }
298
517
  if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
299
518
  logger_default.info("RUNNING AI Transformer as standalone script...");
300
519
  (async () => {
301
520
  try {
302
521
  logger_default.info("Initializing AI Transformer...");
303
522
  const transformer = new AITransformer({
304
- modelName: "gemini-2.0-flash",
523
+ modelName: "gemini-2.5-flash",
305
524
  sourceKey: "INPUT",
306
525
  // Custom source key
307
526
  targetKey: "OUTPUT",
@@ -339,7 +558,7 @@ if (import_meta.url === new URL(`file://${process.argv[1]}`).href) {
339
558
  }
340
559
  return payload;
341
560
  };
342
- const validatedResponse = await transformer.transformWithValidation(
561
+ const validatedResponse = await transformer.messageAndValidate(
343
562
  { "name": "Lynn" },
344
563
  mockValidator
345
564
  );