genai-lite 0.10.0 → 0.12.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 +30 -2
- package/dist/adapters/image/GenaiElectronImageAdapter.d.ts +1 -0
- package/dist/adapters/image/GenaiElectronImageAdapter.js +26 -9
- package/dist/adapters/image/MockImageAdapter.d.ts +1 -0
- package/dist/adapters/image/OpenAIImageAdapter.d.ts +1 -0
- package/dist/adapters/image/OpenAIImageAdapter.js +22 -7
- package/dist/image/ImageService.d.ts +11 -0
- package/dist/image/ImageService.js +80 -32
- package/dist/image/config.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/llm/LLMService.d.ts +23 -1
- package/dist/llm/LLMService.js +326 -186
- package/dist/llm/clients/AnthropicClientAdapter.d.ts +6 -1
- package/dist/llm/clients/AnthropicClientAdapter.js +271 -87
- package/dist/llm/clients/GeminiClientAdapter.d.ts +8 -1
- package/dist/llm/clients/GeminiClientAdapter.js +216 -59
- package/dist/llm/clients/LlamaCppClientAdapter.d.ts +7 -1
- package/dist/llm/clients/LlamaCppClientAdapter.js +303 -125
- package/dist/llm/clients/MistralClientAdapter.d.ts +8 -1
- package/dist/llm/clients/MistralClientAdapter.js +222 -41
- package/dist/llm/clients/MockClientAdapter.d.ts +2 -1
- package/dist/llm/clients/MockClientAdapter.js +51 -0
- package/dist/llm/clients/OpenAIClientAdapter.d.ts +6 -1
- package/dist/llm/clients/OpenAIClientAdapter.js +200 -72
- package/dist/llm/clients/OpenRouterClientAdapter.d.ts +6 -1
- package/dist/llm/clients/OpenRouterClientAdapter.js +242 -109
- package/dist/llm/clients/types.d.ts +9 -1
- package/dist/llm/types.d.ts +28 -0
- package/dist/llm/types.js +1 -0
- package/dist/shared/adapters/errorUtils.d.ts +2 -1
- package/dist/shared/adapters/errorUtils.js +56 -18
- package/dist/types/image.d.ts +33 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ A lightweight, portable Node.js/TypeScript library providing a unified interface
|
|
|
6
6
|
|
|
7
7
|
- 🔌 **Unified API** - Single interface for multiple AI providers
|
|
8
8
|
- 🏠 **Local & Cloud Models** - Run models locally with llama.cpp or use cloud APIs
|
|
9
|
+
- ⚡ **Text Streaming** - Async iterable token deltas for OpenAI, Anthropic, Gemini, Mistral, OpenRouter, and llama.cpp
|
|
9
10
|
- 🖼️ **Image Generation** - First-class support for AI image generation (OpenAI, local diffusion)
|
|
10
11
|
- 🔐 **Flexible API Key Management** - Bring your own key storage solution
|
|
11
12
|
- 📦 **Zero Electron Dependencies** - Works in any Node.js environment
|
|
@@ -13,7 +14,7 @@ A lightweight, portable Node.js/TypeScript library providing a unified interface
|
|
|
13
14
|
- ⚡ **Lightweight** - Minimal dependencies, focused functionality
|
|
14
15
|
- 🛡️ **Provider Normalization** - Consistent responses across different AI APIs
|
|
15
16
|
- 🧠 **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
|
|
17
|
+
- 🔁 **Built-in Reliability** - Automatic retries with backoff/`Retry-After`, per-request timeouts, and abort signals — for both LLM and image generation (local diffusion additionally gets server-side cancel and is never blind-retried)
|
|
17
18
|
- 🎨 **Configurable Model Presets** - Built-in presets with full customization options
|
|
18
19
|
- 🎭 **Template Engine** - Sophisticated templating with conditionals and variable substitution
|
|
19
20
|
- 📊 **Configurable Logging** - Debug mode, custom loggers (pino, winston), and silent mode for tests
|
|
@@ -77,6 +78,33 @@ if (response.object === 'chat.completion') {
|
|
|
77
78
|
}
|
|
78
79
|
```
|
|
79
80
|
|
|
81
|
+
### Streaming Text
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { LLMService } from 'genai-lite';
|
|
85
|
+
|
|
86
|
+
const llmService = new LLMService(async () => 'not-needed');
|
|
87
|
+
|
|
88
|
+
for await (const event of llmService.streamMessage({
|
|
89
|
+
providerId: 'llamacpp',
|
|
90
|
+
modelId: 'llamacpp',
|
|
91
|
+
messages: [{ role: 'user', content: 'Say hello in five words.' }],
|
|
92
|
+
settings: { maxTokens: 32 }
|
|
93
|
+
})) {
|
|
94
|
+
if (event.type === 'content_delta') {
|
|
95
|
+
process.stdout.write(event.delta);
|
|
96
|
+
} else if (event.type === 'reasoning_delta') {
|
|
97
|
+
process.stderr.write(event.delta);
|
|
98
|
+
} else if (event.type === 'complete') {
|
|
99
|
+
console.log('\nTokens:', event.response.usage?.total_tokens);
|
|
100
|
+
} else if (event.type === 'error') {
|
|
101
|
+
console.error(event.error.error.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Streaming is implemented for all text providers: `openai`, `anthropic`, `gemini`, `mistral`, `openrouter`, and `llamacpp`. The final `complete.response` event contains the same normalized response shape returned by `sendMessage()`.
|
|
107
|
+
|
|
80
108
|
### Image Generation
|
|
81
109
|
|
|
82
110
|
```typescript
|
|
@@ -134,7 +162,7 @@ Comprehensive documentation is available in the **[`genai-lite-docs`](./genai-li
|
|
|
134
162
|
- **Google Gemini** - Gemini 3 (Pro, Flash preview), Gemini 2.5, Gemma 3 & 4 (free)
|
|
135
163
|
- **Mistral** - Codestral, Devstral
|
|
136
164
|
- **OpenRouter** - Unified gateway to 100+ models (unknown models assumed reasoning-capable)
|
|
137
|
-
- **llama.cpp** - Run any GGUF model locally (no API keys required); local reasoning toggle for detected models
|
|
165
|
+
- **llama.cpp** - Run any GGUF model locally (no API keys required); streaming and local reasoning toggle for detected models
|
|
138
166
|
|
|
139
167
|
### Image Providers
|
|
140
168
|
- **OpenAI Images** - gpt-image-1, dall-e-3, dall-e-2
|
|
@@ -35,6 +35,7 @@ export declare class GenaiElectronImageAdapter implements ImageProviderAdapter {
|
|
|
35
35
|
settings: ResolvedImageGenerationSettings;
|
|
36
36
|
apiKey: string | null;
|
|
37
37
|
signal?: AbortSignal;
|
|
38
|
+
timeoutMs?: number;
|
|
38
39
|
}): Promise<ImageGenerationResponse>;
|
|
39
40
|
/**
|
|
40
41
|
* Builds the request payload for genai-electron
|
|
@@ -43,7 +43,10 @@ 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, signal } = config;
|
|
46
|
+
const { request, resolvedPrompt, settings, signal, timeoutMs } = config;
|
|
47
|
+
// Per-request override of the construction-time timeout; applies to both
|
|
48
|
+
// the POST and the poll-loop budget
|
|
49
|
+
const effectiveTimeout = timeoutMs ?? this.timeout;
|
|
47
50
|
let generationId;
|
|
48
51
|
try {
|
|
49
52
|
if (signal?.aborted) {
|
|
@@ -58,10 +61,10 @@ class GenaiElectronImageAdapter {
|
|
|
58
61
|
steps: payload.steps,
|
|
59
62
|
});
|
|
60
63
|
// Start generation (returns immediately with ID)
|
|
61
|
-
generationId = await this.startGeneration(payload, signal);
|
|
64
|
+
generationId = await this.startGeneration(payload, signal, effectiveTimeout);
|
|
62
65
|
this.logger.info(`GenaiElectron Image API: Generation started with ID: ${generationId}`);
|
|
63
66
|
// Poll for completion
|
|
64
|
-
const result = await this.pollForCompletion(generationId, settings.diffusion?.onProgress, signal);
|
|
67
|
+
const result = await this.pollForCompletion(generationId, settings.diffusion?.onProgress, signal, effectiveTimeout);
|
|
65
68
|
this.logger.info(`GenaiElectron Image API: Generation complete (${result.timeTaken}ms)`);
|
|
66
69
|
// Convert to ImageGenerationResponse
|
|
67
70
|
return this.convertToResponse(result, request);
|
|
@@ -104,7 +107,7 @@ class GenaiElectronImageAdapter {
|
|
|
104
107
|
* so both surface as AbortError — classification comes from adapter-side
|
|
105
108
|
* state (timer-fired flag vs signal.aborted), with the caller's abort winning.
|
|
106
109
|
*/
|
|
107
|
-
async startGeneration(payload, signal) {
|
|
110
|
+
async startGeneration(payload, signal, effectiveTimeout) {
|
|
108
111
|
const url = `${this.baseURL}/v1/images/generations`;
|
|
109
112
|
const controller = new AbortController();
|
|
110
113
|
let timedOut = false;
|
|
@@ -120,7 +123,7 @@ class GenaiElectronImageAdapter {
|
|
|
120
123
|
const timeoutId = setTimeout(() => {
|
|
121
124
|
timedOut = true;
|
|
122
125
|
controller.abort();
|
|
123
|
-
},
|
|
126
|
+
}, effectiveTimeout);
|
|
124
127
|
try {
|
|
125
128
|
const response = await fetch(url, {
|
|
126
129
|
method: 'POST',
|
|
@@ -141,7 +144,7 @@ class GenaiElectronImageAdapter {
|
|
|
141
144
|
throw this.createAbortError('Image generation request was aborted before it started');
|
|
142
145
|
}
|
|
143
146
|
if (timedOut) {
|
|
144
|
-
throw this.createTimeoutError(`Request timeout after ${
|
|
147
|
+
throw this.createTimeoutError(`Request timeout after ${effectiveTimeout}ms (connecting to ${this.baseURL})`);
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
throw error;
|
|
@@ -154,7 +157,7 @@ class GenaiElectronImageAdapter {
|
|
|
154
157
|
/**
|
|
155
158
|
* Polls for generation completion with progress updates
|
|
156
159
|
*/
|
|
157
|
-
async pollForCompletion(generationId, onProgress, signal) {
|
|
160
|
+
async pollForCompletion(generationId, onProgress, signal, effectiveTimeout) {
|
|
158
161
|
const url = `${this.baseURL}/v1/images/generations/${generationId}`;
|
|
159
162
|
const startTime = Date.now();
|
|
160
163
|
while (true) {
|
|
@@ -163,8 +166,8 @@ class GenaiElectronImageAdapter {
|
|
|
163
166
|
throw this.createAbortError(`Image generation request was aborted (ID: ${generationId})`);
|
|
164
167
|
}
|
|
165
168
|
// Check overall timeout
|
|
166
|
-
if (Date.now() - startTime >
|
|
167
|
-
throw this.createTimeoutError(`Generation timeout after ${
|
|
169
|
+
if (Date.now() - startTime > effectiveTimeout) {
|
|
170
|
+
throw this.createTimeoutError(`Generation timeout after ${effectiveTimeout}ms (ID: ${generationId})`);
|
|
168
171
|
}
|
|
169
172
|
// Fetch status
|
|
170
173
|
let response;
|
|
@@ -343,6 +346,17 @@ class GenaiElectronImageAdapter {
|
|
|
343
346
|
errorCode = types_1.ADAPTER_ERROR_CODES.RATE_LIMIT_EXCEEDED;
|
|
344
347
|
errorType = 'rate_limit_error';
|
|
345
348
|
}
|
|
349
|
+
else if (error.code === 'NOT_FOUND') {
|
|
350
|
+
// Server JSON code from a poll GET/DELETE whose generation is gone —
|
|
351
|
+
// typically expired from the registry (default TTL 300s). Without this
|
|
352
|
+
// branch the 404 status maps to MODEL_NOT_FOUND ("model not found"),
|
|
353
|
+
// which is misleading for an expired generation.
|
|
354
|
+
errorMessage =
|
|
355
|
+
'Generation not found on the server — it likely expired from the ' +
|
|
356
|
+
`registry before polling completed. ${error.message}`;
|
|
357
|
+
errorCode = types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR;
|
|
358
|
+
errorType = 'server_error';
|
|
359
|
+
}
|
|
346
360
|
else if (error.code === 'SERVER_NOT_RUNNING') {
|
|
347
361
|
errorMessage = `Image generation server is not running (connecting to ${this.baseURL})`;
|
|
348
362
|
errorCode = types_1.ADAPTER_ERROR_CODES.NETWORK_ERROR;
|
|
@@ -371,6 +385,9 @@ class GenaiElectronImageAdapter {
|
|
|
371
385
|
enhancedError.code = errorCode;
|
|
372
386
|
enhancedError.type = errorType;
|
|
373
387
|
enhancedError.status = mapped.status;
|
|
388
|
+
if (mapped.retryAfterMs !== undefined) {
|
|
389
|
+
enhancedError.retryAfterMs = mapped.retryAfterMs;
|
|
390
|
+
}
|
|
374
391
|
enhancedError.providerId = this.id;
|
|
375
392
|
enhancedError.modelId = request.modelId;
|
|
376
393
|
return enhancedError;
|
|
@@ -36,6 +36,7 @@ export declare class OpenAIImageAdapter implements ImageProviderAdapter {
|
|
|
36
36
|
settings: ResolvedImageGenerationSettings;
|
|
37
37
|
apiKey: string | null;
|
|
38
38
|
signal?: AbortSignal;
|
|
39
|
+
timeoutMs?: number;
|
|
39
40
|
}): Promise<ImageGenerationResponse>;
|
|
40
41
|
/**
|
|
41
42
|
* 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, signal } = config;
|
|
62
|
+
const { request, resolvedPrompt, settings, apiKey, signal, timeoutMs } = config;
|
|
63
63
|
if (!apiKey) {
|
|
64
64
|
throw new Error('OpenAI API key is required but was not provided');
|
|
65
65
|
}
|
|
@@ -102,14 +102,18 @@ class OpenAIImageAdapter {
|
|
|
102
102
|
n: params.n,
|
|
103
103
|
isGptImageModel,
|
|
104
104
|
});
|
|
105
|
-
// Make API call (per-request abort signal when provided)
|
|
106
|
-
const
|
|
105
|
+
// Make API call (per-request abort signal and timeout override when provided)
|
|
106
|
+
const requestOptions = {
|
|
107
|
+
...(signal && { signal }),
|
|
108
|
+
...(timeoutMs !== undefined && { timeout: timeoutMs }),
|
|
109
|
+
};
|
|
110
|
+
const response = await client.images.generate(params, Object.keys(requestOptions).length > 0 ? requestOptions : undefined);
|
|
107
111
|
if (!response.data || response.data.length === 0) {
|
|
108
112
|
throw new Error('OpenAI API returned no images in response');
|
|
109
113
|
}
|
|
110
114
|
this.logger.info(`OpenAI Image API call successful, generated ${response.data.length} images`);
|
|
111
115
|
// Process response
|
|
112
|
-
return await this.processResponse(response, request, isGptImageModel, signal);
|
|
116
|
+
return await this.processResponse(response, request, isGptImageModel, signal, timeoutMs);
|
|
113
117
|
}
|
|
114
118
|
catch (error) {
|
|
115
119
|
this.logger.error('OpenAI Image API error:', error);
|
|
@@ -199,7 +203,7 @@ class OpenAIImageAdapter {
|
|
|
199
203
|
/**
|
|
200
204
|
* Processes the OpenAI API response and converts to ImageGenerationResponse
|
|
201
205
|
*/
|
|
202
|
-
async processResponse(response, request, isGptImageModel, signal) {
|
|
206
|
+
async processResponse(response, request, isGptImageModel, signal, timeoutMs) {
|
|
203
207
|
const images = [];
|
|
204
208
|
for (let i = 0; i < response.data.length; i++) {
|
|
205
209
|
const item = response.data[i];
|
|
@@ -222,9 +226,17 @@ class OpenAIImageAdapter {
|
|
|
222
226
|
else {
|
|
223
227
|
// dall-e-2/dall-e-3: can be url or b64_json
|
|
224
228
|
if (item.url) {
|
|
225
|
-
// Fetch image from URL
|
|
229
|
+
// Fetch image from URL. The SDK timeout only covers images.generate,
|
|
230
|
+
// so bound this fetch with the same per-request budget when one was
|
|
231
|
+
// given (AbortSignal.timeout carries a TimeoutError reason, mapping
|
|
232
|
+
// to REQUEST_TIMEOUT; a caller abort still maps to REQUEST_ABORTED)
|
|
226
233
|
imageUrl = item.url;
|
|
227
|
-
const
|
|
234
|
+
const fetchSignal = timeoutMs !== undefined
|
|
235
|
+
? signal
|
|
236
|
+
? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
|
|
237
|
+
: AbortSignal.timeout(timeoutMs)
|
|
238
|
+
: signal;
|
|
239
|
+
const imageResponse = await fetch(item.url, fetchSignal ? { signal: fetchSignal } : undefined);
|
|
228
240
|
if (!imageResponse.ok) {
|
|
229
241
|
throw new Error(`Failed to fetch image from URL: ${imageResponse.statusText}`);
|
|
230
242
|
}
|
|
@@ -297,6 +309,9 @@ class OpenAIImageAdapter {
|
|
|
297
309
|
enhancedError.code = mapped.errorCode;
|
|
298
310
|
enhancedError.type = mapped.errorType;
|
|
299
311
|
enhancedError.status = mapped.status;
|
|
312
|
+
if (mapped.retryAfterMs !== undefined) {
|
|
313
|
+
enhancedError.retryAfterMs = mapped.retryAfterMs;
|
|
314
|
+
}
|
|
300
315
|
enhancedError.providerId = this.id;
|
|
301
316
|
enhancedError.modelId = request.modelId;
|
|
302
317
|
return enhancedError;
|
|
@@ -17,6 +17,7 @@ export declare class ImageService {
|
|
|
17
17
|
private requestValidator;
|
|
18
18
|
private settingsResolver;
|
|
19
19
|
private modelResolver;
|
|
20
|
+
private retryOptions;
|
|
20
21
|
constructor(getApiKey: ApiKeyProvider, options?: ImageServiceOptions);
|
|
21
22
|
/**
|
|
22
23
|
* Generates images based on the request
|
|
@@ -26,6 +27,16 @@ export declare class ImageService {
|
|
|
26
27
|
* @returns Promise resolving to response or error
|
|
27
28
|
*/
|
|
28
29
|
generateImage(request: ImageGenerationRequest | ImageGenerationRequestWithPreset, options?: GenerateImageOptions): Promise<ImageGenerationResponse | ImageFailureResponse>;
|
|
30
|
+
/**
|
|
31
|
+
* Builds a failure envelope from a thrown error.
|
|
32
|
+
*
|
|
33
|
+
* Adapters throw errors stamped with an ADAPTER_ERROR_CODES code plus
|
|
34
|
+
* type/status/retryAfterMs — that classification is propagated. Anything else
|
|
35
|
+
* (e.g. a failing ApiKeyProvider whose error carries a foreign `.code` like
|
|
36
|
+
* ENOENT) keeps the generic fallback so unrelated codes don't leak into the
|
|
37
|
+
* API surface.
|
|
38
|
+
*/
|
|
39
|
+
private buildFailureEnvelope;
|
|
29
40
|
/**
|
|
30
41
|
* Gets list of supported image providers
|
|
31
42
|
*
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.ImageService = void 0;
|
|
13
13
|
const defaultLogger_1 = require("../logging/defaultLogger");
|
|
14
14
|
const types_1 = require("../llm/clients/types");
|
|
15
|
+
const withRetry_1 = require("../shared/services/withRetry");
|
|
15
16
|
const config_1 = require("./config");
|
|
16
17
|
const image_presets_json_1 = __importDefault(require("../config/image-presets.json"));
|
|
17
18
|
const PresetManager_1 = require("../shared/services/PresetManager");
|
|
@@ -33,6 +34,7 @@ const ADAPTER_ERROR_CODE_VALUES = new Set(Object.values(types_1.ADAPTER_ERROR_CO
|
|
|
33
34
|
class ImageService {
|
|
34
35
|
constructor(getApiKey, options = {}) {
|
|
35
36
|
this.getApiKey = getApiKey;
|
|
37
|
+
this.retryOptions = options.retry;
|
|
36
38
|
// Initialize logger - custom logger takes precedence over logLevel
|
|
37
39
|
this.logger = options.logger ?? (0, defaultLogger_1.createDefaultLogger)(options.logLevel);
|
|
38
40
|
// Initialize helper services
|
|
@@ -136,44 +138,58 @@ class ImageService {
|
|
|
136
138
|
},
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
|
-
// Generate images
|
|
141
|
+
// Generate images. Only providers marked retryable in config are ever
|
|
142
|
+
// retried — genai-electron's POST-then-poll is not idempotent (a blind
|
|
143
|
+
// retry after the POST would start a second GPU generation), and custom
|
|
144
|
+
// adapters without a config entry get the safe default (no retries).
|
|
140
145
|
this.logger.info(`ImageService: Calling adapter for provider: ${providerId}`);
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
const providerRetryable = (0, config_1.getImageProviderById)(providerId)?.retryable === true;
|
|
147
|
+
const retryOnTimeout = this.retryOptions?.retryOnTimeout ?? true;
|
|
148
|
+
const retryableCodes = new Set([
|
|
149
|
+
types_1.ADAPTER_ERROR_CODES.RATE_LIMIT_EXCEEDED,
|
|
150
|
+
types_1.ADAPTER_ERROR_CODES.NETWORK_ERROR,
|
|
151
|
+
...(retryOnTimeout ? [types_1.ADAPTER_ERROR_CODES.REQUEST_TIMEOUT] : []),
|
|
152
|
+
]);
|
|
153
|
+
return await (0, withRetry_1.withRetry)(async () => {
|
|
154
|
+
try {
|
|
155
|
+
const response = await adapter.generate({
|
|
156
|
+
request: fullRequest,
|
|
157
|
+
resolvedPrompt,
|
|
158
|
+
settings: resolvedSettings,
|
|
159
|
+
apiKey,
|
|
160
|
+
signal: options?.signal,
|
|
161
|
+
timeoutMs: options?.timeoutMs,
|
|
162
|
+
});
|
|
163
|
+
this.logger.info('ImageService: Image generation completed successfully');
|
|
164
|
+
return response;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
this.logger.error('ImageService: Error during image generation:', error);
|
|
168
|
+
return this.buildFailureEnvelope(error, providerId, modelId);
|
|
169
|
+
}
|
|
170
|
+
}, (result) => {
|
|
171
|
+
if (result.object !== 'error' || !providerRetryable) {
|
|
172
|
+
return { retry: false };
|
|
173
|
+
}
|
|
174
|
+
const code = String(result.error.code);
|
|
175
|
+
const status = result.error.status;
|
|
176
|
+
const retry = retryableCodes.has(code) ||
|
|
177
|
+
(code === types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR &&
|
|
178
|
+
typeof status === 'number' &&
|
|
179
|
+
(status === 408 || status === 409 || status >= 500));
|
|
180
|
+
return { retry, retryAfterMs: result.error.retryAfterMs };
|
|
181
|
+
}, {
|
|
182
|
+
...this.retryOptions,
|
|
183
|
+
...(options?.maxRetries !== undefined && { maxRetries: options.maxRetries }),
|
|
146
184
|
signal: options?.signal,
|
|
185
|
+
logger: this.logger,
|
|
186
|
+
label: `${providerId}/${modelId}`,
|
|
147
187
|
});
|
|
148
|
-
this.logger.info('ImageService: Image generation completed successfully');
|
|
149
|
-
return response;
|
|
150
188
|
}
|
|
151
189
|
catch (error) {
|
|
190
|
+
// Errors thrown outside the adapter call (e.g. a failing ApiKeyProvider)
|
|
152
191
|
this.logger.error('ImageService: Error during image generation:', error);
|
|
153
|
-
|
|
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);
|
|
160
|
-
return {
|
|
161
|
-
object: 'error',
|
|
162
|
-
providerId: providerId,
|
|
163
|
-
modelId: modelId,
|
|
164
|
-
error: {
|
|
165
|
-
message: error instanceof Error
|
|
166
|
-
? error.message
|
|
167
|
-
: 'An unknown error occurred during image generation',
|
|
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 }),
|
|
174
|
-
providerError: error,
|
|
175
|
-
},
|
|
176
|
-
};
|
|
192
|
+
return this.buildFailureEnvelope(error, providerId, modelId);
|
|
177
193
|
}
|
|
178
194
|
}
|
|
179
195
|
catch (error) {
|
|
@@ -192,6 +208,38 @@ class ImageService {
|
|
|
192
208
|
};
|
|
193
209
|
}
|
|
194
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Builds a failure envelope from a thrown error.
|
|
213
|
+
*
|
|
214
|
+
* Adapters throw errors stamped with an ADAPTER_ERROR_CODES code plus
|
|
215
|
+
* type/status/retryAfterMs — that classification is propagated. Anything else
|
|
216
|
+
* (e.g. a failing ApiKeyProvider whose error carries a foreign `.code` like
|
|
217
|
+
* ENOENT) keeps the generic fallback so unrelated codes don't leak into the
|
|
218
|
+
* API surface.
|
|
219
|
+
*/
|
|
220
|
+
buildFailureEnvelope(error, providerId, modelId) {
|
|
221
|
+
const thrown = error;
|
|
222
|
+
const isAdapterError = typeof thrown?.code === 'string' && ADAPTER_ERROR_CODE_VALUES.has(thrown.code);
|
|
223
|
+
return {
|
|
224
|
+
object: 'error',
|
|
225
|
+
providerId,
|
|
226
|
+
modelId,
|
|
227
|
+
error: {
|
|
228
|
+
message: error instanceof Error
|
|
229
|
+
? error.message
|
|
230
|
+
: 'An unknown error occurred during image generation',
|
|
231
|
+
code: isAdapterError ? thrown.code : 'PROVIDER_ERROR',
|
|
232
|
+
type: isAdapterError && typeof thrown.type === 'string'
|
|
233
|
+
? thrown.type
|
|
234
|
+
: 'server_error',
|
|
235
|
+
...(isAdapterError &&
|
|
236
|
+
typeof thrown.status === 'number' && { status: thrown.status }),
|
|
237
|
+
...(isAdapterError &&
|
|
238
|
+
typeof thrown.retryAfterMs === 'number' && { retryAfterMs: thrown.retryAfterMs }),
|
|
239
|
+
providerError: error,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
195
243
|
/**
|
|
196
244
|
* Gets list of supported image providers
|
|
197
245
|
*
|
package/dist/image/config.js
CHANGED
|
@@ -22,6 +22,8 @@ exports.SUPPORTED_IMAGE_PROVIDERS = [
|
|
|
22
22
|
id: 'openai-images',
|
|
23
23
|
displayName: 'OpenAI Images',
|
|
24
24
|
description: 'DALL-E and GPT-Image models from OpenAI',
|
|
25
|
+
// Single atomic POST per generation — safe to auto-retry transient failures
|
|
26
|
+
retryable: true,
|
|
25
27
|
capabilities: {
|
|
26
28
|
supportsMultipleImages: true,
|
|
27
29
|
supportsB64Json: true,
|
|
@@ -128,6 +130,9 @@ exports.SUPPORTED_IMAGE_PROVIDERS = [
|
|
|
128
130
|
id: 'genai-electron-images',
|
|
129
131
|
displayName: 'Local Diffusion (genai-electron)',
|
|
130
132
|
description: 'Local stable-diffusion models via genai-electron wrapper',
|
|
133
|
+
// POST-then-poll: a blind retry of a mid-poll failure would start a second
|
|
134
|
+
// GPU generation, so the service-level retry layer must never touch this
|
|
135
|
+
retryable: false,
|
|
131
136
|
capabilities: {
|
|
132
137
|
supportsMultipleImages: true,
|
|
133
138
|
supportsB64Json: true,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { ApiKeyProvider, PresetMode } from "./types";
|
|
2
2
|
export { LLMService } from "./llm/LLMService";
|
|
3
|
-
export type { LLMServiceOptions, SendMessageOptions, CreateMessagesResult } from "./llm/LLMService";
|
|
3
|
+
export type { LLMServiceOptions, SendMessageOptions, StreamMessageOptions, CreateMessagesResult } from "./llm/LLMService";
|
|
4
4
|
export { withRetry, DEFAULT_RETRY_POLICY } from "./shared/services/withRetry";
|
|
5
5
|
export type { RetryPolicy, RetryVerdict, WithRetryOptions } from "./shared/services/withRetry";
|
|
6
6
|
export type { ModelPreset } from "./types/presets";
|
package/dist/llm/LLMService.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ApiKeyProvider, PresetMode } from '../types';
|
|
2
2
|
import type { Logger, LogLevel } from '../logging/types';
|
|
3
|
-
import type { LLMChatRequest, LLMChatRequestWithPreset, LLMResponse, LLMFailureResponse, ProviderInfo, ModelInfo, ApiProviderId, LLMSettings, ModelContext, LLMMessage } from "./types";
|
|
3
|
+
import type { LLMChatRequest, LLMChatRequestWithPreset, LLMResponse, LLMFailureResponse, ProviderInfo, ModelInfo, ApiProviderId, LLMSettings, ModelContext, LLMMessage, LLMStreamEvent } from "./types";
|
|
4
4
|
import type { ModelPreset } from "../types/presets";
|
|
5
5
|
import { type RetryPolicy } from "../shared/services/withRetry";
|
|
6
6
|
export type { PresetMode };
|
|
@@ -43,6 +43,18 @@ export interface SendMessageOptions {
|
|
|
43
43
|
/** Per-request retry cap (overrides the service-level retry.maxRetries) */
|
|
44
44
|
maxRetries?: number;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Per-call options for LLMService.streamMessage
|
|
48
|
+
*/
|
|
49
|
+
export interface StreamMessageOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Abort signal to cancel the request (client-side - the provider may still
|
|
52
|
+
* process and bill an already-dispatched request).
|
|
53
|
+
*/
|
|
54
|
+
signal?: AbortSignal;
|
|
55
|
+
/** Per-request timeout in ms (overrides the service-level timeoutMs) */
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
}
|
|
46
58
|
/**
|
|
47
59
|
* Result from createMessages method
|
|
48
60
|
*/
|
|
@@ -96,6 +108,16 @@ export declare class LLMService {
|
|
|
96
108
|
* @returns Promise resolving to either success or failure response
|
|
97
109
|
*/
|
|
98
110
|
sendMessage(request: LLMChatRequest | LLMChatRequestWithPreset, callOptions?: SendMessageOptions): Promise<LLMResponse | LLMFailureResponse>;
|
|
111
|
+
/**
|
|
112
|
+
* Streams a chat message from an LLM provider.
|
|
113
|
+
*
|
|
114
|
+
* The final `complete` event contains the same normalized response shape as
|
|
115
|
+
* sendMessage(). Validation/API key failures and unsupported streaming adapters
|
|
116
|
+
* are yielded as a single `error` event.
|
|
117
|
+
*/
|
|
118
|
+
streamMessage(request: LLMChatRequest | LLMChatRequestWithPreset, callOptions?: StreamMessageOptions): AsyncGenerator<LLMStreamEvent>;
|
|
119
|
+
private prepareRequest;
|
|
120
|
+
private postProcessResponse;
|
|
99
121
|
/**
|
|
100
122
|
* Gets all configured model presets
|
|
101
123
|
*
|