@loonylabs/tti-middleware 1.5.1 → 1.7.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
@@ -45,6 +45,7 @@
45
45
  - **IONOS**: German cloud provider with OpenAI-compatible API (experimental)
46
46
  - **Character Consistency**: Generate consistent characters across multiple images (perfect for children's book illustrations)
47
47
  - **GDPR/DSGVO Compliance**: Built-in EU region support with automatic fallback
48
+ - **Region Rotation**: Opt-in region rotation on quota errors (429) for Google Cloud — rotate through regions instead of retrying the same exhausted region
48
49
  - **Retry Logic**: Exponential backoff with jitter for transient errors (429, 408, 5xx, timeouts)
49
50
  - **TypeScript First**: Full type safety with comprehensive interfaces
50
51
  - **Logging Control**: Configurable log levels via environment or API
@@ -400,6 +401,31 @@ interface TTIResponse {
400
401
 
401
402
  ## Advanced Features
402
403
 
404
+ <details>
405
+ <summary><strong>Region Rotation (Google Cloud)</strong></summary>
406
+
407
+ When Vertex AI returns 429 (Resource Exhausted) due to Dynamic Shared Quota, the middleware can rotate through a list of regions instead of retrying the same exhausted region:
408
+
409
+ ```typescript
410
+ const provider = new GoogleCloudTTIProvider({
411
+ projectId: 'my-project',
412
+ region: 'europe-west4',
413
+ regionRotation: {
414
+ regions: ['europe-west4', 'europe-west1', 'europe-north1', 'europe-central2'],
415
+ fallback: 'global',
416
+ alwaysTryFallback: true, // Default: one bonus attempt on fallback after budget exhausted
417
+ },
418
+ });
419
+ ```
420
+
421
+ **Key behavior:**
422
+ - `maxRetries` is the **total budget** across all regions (no multiplier)
423
+ - Only **quota errors** (429, Resource Exhausted) trigger rotation — server errors (500, 503) retry the same region
424
+ - `alwaysTryFallback: true` (default): one bonus attempt on fallback even if retry budget is exhausted
425
+ - Without `regionRotation`: existing behavior unchanged
426
+
427
+ </details>
428
+
403
429
  <details>
404
430
  <summary><strong>Retry Configuration</strong></summary>
405
431
 
@@ -112,6 +112,12 @@ export declare abstract class BaseTTIProvider implements ITTIProvider {
112
112
  * Check if an error is a timeout error (from our withTimeout wrapper).
113
113
  */
114
114
  private isTimeoutError;
115
+ /**
116
+ * Check if an error is a quota/rate-limit error (429 / Resource Exhausted).
117
+ * Used by providers to distinguish quota errors from other retryable errors
118
+ * (e.g., for region rotation on quota errors only).
119
+ */
120
+ protected isQuotaError(error: Error): boolean;
115
121
  /**
116
122
  * Execute a generation function with retry logic for transient errors.
117
123
  * Retries on: 429, 408, 5xx, network timeouts, TCP disconnects.
@@ -121,8 +127,15 @@ export declare abstract class BaseTTIProvider implements ITTIProvider {
121
127
  * retry.timeoutMs, default 45s). Timeout errors have their own retry
122
128
  * counter (timeoutRetries, default 2) independent from the general
123
129
  * maxRetries used for quota/server errors.
124
- */
125
- protected executeWithRetry<T>(request: TTIRequest, operation: () => Promise<T>, operationName: string): Promise<T>;
130
+ *
131
+ * @param options.onRetry - Optional callback invoked before each retry.
132
+ * Receives the error that triggered the retry and the current general
133
+ * retry count. Providers can use this to adjust state between retries
134
+ * (e.g., rotate regions on quota errors).
135
+ */
136
+ protected executeWithRetry<T>(request: TTIRequest, operation: () => Promise<T>, operationName: string, options?: {
137
+ onRetry?: (error: Error, generalRetryCount: number) => void;
138
+ }): Promise<T>;
126
139
  /**
127
140
  * Check if an error is retryable (transient).
128
141
  * Retryable: 429, 408, 500, 502, 503, 504, network errors, timeouts.
@@ -301,6 +301,19 @@ class BaseTTIProvider {
301
301
  isTimeoutError(error) {
302
302
  return error.message.toLowerCase().startsWith('timeout:');
303
303
  }
304
+ /**
305
+ * Check if an error is a quota/rate-limit error (429 / Resource Exhausted).
306
+ * Used by providers to distinguish quota errors from other retryable errors
307
+ * (e.g., for region rotation on quota errors only).
308
+ */
309
+ isQuotaError(error) {
310
+ const message = error.message.toLowerCase();
311
+ return (message.includes('429') ||
312
+ message.includes('resource exhausted') ||
313
+ message.includes('quota exceeded') ||
314
+ message.includes('rate limit') ||
315
+ message.includes('too many requests'));
316
+ }
304
317
  /**
305
318
  * Execute a generation function with retry logic for transient errors.
306
319
  * Retries on: 429, 408, 5xx, network timeouts, TCP disconnects.
@@ -310,8 +323,13 @@ class BaseTTIProvider {
310
323
  * retry.timeoutMs, default 45s). Timeout errors have their own retry
311
324
  * counter (timeoutRetries, default 2) independent from the general
312
325
  * maxRetries used for quota/server errors.
326
+ *
327
+ * @param options.onRetry - Optional callback invoked before each retry.
328
+ * Receives the error that triggered the retry and the current general
329
+ * retry count. Providers can use this to adjust state between retries
330
+ * (e.g., rotate regions on quota errors).
313
331
  */
314
- async executeWithRetry(request, operation, operationName) {
332
+ async executeWithRetry(request, operation, operationName, options) {
315
333
  const retryConfig = this.resolveRetryConfig(request);
316
334
  // No retry configured
317
335
  if (!retryConfig) {
@@ -371,6 +389,10 @@ class BaseTTIProvider {
371
389
  this.log('error', `${operationName} general retry budget exhausted (${maxGeneralRetries} retries): ${error.message}`, { attempt, generalRetryCount, durationMs: duration });
372
390
  throw error;
373
391
  }
392
+ // Notify provider before retry (e.g., for region rotation)
393
+ if (options?.onRetry) {
394
+ options.onRetry(error, generalRetryCount);
395
+ }
374
396
  const delay = this.calculateRetryDelay(generalRetryCount, retryConfig);
375
397
  this.log('warn', `Transient error during ${operationName} after ${duration}ms. Retry ${generalRetryCount}/${maxGeneralRetries} in ${delay}ms: ${error.message}`, { attempt, generalRetryCount, maxGeneralRetries, delayMs: delay, durationMs: duration });
376
398
  await this.sleep(delay);
@@ -8,6 +8,7 @@
8
8
  * - Imagen 4 Ultra (imagen-4.0-ultra-generate-001) - Highest quality variant
9
9
  * - Gemini 2.5 Flash Image - Text-to-image with character consistency
10
10
  * - Gemini 3 Pro Image (gemini-3-pro-image-preview) - 4K, text rendering
11
+ * - Gemini 3.1 Flash Image (gemini-3.1-flash-image-preview) - 4K, improved text rendering (global endpoint)
11
12
  *
12
13
  * All requests go through Google Cloud (Vertex AI) with proper DPA.
13
14
  * EU-compliant when using EU regions.
@@ -15,12 +16,12 @@
15
16
  * @see https://cloud.google.com/vertex-ai/generative-ai/pricing
16
17
  * @see https://cloud.google.com/terms/data-processing-addendum
17
18
  */
18
- import { TTIRequest, TTIResponse, ModelInfo, GoogleCloudRegion } from '../../../types';
19
+ import { TTIRequest, TTIResponse, ModelInfo, GoogleCloudRegion, RegionRotationConfig } from '../../../types';
19
20
  import { BaseTTIProvider } from './base-tti-provider';
20
21
  interface GoogleCloudConfig {
21
22
  /** Google Cloud Project ID */
22
23
  projectId: string;
23
- /** Default region for requests */
24
+ /** Default region for requests (used when regionRotation is not configured) */
24
25
  region: GoogleCloudRegion;
25
26
  /** Path to service account JSON file */
26
27
  keyFilename?: string;
@@ -30,11 +31,17 @@ interface GoogleCloudConfig {
30
31
  private_key: string;
31
32
  project_id?: string;
32
33
  };
34
+ /**
35
+ * Opt-in region rotation for quota errors (429 / Resource Exhausted).
36
+ * When configured, the middleware rotates through the listed regions
37
+ * on quota errors instead of retrying the same region.
38
+ */
39
+ regionRotation?: RegionRotationConfig;
33
40
  }
34
41
  export declare class GoogleCloudTTIProvider extends BaseTTIProvider {
35
42
  private config;
36
43
  private lastUsedRegion;
37
- private aiplatformClient;
44
+ private aiplatformClients;
38
45
  private genaiClients;
39
46
  constructor(config?: Partial<GoogleCloudConfig>);
40
47
  getDisplayName(): string;
@@ -9,6 +9,7 @@
9
9
  * - Imagen 4 Ultra (imagen-4.0-ultra-generate-001) - Highest quality variant
10
10
  * - Gemini 2.5 Flash Image - Text-to-image with character consistency
11
11
  * - Gemini 3 Pro Image (gemini-3-pro-image-preview) - 4K, text rendering
12
+ * - Gemini 3.1 Flash Image (gemini-3.1-flash-image-preview) - 4K, improved text rendering (global endpoint)
12
13
  *
13
14
  * All requests go through Google Cloud (Vertex AI) with proper DPA.
14
15
  * EU-compliant when using EU regions.
@@ -163,6 +164,20 @@ const GOOGLE_CLOUD_MODELS = [
163
164
  availableRegions: ['global'],
164
165
  pricingUrl: 'https://cloud.google.com/vertex-ai/generative-ai/pricing',
165
166
  },
167
+ {
168
+ id: 'gemini-flash-image-2',
169
+ displayName: 'Gemini 3.1 Flash Image',
170
+ capabilities: {
171
+ textToImage: true,
172
+ characterConsistency: true, // Up to 5 characters + 14 objects
173
+ imageEditing: false,
174
+ maxImagesPerRequest: 1,
175
+ },
176
+ // Preview model — requires global endpoint (same as gemini-pro-image).
177
+ // Will likely get regional endpoints once GA.
178
+ availableRegions: ['global'],
179
+ pricingUrl: 'https://cloud.google.com/vertex-ai/generative-ai/pricing',
180
+ },
166
181
  ];
167
182
  // Internal model IDs used in Vertex AI API calls
168
183
  const MODEL_ID_MAP = {
@@ -172,9 +187,10 @@ const MODEL_ID_MAP = {
172
187
  'imagen-4-ultra': 'imagen-4.0-ultra-generate-001',
173
188
  'gemini-flash-image': 'gemini-2.5-flash-image',
174
189
  'gemini-pro-image': 'gemini-3-pro-image-preview',
190
+ 'gemini-flash-image-2': 'gemini-3.1-flash-image-preview',
175
191
  };
176
192
  // Models that use the Gemini generateContent API (vs Imagen predict API)
177
- const GEMINI_API_MODELS = new Set(['gemini-flash-image', 'gemini-pro-image']);
193
+ const GEMINI_API_MODELS = new Set(['gemini-flash-image', 'gemini-pro-image', 'gemini-flash-image-2']);
178
194
  // ============================================================
179
195
  // PROVIDER IMPLEMENTATION
180
196
  // ============================================================
@@ -182,8 +198,8 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
182
198
  constructor(config) {
183
199
  super(types_1.TTIProvider.GOOGLE_CLOUD);
184
200
  this.lastUsedRegion = null;
185
- // Lazy-loaded SDK clients
186
- this.aiplatformClient = null;
201
+ // Lazy-loaded SDK clients (one per region, since region is baked into the client)
202
+ this.aiplatformClients = new Map();
187
203
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
204
  this.genaiClients = new Map();
189
205
  const projectId = config?.projectId ||
@@ -202,10 +218,27 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
202
218
  region,
203
219
  keyFilename: config?.keyFilename || process.env.GOOGLE_APPLICATION_CREDENTIALS,
204
220
  credentials: config?.credentials,
221
+ regionRotation: config?.regionRotation,
205
222
  };
223
+ // Validate regionRotation config
224
+ if (this.config.regionRotation) {
225
+ if (!this.config.regionRotation.regions || this.config.regionRotation.regions.length === 0) {
226
+ throw new base_tti_provider_1.InvalidConfigError(types_1.TTIProvider.GOOGLE_CLOUD, 'regionRotation.regions must contain at least one region');
227
+ }
228
+ if (!this.config.regionRotation.fallback) {
229
+ throw new base_tti_provider_1.InvalidConfigError(types_1.TTIProvider.GOOGLE_CLOUD, 'regionRotation.fallback is required');
230
+ }
231
+ }
206
232
  this.log('info', 'Google Cloud TTI Provider initialized', {
207
233
  projectId: this.config.projectId,
208
234
  region: this.config.region,
235
+ regionRotation: this.config.regionRotation
236
+ ? {
237
+ regions: this.config.regionRotation.regions,
238
+ fallback: this.config.regionRotation.fallback,
239
+ alwaysTryFallback: this.config.regionRotation.alwaysTryFallback ?? true,
240
+ }
241
+ : undefined,
209
242
  isEURegion: (0, base_tti_provider_1.isEURegion)(this.config.region),
210
243
  models: this.listModels().map((m) => m.id),
211
244
  });
@@ -232,29 +265,66 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
232
265
  .map((m) => m.id)
233
266
  .join(', ')}`);
234
267
  }
235
- // Validate region availability
236
- const effectiveRegion = this.getEffectiveRegion(modelId);
268
+ // Determine base region (handles global-only models, region availability)
269
+ const baseRegion = this.getEffectiveRegion(modelId);
270
+ // Region rotation: only for non-global models with rotation configured
271
+ const rotation = this.config.regionRotation;
272
+ const useRotation = !!(rotation && baseRegion !== 'global');
273
+ // Mutable region — the onRetry callback advances this on quota errors
274
+ let currentRegion = useRotation ? rotation.regions[0] : baseRegion;
275
+ // Build region sequence: [...regions, fallback]
276
+ let regionIndex = 0;
277
+ let regionSequence = [];
278
+ if (useRotation) {
279
+ regionSequence = [...rotation.regions, rotation.fallback];
280
+ this.log('info', 'Region rotation enabled', {
281
+ sequence: regionSequence,
282
+ fallback: rotation.fallback,
283
+ alwaysTryFallback: rotation.alwaysTryFallback ?? true,
284
+ });
285
+ }
237
286
  // Create debug info for logging
238
287
  let debugInfo = null;
239
288
  if (debug_tti_utils_1.TTIDebugger.isEnabled) {
240
- debugInfo = debug_tti_utils_1.TTIDebugger.createDebugInfo(request, this.providerName, modelId, { region: effectiveRegion });
289
+ debugInfo = debug_tti_utils_1.TTIDebugger.createDebugInfo(request, this.providerName, modelId, { region: currentRegion });
241
290
  await debug_tti_utils_1.TTIDebugger.logRequest(debugInfo);
242
291
  }
243
292
  this.log('debug', 'Generating image', {
244
293
  model: modelId,
245
- region: effectiveRegion,
294
+ region: currentRegion,
295
+ regionRotation: useRotation,
246
296
  hasReferenceImages: (0, base_tti_provider_1.hasReferenceImages)(request),
247
297
  });
248
- try {
249
- // Route to appropriate API based on model type
250
- let response;
251
- if (GEMINI_API_MODELS.has(modelId)) {
252
- response = await this.executeWithRetry(request, () => this.generateWithGemini(request, modelId, effectiveRegion), 'Gemini API call');
298
+ const isGeminiModel = GEMINI_API_MODELS.has(modelId);
299
+ const operationName = isGeminiModel ? 'Gemini API call' : 'Imagen API call';
300
+ // Operation lambda reads currentRegion from closure
301
+ const operation = () => {
302
+ if (isGeminiModel) {
303
+ return this.generateWithGemini(request, modelId, currentRegion);
253
304
  }
254
305
  else {
255
- response = await this.executeWithRetry(request, () => this.generateWithImagen(request, modelId, effectiveRegion), 'Imagen API call');
306
+ return this.generateWithImagen(request, modelId, currentRegion);
256
307
  }
257
- // Log successful response
308
+ };
309
+ // onRetry: advance region on quota errors, stay on same region otherwise
310
+ const onRetry = useRotation
311
+ ? (error) => {
312
+ if (this.isQuotaError(error) && regionIndex < regionSequence.length - 1) {
313
+ regionIndex++;
314
+ currentRegion = regionSequence[regionIndex];
315
+ this.log('info', `Quota error — rotating to region ${currentRegion}`, {
316
+ regionIndex,
317
+ totalRegions: regionSequence.length,
318
+ region: currentRegion,
319
+ });
320
+ }
321
+ // Non-quota retryable errors: stay on same region
322
+ }
323
+ : undefined;
324
+ try {
325
+ const response = await this.executeWithRetry(request, operation, operationName, {
326
+ onRetry,
327
+ });
258
328
  if (debugInfo) {
259
329
  debugInfo = debug_tti_utils_1.TTIDebugger.updateWithResponse(debugInfo, response);
260
330
  await debug_tti_utils_1.TTIDebugger.logResponse(debugInfo);
@@ -262,7 +332,32 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
262
332
  return response;
263
333
  }
264
334
  catch (error) {
265
- // Log error
335
+ // alwaysTryFallback: one bonus attempt on fallback after budget exhausted
336
+ if (useRotation &&
337
+ this.isQuotaError(error) &&
338
+ (rotation.alwaysTryFallback !== false) &&
339
+ currentRegion !== rotation.fallback) {
340
+ this.log('info', `Retry budget exhausted — bonus attempt on fallback region ${rotation.fallback}`, {
341
+ exhaustedRegion: currentRegion,
342
+ fallback: rotation.fallback,
343
+ });
344
+ currentRegion = rotation.fallback;
345
+ try {
346
+ const response = await operation();
347
+ if (debugInfo) {
348
+ debugInfo = debug_tti_utils_1.TTIDebugger.updateWithResponse(debugInfo, response);
349
+ await debug_tti_utils_1.TTIDebugger.logResponse(debugInfo);
350
+ }
351
+ return response;
352
+ }
353
+ catch (fallbackError) {
354
+ if (debugInfo) {
355
+ debugInfo = debug_tti_utils_1.TTIDebugger.updateWithError(debugInfo, fallbackError);
356
+ await debug_tti_utils_1.TTIDebugger.logError(debugInfo);
357
+ }
358
+ throw fallbackError;
359
+ }
360
+ }
266
361
  if (debugInfo) {
267
362
  debugInfo = debug_tti_utils_1.TTIDebugger.updateWithError(debugInfo, error);
268
363
  await debug_tti_utils_1.TTIDebugger.logError(debugInfo);
@@ -325,7 +420,7 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
325
420
  const internalModelId = MODEL_ID_MAP[modelId];
326
421
  this.lastUsedRegion = region;
327
422
  try {
328
- const { client, helpers } = await this.getAiplatformClient();
423
+ const { client, helpers } = await this.getAiplatformClient(region);
329
424
  const endpoint = `projects/${this.config.projectId}/locations/${region}/publishers/google/models/${internalModelId}`;
330
425
  // Build instance
331
426
  const instanceValue = { prompt: request.prompt };
@@ -370,12 +465,12 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
370
465
  throw this.handleError(error, 'during Imagen API call');
371
466
  }
372
467
  }
373
- async getAiplatformClient() {
374
- if (!this.aiplatformClient) {
468
+ async getAiplatformClient(region) {
469
+ if (!this.aiplatformClients.has(region)) {
375
470
  try {
376
471
  const { v1, helpers } = await Promise.resolve().then(() => __importStar(require('@google-cloud/aiplatform')));
377
472
  const clientOptions = {
378
- apiEndpoint: `${this.config.region}-aiplatform.googleapis.com`,
473
+ apiEndpoint: `${region}-aiplatform.googleapis.com`,
379
474
  };
380
475
  if (this.config.keyFilename) {
381
476
  clientOptions.keyFilename = this.config.keyFilename;
@@ -384,9 +479,13 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
384
479
  clientOptions.credentials = this.config.credentials;
385
480
  }
386
481
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
387
- this.aiplatformClient = new v1.PredictionServiceClient(clientOptions);
482
+ this.aiplatformClients.set(region, new v1.PredictionServiceClient(clientOptions));
483
+ this.log('debug', 'Initialized @google-cloud/aiplatform client', {
484
+ region,
485
+ apiEndpoint: clientOptions.apiEndpoint,
486
+ });
388
487
  return {
389
- client: this.aiplatformClient,
488
+ client: this.aiplatformClients.get(region),
390
489
  helpers: helpers,
391
490
  };
392
491
  }
@@ -396,7 +495,7 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
396
495
  }
397
496
  const { helpers } = await Promise.resolve().then(() => __importStar(require('@google-cloud/aiplatform')));
398
497
  return {
399
- client: this.aiplatformClient,
498
+ client: this.aiplatformClients.get(region),
400
499
  helpers: helpers,
401
500
  };
402
501
  }
@@ -470,11 +569,17 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
470
569
  const config = {
471
570
  responseModalities: ['TEXT', 'IMAGE'],
472
571
  };
473
- // Add imageConfig with aspectRatio if provided
474
- if (request.aspectRatio) {
475
- config.imageConfig = {
476
- aspectRatio: request.aspectRatio,
477
- };
572
+ // Add imageConfig with aspectRatio and/or imageSize if provided
573
+ if (request.aspectRatio || request.providerOptions?.imageSize) {
574
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
575
+ const imageConfig = {};
576
+ if (request.aspectRatio) {
577
+ imageConfig.aspectRatio = request.aspectRatio;
578
+ }
579
+ if (request.providerOptions?.imageSize) {
580
+ imageConfig.imageSize = request.providerOptions.imageSize;
581
+ }
582
+ config.imageConfig = imageConfig;
478
583
  }
479
584
  // Add temperature if provided
480
585
  if (request.providerOptions?.temperature !== undefined) {
@@ -50,7 +50,29 @@ export interface ModelInfo {
50
50
  * Google Cloud regions
51
51
  * EU regions are GDPR-compliant
52
52
  */
53
- export type GoogleCloudRegion = 'global' | 'europe-west1' | 'europe-west2' | 'europe-west3' | 'europe-west4' | 'europe-west9' | 'us-central1' | 'us-east4';
53
+ export type GoogleCloudRegion = 'global' | 'europe-west1' | 'europe-west2' | 'europe-west3' | 'europe-west4' | 'europe-west6' | 'europe-west8' | 'europe-west9' | 'europe-north1' | 'europe-central2' | 'europe-southwest1' | 'us-central1' | 'us-east1' | 'us-east4' | 'us-east5' | 'us-south1' | 'us-west1' | 'us-west4' | 'asia-east1' | 'asia-east2' | 'asia-northeast1' | 'asia-northeast3' | 'asia-south1' | 'asia-southeast1' | 'australia-southeast1' | 'me-central1' | 'me-central2' | 'me-west1';
54
+ /**
55
+ * Configuration for region rotation on quota errors (429 / Resource Exhausted).
56
+ *
57
+ * When Vertex AI returns a quota error, the middleware rotates through the
58
+ * configured regions instead of retrying the same region. This is useful
59
+ * when Dynamic Shared Quota is temporarily exhausted in a single region.
60
+ *
61
+ * The total retry budget (from RetryOptions.maxRetries) is shared across
62
+ * all regions — region rotation does NOT multiply the retry count.
63
+ */
64
+ export interface RegionRotationConfig {
65
+ /** Ordered list of regions to try. First entry = primary region. */
66
+ regions: GoogleCloudRegion[];
67
+ /** Last-resort region after all regions exhausted (typically 'global'). */
68
+ fallback: GoogleCloudRegion;
69
+ /**
70
+ * If true: when maxRetries is exhausted before reaching the fallback,
71
+ * one final bonus attempt on the fallback region is made.
72
+ * @default true
73
+ */
74
+ alwaysTryFallback?: boolean;
75
+ }
54
76
  /**
55
77
  * Reference image for character consistency
56
78
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loonylabs/tti-middleware",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "Provider-agnostic Text-to-Image middleware with GDPR compliance. Supports Google Cloud (Imagen, Gemini), Eden AI, and IONOS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",