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 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://localhost:8081
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://localhost:8081
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://localhost:8081';
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
- const generationId = await this.startGeneration(payload);
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
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
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
- throw new Error(`Request timeout after ${this.timeout}ms (connecting to ${this.baseURL})`);
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 new Error(`Generation timeout after ${this.timeout}ms (ID: ${generationId})`);
167
+ throw this.createTimeoutError(`Generation timeout after ${this.timeout}ms (ID: ${generationId})`);
132
168
  }
133
169
  // Fetch status
134
- const response = await fetch(url);
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
- error.type = 'rate_limit_error';
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
- error.type = 'connection_error';
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
- error.type = 'server_error';
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
- error.type = 'server_error';
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) => setTimeout(resolve, ms));
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;
@@ -19,5 +19,6 @@ export declare class MockImageAdapter implements ImageProviderAdapter {
19
19
  resolvedPrompt: string;
20
20
  settings: ResolvedImageGenerationSettings;
21
21
  apiKey: string | null;
22
+ signal?: AbortSignal;
22
23
  }): Promise<ImageGenerationResponse>;
23
24
  }
@@ -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: 'server_error',
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
  };
@@ -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
- baseURL: process.env.GENAI_ELECTRON_IMAGE_BASE_URL || 'http://localhost:8081',
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";
@@ -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.9.2",
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.71.2",
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",