@j0hanz/code-assistant 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +437 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +72 -0
  4. package/dist/lib/concurrency.d.ts +13 -0
  5. package/dist/lib/concurrency.js +77 -0
  6. package/dist/lib/config.d.ts +43 -0
  7. package/dist/lib/config.js +87 -0
  8. package/dist/lib/diff.d.ts +49 -0
  9. package/dist/lib/diff.js +241 -0
  10. package/dist/lib/errors.d.ts +8 -0
  11. package/dist/lib/errors.js +69 -0
  12. package/dist/lib/format.d.ts +14 -0
  13. package/dist/lib/format.js +33 -0
  14. package/dist/lib/gemini.d.ts +45 -0
  15. package/dist/lib/gemini.js +833 -0
  16. package/dist/lib/progress.d.ts +72 -0
  17. package/dist/lib/progress.js +204 -0
  18. package/dist/lib/tools.d.ts +274 -0
  19. package/dist/lib/tools.js +646 -0
  20. package/dist/prompts/index.d.ts +11 -0
  21. package/dist/prompts/index.js +96 -0
  22. package/dist/resources/index.d.ts +12 -0
  23. package/dist/resources/index.js +115 -0
  24. package/dist/resources/instructions.d.ts +1 -0
  25. package/dist/resources/instructions.js +71 -0
  26. package/dist/resources/server-config.d.ts +1 -0
  27. package/dist/resources/server-config.js +75 -0
  28. package/dist/resources/tool-catalog.d.ts +1 -0
  29. package/dist/resources/tool-catalog.js +30 -0
  30. package/dist/resources/tool-info.d.ts +5 -0
  31. package/dist/resources/tool-info.js +105 -0
  32. package/dist/resources/workflows.d.ts +1 -0
  33. package/dist/resources/workflows.js +59 -0
  34. package/dist/schemas/inputs.d.ts +21 -0
  35. package/dist/schemas/inputs.js +46 -0
  36. package/dist/schemas/outputs.d.ts +121 -0
  37. package/dist/schemas/outputs.js +162 -0
  38. package/dist/server.d.ts +6 -0
  39. package/dist/server.js +88 -0
  40. package/dist/tools/analyze-complexity.d.ts +2 -0
  41. package/dist/tools/analyze-complexity.js +50 -0
  42. package/dist/tools/analyze-pr-impact.d.ts +2 -0
  43. package/dist/tools/analyze-pr-impact.js +62 -0
  44. package/dist/tools/detect-api-breaking.d.ts +2 -0
  45. package/dist/tools/detect-api-breaking.js +49 -0
  46. package/dist/tools/generate-diff.d.ts +2 -0
  47. package/dist/tools/generate-diff.js +140 -0
  48. package/dist/tools/generate-review-summary.d.ts +2 -0
  49. package/dist/tools/generate-review-summary.js +71 -0
  50. package/dist/tools/generate-test-plan.d.ts +2 -0
  51. package/dist/tools/generate-test-plan.js +67 -0
  52. package/dist/tools/index.d.ts +2 -0
  53. package/dist/tools/index.js +19 -0
  54. package/package.json +79 -0
@@ -0,0 +1,833 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { randomInt } from 'node:crypto';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { EventEmitter } from 'node:events';
5
+ import { performance } from 'node:perf_hooks';
6
+ import { setTimeout as sleep } from 'node:timers/promises';
7
+ import { debuglog } from 'node:util';
8
+ import { FinishReason, GoogleGenAI, HarmBlockThreshold, HarmCategory, ThinkingLevel, } from '@google/genai';
9
+ import { ConcurrencyLimiter } from './concurrency.js';
10
+ import { createCachedEnvInt } from './config.js';
11
+ import { getErrorMessage, RETRYABLE_UPSTREAM_ERROR_PATTERN, toRecord, } from './errors.js';
12
+ import { formatUsNumber } from './format.js';
13
+ const CONSTRAINT_KEY_VALUES = [
14
+ 'minLength',
15
+ 'maxLength',
16
+ 'minimum',
17
+ 'maximum',
18
+ 'exclusiveMinimum',
19
+ 'exclusiveMaximum',
20
+ 'minItems',
21
+ 'maxItems',
22
+ 'multipleOf',
23
+ 'pattern',
24
+ 'format',
25
+ ];
26
+ const CONSTRAINT_KEYS = new Set(CONSTRAINT_KEY_VALUES);
27
+ const INTEGER_JSON_TYPE = 'integer';
28
+ const NUMBER_JSON_TYPE = 'number';
29
+ function isJsonRecord(value) {
30
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
31
+ }
32
+ function stripConstraintValue(value) {
33
+ if (Array.isArray(value)) {
34
+ const stripped = new Array(value.length);
35
+ for (let index = 0; index < value.length; index += 1) {
36
+ stripped[index] = stripConstraintValue(value[index]);
37
+ }
38
+ return stripped;
39
+ }
40
+ if (isJsonRecord(value)) {
41
+ return stripJsonSchemaConstraints(value);
42
+ }
43
+ return value;
44
+ }
45
+ export function stripJsonSchemaConstraints(schema) {
46
+ const result = {};
47
+ for (const [key, value] of Object.entries(schema)) {
48
+ if (CONSTRAINT_KEYS.has(key)) {
49
+ continue;
50
+ }
51
+ if (key === 'type' && value === INTEGER_JSON_TYPE) {
52
+ result[key] = NUMBER_JSON_TYPE;
53
+ continue;
54
+ }
55
+ result[key] = stripConstraintValue(value);
56
+ }
57
+ return result;
58
+ }
59
+ const DIGITS_ONLY_PATTERN = /^\d+$/;
60
+ const RETRY_DELAY_BASE_MS = 300;
61
+ const RETRY_DELAY_MAX_MS = 5_000;
62
+ const RETRY_JITTER_RATIO = 0.2;
63
+ export const RETRYABLE_NUMERIC_CODES = new Set([429, 500, 502, 503, 504]);
64
+ export const RETRYABLE_TRANSIENT_CODES = new Set([
65
+ 'RESOURCE_EXHAUSTED',
66
+ 'UNAVAILABLE',
67
+ 'DEADLINE_EXCEEDED',
68
+ 'INTERNAL',
69
+ 'ABORTED',
70
+ ]);
71
+ function getNestedError(error) {
72
+ const record = toRecord(error);
73
+ if (!record) {
74
+ return undefined;
75
+ }
76
+ const nested = record.error;
77
+ const nestedRecord = toRecord(nested);
78
+ if (!nestedRecord) {
79
+ return record;
80
+ }
81
+ return nestedRecord;
82
+ }
83
+ function toNumericCode(candidate) {
84
+ if (typeof candidate === 'number' && Number.isFinite(candidate)) {
85
+ return candidate;
86
+ }
87
+ if (typeof candidate === 'string' && DIGITS_ONLY_PATTERN.test(candidate)) {
88
+ return Number.parseInt(candidate, 10);
89
+ }
90
+ return undefined;
91
+ }
92
+ export function toUpperStringCode(candidate) {
93
+ if (typeof candidate !== 'string') {
94
+ return undefined;
95
+ }
96
+ const normalized = candidate.trim().toUpperCase();
97
+ return normalized.length > 0 ? normalized : undefined;
98
+ }
99
+ function findFirstNumericCode(record, keys) {
100
+ for (const key of keys) {
101
+ const numericCode = toNumericCode(record[key]);
102
+ if (numericCode !== undefined) {
103
+ return numericCode;
104
+ }
105
+ }
106
+ return undefined;
107
+ }
108
+ function findFirstStringCode(record, keys) {
109
+ for (const key of keys) {
110
+ const stringCode = toUpperStringCode(record[key]);
111
+ if (stringCode !== undefined) {
112
+ return stringCode;
113
+ }
114
+ }
115
+ return undefined;
116
+ }
117
+ const NUMERIC_ERROR_KEYS = ['status', 'statusCode', 'code'];
118
+ export function getNumericErrorCode(error) {
119
+ const record = getNestedError(error);
120
+ if (!record) {
121
+ return undefined;
122
+ }
123
+ return findFirstNumericCode(record, NUMERIC_ERROR_KEYS);
124
+ }
125
+ const TRANSIENT_ERROR_KEYS = ['code', 'status', 'statusText'];
126
+ function getTransientErrorCode(error) {
127
+ const record = getNestedError(error);
128
+ if (!record) {
129
+ return undefined;
130
+ }
131
+ return findFirstStringCode(record, TRANSIENT_ERROR_KEYS);
132
+ }
133
+ export function shouldRetry(error) {
134
+ const numericCode = getNumericErrorCode(error);
135
+ if (numericCode !== undefined && RETRYABLE_NUMERIC_CODES.has(numericCode)) {
136
+ return true;
137
+ }
138
+ const transientCode = getTransientErrorCode(error);
139
+ if (transientCode !== undefined &&
140
+ RETRYABLE_TRANSIENT_CODES.has(transientCode)) {
141
+ return true;
142
+ }
143
+ const message = getErrorMessage(error);
144
+ return RETRYABLE_UPSTREAM_ERROR_PATTERN.test(message);
145
+ }
146
+ export function getRetryDelayMs(attempt) {
147
+ const exponentialDelay = RETRY_DELAY_BASE_MS * 2 ** attempt;
148
+ const boundedDelay = Math.min(RETRY_DELAY_MAX_MS, exponentialDelay);
149
+ const jitterWindow = Math.max(1, Math.floor(boundedDelay * RETRY_JITTER_RATIO));
150
+ const jitter = randomInt(0, jitterWindow);
151
+ return Math.min(RETRY_DELAY_MAX_MS, boundedDelay + jitter);
152
+ }
153
+ export function canRetryAttempt(attempt, maxRetries, error) {
154
+ return attempt < maxRetries && shouldRetry(error);
155
+ }
156
+ // Lazy-cached: first call happens after parseCommandLineArgs() sets GEMINI_MODEL.
157
+ let _defaultModel;
158
+ const DEFAULT_MODEL = 'gemini-3-flash-preview';
159
+ const MODEL_FALLBACK_TARGET = 'gemini-2.5-flash';
160
+ const GEMINI_MODEL_ENV_VAR = 'GEMINI_MODEL';
161
+ const GEMINI_HARM_BLOCK_THRESHOLD_ENV_VAR = 'GEMINI_HARM_BLOCK_THRESHOLD';
162
+ const GEMINI_INCLUDE_THOUGHTS_ENV_VAR = 'GEMINI_INCLUDE_THOUGHTS';
163
+ const GEMINI_BATCH_MODE_ENV_VAR = 'GEMINI_BATCH_MODE';
164
+ const GEMINI_API_KEY_ENV_VAR = 'GEMINI_API_KEY';
165
+ const GOOGLE_API_KEY_ENV_VAR = 'GOOGLE_API_KEY';
166
+ function getDefaultModel() {
167
+ _defaultModel ??= process.env[GEMINI_MODEL_ENV_VAR] ?? DEFAULT_MODEL;
168
+ return _defaultModel;
169
+ }
170
+ const DEFAULT_MAX_RETRIES = 3;
171
+ const DEFAULT_TIMEOUT_MS = 90_000;
172
+ const DEFAULT_MAX_OUTPUT_TOKENS = 16_384;
173
+ const DEFAULT_SAFETY_THRESHOLD = HarmBlockThreshold.BLOCK_NONE;
174
+ const DEFAULT_INCLUDE_THOUGHTS = false;
175
+ const DEFAULT_BATCH_MODE = 'off';
176
+ const UNKNOWN_REQUEST_CONTEXT_VALUE = 'unknown';
177
+ const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
178
+ const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']);
179
+ const SLEEP_UNREF_OPTIONS = { ref: false };
180
+ const JSON_CODE_BLOCK_PATTERN = /```(?:json)?\n?([\s\S]*?)(?=\n?```)/u;
181
+ const NEVER_ABORT_SIGNAL = new AbortController().signal;
182
+ const CANCELLED_REQUEST_MESSAGE = 'Gemini request was cancelled.';
183
+ const maxConcurrentCallsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS', 10);
184
+ const maxConcurrentBatchCallsConfig = createCachedEnvInt('MAX_CONCURRENT_BATCH_CALLS', 2);
185
+ const concurrencyWaitMsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS_WAIT_MS', 2_000);
186
+ const batchPollIntervalMsConfig = createCachedEnvInt('GEMINI_BATCH_POLL_INTERVAL_MS', 2_000);
187
+ const batchTimeoutMsConfig = createCachedEnvInt('GEMINI_BATCH_TIMEOUT_MS', 120_000);
188
+ const callLimiter = new ConcurrencyLimiter(() => maxConcurrentCallsConfig.get(), () => concurrencyWaitMsConfig.get(), (limit, ms) => formatConcurrencyLimitErrorMessage(limit, ms), () => CANCELLED_REQUEST_MESSAGE);
189
+ const batchCallLimiter = new ConcurrencyLimiter(() => maxConcurrentBatchCallsConfig.get(), () => concurrencyWaitMsConfig.get(), (limit, ms) => formatConcurrencyLimitErrorMessage(limit, ms), () => CANCELLED_REQUEST_MESSAGE);
190
+ const SAFETY_CATEGORIES = [
191
+ HarmCategory.HARM_CATEGORY_HATE_SPEECH,
192
+ HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
193
+ HarmCategory.HARM_CATEGORY_HARASSMENT,
194
+ HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
195
+ ];
196
+ const SAFETY_THRESHOLD_BY_NAME = {
197
+ BLOCK_NONE: HarmBlockThreshold.BLOCK_NONE,
198
+ BLOCK_ONLY_HIGH: HarmBlockThreshold.BLOCK_ONLY_HIGH,
199
+ BLOCK_MEDIUM_AND_ABOVE: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
200
+ BLOCK_LOW_AND_ABOVE: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
201
+ };
202
+ let cachedSafetyThresholdEnv;
203
+ let cachedSafetyThreshold = DEFAULT_SAFETY_THRESHOLD;
204
+ let cachedIncludeThoughtsEnv;
205
+ let cachedIncludeThoughts = DEFAULT_INCLUDE_THOUGHTS;
206
+ const safetySettingsCache = new Map();
207
+ function getSafetyThreshold() {
208
+ const threshold = process.env[GEMINI_HARM_BLOCK_THRESHOLD_ENV_VAR];
209
+ if (threshold === cachedSafetyThresholdEnv) {
210
+ return cachedSafetyThreshold;
211
+ }
212
+ cachedSafetyThresholdEnv = threshold;
213
+ if (!threshold) {
214
+ cachedSafetyThreshold = DEFAULT_SAFETY_THRESHOLD;
215
+ return cachedSafetyThreshold;
216
+ }
217
+ const parsedThreshold = parseSafetyThreshold(threshold);
218
+ if (parsedThreshold) {
219
+ cachedSafetyThreshold = parsedThreshold;
220
+ return cachedSafetyThreshold;
221
+ }
222
+ cachedSafetyThreshold = DEFAULT_SAFETY_THRESHOLD;
223
+ return cachedSafetyThreshold;
224
+ }
225
+ function parseSafetyThreshold(threshold) {
226
+ const normalizedThreshold = threshold.trim().toUpperCase();
227
+ if (!(normalizedThreshold in SAFETY_THRESHOLD_BY_NAME)) {
228
+ return undefined;
229
+ }
230
+ return SAFETY_THRESHOLD_BY_NAME[normalizedThreshold];
231
+ }
232
+ const THINKING_LEVEL_MAP = {
233
+ minimal: ThinkingLevel.MINIMAL,
234
+ low: ThinkingLevel.LOW,
235
+ medium: ThinkingLevel.MEDIUM,
236
+ high: ThinkingLevel.HIGH,
237
+ };
238
+ function getThinkingConfig(thinkingLevel, includeThoughts) {
239
+ if (!thinkingLevel && !includeThoughts) {
240
+ return undefined;
241
+ }
242
+ return {
243
+ ...(thinkingLevel
244
+ ? { thinkingLevel: THINKING_LEVEL_MAP[thinkingLevel] }
245
+ : {}),
246
+ ...(includeThoughts ? { includeThoughts: true } : {}),
247
+ };
248
+ }
249
+ function parseBooleanEnv(value) {
250
+ const normalized = value.trim().toLowerCase();
251
+ if (normalized.length === 0) {
252
+ return undefined;
253
+ }
254
+ if (TRUE_ENV_VALUES.has(normalized)) {
255
+ return true;
256
+ }
257
+ if (FALSE_ENV_VALUES.has(normalized)) {
258
+ return false;
259
+ }
260
+ return undefined;
261
+ }
262
+ function getDefaultIncludeThoughts() {
263
+ const value = process.env[GEMINI_INCLUDE_THOUGHTS_ENV_VAR];
264
+ if (value === cachedIncludeThoughtsEnv) {
265
+ return cachedIncludeThoughts;
266
+ }
267
+ cachedIncludeThoughtsEnv = value;
268
+ if (!value) {
269
+ cachedIncludeThoughts = DEFAULT_INCLUDE_THOUGHTS;
270
+ return cachedIncludeThoughts;
271
+ }
272
+ cachedIncludeThoughts = parseBooleanEnv(value) ?? DEFAULT_INCLUDE_THOUGHTS;
273
+ return cachedIncludeThoughts;
274
+ }
275
+ function getDefaultBatchMode() {
276
+ const value = process.env[GEMINI_BATCH_MODE_ENV_VAR]?.trim().toLowerCase();
277
+ if (value === 'inline') {
278
+ return 'inline';
279
+ }
280
+ return DEFAULT_BATCH_MODE;
281
+ }
282
+ function applyResponseKeyOrdering(responseSchema, responseKeyOrdering) {
283
+ if (!responseKeyOrdering || responseKeyOrdering.length === 0) {
284
+ return responseSchema;
285
+ }
286
+ return {
287
+ ...responseSchema,
288
+ propertyOrdering: [...responseKeyOrdering],
289
+ };
290
+ }
291
+ function getPromptWithFunctionCallingContext(request) {
292
+ return request.prompt;
293
+ }
294
+ function getSafetySettings(threshold) {
295
+ const cached = safetySettingsCache.get(threshold);
296
+ if (cached) {
297
+ return cached;
298
+ }
299
+ const settings = SAFETY_CATEGORIES.map((category) => ({
300
+ category,
301
+ threshold,
302
+ }));
303
+ safetySettingsCache.set(threshold, settings);
304
+ return settings;
305
+ }
306
+ let cachedClient;
307
+ export const geminiEvents = new EventEmitter();
308
+ const debug = debuglog('gemini');
309
+ geminiEvents.on('log', (payload) => {
310
+ if (debug.enabled) {
311
+ debug('%j', payload);
312
+ }
313
+ });
314
+ const geminiContext = new AsyncLocalStorage({
315
+ name: 'gemini_request',
316
+ defaultValue: {
317
+ requestId: UNKNOWN_REQUEST_CONTEXT_VALUE,
318
+ model: UNKNOWN_REQUEST_CONTEXT_VALUE,
319
+ },
320
+ });
321
+ const UNKNOWN_CONTEXT = {
322
+ requestId: UNKNOWN_REQUEST_CONTEXT_VALUE,
323
+ model: UNKNOWN_REQUEST_CONTEXT_VALUE,
324
+ };
325
+ export function getCurrentRequestId() {
326
+ const context = geminiContext.getStore();
327
+ return context?.requestId ?? UNKNOWN_REQUEST_CONTEXT_VALUE;
328
+ }
329
+ function getApiKey() {
330
+ const apiKey = process.env[GEMINI_API_KEY_ENV_VAR] ?? process.env[GOOGLE_API_KEY_ENV_VAR];
331
+ if (!apiKey) {
332
+ throw new Error(`Missing ${GEMINI_API_KEY_ENV_VAR} or ${GOOGLE_API_KEY_ENV_VAR}.`);
333
+ }
334
+ return apiKey;
335
+ }
336
+ function getClient() {
337
+ cachedClient ??= new GoogleGenAI({
338
+ apiKey: getApiKey(),
339
+ apiVersion: 'v1beta',
340
+ });
341
+ return cachedClient;
342
+ }
343
+ export function setClientForTesting(client) {
344
+ cachedClient = client;
345
+ }
346
+ function nextRequestId() {
347
+ return randomUUID();
348
+ }
349
+ function logEvent(event, details) {
350
+ const context = geminiContext.getStore() ?? UNKNOWN_CONTEXT;
351
+ geminiEvents.emit('log', {
352
+ event,
353
+ requestId: context.requestId,
354
+ model: context.model,
355
+ ...details,
356
+ });
357
+ }
358
+ async function safeCallOnLog(onLog, level, data) {
359
+ try {
360
+ await onLog?.(level, data);
361
+ }
362
+ catch {
363
+ // Log callbacks are best-effort; never fail the tool call.
364
+ }
365
+ }
366
+ async function emitGeminiLog(onLog, level, payload) {
367
+ logEvent(payload.event, payload.details);
368
+ await safeCallOnLog(onLog, level, {
369
+ event: payload.event,
370
+ ...payload.details,
371
+ });
372
+ }
373
+ function buildGenerationConfig(request, abortSignal) {
374
+ const includeThoughts = request.includeThoughts ?? getDefaultIncludeThoughts();
375
+ const thinkingConfig = getThinkingConfig(request.thinkingLevel, includeThoughts);
376
+ const config = {
377
+ temperature: request.temperature ?? 1.0,
378
+ maxOutputTokens: request.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
379
+ responseMimeType: 'application/json',
380
+ responseSchema: applyResponseKeyOrdering(request.responseSchema, request.responseKeyOrdering),
381
+ safetySettings: getSafetySettings(getSafetyThreshold()),
382
+ abortSignal,
383
+ };
384
+ if (request.systemInstruction) {
385
+ config.systemInstruction = request.systemInstruction;
386
+ }
387
+ if (thinkingConfig) {
388
+ config.thinkingConfig = thinkingConfig;
389
+ }
390
+ return config;
391
+ }
392
+ function combineSignals(signal, requestSignal) {
393
+ return requestSignal ? AbortSignal.any([signal, requestSignal]) : signal;
394
+ }
395
+ function throwIfRequestCancelled(requestSignal) {
396
+ if (requestSignal?.aborted) {
397
+ throw new Error(CANCELLED_REQUEST_MESSAGE);
398
+ }
399
+ }
400
+ function getSleepOptions(signal) {
401
+ return signal ? { ...SLEEP_UNREF_OPTIONS, signal } : SLEEP_UNREF_OPTIONS;
402
+ }
403
+ function parseStructuredResponse(responseText) {
404
+ if (!responseText) {
405
+ throw new Error('Gemini returned an empty response body.');
406
+ }
407
+ try {
408
+ return JSON.parse(responseText);
409
+ }
410
+ catch {
411
+ // fast-path failed; try extracting from markdown block
412
+ }
413
+ const jsonMatch = JSON_CODE_BLOCK_PATTERN.exec(responseText);
414
+ const jsonText = jsonMatch?.[1] ?? responseText;
415
+ try {
416
+ return JSON.parse(jsonText);
417
+ }
418
+ catch (error) {
419
+ throw new Error(`Model produced invalid JSON: ${getErrorMessage(error)}`, {
420
+ cause: error,
421
+ });
422
+ }
423
+ }
424
+ function formatTimeoutErrorMessage(timeoutMs) {
425
+ return `Gemini request timed out after ${formatUsNumber(timeoutMs)}ms.`;
426
+ }
427
+ function formatConcurrencyLimitErrorMessage(limit, waitLimitMs) {
428
+ return `Too many concurrent Gemini calls (limit: ${formatUsNumber(limit)}; waited ${formatUsNumber(waitLimitMs)}ms).`;
429
+ }
430
+ async function generateContentWithTimeout(request, model, timeoutMs) {
431
+ const controller = new AbortController();
432
+ const timeout = setTimeout(() => {
433
+ controller.abort();
434
+ }, timeoutMs);
435
+ timeout.unref();
436
+ const signal = combineSignals(controller.signal, request.signal);
437
+ try {
438
+ return await getClient().models.generateContent({
439
+ model,
440
+ contents: getPromptWithFunctionCallingContext(request),
441
+ config: buildGenerationConfig(request, signal),
442
+ });
443
+ }
444
+ catch (error) {
445
+ throwIfRequestCancelled(request.signal);
446
+ if (controller.signal.aborted) {
447
+ throw new Error(formatTimeoutErrorMessage(timeoutMs), { cause: error });
448
+ }
449
+ throw error;
450
+ }
451
+ finally {
452
+ clearTimeout(timeout);
453
+ }
454
+ }
455
+ function extractThoughtsFromParts(parts) {
456
+ if (!Array.isArray(parts)) {
457
+ return undefined;
458
+ }
459
+ const thoughtParts = parts.filter((part) => typeof part === 'object' &&
460
+ part !== null &&
461
+ part.thought === true &&
462
+ typeof part.text === 'string');
463
+ if (thoughtParts.length === 0) {
464
+ return undefined;
465
+ }
466
+ return thoughtParts.map((part) => part.text).join('\n\n');
467
+ }
468
+ async function executeAttempt(request, model, timeoutMs, attempt, onLog) {
469
+ const startedAt = performance.now();
470
+ const response = await generateContentWithTimeout(request, model, timeoutMs);
471
+ const latencyMs = Math.round(performance.now() - startedAt);
472
+ const finishReason = response.candidates?.[0]?.finishReason;
473
+ const thoughts = extractThoughtsFromParts(response.candidates?.[0]?.content?.parts);
474
+ await emitGeminiLog(onLog, 'info', {
475
+ event: 'gemini_call',
476
+ details: {
477
+ attempt,
478
+ latencyMs,
479
+ finishReason: finishReason ?? null,
480
+ usageMetadata: response.usageMetadata ?? null,
481
+ ...(thoughts ? { thoughts } : {}),
482
+ },
483
+ });
484
+ if (finishReason === FinishReason.MAX_TOKENS) {
485
+ const limit = request.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
486
+ throw new Error(`Response truncated: model output exceeds limit (maxOutputTokens=${formatUsNumber(limit)}). Increase maxOutputTokens or reduce prompt complexity.`);
487
+ }
488
+ return parseStructuredResponse(response.text);
489
+ }
490
+ async function waitBeforeRetry(attempt, error, onLog, requestSignal) {
491
+ const delayMs = getRetryDelayMs(attempt);
492
+ const reason = getErrorMessage(error);
493
+ await emitGeminiLog(onLog, 'warning', {
494
+ event: 'gemini_retry',
495
+ details: {
496
+ attempt,
497
+ delayMs,
498
+ reason,
499
+ },
500
+ });
501
+ throwIfRequestCancelled(requestSignal);
502
+ try {
503
+ await sleep(delayMs, undefined, getSleepOptions(requestSignal));
504
+ }
505
+ catch (sleepError) {
506
+ throwIfRequestCancelled(requestSignal);
507
+ throw sleepError;
508
+ }
509
+ }
510
+ async function throwGeminiFailure(attemptsMade, lastError, onLog) {
511
+ const message = getErrorMessage(lastError);
512
+ await emitGeminiLog(onLog, 'error', {
513
+ event: 'gemini_failure',
514
+ details: {
515
+ error: message,
516
+ attempts: attemptsMade,
517
+ },
518
+ });
519
+ throw new Error(`Gemini request failed after ${attemptsMade} attempts: ${message}`, { cause: lastError });
520
+ }
521
+ function shouldUseModelFallback(error, model) {
522
+ return getNumericErrorCode(error) === 404 && model === DEFAULT_MODEL;
523
+ }
524
+ async function applyModelFallback(request, onLog, reason) {
525
+ await emitGeminiLog(onLog, 'warning', {
526
+ event: 'gemini_model_fallback',
527
+ details: {
528
+ from: DEFAULT_MODEL,
529
+ to: MODEL_FALLBACK_TARGET,
530
+ reason,
531
+ },
532
+ });
533
+ return {
534
+ model: MODEL_FALLBACK_TARGET,
535
+ request: omitThinkingLevel(request),
536
+ };
537
+ }
538
+ async function tryApplyModelFallback(error, model, request, onLog, reason) {
539
+ if (!shouldUseModelFallback(error, model)) {
540
+ return undefined;
541
+ }
542
+ return applyModelFallback(request, onLog, reason);
543
+ }
544
+ function countAttemptsMade(attempt) {
545
+ return attempt + 1;
546
+ }
547
+ async function runWithRetries(request, model, timeoutMs, maxRetries, onLog) {
548
+ let lastError;
549
+ let currentModel = model;
550
+ let effectiveRequest = request;
551
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
552
+ try {
553
+ return await executeAttempt(effectiveRequest, currentModel, timeoutMs, attempt, onLog);
554
+ }
555
+ catch (error) {
556
+ lastError = error;
557
+ const fallback = await tryApplyModelFallback(error, currentModel, request, onLog, 'Model not found (404)');
558
+ if (fallback) {
559
+ currentModel = fallback.model;
560
+ effectiveRequest = fallback.request;
561
+ continue;
562
+ }
563
+ if (!canRetryAttempt(attempt, maxRetries, error)) {
564
+ return throwGeminiFailure(countAttemptsMade(attempt), lastError, onLog);
565
+ }
566
+ await waitBeforeRetry(attempt, error, onLog, request.signal);
567
+ }
568
+ }
569
+ return throwGeminiFailure(maxRetries + 1, lastError, onLog);
570
+ }
571
+ function omitThinkingLevel(request) {
572
+ const copy = { ...request };
573
+ Reflect.deleteProperty(copy, 'thinkingLevel');
574
+ return copy;
575
+ }
576
+ function isInlineBatchMode(mode) {
577
+ return mode === 'inline';
578
+ }
579
+ async function acquireQueueSlot(mode, requestSignal) {
580
+ const queueWaitStartedAt = performance.now();
581
+ if (isInlineBatchMode(mode)) {
582
+ await batchCallLimiter.acquire(requestSignal);
583
+ }
584
+ else {
585
+ await callLimiter.acquire(requestSignal);
586
+ }
587
+ return {
588
+ queueWaitMs: Math.round(performance.now() - queueWaitStartedAt),
589
+ waitingCalls: isInlineBatchMode(mode)
590
+ ? batchCallLimiter.pendingCount
591
+ : callLimiter.pendingCount,
592
+ };
593
+ }
594
+ function releaseQueueSlot(mode) {
595
+ if (isInlineBatchMode(mode)) {
596
+ batchCallLimiter.release();
597
+ return;
598
+ }
599
+ callLimiter.release();
600
+ }
601
+ const BatchHelper = {
602
+ getState(payload) {
603
+ const record = toRecord(payload);
604
+ if (!record)
605
+ return undefined;
606
+ const directState = toUpperStringCode(record.state);
607
+ if (directState)
608
+ return directState;
609
+ const metadata = toRecord(record.metadata);
610
+ return metadata ? toUpperStringCode(metadata.state) : undefined;
611
+ },
612
+ getResponseText(payload) {
613
+ const record = toRecord(payload);
614
+ if (!record)
615
+ return undefined;
616
+ // Try inlineResponse.text
617
+ const inline = toRecord(record.inlineResponse);
618
+ if (typeof inline?.text === 'string')
619
+ return inline.text;
620
+ const response = toRecord(record.response);
621
+ if (!response)
622
+ return undefined;
623
+ // Try response.text
624
+ if (typeof response.text === 'string')
625
+ return response.text;
626
+ // Try response.inlineResponses[0].text
627
+ if (Array.isArray(response.inlineResponses) &&
628
+ response.inlineResponses.length > 0) {
629
+ const first = toRecord(response.inlineResponses[0]);
630
+ if (typeof first?.text === 'string')
631
+ return first.text;
632
+ }
633
+ return undefined;
634
+ },
635
+ getErrorDetail(payload) {
636
+ const record = toRecord(payload);
637
+ if (!record)
638
+ return undefined;
639
+ // Try error.message
640
+ const directError = toRecord(record.error);
641
+ if (typeof directError?.message === 'string')
642
+ return directError.message;
643
+ // Try metadata.error.message
644
+ const metadata = toRecord(record.metadata);
645
+ const metaError = toRecord(metadata?.error);
646
+ if (typeof metaError?.message === 'string')
647
+ return metaError.message;
648
+ // Try response.error.message
649
+ const response = toRecord(record.response);
650
+ const respError = toRecord(response?.error);
651
+ return typeof respError?.message === 'string'
652
+ ? respError.message
653
+ : undefined;
654
+ },
655
+ getSuccessResponseText(polled) {
656
+ const text = this.getResponseText(polled);
657
+ if (text)
658
+ return text;
659
+ const err = this.getErrorDetail(polled);
660
+ throw new Error(err
661
+ ? `Gemini batch request succeeded but returned no response text: ${err}`
662
+ : 'Gemini batch request succeeded but returned no response text.');
663
+ },
664
+ handleTerminalState(state, payload) {
665
+ if (state === 'JOB_STATE_FAILED' || state === 'JOB_STATE_CANCELLED') {
666
+ const err = this.getErrorDetail(payload);
667
+ throw new Error(err
668
+ ? `Gemini batch request ended with state ${state}: ${err}`
669
+ : `Gemini batch request ended with state ${state}.`);
670
+ }
671
+ },
672
+ };
673
+ async function pollBatchStatusWithRetries(batches, batchName, onLog, requestSignal) {
674
+ const maxPollRetries = 2;
675
+ for (let attempt = 0; attempt <= maxPollRetries; attempt += 1) {
676
+ try {
677
+ return await batches.get({ name: batchName });
678
+ }
679
+ catch (error) {
680
+ if (!canRetryAttempt(attempt, maxPollRetries, error)) {
681
+ throw error;
682
+ }
683
+ await waitBeforeRetry(attempt, error, onLog, requestSignal);
684
+ }
685
+ }
686
+ throw new Error('Batch polling retries exhausted unexpectedly.');
687
+ }
688
+ async function cancelBatchIfNeeded(request, batches, batchName, onLog, completed, timedOut) {
689
+ const aborted = request.signal?.aborted === true;
690
+ const shouldCancel = !completed && (aborted || timedOut);
691
+ if (!shouldCancel || !batchName || !batches.cancel) {
692
+ return;
693
+ }
694
+ const reason = timedOut ? 'timeout' : 'aborted';
695
+ try {
696
+ await batches.cancel({ name: batchName });
697
+ await emitGeminiLog(onLog, 'info', {
698
+ event: 'gemini_batch_cancelled',
699
+ details: { batchName, reason },
700
+ });
701
+ }
702
+ catch (error) {
703
+ await emitGeminiLog(onLog, 'warning', {
704
+ event: 'gemini_batch_cancel_failed',
705
+ details: {
706
+ batchName,
707
+ reason,
708
+ error: getErrorMessage(error),
709
+ },
710
+ });
711
+ }
712
+ }
713
+ async function createBatchJobWithFallback(request, batches, model, onLog) {
714
+ let currentModel = model;
715
+ let effectiveRequest = request;
716
+ const createSignal = request.signal ?? NEVER_ABORT_SIGNAL;
717
+ for (let attempt = 0; attempt <= 1; attempt += 1) {
718
+ try {
719
+ const createPayload = {
720
+ model: currentModel,
721
+ src: [
722
+ {
723
+ contents: [
724
+ { role: 'user', parts: [{ text: effectiveRequest.prompt }] },
725
+ ],
726
+ config: buildGenerationConfig(effectiveRequest, createSignal),
727
+ },
728
+ ],
729
+ };
730
+ return await batches.create(createPayload);
731
+ }
732
+ catch (error) {
733
+ if (attempt === 0 && shouldUseModelFallback(error, currentModel)) {
734
+ const fallback = await applyModelFallback(request, onLog, 'Model not found (404) during batch create');
735
+ currentModel = fallback.model;
736
+ effectiveRequest = fallback.request;
737
+ continue;
738
+ }
739
+ throw error;
740
+ }
741
+ }
742
+ throw new Error('Unexpected state: batch creation loop exited without returning or throwing.');
743
+ }
744
+ async function pollBatchForCompletion(batches, batchName, onLog, requestSignal) {
745
+ const pollIntervalMs = batchPollIntervalMsConfig.get();
746
+ const timeoutMs = batchTimeoutMsConfig.get();
747
+ const pollStart = performance.now();
748
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
749
+ while (true) {
750
+ throwIfRequestCancelled(requestSignal);
751
+ const elapsedMs = Math.round(performance.now() - pollStart);
752
+ if (elapsedMs > timeoutMs) {
753
+ throw new Error(`Gemini batch request timed out after ${formatUsNumber(timeoutMs)}ms.`);
754
+ }
755
+ const polled = await pollBatchStatusWithRetries(batches, batchName, onLog, requestSignal);
756
+ const state = BatchHelper.getState(polled);
757
+ if (state === 'JOB_STATE_SUCCEEDED') {
758
+ const responseText = BatchHelper.getSuccessResponseText(polled);
759
+ return parseStructuredResponse(responseText);
760
+ }
761
+ BatchHelper.handleTerminalState(state, polled);
762
+ await sleep(pollIntervalMs, undefined, getSleepOptions(requestSignal));
763
+ }
764
+ }
765
+ async function runInlineBatchWithPolling(request, model, onLog) {
766
+ const client = getClient();
767
+ const { batches } = client;
768
+ if (!batches) {
769
+ throw new Error('Batch mode requires SDK batch support, but batches API is unavailable.');
770
+ }
771
+ let batchName;
772
+ let completed = false;
773
+ let timedOut = false;
774
+ try {
775
+ const createdJob = await createBatchJobWithFallback(request, batches, model, onLog);
776
+ const createdRecord = toRecord(createdJob);
777
+ batchName =
778
+ typeof createdRecord?.name === 'string' ? createdRecord.name : undefined;
779
+ if (!batchName)
780
+ throw new Error('Batch mode failed to return a job name.');
781
+ await emitGeminiLog(onLog, 'info', {
782
+ event: 'gemini_batch_created',
783
+ details: { batchName },
784
+ });
785
+ const result = await pollBatchForCompletion(batches, batchName, onLog, request.signal);
786
+ completed = true;
787
+ return result;
788
+ }
789
+ catch (error) {
790
+ if (getErrorMessage(error).includes('timed out')) {
791
+ timedOut = true;
792
+ }
793
+ throw error;
794
+ }
795
+ finally {
796
+ await cancelBatchIfNeeded(request, batches, batchName, onLog, completed, timedOut);
797
+ }
798
+ }
799
+ export function getGeminiQueueSnapshot() {
800
+ return {
801
+ activeWaiters: callLimiter.pendingCount,
802
+ activeCalls: callLimiter.active,
803
+ activeBatchWaiters: batchCallLimiter.pendingCount,
804
+ activeBatchCalls: batchCallLimiter.active,
805
+ };
806
+ }
807
+ export async function generateStructuredJson(request) {
808
+ const model = request.model ?? getDefaultModel();
809
+ const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
810
+ const maxRetries = request.maxRetries ?? DEFAULT_MAX_RETRIES;
811
+ const batchMode = request.batchMode ?? getDefaultBatchMode();
812
+ const { onLog } = request;
813
+ const { queueWaitMs, waitingCalls } = await acquireQueueSlot(batchMode, request.signal);
814
+ await safeCallOnLog(onLog, 'info', {
815
+ event: 'gemini_queue_acquired',
816
+ queueWaitMs,
817
+ waitingCalls,
818
+ activeCalls: callLimiter.active,
819
+ activeBatchCalls: batchCallLimiter.active,
820
+ mode: batchMode,
821
+ });
822
+ try {
823
+ return await geminiContext.run({ requestId: nextRequestId(), model }, () => {
824
+ if (isInlineBatchMode(batchMode)) {
825
+ return runInlineBatchWithPolling(request, model, onLog);
826
+ }
827
+ return runWithRetries(request, model, timeoutMs, maxRetries, onLog);
828
+ });
829
+ }
830
+ finally {
831
+ releaseQueueSlot(batchMode);
832
+ }
833
+ }