genai-lite 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/image/GenaiElectronImageAdapter.d.ts +25 -2
- package/dist/adapters/image/GenaiElectronImageAdapter.js +150 -26
- package/dist/adapters/image/MockImageAdapter.d.ts +1 -0
- package/dist/adapters/image/MockImageAdapter.js +8 -1
- package/dist/adapters/image/OpenAIImageAdapter.d.ts +1 -0
- package/dist/adapters/image/OpenAIImageAdapter.js +6 -6
- package/dist/image/ImageService.d.ts +3 -2
- package/dist/image/ImageService.js +33 -3
- package/dist/image/config.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/llm/clients/GeminiClientAdapter.d.ts +1 -0
- package/dist/llm/clients/GeminiClientAdapter.js +69 -4
- package/dist/types/image.d.ts +16 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ A lightweight, portable Node.js/TypeScript library providing a unified interface
|
|
|
13
13
|
- ⚡ **Lightweight** - Minimal dependencies, focused functionality
|
|
14
14
|
- 🛡️ **Provider Normalization** - Consistent responses across different AI APIs
|
|
15
15
|
- 🧠 **Local Reasoning Toggle** - Turn thinking on/off for detected GGUF models (Qwen 3.5-class, Gemma 4, and more) with vendor-tuned sampling defaults
|
|
16
|
-
- 🔁 **Built-in Reliability** - Automatic retries with backoff/`Retry-After`, per-request timeouts, and abort signals
|
|
16
|
+
- 🔁 **Built-in Reliability** - Automatic retries with backoff/`Retry-After`, per-request timeouts, and abort signals; image generation supports cancellation too (including server-side cancel for local diffusion)
|
|
17
17
|
- 🎨 **Configurable Model Presets** - Built-in presets with full customization options
|
|
18
18
|
- 🎭 **Template Engine** - Sophisticated templating with conditionals and variable substitution
|
|
19
19
|
- 📊 **Configurable Logging** - Debug mode, custom loggers (pino, winston), and silent mode for tests
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Supports stable-diffusion.cpp through HTTP wrapper with async polling for progress.
|
|
6
6
|
*
|
|
7
7
|
* Provider ID: 'genai-electron-images'
|
|
8
|
-
* Default endpoint: http://
|
|
8
|
+
* Default endpoint: http://127.0.0.1:8081
|
|
9
9
|
* Configure via: GENAI_ELECTRON_IMAGE_BASE_URL environment variable
|
|
10
10
|
*
|
|
11
11
|
* This adapter uses genai-electron's async image generation API which:
|
|
@@ -34,6 +34,7 @@ export declare class GenaiElectronImageAdapter implements ImageProviderAdapter {
|
|
|
34
34
|
resolvedPrompt: string;
|
|
35
35
|
settings: ResolvedImageGenerationSettings;
|
|
36
36
|
apiKey: string | null;
|
|
37
|
+
signal?: AbortSignal;
|
|
37
38
|
}): Promise<ImageGenerationResponse>;
|
|
38
39
|
/**
|
|
39
40
|
* Builds the request payload for genai-electron
|
|
@@ -41,12 +42,24 @@ export declare class GenaiElectronImageAdapter implements ImageProviderAdapter {
|
|
|
41
42
|
private buildRequestPayload;
|
|
42
43
|
/**
|
|
43
44
|
* Starts generation and returns the generation ID
|
|
45
|
+
*
|
|
46
|
+
* The adapter's timeout and the caller's abort signal share one controller,
|
|
47
|
+
* so both surface as AbortError — classification comes from adapter-side
|
|
48
|
+
* state (timer-fired flag vs signal.aborted), with the caller's abort winning.
|
|
44
49
|
*/
|
|
45
50
|
private startGeneration;
|
|
46
51
|
/**
|
|
47
52
|
* Polls for generation completion with progress updates
|
|
48
53
|
*/
|
|
49
54
|
private pollForCompletion;
|
|
55
|
+
/**
|
|
56
|
+
* Best-effort cancellation of a server-side generation
|
|
57
|
+
* (DELETE /v1/images/generations/:id, available since genai-electron 0.6.0).
|
|
58
|
+
*
|
|
59
|
+
* All failures are swallowed: 404/409 just mean there is nothing left to
|
|
60
|
+
* cancel, and cleanup must never mask the caller's primary error.
|
|
61
|
+
*/
|
|
62
|
+
private cancelGeneration;
|
|
50
63
|
/**
|
|
51
64
|
* Converts genai-electron result to ImageGenerationResponse
|
|
52
65
|
*/
|
|
@@ -55,6 +68,16 @@ export declare class GenaiElectronImageAdapter implements ImageProviderAdapter {
|
|
|
55
68
|
* Creates an HTTP error with context
|
|
56
69
|
*/
|
|
57
70
|
private createHttpError;
|
|
71
|
+
/**
|
|
72
|
+
* Creates a timeout-typed error: name 'TimeoutError' is recognized by the
|
|
73
|
+
* shared error mapping and classified as REQUEST_TIMEOUT / timeout_error
|
|
74
|
+
*/
|
|
75
|
+
private createTimeoutError;
|
|
76
|
+
/**
|
|
77
|
+
* Creates an abort-typed error: name 'AbortError' is recognized by the
|
|
78
|
+
* shared error mapping and classified as REQUEST_ABORTED / abort_error
|
|
79
|
+
*/
|
|
80
|
+
private createAbortError;
|
|
58
81
|
/**
|
|
59
82
|
* Creates a generation error from genai-electron error codes
|
|
60
83
|
*/
|
|
@@ -64,7 +87,7 @@ export declare class GenaiElectronImageAdapter implements ImageProviderAdapter {
|
|
|
64
87
|
*/
|
|
65
88
|
private handleError;
|
|
66
89
|
/**
|
|
67
|
-
* Sleep helper for polling
|
|
90
|
+
* Sleep helper for polling; rejects immediately when the signal aborts
|
|
68
91
|
*/
|
|
69
92
|
private sleep;
|
|
70
93
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Supports stable-diffusion.cpp through HTTP wrapper with async polling for progress.
|
|
7
7
|
*
|
|
8
8
|
* Provider ID: 'genai-electron-images'
|
|
9
|
-
* Default endpoint: http://
|
|
9
|
+
* Default endpoint: http://127.0.0.1:8081
|
|
10
10
|
* Configure via: GENAI_ELECTRON_IMAGE_BASE_URL environment variable
|
|
11
11
|
*
|
|
12
12
|
* This adapter uses genai-electron's async image generation API which:
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
19
|
exports.GenaiElectronImageAdapter = void 0;
|
|
20
20
|
const errorUtils_1 = require("../../shared/adapters/errorUtils");
|
|
21
|
+
const types_1 = require("../../llm/clients/types");
|
|
21
22
|
const defaultLogger_1 = require("../../logging/defaultLogger");
|
|
22
23
|
/**
|
|
23
24
|
* Adapter for genai-electron's local diffusion image generation
|
|
@@ -33,7 +34,7 @@ class GenaiElectronImageAdapter {
|
|
|
33
34
|
supportsNegativePrompt: true, // full diffusion support
|
|
34
35
|
defaultModelId: 'sdxl',
|
|
35
36
|
};
|
|
36
|
-
this.baseURL = config?.baseURL || 'http://
|
|
37
|
+
this.baseURL = config?.baseURL || 'http://127.0.0.1:8081';
|
|
37
38
|
this.timeout = config?.timeout || 120000; // 120 seconds for diffusion
|
|
38
39
|
this.pollInterval = 500; // Poll every 500ms
|
|
39
40
|
this.logger = config?.logger ?? (0, defaultLogger_1.createDefaultLogger)();
|
|
@@ -42,8 +43,12 @@ class GenaiElectronImageAdapter {
|
|
|
42
43
|
* Generates images using genai-electron's async API with progress polling
|
|
43
44
|
*/
|
|
44
45
|
async generate(config) {
|
|
45
|
-
const { request, resolvedPrompt, settings } = config;
|
|
46
|
+
const { request, resolvedPrompt, settings, signal } = config;
|
|
47
|
+
let generationId;
|
|
46
48
|
try {
|
|
49
|
+
if (signal?.aborted) {
|
|
50
|
+
throw this.createAbortError('Image generation request was aborted before it started');
|
|
51
|
+
}
|
|
47
52
|
// Build request payload
|
|
48
53
|
const payload = this.buildRequestPayload(resolvedPrompt, request, settings);
|
|
49
54
|
this.logger.debug(`GenaiElectron Image API: Starting generation`, {
|
|
@@ -53,16 +58,22 @@ class GenaiElectronImageAdapter {
|
|
|
53
58
|
steps: payload.steps,
|
|
54
59
|
});
|
|
55
60
|
// Start generation (returns immediately with ID)
|
|
56
|
-
|
|
61
|
+
generationId = await this.startGeneration(payload, signal);
|
|
57
62
|
this.logger.info(`GenaiElectron Image API: Generation started with ID: ${generationId}`);
|
|
58
63
|
// Poll for completion
|
|
59
|
-
const result = await this.pollForCompletion(generationId, settings.diffusion?.onProgress);
|
|
64
|
+
const result = await this.pollForCompletion(generationId, settings.diffusion?.onProgress, signal);
|
|
60
65
|
this.logger.info(`GenaiElectron Image API: Generation complete (${result.timeTaken}ms)`);
|
|
61
66
|
// Convert to ImageGenerationResponse
|
|
62
67
|
return this.convertToResponse(result, request);
|
|
63
68
|
}
|
|
64
69
|
catch (error) {
|
|
65
70
|
this.logger.error('GenaiElectron Image API error:', error);
|
|
71
|
+
// Best-effort server-side cancellation when a started generation is being
|
|
72
|
+
// abandoned (caller abort, or client-side poll timeout — cancel frees the
|
|
73
|
+
// GPU; the DELETE is cleanup, not a reclassification)
|
|
74
|
+
if (generationId && (signal?.aborted || error?.name === 'TimeoutError')) {
|
|
75
|
+
await this.cancelGeneration(generationId);
|
|
76
|
+
}
|
|
66
77
|
throw this.handleError(error, request);
|
|
67
78
|
}
|
|
68
79
|
}
|
|
@@ -88,12 +99,28 @@ class GenaiElectronImageAdapter {
|
|
|
88
99
|
}
|
|
89
100
|
/**
|
|
90
101
|
* Starts generation and returns the generation ID
|
|
102
|
+
*
|
|
103
|
+
* The adapter's timeout and the caller's abort signal share one controller,
|
|
104
|
+
* so both surface as AbortError — classification comes from adapter-side
|
|
105
|
+
* state (timer-fired flag vs signal.aborted), with the caller's abort winning.
|
|
91
106
|
*/
|
|
92
|
-
async startGeneration(payload) {
|
|
107
|
+
async startGeneration(payload, signal) {
|
|
93
108
|
const url = `${this.baseURL}/v1/images/generations`;
|
|
94
|
-
// Create abort controller for timeout
|
|
95
109
|
const controller = new AbortController();
|
|
96
|
-
|
|
110
|
+
let timedOut = false;
|
|
111
|
+
const abortFromCaller = () => controller.abort();
|
|
112
|
+
if (signal) {
|
|
113
|
+
if (signal.aborted) {
|
|
114
|
+
controller.abort();
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
signal.addEventListener('abort', abortFromCaller, { once: true });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const timeoutId = setTimeout(() => {
|
|
121
|
+
timedOut = true;
|
|
122
|
+
controller.abort();
|
|
123
|
+
}, this.timeout);
|
|
97
124
|
try {
|
|
98
125
|
const response = await fetch(url, {
|
|
99
126
|
method: 'POST',
|
|
@@ -101,7 +128,6 @@ class GenaiElectronImageAdapter {
|
|
|
101
128
|
body: JSON.stringify(payload),
|
|
102
129
|
signal: controller.signal,
|
|
103
130
|
});
|
|
104
|
-
clearTimeout(timeoutId);
|
|
105
131
|
if (!response.ok) {
|
|
106
132
|
const errorText = await response.text();
|
|
107
133
|
throw this.createHttpError(response.status, errorText, url);
|
|
@@ -110,27 +136,47 @@ class GenaiElectronImageAdapter {
|
|
|
110
136
|
return data.id;
|
|
111
137
|
}
|
|
112
138
|
catch (error) {
|
|
113
|
-
clearTimeout(timeoutId);
|
|
114
|
-
// Handle AbortError
|
|
115
139
|
if (error.name === 'AbortError') {
|
|
116
|
-
|
|
140
|
+
if (signal?.aborted) {
|
|
141
|
+
throw this.createAbortError('Image generation request was aborted before it started');
|
|
142
|
+
}
|
|
143
|
+
if (timedOut) {
|
|
144
|
+
throw this.createTimeoutError(`Request timeout after ${this.timeout}ms (connecting to ${this.baseURL})`);
|
|
145
|
+
}
|
|
117
146
|
}
|
|
118
147
|
throw error;
|
|
119
148
|
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(timeoutId);
|
|
151
|
+
signal?.removeEventListener('abort', abortFromCaller);
|
|
152
|
+
}
|
|
120
153
|
}
|
|
121
154
|
/**
|
|
122
155
|
* Polls for generation completion with progress updates
|
|
123
156
|
*/
|
|
124
|
-
async pollForCompletion(generationId, onProgress) {
|
|
157
|
+
async pollForCompletion(generationId, onProgress, signal) {
|
|
125
158
|
const url = `${this.baseURL}/v1/images/generations/${generationId}`;
|
|
126
159
|
const startTime = Date.now();
|
|
127
160
|
while (true) {
|
|
161
|
+
// Check caller abort (before timeout — user intent wins)
|
|
162
|
+
if (signal?.aborted) {
|
|
163
|
+
throw this.createAbortError(`Image generation request was aborted (ID: ${generationId})`);
|
|
164
|
+
}
|
|
128
165
|
// Check overall timeout
|
|
129
166
|
if (Date.now() - startTime > this.timeout) {
|
|
130
|
-
throw
|
|
167
|
+
throw this.createTimeoutError(`Generation timeout after ${this.timeout}ms (ID: ${generationId})`);
|
|
131
168
|
}
|
|
132
169
|
// Fetch status
|
|
133
|
-
|
|
170
|
+
let response;
|
|
171
|
+
try {
|
|
172
|
+
response = await fetch(url, signal ? { signal } : undefined);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error.name === 'AbortError' && signal?.aborted) {
|
|
176
|
+
throw this.createAbortError(`Image generation request was aborted (ID: ${generationId})`);
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
134
180
|
if (!response.ok) {
|
|
135
181
|
const errorText = await response.text();
|
|
136
182
|
throw this.createHttpError(response.status, errorText, url);
|
|
@@ -157,8 +203,36 @@ class GenaiElectronImageAdapter {
|
|
|
157
203
|
const error = state.error || { message: 'Unknown error', code: 'UNKNOWN_ERROR' };
|
|
158
204
|
throw this.createGenerationError(error.message, error.code);
|
|
159
205
|
}
|
|
206
|
+
// Handle out-of-band cancellation (terminal status since genai-electron 0.6.0)
|
|
207
|
+
if (state.status === 'cancelled') {
|
|
208
|
+
const cancelError = new Error(`Image generation was cancelled on the server (ID: ${generationId})`);
|
|
209
|
+
cancelError.code = 'GENERATION_CANCELLED';
|
|
210
|
+
throw cancelError;
|
|
211
|
+
}
|
|
160
212
|
// Wait before next poll
|
|
161
|
-
await this.sleep(this.pollInterval);
|
|
213
|
+
await this.sleep(this.pollInterval, signal);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Best-effort cancellation of a server-side generation
|
|
218
|
+
* (DELETE /v1/images/generations/:id, available since genai-electron 0.6.0).
|
|
219
|
+
*
|
|
220
|
+
* All failures are swallowed: 404/409 just mean there is nothing left to
|
|
221
|
+
* cancel, and cleanup must never mask the caller's primary error.
|
|
222
|
+
*/
|
|
223
|
+
async cancelGeneration(generationId) {
|
|
224
|
+
const url = `${this.baseURL}/v1/images/generations/${generationId}`;
|
|
225
|
+
const controller = new AbortController();
|
|
226
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
227
|
+
try {
|
|
228
|
+
await fetch(url, { method: 'DELETE', signal: controller.signal });
|
|
229
|
+
this.logger.debug(`GenaiElectron Image API: Requested cancellation of generation ${generationId}`);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
this.logger.debug(`GenaiElectron Image API: Best-effort cancellation failed for ${generationId}:`, error);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
clearTimeout(timeoutId);
|
|
162
236
|
}
|
|
163
237
|
}
|
|
164
238
|
/**
|
|
@@ -198,11 +272,15 @@ class GenaiElectronImageAdapter {
|
|
|
198
272
|
*/
|
|
199
273
|
createHttpError(status, errorText, url) {
|
|
200
274
|
let errorMessage = `HTTP ${status} error`;
|
|
275
|
+
let serverCode;
|
|
201
276
|
try {
|
|
202
277
|
const errorData = JSON.parse(errorText);
|
|
203
278
|
if (errorData.error?.message) {
|
|
204
279
|
errorMessage = errorData.error.message;
|
|
205
280
|
}
|
|
281
|
+
if (typeof errorData.error?.code === 'string') {
|
|
282
|
+
serverCode = errorData.error.code;
|
|
283
|
+
}
|
|
206
284
|
}
|
|
207
285
|
catch {
|
|
208
286
|
// Not JSON, use raw text
|
|
@@ -213,6 +291,27 @@ class GenaiElectronImageAdapter {
|
|
|
213
291
|
const error = new Error(`${errorMessage} (${url})`);
|
|
214
292
|
error.status = status;
|
|
215
293
|
error.url = url;
|
|
294
|
+
if (serverCode) {
|
|
295
|
+
error.code = serverCode;
|
|
296
|
+
}
|
|
297
|
+
return error;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Creates a timeout-typed error: name 'TimeoutError' is recognized by the
|
|
301
|
+
* shared error mapping and classified as REQUEST_TIMEOUT / timeout_error
|
|
302
|
+
*/
|
|
303
|
+
createTimeoutError(message) {
|
|
304
|
+
const error = new Error(message);
|
|
305
|
+
error.name = 'TimeoutError';
|
|
306
|
+
return error;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Creates an abort-typed error: name 'AbortError' is recognized by the
|
|
310
|
+
* shared error mapping and classified as REQUEST_ABORTED / abort_error
|
|
311
|
+
*/
|
|
312
|
+
createAbortError(message) {
|
|
313
|
+
const error = new Error(message);
|
|
314
|
+
error.name = 'AbortError';
|
|
216
315
|
return error;
|
|
217
316
|
}
|
|
218
317
|
/**
|
|
@@ -231,22 +330,33 @@ class GenaiElectronImageAdapter {
|
|
|
231
330
|
const mapped = (0, errorUtils_1.getCommonMappedErrorDetails)(error);
|
|
232
331
|
// Enhance error message with context
|
|
233
332
|
let errorMessage = mapped.errorMessage;
|
|
333
|
+
let errorCode = mapped.errorCode;
|
|
334
|
+
let errorType = mapped.errorType;
|
|
234
335
|
// Special handling for genai-electron specific errors
|
|
235
|
-
if (error.code === '
|
|
336
|
+
if (error.code === 'GENERATION_CANCELLED') {
|
|
337
|
+
errorMessage = error.message;
|
|
338
|
+
errorCode = types_1.ADAPTER_ERROR_CODES.REQUEST_ABORTED;
|
|
339
|
+
errorType = 'abort_error';
|
|
340
|
+
}
|
|
341
|
+
else if (error.code === 'SERVER_BUSY') {
|
|
236
342
|
errorMessage = 'Image generation server is busy. Wait for current generation to complete.';
|
|
237
|
-
|
|
343
|
+
errorCode = types_1.ADAPTER_ERROR_CODES.RATE_LIMIT_EXCEEDED;
|
|
344
|
+
errorType = 'rate_limit_error';
|
|
238
345
|
}
|
|
239
346
|
else if (error.code === 'SERVER_NOT_RUNNING') {
|
|
240
347
|
errorMessage = `Image generation server is not running (connecting to ${this.baseURL})`;
|
|
241
|
-
|
|
348
|
+
errorCode = types_1.ADAPTER_ERROR_CODES.NETWORK_ERROR;
|
|
349
|
+
errorType = 'connection_error';
|
|
242
350
|
}
|
|
243
351
|
else if (error.code === 'BACKEND_ERROR') {
|
|
244
352
|
errorMessage = `Diffusion backend error: ${error.message}`;
|
|
245
|
-
|
|
353
|
+
errorCode = types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR;
|
|
354
|
+
errorType = 'server_error';
|
|
246
355
|
}
|
|
247
356
|
else if (error.code === 'IO_ERROR') {
|
|
248
357
|
errorMessage = `Image I/O error: ${error.message}`;
|
|
249
|
-
|
|
358
|
+
errorCode = types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR;
|
|
359
|
+
errorType = 'server_error';
|
|
250
360
|
}
|
|
251
361
|
// Add baseURL context for network errors
|
|
252
362
|
if (mapped.errorCode === 'NETWORK_ERROR') {
|
|
@@ -258,18 +368,32 @@ class GenaiElectronImageAdapter {
|
|
|
258
368
|
}
|
|
259
369
|
// Create enhanced error with all details
|
|
260
370
|
const enhancedError = new Error(errorMessage);
|
|
261
|
-
enhancedError.code =
|
|
262
|
-
enhancedError.type =
|
|
371
|
+
enhancedError.code = errorCode;
|
|
372
|
+
enhancedError.type = errorType;
|
|
263
373
|
enhancedError.status = mapped.status;
|
|
264
374
|
enhancedError.providerId = this.id;
|
|
265
375
|
enhancedError.modelId = request.modelId;
|
|
266
376
|
return enhancedError;
|
|
267
377
|
}
|
|
268
378
|
/**
|
|
269
|
-
* Sleep helper for polling
|
|
379
|
+
* Sleep helper for polling; rejects immediately when the signal aborts
|
|
270
380
|
*/
|
|
271
|
-
sleep(ms) {
|
|
272
|
-
return new Promise((resolve) =>
|
|
381
|
+
sleep(ms, signal) {
|
|
382
|
+
return new Promise((resolve, reject) => {
|
|
383
|
+
if (signal?.aborted) {
|
|
384
|
+
reject(this.createAbortError('Image generation request was aborted'));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const onAbort = () => {
|
|
388
|
+
clearTimeout(timer);
|
|
389
|
+
reject(this.createAbortError('Image generation request was aborted'));
|
|
390
|
+
};
|
|
391
|
+
const timer = setTimeout(() => {
|
|
392
|
+
signal?.removeEventListener('abort', onAbort);
|
|
393
|
+
resolve();
|
|
394
|
+
}, ms);
|
|
395
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
396
|
+
});
|
|
273
397
|
}
|
|
274
398
|
}
|
|
275
399
|
exports.GenaiElectronImageAdapter = GenaiElectronImageAdapter;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.MockImageAdapter = void 0;
|
|
10
|
+
const types_1 = require("../../llm/clients/types");
|
|
10
11
|
/**
|
|
11
12
|
* Mock adapter for testing image generation
|
|
12
13
|
*/
|
|
@@ -26,7 +27,13 @@ class MockImageAdapter {
|
|
|
26
27
|
* Generates mock images
|
|
27
28
|
*/
|
|
28
29
|
async generate(config) {
|
|
29
|
-
const { request, resolvedPrompt } = config;
|
|
30
|
+
const { request, resolvedPrompt, signal } = config;
|
|
31
|
+
if (signal?.aborted) {
|
|
32
|
+
const error = new Error('Image generation request was aborted');
|
|
33
|
+
error.code = types_1.ADAPTER_ERROR_CODES.REQUEST_ABORTED;
|
|
34
|
+
error.type = 'abort_error';
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
30
37
|
const count = request.count || 1;
|
|
31
38
|
// Create mock image data (1x1 PNG)
|
|
32
39
|
const mockImageBuffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64');
|
|
@@ -35,6 +35,7 @@ export declare class OpenAIImageAdapter implements ImageProviderAdapter {
|
|
|
35
35
|
resolvedPrompt: string;
|
|
36
36
|
settings: ResolvedImageGenerationSettings;
|
|
37
37
|
apiKey: string | null;
|
|
38
|
+
signal?: AbortSignal;
|
|
38
39
|
}): Promise<ImageGenerationResponse>;
|
|
39
40
|
/**
|
|
40
41
|
* Validates prompt length for the given model
|
|
@@ -59,7 +59,7 @@ class OpenAIImageAdapter {
|
|
|
59
59
|
* Generates images using OpenAI's API
|
|
60
60
|
*/
|
|
61
61
|
async generate(config) {
|
|
62
|
-
const { request, resolvedPrompt, settings, apiKey } = config;
|
|
62
|
+
const { request, resolvedPrompt, settings, apiKey, signal } = config;
|
|
63
63
|
if (!apiKey) {
|
|
64
64
|
throw new Error('OpenAI API key is required but was not provided');
|
|
65
65
|
}
|
|
@@ -102,14 +102,14 @@ class OpenAIImageAdapter {
|
|
|
102
102
|
n: params.n,
|
|
103
103
|
isGptImageModel,
|
|
104
104
|
});
|
|
105
|
-
// Make API call
|
|
106
|
-
const response = await client.images.generate(params);
|
|
105
|
+
// Make API call (per-request abort signal when provided)
|
|
106
|
+
const response = await client.images.generate(params, signal ? { signal } : undefined);
|
|
107
107
|
if (!response.data || response.data.length === 0) {
|
|
108
108
|
throw new Error('OpenAI API returned no images in response');
|
|
109
109
|
}
|
|
110
110
|
this.logger.info(`OpenAI Image API call successful, generated ${response.data.length} images`);
|
|
111
111
|
// Process response
|
|
112
|
-
return await this.processResponse(response, request, isGptImageModel);
|
|
112
|
+
return await this.processResponse(response, request, isGptImageModel, signal);
|
|
113
113
|
}
|
|
114
114
|
catch (error) {
|
|
115
115
|
this.logger.error('OpenAI Image API error:', error);
|
|
@@ -199,7 +199,7 @@ class OpenAIImageAdapter {
|
|
|
199
199
|
/**
|
|
200
200
|
* Processes the OpenAI API response and converts to ImageGenerationResponse
|
|
201
201
|
*/
|
|
202
|
-
async processResponse(response, request, isGptImageModel) {
|
|
202
|
+
async processResponse(response, request, isGptImageModel, signal) {
|
|
203
203
|
const images = [];
|
|
204
204
|
for (let i = 0; i < response.data.length; i++) {
|
|
205
205
|
const item = response.data[i];
|
|
@@ -224,7 +224,7 @@ class OpenAIImageAdapter {
|
|
|
224
224
|
if (item.url) {
|
|
225
225
|
// Fetch image from URL
|
|
226
226
|
imageUrl = item.url;
|
|
227
|
-
const imageResponse = await fetch(item.url);
|
|
227
|
+
const imageResponse = await fetch(item.url, signal ? { signal } : undefined);
|
|
228
228
|
if (!imageResponse.ok) {
|
|
229
229
|
throw new Error(`Failed to fetch image from URL: ${imageResponse.statusText}`);
|
|
230
230
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* manages presets, validates requests, and handles settings resolution.
|
|
6
6
|
*/
|
|
7
7
|
import type { ApiKeyProvider } from '../types';
|
|
8
|
-
import type { ImageGenerationRequest, ImageGenerationRequestWithPreset, ImageGenerationResponse, ImageFailureResponse, ImageProviderInfo, ImageModelInfo, ImageProviderId, ImagePreset, ImageServiceOptions } from '../types/image';
|
|
8
|
+
import type { ImageGenerationRequest, ImageGenerationRequestWithPreset, ImageGenerationResponse, ImageFailureResponse, ImageProviderInfo, ImageModelInfo, ImageProviderId, ImagePreset, ImageServiceOptions, GenerateImageOptions } from '../types/image';
|
|
9
9
|
/**
|
|
10
10
|
* Main service for image generation operations
|
|
11
11
|
*/
|
|
@@ -22,9 +22,10 @@ export declare class ImageService {
|
|
|
22
22
|
* Generates images based on the request
|
|
23
23
|
*
|
|
24
24
|
* @param request - Image generation request
|
|
25
|
+
* @param options - Per-call options (e.g. an AbortSignal for cancellation)
|
|
25
26
|
* @returns Promise resolving to response or error
|
|
26
27
|
*/
|
|
27
|
-
generateImage(request: ImageGenerationRequest | ImageGenerationRequestWithPreset): Promise<ImageGenerationResponse | ImageFailureResponse>;
|
|
28
|
+
generateImage(request: ImageGenerationRequest | ImageGenerationRequestWithPreset, options?: GenerateImageOptions): Promise<ImageGenerationResponse | ImageFailureResponse>;
|
|
28
29
|
/**
|
|
29
30
|
* Gets list of supported image providers
|
|
30
31
|
*
|
|
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.ImageService = void 0;
|
|
13
13
|
const defaultLogger_1 = require("../logging/defaultLogger");
|
|
14
|
+
const types_1 = require("../llm/clients/types");
|
|
14
15
|
const config_1 = require("./config");
|
|
15
16
|
const image_presets_json_1 = __importDefault(require("../config/image-presets.json"));
|
|
16
17
|
const PresetManager_1 = require("../shared/services/PresetManager");
|
|
@@ -23,6 +24,9 @@ const ImageSettingsResolver_1 = require("./services/ImageSettingsResolver");
|
|
|
23
24
|
const ImageModelResolver_1 = require("./services/ImageModelResolver");
|
|
24
25
|
// Type assertion for the imported JSON
|
|
25
26
|
const defaultImagePresets = image_presets_json_1.default;
|
|
27
|
+
// Error codes adapters stamp on the errors they throw; used to decide whether a
|
|
28
|
+
// caught error's classification is safe to surface on the failure envelope
|
|
29
|
+
const ADAPTER_ERROR_CODE_VALUES = new Set(Object.values(types_1.ADAPTER_ERROR_CODES));
|
|
26
30
|
/**
|
|
27
31
|
* Main service for image generation operations
|
|
28
32
|
*/
|
|
@@ -69,9 +73,10 @@ class ImageService {
|
|
|
69
73
|
* Generates images based on the request
|
|
70
74
|
*
|
|
71
75
|
* @param request - Image generation request
|
|
76
|
+
* @param options - Per-call options (e.g. an AbortSignal for cancellation)
|
|
72
77
|
* @returns Promise resolving to response or error
|
|
73
78
|
*/
|
|
74
|
-
async generateImage(request) {
|
|
79
|
+
async generateImage(request, options) {
|
|
75
80
|
this.logger.info('ImageService.generateImage called');
|
|
76
81
|
try {
|
|
77
82
|
// Resolve model information
|
|
@@ -104,6 +109,19 @@ class ImageService {
|
|
|
104
109
|
const adapter = this.adapterRegistry.getAdapter(providerId);
|
|
105
110
|
// Get API key
|
|
106
111
|
try {
|
|
112
|
+
// Short-circuit if the caller already aborted — don't touch the adapter
|
|
113
|
+
if (options?.signal?.aborted) {
|
|
114
|
+
return {
|
|
115
|
+
object: 'error',
|
|
116
|
+
providerId: providerId,
|
|
117
|
+
modelId: modelId,
|
|
118
|
+
error: {
|
|
119
|
+
message: 'Image generation request was aborted',
|
|
120
|
+
code: types_1.ADAPTER_ERROR_CODES.REQUEST_ABORTED,
|
|
121
|
+
type: 'abort_error',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
107
125
|
const apiKey = await this.getApiKey(providerId);
|
|
108
126
|
// Validate API key if adapter supports it
|
|
109
127
|
if (apiKey && adapter.validateApiKey && !adapter.validateApiKey(apiKey)) {
|
|
@@ -125,12 +143,20 @@ class ImageService {
|
|
|
125
143
|
resolvedPrompt,
|
|
126
144
|
settings: resolvedSettings,
|
|
127
145
|
apiKey,
|
|
146
|
+
signal: options?.signal,
|
|
128
147
|
});
|
|
129
148
|
this.logger.info('ImageService: Image generation completed successfully');
|
|
130
149
|
return response;
|
|
131
150
|
}
|
|
132
151
|
catch (error) {
|
|
133
152
|
this.logger.error('ImageService: Error during image generation:', error);
|
|
153
|
+
// Adapters throw errors stamped with an ADAPTER_ERROR_CODES code plus
|
|
154
|
+
// type/status — propagate that classification. Anything else caught here
|
|
155
|
+
// (e.g. a failing ApiKeyProvider whose error carries a foreign `.code`
|
|
156
|
+
// like ENOENT) keeps the generic fallback so unrelated codes don't leak
|
|
157
|
+
// into the API surface.
|
|
158
|
+
const thrown = error;
|
|
159
|
+
const isAdapterError = typeof thrown?.code === 'string' && ADAPTER_ERROR_CODE_VALUES.has(thrown.code);
|
|
134
160
|
return {
|
|
135
161
|
object: 'error',
|
|
136
162
|
providerId: providerId,
|
|
@@ -139,8 +165,12 @@ class ImageService {
|
|
|
139
165
|
message: error instanceof Error
|
|
140
166
|
? error.message
|
|
141
167
|
: 'An unknown error occurred during image generation',
|
|
142
|
-
code: 'PROVIDER_ERROR',
|
|
143
|
-
type: '
|
|
168
|
+
code: isAdapterError ? thrown.code : 'PROVIDER_ERROR',
|
|
169
|
+
type: isAdapterError && typeof thrown.type === 'string'
|
|
170
|
+
? thrown.type
|
|
171
|
+
: 'server_error',
|
|
172
|
+
...(isAdapterError &&
|
|
173
|
+
typeof thrown.status === 'number' && { status: thrown.status }),
|
|
144
174
|
providerError: error,
|
|
145
175
|
},
|
|
146
176
|
};
|
package/dist/image/config.js
CHANGED
|
@@ -174,7 +174,9 @@ exports.IMAGE_ADAPTER_CONFIGS = {
|
|
|
174
174
|
timeout: 60000, // 60 seconds for image generation
|
|
175
175
|
},
|
|
176
176
|
'genai-electron-images': {
|
|
177
|
-
|
|
177
|
+
// 127.0.0.1 (not localhost) avoids a ~2s/request IPv6-fallback stall on
|
|
178
|
+
// Windows — especially costly for the 500ms polling loop
|
|
179
|
+
baseURL: process.env.GENAI_ELECTRON_IMAGE_BASE_URL || 'http://127.0.0.1:8081',
|
|
178
180
|
timeout: 120000, // 120 seconds for local diffusion
|
|
179
181
|
checkHealth: false, // Optional health checks
|
|
180
182
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type { OpenRouterClientConfig } from "./llm/clients/OpenRouterClientAdapt
|
|
|
16
16
|
export { MistralClientAdapter } from "./llm/clients/MistralClientAdapter";
|
|
17
17
|
export type { MistralClientConfig } from "./llm/clients/MistralClientAdapter";
|
|
18
18
|
export { ImageService } from "./image/ImageService";
|
|
19
|
-
export type { ImageProviderId, ImageMimeType, ImageResponseFormat, ImageQuality, ImageStyle, DiffusionSampler, ImageProgressStage, ImageProgressCallback, DiffusionSettings, OpenAISpecificSettings, ImageGenerationSettings, ResolvedImageGenerationSettings, ImageUsage, GeneratedImage, ImageGenerationRequestBase, ImageGenerationRequest, ImageGenerationRequestWithPreset, ImageGenerationResponse, ImageFailureResponse, ImageProviderCapabilities, ImageModelInfo, ImageProviderInfo, ImagePreset, ImageProviderAdapterConfig, ImageProviderAdapter, ImageServiceOptions, CreatePromptResult, } from "./types/image";
|
|
19
|
+
export type { ImageProviderId, ImageMimeType, ImageResponseFormat, ImageQuality, ImageStyle, DiffusionSampler, ImageProgressStage, ImageProgressCallback, DiffusionSettings, OpenAISpecificSettings, ImageGenerationSettings, ResolvedImageGenerationSettings, ImageUsage, GeneratedImage, ImageGenerationRequestBase, ImageGenerationRequest, ImageGenerationRequestWithPreset, ImageGenerationResponse, ImageFailureResponse, ImageProviderCapabilities, ImageModelInfo, ImageProviderInfo, ImagePreset, ImageProviderAdapterConfig, ImageProviderAdapter, ImageServiceOptions, GenerateImageOptions, CreatePromptResult, } from "./types/image";
|
|
20
20
|
export { renderTemplate } from "./prompting/template";
|
|
21
21
|
export { countTokens, getSmartPreview, extractRandomVariables } from "./prompting/content";
|
|
22
22
|
export { parseStructuredContent, parseRoleTags, extractInitialTaggedContent, extractMarkerDelimitedContent, parseTemplateWithMetadata } from "./prompting/parser";
|
|
@@ -75,6 +75,7 @@ export declare class GeminiClientAdapter implements ILLMClientAdapter {
|
|
|
75
75
|
*
|
|
76
76
|
* @param error - The error from Gemini API
|
|
77
77
|
* @param request - Original request for context
|
|
78
|
+
* @param context - Adapter-side transport state captured at catch time
|
|
78
79
|
* @returns Standardized LLM failure response
|
|
79
80
|
*/
|
|
80
81
|
private createErrorResponse;
|
|
@@ -38,6 +38,12 @@ class GeminiClientAdapter {
|
|
|
38
38
|
* @returns Promise resolving to success or failure response
|
|
39
39
|
*/
|
|
40
40
|
async sendMessage(request, apiKey, options) {
|
|
41
|
+
// The SDK enforces httpOptions.timeout by aborting an internal controller and
|
|
42
|
+
// wraps any fetch rejection in a plain Error (no name/status/code), so timeouts
|
|
43
|
+
// and caller aborts cannot be classified from the error object. The adapter owns
|
|
44
|
+
// the timeout instead and classifies from its own state in the catch block.
|
|
45
|
+
let timedOut = false;
|
|
46
|
+
let timeoutHandle;
|
|
41
47
|
try {
|
|
42
48
|
// Initialize Gemini client (note: @google/genai performs no automatic retries)
|
|
43
49
|
const genAI = new genai_1.GoogleGenAI({ apiKey });
|
|
@@ -52,6 +58,26 @@ class GeminiClientAdapter {
|
|
|
52
58
|
contentsLength: contents.length,
|
|
53
59
|
safetySettingsCount: safetySettings?.length || 0,
|
|
54
60
|
});
|
|
61
|
+
let abortSignal = options?.signal;
|
|
62
|
+
if (options?.timeoutMs !== undefined) {
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
if (options.signal) {
|
|
65
|
+
if (options.signal.aborted) {
|
|
66
|
+
controller.abort();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
options.signal.addEventListener("abort", () => controller.abort(), {
|
|
70
|
+
once: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
timeoutHandle = setTimeout(() => {
|
|
75
|
+
timedOut = true;
|
|
76
|
+
controller.abort();
|
|
77
|
+
}, options.timeoutMs);
|
|
78
|
+
timeoutHandle.unref?.();
|
|
79
|
+
abortSignal = controller.signal;
|
|
80
|
+
}
|
|
55
81
|
// Generate content using the modern API
|
|
56
82
|
const result = await genAI.models.generateContent({
|
|
57
83
|
model: request.modelId,
|
|
@@ -60,9 +86,11 @@ class GeminiClientAdapter {
|
|
|
60
86
|
...generationConfig,
|
|
61
87
|
safetySettings: safetySettings,
|
|
62
88
|
...(systemInstruction && { systemInstruction: systemInstruction }),
|
|
63
|
-
...(
|
|
89
|
+
...(abortSignal && { abortSignal }),
|
|
90
|
+
// Keep the SDK's server-side timeout hint (X-Server-Timeout header),
|
|
91
|
+
// padded so the adapter's local timer always fires first
|
|
64
92
|
...(options?.timeoutMs !== undefined && {
|
|
65
|
-
httpOptions: { timeout: options.timeoutMs },
|
|
93
|
+
httpOptions: { timeout: options.timeoutMs + 1000 },
|
|
66
94
|
}),
|
|
67
95
|
},
|
|
68
96
|
});
|
|
@@ -72,7 +100,15 @@ class GeminiClientAdapter {
|
|
|
72
100
|
}
|
|
73
101
|
catch (error) {
|
|
74
102
|
this.logger.error("Gemini API error:", error);
|
|
75
|
-
return this.createErrorResponse(error, request
|
|
103
|
+
return this.createErrorResponse(error, request, {
|
|
104
|
+
timedOut,
|
|
105
|
+
aborted: options?.signal?.aborted === true,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
if (timeoutHandle !== undefined) {
|
|
110
|
+
clearTimeout(timeoutHandle);
|
|
111
|
+
}
|
|
76
112
|
}
|
|
77
113
|
}
|
|
78
114
|
/**
|
|
@@ -312,9 +348,38 @@ class GeminiClientAdapter {
|
|
|
312
348
|
*
|
|
313
349
|
* @param error - The error from Gemini API
|
|
314
350
|
* @param request - Original request for context
|
|
351
|
+
* @param context - Adapter-side transport state captured at catch time
|
|
315
352
|
* @returns Standardized LLM failure response
|
|
316
353
|
*/
|
|
317
|
-
createErrorResponse(error, request) {
|
|
354
|
+
createErrorResponse(error, request, context) {
|
|
355
|
+
// Classify adapter-owned aborts/timeouts first — the SDK surfaces both as
|
|
356
|
+
// plain Errors the common mapping cannot recognize (see sendMessage)
|
|
357
|
+
if (context?.aborted) {
|
|
358
|
+
return {
|
|
359
|
+
provider: request.providerId,
|
|
360
|
+
model: request.modelId,
|
|
361
|
+
error: {
|
|
362
|
+
message: "Request was aborted",
|
|
363
|
+
code: types_1.ADAPTER_ERROR_CODES.REQUEST_ABORTED,
|
|
364
|
+
type: "abort_error",
|
|
365
|
+
providerError: error,
|
|
366
|
+
},
|
|
367
|
+
object: "error",
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (context?.timedOut) {
|
|
371
|
+
return {
|
|
372
|
+
provider: request.providerId,
|
|
373
|
+
model: request.modelId,
|
|
374
|
+
error: {
|
|
375
|
+
message: "Request timed out",
|
|
376
|
+
code: types_1.ADAPTER_ERROR_CODES.REQUEST_TIMEOUT,
|
|
377
|
+
type: "timeout_error",
|
|
378
|
+
providerError: error,
|
|
379
|
+
},
|
|
380
|
+
object: "error",
|
|
381
|
+
};
|
|
382
|
+
}
|
|
318
383
|
// Use shared error mapping utility for common error patterns
|
|
319
384
|
const initialProviderMessage = error?.message;
|
|
320
385
|
let { errorCode, errorMessage, errorType, status, retryAfterMs } = (0, errorUtils_1.getCommonMappedErrorDetails)(error, initialProviderMessage);
|
package/dist/types/image.d.ts
CHANGED
|
@@ -251,6 +251,8 @@ export interface ImageFailureResponse {
|
|
|
251
251
|
code?: string | number;
|
|
252
252
|
/** Error type (authentication_error, rate_limit_error, etc.) */
|
|
253
253
|
type?: string;
|
|
254
|
+
/** HTTP status code reported by the provider, when available */
|
|
255
|
+
status?: number;
|
|
254
256
|
/** Parameter that caused the error (if applicable) */
|
|
255
257
|
param?: string;
|
|
256
258
|
/** Original provider error (for debugging) */
|
|
@@ -359,6 +361,8 @@ export interface ImageProviderAdapter {
|
|
|
359
361
|
resolvedPrompt: string;
|
|
360
362
|
settings: ResolvedImageGenerationSettings;
|
|
361
363
|
apiKey: string | null;
|
|
364
|
+
/** Abort signal for request-side cancellation (optional) */
|
|
365
|
+
signal?: AbortSignal;
|
|
362
366
|
}): Promise<ImageGenerationResponse>;
|
|
363
367
|
/**
|
|
364
368
|
* Optional method to get available models from this provider
|
|
@@ -395,6 +399,18 @@ export interface ImageServiceOptions {
|
|
|
395
399
|
/** Custom logger implementation. If provided, logLevel is ignored. */
|
|
396
400
|
logger?: Logger;
|
|
397
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Per-call options for ImageService.generateImage
|
|
404
|
+
*/
|
|
405
|
+
export interface GenerateImageOptions {
|
|
406
|
+
/**
|
|
407
|
+
* Abort signal to cancel the request (client-side — the provider may still
|
|
408
|
+
* process an already-dispatched request; for local diffusion the adapter
|
|
409
|
+
* additionally issues a best-effort server-side cancellation). Aborted
|
|
410
|
+
* requests surface as REQUEST_ABORTED / abort_error failures.
|
|
411
|
+
*/
|
|
412
|
+
signal?: AbortSignal;
|
|
413
|
+
}
|
|
398
414
|
/**
|
|
399
415
|
* Result from createPrompt utility
|
|
400
416
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genai-lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A lightweight, portable toolkit for interacting with various Generative AI APIs.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"test:e2e:reasoning": "npm run build && jest --config jest.e2e.config.js reasoning.e2e.test.ts"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@anthropic-ai/sdk": "^0.
|
|
46
|
+
"@anthropic-ai/sdk": "^0.72.1",
|
|
47
47
|
"@google/genai": "^1.0.1",
|
|
48
48
|
"@mistralai/mistralai": "^1.11.0",
|
|
49
49
|
"js-tiktoken": "^1.0.20",
|