genai-lite 0.9.2 → 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 +133 -23
- 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/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:
|
|
@@ -34,7 +34,7 @@ class GenaiElectronImageAdapter {
|
|
|
34
34
|
supportsNegativePrompt: true, // full diffusion support
|
|
35
35
|
defaultModelId: 'sdxl',
|
|
36
36
|
};
|
|
37
|
-
this.baseURL = config?.baseURL || 'http://
|
|
37
|
+
this.baseURL = config?.baseURL || 'http://127.0.0.1:8081';
|
|
38
38
|
this.timeout = config?.timeout || 120000; // 120 seconds for diffusion
|
|
39
39
|
this.pollInterval = 500; // Poll every 500ms
|
|
40
40
|
this.logger = config?.logger ?? (0, defaultLogger_1.createDefaultLogger)();
|
|
@@ -43,8 +43,12 @@ class GenaiElectronImageAdapter {
|
|
|
43
43
|
* Generates images using genai-electron's async API with progress polling
|
|
44
44
|
*/
|
|
45
45
|
async generate(config) {
|
|
46
|
-
const { request, resolvedPrompt, settings } = config;
|
|
46
|
+
const { request, resolvedPrompt, settings, signal } = config;
|
|
47
|
+
let generationId;
|
|
47
48
|
try {
|
|
49
|
+
if (signal?.aborted) {
|
|
50
|
+
throw this.createAbortError('Image generation request was aborted before it started');
|
|
51
|
+
}
|
|
48
52
|
// Build request payload
|
|
49
53
|
const payload = this.buildRequestPayload(resolvedPrompt, request, settings);
|
|
50
54
|
this.logger.debug(`GenaiElectron Image API: Starting generation`, {
|
|
@@ -54,16 +58,22 @@ class GenaiElectronImageAdapter {
|
|
|
54
58
|
steps: payload.steps,
|
|
55
59
|
});
|
|
56
60
|
// Start generation (returns immediately with ID)
|
|
57
|
-
|
|
61
|
+
generationId = await this.startGeneration(payload, signal);
|
|
58
62
|
this.logger.info(`GenaiElectron Image API: Generation started with ID: ${generationId}`);
|
|
59
63
|
// Poll for completion
|
|
60
|
-
const result = await this.pollForCompletion(generationId, settings.diffusion?.onProgress);
|
|
64
|
+
const result = await this.pollForCompletion(generationId, settings.diffusion?.onProgress, signal);
|
|
61
65
|
this.logger.info(`GenaiElectron Image API: Generation complete (${result.timeTaken}ms)`);
|
|
62
66
|
// Convert to ImageGenerationResponse
|
|
63
67
|
return this.convertToResponse(result, request);
|
|
64
68
|
}
|
|
65
69
|
catch (error) {
|
|
66
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
|
+
}
|
|
67
77
|
throw this.handleError(error, request);
|
|
68
78
|
}
|
|
69
79
|
}
|
|
@@ -89,12 +99,28 @@ class GenaiElectronImageAdapter {
|
|
|
89
99
|
}
|
|
90
100
|
/**
|
|
91
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.
|
|
92
106
|
*/
|
|
93
|
-
async startGeneration(payload) {
|
|
107
|
+
async startGeneration(payload, signal) {
|
|
94
108
|
const url = `${this.baseURL}/v1/images/generations`;
|
|
95
|
-
// Create abort controller for timeout
|
|
96
109
|
const controller = new AbortController();
|
|
97
|
-
|
|
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);
|
|
98
124
|
try {
|
|
99
125
|
const response = await fetch(url, {
|
|
100
126
|
method: 'POST',
|
|
@@ -102,7 +128,6 @@ class GenaiElectronImageAdapter {
|
|
|
102
128
|
body: JSON.stringify(payload),
|
|
103
129
|
signal: controller.signal,
|
|
104
130
|
});
|
|
105
|
-
clearTimeout(timeoutId);
|
|
106
131
|
if (!response.ok) {
|
|
107
132
|
const errorText = await response.text();
|
|
108
133
|
throw this.createHttpError(response.status, errorText, url);
|
|
@@ -111,27 +136,47 @@ class GenaiElectronImageAdapter {
|
|
|
111
136
|
return data.id;
|
|
112
137
|
}
|
|
113
138
|
catch (error) {
|
|
114
|
-
clearTimeout(timeoutId);
|
|
115
|
-
// Handle AbortError
|
|
116
139
|
if (error.name === 'AbortError') {
|
|
117
|
-
|
|
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
|
+
}
|
|
118
146
|
}
|
|
119
147
|
throw error;
|
|
120
148
|
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(timeoutId);
|
|
151
|
+
signal?.removeEventListener('abort', abortFromCaller);
|
|
152
|
+
}
|
|
121
153
|
}
|
|
122
154
|
/**
|
|
123
155
|
* Polls for generation completion with progress updates
|
|
124
156
|
*/
|
|
125
|
-
async pollForCompletion(generationId, onProgress) {
|
|
157
|
+
async pollForCompletion(generationId, onProgress, signal) {
|
|
126
158
|
const url = `${this.baseURL}/v1/images/generations/${generationId}`;
|
|
127
159
|
const startTime = Date.now();
|
|
128
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
|
+
}
|
|
129
165
|
// Check overall timeout
|
|
130
166
|
if (Date.now() - startTime > this.timeout) {
|
|
131
|
-
throw
|
|
167
|
+
throw this.createTimeoutError(`Generation timeout after ${this.timeout}ms (ID: ${generationId})`);
|
|
132
168
|
}
|
|
133
169
|
// Fetch status
|
|
134
|
-
|
|
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
|
+
}
|
|
135
180
|
if (!response.ok) {
|
|
136
181
|
const errorText = await response.text();
|
|
137
182
|
throw this.createHttpError(response.status, errorText, url);
|
|
@@ -165,7 +210,29 @@ class GenaiElectronImageAdapter {
|
|
|
165
210
|
throw cancelError;
|
|
166
211
|
}
|
|
167
212
|
// Wait before next poll
|
|
168
|
-
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);
|
|
169
236
|
}
|
|
170
237
|
}
|
|
171
238
|
/**
|
|
@@ -205,11 +272,15 @@ class GenaiElectronImageAdapter {
|
|
|
205
272
|
*/
|
|
206
273
|
createHttpError(status, errorText, url) {
|
|
207
274
|
let errorMessage = `HTTP ${status} error`;
|
|
275
|
+
let serverCode;
|
|
208
276
|
try {
|
|
209
277
|
const errorData = JSON.parse(errorText);
|
|
210
278
|
if (errorData.error?.message) {
|
|
211
279
|
errorMessage = errorData.error.message;
|
|
212
280
|
}
|
|
281
|
+
if (typeof errorData.error?.code === 'string') {
|
|
282
|
+
serverCode = errorData.error.code;
|
|
283
|
+
}
|
|
213
284
|
}
|
|
214
285
|
catch {
|
|
215
286
|
// Not JSON, use raw text
|
|
@@ -220,6 +291,27 @@ class GenaiElectronImageAdapter {
|
|
|
220
291
|
const error = new Error(`${errorMessage} (${url})`);
|
|
221
292
|
error.status = status;
|
|
222
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';
|
|
223
315
|
return error;
|
|
224
316
|
}
|
|
225
317
|
/**
|
|
@@ -248,19 +340,23 @@ class GenaiElectronImageAdapter {
|
|
|
248
340
|
}
|
|
249
341
|
else if (error.code === 'SERVER_BUSY') {
|
|
250
342
|
errorMessage = 'Image generation server is busy. Wait for current generation to complete.';
|
|
251
|
-
|
|
343
|
+
errorCode = types_1.ADAPTER_ERROR_CODES.RATE_LIMIT_EXCEEDED;
|
|
344
|
+
errorType = 'rate_limit_error';
|
|
252
345
|
}
|
|
253
346
|
else if (error.code === 'SERVER_NOT_RUNNING') {
|
|
254
347
|
errorMessage = `Image generation server is not running (connecting to ${this.baseURL})`;
|
|
255
|
-
|
|
348
|
+
errorCode = types_1.ADAPTER_ERROR_CODES.NETWORK_ERROR;
|
|
349
|
+
errorType = 'connection_error';
|
|
256
350
|
}
|
|
257
351
|
else if (error.code === 'BACKEND_ERROR') {
|
|
258
352
|
errorMessage = `Diffusion backend error: ${error.message}`;
|
|
259
|
-
|
|
353
|
+
errorCode = types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR;
|
|
354
|
+
errorType = 'server_error';
|
|
260
355
|
}
|
|
261
356
|
else if (error.code === 'IO_ERROR') {
|
|
262
357
|
errorMessage = `Image I/O error: ${error.message}`;
|
|
263
|
-
|
|
358
|
+
errorCode = types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR;
|
|
359
|
+
errorType = 'server_error';
|
|
264
360
|
}
|
|
265
361
|
// Add baseURL context for network errors
|
|
266
362
|
if (mapped.errorCode === 'NETWORK_ERROR') {
|
|
@@ -280,10 +376,24 @@ class GenaiElectronImageAdapter {
|
|
|
280
376
|
return enhancedError;
|
|
281
377
|
}
|
|
282
378
|
/**
|
|
283
|
-
* Sleep helper for polling
|
|
379
|
+
* Sleep helper for polling; rejects immediately when the signal aborts
|
|
284
380
|
*/
|
|
285
|
-
sleep(ms) {
|
|
286
|
-
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
|
+
});
|
|
287
397
|
}
|
|
288
398
|
}
|
|
289
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";
|
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",
|