@loonylabs/tti-middleware 1.1.1 → 1.2.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.
@@ -84,24 +84,36 @@ export declare abstract class BaseTTIProvider implements ITTIProvider {
84
84
  * Validate that the request is valid
85
85
  */
86
86
  protected validateRequest(request: TTIRequest): void;
87
+ /** Resolved retry config type (without deprecated fields) */
88
+ protected static readonly RESOLVED_RETRY_DEFAULTS: Required<Omit<RetryOptions, "incrementalBackoff">>;
87
89
  /**
88
90
  * Resolve retry configuration from request
89
91
  */
90
- protected resolveRetryConfig(request: TTIRequest): Required<RetryOptions> | null;
92
+ protected resolveRetryConfig(request: TTIRequest): Required<Omit<RetryOptions, 'incrementalBackoff'>> | null;
91
93
  /**
92
- * Calculate delay for a specific retry attempt
94
+ * Calculate delay for a specific retry attempt using exponential backoff.
95
+ * Formula: min(delayMs * backoffMultiplier^(attempt-1), maxDelayMs)
96
+ * With optional jitter: random value between 0 and computed delay.
93
97
  */
94
- protected calculateRetryDelay(attempt: number, config: Required<RetryOptions>): number;
98
+ protected calculateRetryDelay(attempt: number, config: Required<Omit<RetryOptions, 'incrementalBackoff'>>): number;
95
99
  /**
96
100
  * Sleep for a specified duration
97
101
  */
98
102
  protected sleep(ms: number): Promise<void>;
99
103
  /**
100
- * Execute a generation function with retry logic for rate limits
104
+ * Execute a generation function with retry logic for transient errors.
105
+ * Retries on: 429, 408, 5xx, network timeouts, TCP disconnects.
106
+ * Does NOT retry on: 400, 401, 403, and other client errors.
101
107
  */
102
108
  protected executeWithRetry<T>(request: TTIRequest, operation: () => Promise<T>, operationName: string): Promise<T>;
103
109
  /**
104
- * Check if an error is a rate limit error (429)
110
+ * Check if an error is retryable (transient).
111
+ * Retryable: 429, 408, 500, 502, 503, 504, network errors, timeouts.
112
+ * Not retryable: 400, 401, 403, and other client errors.
113
+ */
114
+ protected isRetryableError(error: Error): boolean;
115
+ /**
116
+ * @deprecated Use isRetryableError() instead
105
117
  */
106
118
  protected isRateLimitError(error: Error): boolean;
107
119
  /**
@@ -220,9 +220,6 @@ class BaseTTIProvider {
220
220
  }
221
221
  }
222
222
  }
223
- // ============================================================
224
- // RETRY LOGIC
225
- // ============================================================
226
223
  /**
227
224
  * Resolve retry configuration from request
228
225
  */
@@ -236,23 +233,36 @@ class BaseTTIProvider {
236
233
  if (retryOption === undefined || retryOption === true) {
237
234
  return { ...types_1.DEFAULT_RETRY_OPTIONS };
238
235
  }
236
+ // Handle deprecated incrementalBackoff
237
+ let backoffMultiplier = retryOption.backoffMultiplier ?? types_1.DEFAULT_RETRY_OPTIONS.backoffMultiplier;
238
+ if (retryOption.incrementalBackoff !== undefined && retryOption.backoffMultiplier === undefined) {
239
+ // Legacy: incrementalBackoff=true mapped to linear scaling (multiplier 1.0)
240
+ backoffMultiplier = retryOption.incrementalBackoff ? 1.0 : 1.0;
241
+ }
239
242
  // Custom configuration: merge with defaults
240
243
  return {
241
244
  maxRetries: retryOption.maxRetries ?? types_1.DEFAULT_RETRY_OPTIONS.maxRetries,
242
245
  delayMs: retryOption.delayMs ?? types_1.DEFAULT_RETRY_OPTIONS.delayMs,
243
- incrementalBackoff: retryOption.incrementalBackoff ?? types_1.DEFAULT_RETRY_OPTIONS.incrementalBackoff,
246
+ backoffMultiplier,
247
+ maxDelayMs: retryOption.maxDelayMs ?? types_1.DEFAULT_RETRY_OPTIONS.maxDelayMs,
248
+ jitter: retryOption.jitter ?? types_1.DEFAULT_RETRY_OPTIONS.jitter,
244
249
  };
245
250
  }
246
251
  /**
247
- * Calculate delay for a specific retry attempt
252
+ * Calculate delay for a specific retry attempt using exponential backoff.
253
+ * Formula: min(delayMs * backoffMultiplier^(attempt-1), maxDelayMs)
254
+ * With optional jitter: random value between 0 and computed delay.
248
255
  */
249
256
  calculateRetryDelay(attempt, config) {
250
- if (config.incrementalBackoff) {
251
- // Incremental: 1s, 2s, 3s, ...
252
- return config.delayMs * attempt;
257
+ // Exponential backoff: delayMs * multiplier^(attempt-1)
258
+ const exponentialDelay = config.delayMs * Math.pow(config.backoffMultiplier, attempt - 1);
259
+ // Cap at maxDelayMs
260
+ const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
261
+ // Apply jitter: random value between 0 and cappedDelay
262
+ if (config.jitter) {
263
+ return Math.round(Math.random() * cappedDelay);
253
264
  }
254
- // Static: always same delay
255
- return config.delayMs;
265
+ return Math.round(cappedDelay);
256
266
  }
257
267
  /**
258
268
  * Sleep for a specified duration
@@ -261,7 +271,9 @@ class BaseTTIProvider {
261
271
  return new Promise((resolve) => setTimeout(resolve, ms));
262
272
  }
263
273
  /**
264
- * Execute a generation function with retry logic for rate limits
274
+ * Execute a generation function with retry logic for transient errors.
275
+ * Retries on: 429, 408, 5xx, network timeouts, TCP disconnects.
276
+ * Does NOT retry on: 400, 401, 403, and other client errors.
265
277
  */
266
278
  async executeWithRetry(request, operation, operationName) {
267
279
  const retryConfig = this.resolveRetryConfig(request);
@@ -277,33 +289,77 @@ class BaseTTIProvider {
277
289
  }
278
290
  catch (error) {
279
291
  lastError = error;
280
- // Check if this is a rate limit error (429)
281
- const isRateLimitError = this.isRateLimitError(error);
282
- // Only retry on rate limit errors
283
- if (!isRateLimitError) {
292
+ // Only retry on retryable errors
293
+ if (!this.isRetryableError(error)) {
284
294
  throw error;
285
295
  }
286
296
  // Check if we have retries left
287
297
  if (attempt < maxAttempts) {
288
298
  const delay = this.calculateRetryDelay(attempt, retryConfig);
289
- this.log('warn', `Rate limit hit during ${operationName}. Retry ${attempt}/${retryConfig.maxRetries} in ${delay}ms...`, { attempt, maxRetries: retryConfig.maxRetries, delayMs: delay });
299
+ this.log('warn', `Transient error during ${operationName}. Retry ${attempt}/${retryConfig.maxRetries} in ${delay}ms...`, { attempt, maxRetries: retryConfig.maxRetries, delayMs: delay, error: error.message });
290
300
  await this.sleep(delay);
291
301
  }
292
302
  }
293
303
  }
294
304
  // All retries exhausted
305
+ this.log('error', `All ${retryConfig.maxRetries} retries exhausted for ${operationName}`, {
306
+ lastError: lastError?.message,
307
+ });
295
308
  throw lastError;
296
309
  }
297
310
  /**
298
- * Check if an error is a rate limit error (429)
311
+ * Check if an error is retryable (transient).
312
+ * Retryable: 429, 408, 500, 502, 503, 504, network errors, timeouts.
313
+ * Not retryable: 400, 401, 403, and other client errors.
299
314
  */
300
- isRateLimitError(error) {
315
+ isRetryableError(error) {
301
316
  const message = error.message.toLowerCase();
302
- return (message.includes('429') ||
303
- message.includes('rate limit') ||
317
+ // Non-retryable client errors (check first to avoid false positives)
318
+ if (message.includes('401') ||
319
+ message.includes('403') ||
320
+ message.includes('400') ||
321
+ message.includes('authentication') ||
322
+ message.includes('unauthorized') ||
323
+ message.includes('forbidden')) {
324
+ return false;
325
+ }
326
+ // Retryable HTTP status codes
327
+ if (message.includes('429') ||
328
+ message.includes('408') ||
329
+ message.includes('500') ||
330
+ message.includes('502') ||
331
+ message.includes('503') ||
332
+ message.includes('504')) {
333
+ return true;
334
+ }
335
+ // Retryable by error description
336
+ if (message.includes('rate limit') ||
304
337
  message.includes('quota exceeded') ||
305
338
  message.includes('too many requests') ||
306
- message.includes('resource exhausted'));
339
+ message.includes('resource exhausted')) {
340
+ return true;
341
+ }
342
+ // Network / timeout errors
343
+ if (message.includes('timeout') ||
344
+ message.includes('etimedout') ||
345
+ message.includes('esockettimedout') ||
346
+ message.includes('econnreset') ||
347
+ message.includes('econnrefused') ||
348
+ message.includes('enotfound') ||
349
+ message.includes('econnaborted') ||
350
+ message.includes('epipe') ||
351
+ message.includes('ehostunreach') ||
352
+ message.includes('enetunreach') ||
353
+ message.includes('socket hang up')) {
354
+ return true;
355
+ }
356
+ return false;
357
+ }
358
+ /**
359
+ * @deprecated Use isRetryableError() instead
360
+ */
361
+ isRateLimitError(error) {
362
+ return this.isRetryableError(error);
307
363
  }
308
364
  /**
309
365
  * Convert errors to TTIError instances with proper classification
@@ -354,6 +410,11 @@ class BaseTTIProvider {
354
410
  }
355
411
  exports.BaseTTIProvider = BaseTTIProvider;
356
412
  // ============================================================
413
+ // RETRY LOGIC
414
+ // ============================================================
415
+ /** Resolved retry config type (without deprecated fields) */
416
+ BaseTTIProvider.RESOLVED_RETRY_DEFAULTS = types_1.DEFAULT_RETRY_OPTIONS;
417
+ // ============================================================
357
418
  // HELPER FUNCTIONS
358
419
  // ============================================================
359
420
  /**
@@ -449,8 +449,8 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
449
449
  }
450
450
  buildCharacterConsistencyPrompt(userPrompt, subjectDescription, referenceCount) {
451
451
  const referenceText = referenceCount === 1 ? 'the reference image' : `the ${referenceCount} reference images`;
452
- return `Using ${referenceText} as a reference for the subject "${subjectDescription}", generate a new image where: ${userPrompt}
453
-
452
+ return `Using ${referenceText} as a reference for the subject "${subjectDescription}", generate a new image where: ${userPrompt}
453
+
454
454
  IMPORTANT: Maintain exact visual consistency with the subject in the reference - same style, colors, proportions, and distinctive features. The subject should be immediately recognizable as the same one from the reference.`;
455
455
  }
456
456
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -165,11 +165,16 @@ export interface TTIResponse {
165
165
  }
166
166
  export type TTIErrorCode = 'INVALID_CONFIG' | 'QUOTA_EXCEEDED' | 'PROVIDER_UNAVAILABLE' | 'GENERATION_FAILED' | 'NETWORK_ERROR' | 'UNAUTHORIZED' | 'CAPABILITY_NOT_SUPPORTED';
167
167
  /**
168
- * Configuration for retry behavior on rate limits (429 errors)
168
+ * Configuration for retry behavior on transient errors.
169
+ *
170
+ * Retryable errors: 429 (rate limit), 408 (timeout), 5xx (server errors),
171
+ * network timeouts, and TCP disconnects.
172
+ *
173
+ * Non-retryable errors: 400, 401, 403, and other client errors.
169
174
  */
170
175
  export interface RetryOptions {
171
176
  /**
172
- * Maximum number of retry attempts (default: 2)
177
+ * Maximum number of retry attempts (default: 3)
173
178
  * Total attempts = 1 (initial) + maxRetries
174
179
  */
175
180
  maxRetries?: number;
@@ -178,17 +183,32 @@ export interface RetryOptions {
178
183
  */
179
184
  delayMs?: number;
180
185
  /**
181
- * Use incremental backoff: delay increases by delayMs each retry
182
- * false: always wait delayMs (e.g., 1s, 1s, 1s)
183
- * true: wait delayMs * attempt (e.g., 1s, 2s, 3s)
184
- * Default: false
186
+ * Backoff multiplier for exponential backoff (default: 2.0)
187
+ * Delay formula: delayMs * (backoffMultiplier ^ (attempt - 1))
188
+ * Set to 1.0 for constant delay.
189
+ */
190
+ backoffMultiplier?: number;
191
+ /**
192
+ * Maximum delay in milliseconds (default: 30000)
193
+ * Caps the computed delay to prevent excessively long waits.
194
+ */
195
+ maxDelayMs?: number;
196
+ /**
197
+ * Enable jitter to randomize delay and prevent thundering herd (default: true)
198
+ * When enabled, actual delay is randomized between 0 and the computed delay.
199
+ */
200
+ jitter?: boolean;
201
+ /**
202
+ * @deprecated Use `backoffMultiplier` instead. Will be removed in v2.0.
203
+ * When true, equivalent to backoffMultiplier of 1.0 with linear scaling (delayMs * attempt).
185
204
  */
186
205
  incrementalBackoff?: boolean;
187
206
  }
188
207
  /**
189
- * Default retry configuration
208
+ * Default retry configuration following Google Cloud best practices.
209
+ * @see https://cloud.google.com/storage/docs/retry-strategy
190
210
  */
191
- export declare const DEFAULT_RETRY_OPTIONS: Required<RetryOptions>;
211
+ export declare const DEFAULT_RETRY_OPTIONS: Required<Omit<RetryOptions, 'incrementalBackoff'>>;
192
212
  /**
193
213
  * Interface that all TTI providers must implement
194
214
  */
@@ -28,10 +28,13 @@ exports.LOG_LEVEL_PRIORITY = {
28
28
  silent: 4,
29
29
  };
30
30
  /**
31
- * Default retry configuration
31
+ * Default retry configuration following Google Cloud best practices.
32
+ * @see https://cloud.google.com/storage/docs/retry-strategy
32
33
  */
33
34
  exports.DEFAULT_RETRY_OPTIONS = {
34
- maxRetries: 2,
35
+ maxRetries: 3,
35
36
  delayMs: 1000,
36
- incrementalBackoff: false,
37
+ backoffMultiplier: 2.0,
38
+ maxDelayMs: 30000,
39
+ jitter: true,
37
40
  };
package/package.json CHANGED
@@ -1,90 +1,90 @@
1
- {
2
- "name": "@loonylabs/tti-middleware",
3
- "version": "1.1.1",
4
- "description": "Provider-agnostic Text-to-Image middleware with GDPR compliance. Supports Google Cloud (Imagen, Gemini), Eden AI, and IONOS.",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "files": [
8
- "dist",
9
- "LICENSE",
10
- "README.md",
11
- ".env.example"
12
- ],
13
- "scripts": {
14
- "build": "tsc",
15
- "test": "npm run test:unit",
16
- "test:unit": "jest --testPathPattern=tests/unit",
17
- "test:unit:watch": "jest --testPathPattern=tests/unit --watch",
18
- "test:unit:coverage": "jest --testPathPattern=tests/unit --coverage",
19
- "test:integration": "cross-env TTI_INTEGRATION_TESTS=true jest --testPathPattern=tests/integration",
20
- "test:ci": "jest --runInBand --ci --coverage --testPathPattern=tests/unit",
21
- "test:live": "TTI_INTEGRATION_TESTS=true jest tests/integration/*.integration.test.ts --testTimeout=120000",
22
- "test:manual": "ts-node scripts/manual-test-edenai.ts",
23
- "test:manual:google-cloud": "ts-node scripts/manual-test-google-cloud.ts",
24
- "lint": "eslint src/**/*.ts",
25
- "format": "prettier --write \"src/**/*.ts\"",
26
- "clean": "node -e \"const fs=require('fs');if(fs.existsSync('dist'))fs.rmSync('dist',{recursive:true})\"",
27
- "prepare": "npm run build",
28
- "prepublishOnly": "npm run clean && npm run build && npm run test"
29
- },
30
- "keywords": [
31
- "tti",
32
- "text-to-image",
33
- "image-generation",
34
- "ai",
35
- "vertex-ai",
36
- "google-cloud",
37
- "gemini",
38
- "imagen",
39
- "middleware",
40
- "provider-agnostic",
41
- "typescript",
42
- "gdpr",
43
- "dsgvo",
44
- "character-consistency"
45
- ],
46
- "author": "loonylabs-dev",
47
- "license": "MIT",
48
- "repository": {
49
- "type": "git",
50
- "url": "https://github.com/loonylabs-dev/tti-middleware.git"
51
- },
52
- "bugs": {
53
- "url": "https://github.com/loonylabs-dev/tti-middleware/issues"
54
- },
55
- "homepage": "https://github.com/loonylabs-dev/tti-middleware#readme",
56
- "peerDependencies": {
57
- "@google-cloud/aiplatform": ">=3.0.0",
58
- "@google/genai": ">=0.14.0"
59
- },
60
- "peerDependenciesMeta": {
61
- "@google-cloud/aiplatform": {
62
- "optional": true
63
- },
64
- "@google/genai": {
65
- "optional": true
66
- }
67
- },
68
- "devDependencies": {
69
- "@google-cloud/aiplatform": "^3.29.0",
70
- "@google/genai": "^0.14.0",
71
- "@types/jest": "^29.5.8",
72
- "@types/node": "^20.10.0",
73
- "@typescript-eslint/eslint-plugin": "^6.13.0",
74
- "@typescript-eslint/parser": "^6.13.0",
75
- "cross-env": "^10.1.0",
76
- "dotenv": "^17.2.3",
77
- "eslint": "^8.54.0",
78
- "jest": "^29.7.0",
79
- "prettier": "^3.1.0",
80
- "ts-jest": "^29.1.1",
81
- "ts-node": "^10.9.2",
82
- "typescript": "^5.3.2"
83
- },
84
- "engines": {
85
- "node": ">=18.0.0"
86
- },
87
- "publishConfig": {
88
- "access": "public"
89
- }
90
- }
1
+ {
2
+ "name": "@loonylabs/tti-middleware",
3
+ "version": "1.2.0",
4
+ "description": "Provider-agnostic Text-to-Image middleware with GDPR compliance. Supports Google Cloud (Imagen, Gemini), Eden AI, and IONOS.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "LICENSE",
10
+ "README.md",
11
+ ".env.example"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "test": "npm run test:unit",
16
+ "test:unit": "jest --testPathPattern=tests/unit",
17
+ "test:unit:watch": "jest --testPathPattern=tests/unit --watch",
18
+ "test:unit:coverage": "jest --testPathPattern=tests/unit --coverage",
19
+ "test:integration": "cross-env TTI_INTEGRATION_TESTS=true jest --testPathPattern=tests/integration",
20
+ "test:ci": "jest --runInBand --ci --coverage --testPathPattern=tests/unit",
21
+ "test:live": "TTI_INTEGRATION_TESTS=true jest tests/integration/*.integration.test.ts --testTimeout=120000",
22
+ "test:manual": "ts-node scripts/manual-test-edenai.ts",
23
+ "test:manual:google-cloud": "ts-node scripts/manual-test-google-cloud.ts",
24
+ "lint": "eslint src/**/*.ts",
25
+ "format": "prettier --write \"src/**/*.ts\"",
26
+ "clean": "node -e \"const fs=require('fs');if(fs.existsSync('dist'))fs.rmSync('dist',{recursive:true})\"",
27
+ "prepare": "npm run build",
28
+ "prepublishOnly": "npm run clean && npm run build && npm run test"
29
+ },
30
+ "keywords": [
31
+ "tti",
32
+ "text-to-image",
33
+ "image-generation",
34
+ "ai",
35
+ "vertex-ai",
36
+ "google-cloud",
37
+ "gemini",
38
+ "imagen",
39
+ "middleware",
40
+ "provider-agnostic",
41
+ "typescript",
42
+ "gdpr",
43
+ "dsgvo",
44
+ "character-consistency"
45
+ ],
46
+ "author": "loonylabs-dev",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/loonylabs-dev/tti-middleware.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/loonylabs-dev/tti-middleware/issues"
54
+ },
55
+ "homepage": "https://github.com/loonylabs-dev/tti-middleware#readme",
56
+ "peerDependencies": {
57
+ "@google-cloud/aiplatform": ">=3.0.0",
58
+ "@google/genai": ">=0.14.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@google-cloud/aiplatform": {
62
+ "optional": true
63
+ },
64
+ "@google/genai": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "@google-cloud/aiplatform": "^3.29.0",
70
+ "@google/genai": "^0.14.0",
71
+ "@types/jest": "^29.5.8",
72
+ "@types/node": "^20.10.0",
73
+ "@typescript-eslint/eslint-plugin": "^6.13.0",
74
+ "@typescript-eslint/parser": "^6.13.0",
75
+ "cross-env": "^10.1.0",
76
+ "dotenv": "^17.2.3",
77
+ "eslint": "^8.54.0",
78
+ "jest": "^29.7.0",
79
+ "prettier": "^3.1.0",
80
+ "ts-jest": "^29.1.1",
81
+ "ts-node": "^10.9.2",
82
+ "typescript": "^5.3.2"
83
+ },
84
+ "engines": {
85
+ "node": ">=18.0.0"
86
+ },
87
+ "publishConfig": {
88
+ "access": "public"
89
+ }
90
+ }