@loonylabs/tti-middleware 1.0.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.
@@ -0,0 +1,473 @@
1
+ "use strict";
2
+ /**
3
+ * Google Cloud TTI Provider
4
+ *
5
+ * Unified provider for Google Cloud's image generation services:
6
+ * - Imagen 3 (imagegeneration@006) - High quality text-to-image
7
+ * - Gemini 2.5 Flash Image - Text-to-image with character consistency
8
+ *
9
+ * All requests go through Google Cloud (Vertex AI) with proper DPA.
10
+ * EU-compliant when using EU regions.
11
+ *
12
+ * @see https://cloud.google.com/vertex-ai/generative-ai/pricing
13
+ * @see https://cloud.google.com/terms/data-processing-addendum
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.GoogleCloudTTIProvider = void 0;
50
+ const types_1 = require("../../../types");
51
+ const base_tti_provider_1 = require("./base-tti-provider");
52
+ // ============================================================
53
+ // MODEL DEFINITIONS
54
+ // ============================================================
55
+ const GOOGLE_CLOUD_MODELS = [
56
+ {
57
+ id: 'imagen-3',
58
+ displayName: 'Imagen 3',
59
+ capabilities: {
60
+ textToImage: true,
61
+ characterConsistency: false, // Requires allowlist for Customization API
62
+ imageEditing: false,
63
+ maxImagesPerRequest: 4,
64
+ },
65
+ availableRegions: [
66
+ 'europe-west1',
67
+ 'europe-west2',
68
+ 'europe-west3',
69
+ 'europe-west4',
70
+ 'europe-west9',
71
+ 'us-central1',
72
+ 'us-east4',
73
+ ],
74
+ pricingUrl: 'https://cloud.google.com/vertex-ai/generative-ai/pricing',
75
+ },
76
+ {
77
+ id: 'gemini-flash-image',
78
+ displayName: 'Gemini 2.5 Flash Image',
79
+ capabilities: {
80
+ textToImage: true,
81
+ characterConsistency: true, // Built-in support!
82
+ imageEditing: false,
83
+ maxImagesPerRequest: 1,
84
+ },
85
+ // Note: NOT available in europe-west3 (Frankfurt)!
86
+ availableRegions: [
87
+ 'europe-west1',
88
+ 'europe-west4',
89
+ 'europe-north1',
90
+ 'us-central1',
91
+ 'us-east4',
92
+ ],
93
+ pricingUrl: 'https://cloud.google.com/vertex-ai/generative-ai/pricing',
94
+ },
95
+ ];
96
+ // Internal model IDs used in API calls
97
+ const MODEL_ID_MAP = {
98
+ 'imagen-3': 'imagegeneration@006',
99
+ 'gemini-flash-image': 'gemini-2.5-flash-image',
100
+ };
101
+ // ============================================================
102
+ // PROVIDER IMPLEMENTATION
103
+ // ============================================================
104
+ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
105
+ constructor(config) {
106
+ super(types_1.TTIProvider.GOOGLE_CLOUD);
107
+ this.lastUsedRegion = null;
108
+ // Lazy-loaded SDK clients
109
+ this.aiplatformClient = null;
110
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
+ this.genaiClient = null;
112
+ const projectId = config?.projectId ||
113
+ process.env.GOOGLE_CLOUD_PROJECT ||
114
+ process.env.GCLOUD_PROJECT ||
115
+ '';
116
+ const region = config?.region ||
117
+ process.env.GOOGLE_CLOUD_REGION ||
118
+ process.env.VERTEX_AI_REGION ||
119
+ 'europe-west4'; // Default to Netherlands (supports all models)
120
+ if (!projectId) {
121
+ throw new base_tti_provider_1.InvalidConfigError(types_1.TTIProvider.GOOGLE_CLOUD, 'Google Cloud Project ID is required. Set GOOGLE_CLOUD_PROJECT or pass projectId in config.');
122
+ }
123
+ this.config = {
124
+ projectId,
125
+ region,
126
+ keyFilename: config?.keyFilename || process.env.GOOGLE_APPLICATION_CREDENTIALS,
127
+ credentials: config?.credentials,
128
+ };
129
+ this.log('info', 'Google Cloud TTI Provider initialized', {
130
+ projectId: this.config.projectId,
131
+ region: this.config.region,
132
+ isEURegion: (0, base_tti_provider_1.isEURegion)(this.config.region),
133
+ models: this.listModels().map((m) => m.id),
134
+ });
135
+ }
136
+ // ============================================================
137
+ // ITTIProvider IMPLEMENTATION
138
+ // ============================================================
139
+ getDisplayName() {
140
+ return 'Google Cloud';
141
+ }
142
+ listModels() {
143
+ return GOOGLE_CLOUD_MODELS;
144
+ }
145
+ getDefaultModel() {
146
+ // Default to Gemini Flash Image as it supports character consistency
147
+ return 'gemini-flash-image';
148
+ }
149
+ async generate(request) {
150
+ this.validateRequest(request);
151
+ const modelId = request.model || this.getDefaultModel();
152
+ const modelInfo = this.getModelInfo(modelId);
153
+ if (!modelInfo) {
154
+ throw new base_tti_provider_1.InvalidConfigError(this.providerName, `Unknown model: ${modelId}. Available models: ${this.listModels()
155
+ .map((m) => m.id)
156
+ .join(', ')}`);
157
+ }
158
+ // Validate region availability
159
+ const effectiveRegion = this.getEffectiveRegion(modelId);
160
+ this.log('debug', 'Generating image', {
161
+ model: modelId,
162
+ region: effectiveRegion,
163
+ hasReferenceImages: (0, base_tti_provider_1.hasReferenceImages)(request),
164
+ });
165
+ // Route to appropriate implementation with retry support
166
+ switch (modelId) {
167
+ case 'imagen-3':
168
+ return this.executeWithRetry(request, () => this.generateWithImagen(request, effectiveRegion), 'Imagen API call');
169
+ case 'gemini-flash-image':
170
+ return this.executeWithRetry(request, () => this.generateWithGemini(request, effectiveRegion), 'Gemini API call');
171
+ default:
172
+ throw new base_tti_provider_1.InvalidConfigError(this.providerName, `Unknown model: ${modelId}`);
173
+ }
174
+ }
175
+ // ============================================================
176
+ // PUBLIC HELPER METHODS
177
+ // ============================================================
178
+ /**
179
+ * Get the configured region
180
+ */
181
+ getRegion() {
182
+ return this.config.region;
183
+ }
184
+ /**
185
+ * Check if the configured region is hosted in the EU
186
+ */
187
+ isEURegion() {
188
+ return (0, base_tti_provider_1.isEURegion)(this.config.region);
189
+ }
190
+ // ============================================================
191
+ // PRIVATE: REGION HANDLING
192
+ // ============================================================
193
+ /**
194
+ * Get the effective region for a model, considering availability
195
+ */
196
+ getEffectiveRegion(modelId) {
197
+ const modelInfo = this.getModelInfo(modelId);
198
+ if (!modelInfo?.availableRegions) {
199
+ return this.config.region;
200
+ }
201
+ // Check if configured region supports this model
202
+ if (modelInfo.availableRegions.includes(this.config.region)) {
203
+ return this.config.region;
204
+ }
205
+ // Find best alternative (prefer EU regions)
206
+ const euAlternatives = modelInfo.availableRegions.filter(base_tti_provider_1.isEURegion);
207
+ if (euAlternatives.length > 0) {
208
+ const fallback = euAlternatives[0];
209
+ this.log('warn', `Model ${modelId} not available in ${this.config.region}, using ${fallback}`, { configuredRegion: this.config.region, fallbackRegion: fallback });
210
+ return fallback;
211
+ }
212
+ // No EU region available, use first available
213
+ const fallback = modelInfo.availableRegions[0];
214
+ this.log('warn', `Model ${modelId} not available in EU regions, using ${fallback}`, { configuredRegion: this.config.region, fallbackRegion: fallback });
215
+ return fallback;
216
+ }
217
+ // ============================================================
218
+ // PRIVATE: IMAGEN 3 IMPLEMENTATION
219
+ // ============================================================
220
+ async generateWithImagen(request, region) {
221
+ const startTime = Date.now();
222
+ const internalModelId = MODEL_ID_MAP['imagen-3'];
223
+ this.lastUsedRegion = region;
224
+ try {
225
+ const { client, helpers } = await this.getAiplatformClient();
226
+ const endpoint = `projects/${this.config.projectId}/locations/${region}/publishers/google/models/${internalModelId}`;
227
+ // Build instance
228
+ const instanceValue = { prompt: request.prompt };
229
+ const instance = helpers.toValue(instanceValue);
230
+ // Build parameters
231
+ const parameterValue = {
232
+ sampleCount: request.n || 1,
233
+ };
234
+ if (request.aspectRatio) {
235
+ parameterValue.aspectRatio = request.aspectRatio;
236
+ }
237
+ // Pass through provider-specific options
238
+ if (request.providerOptions) {
239
+ if (request.providerOptions.seed !== undefined) {
240
+ parameterValue.seed = request.providerOptions.seed;
241
+ }
242
+ if (request.providerOptions.safetyFilterLevel) {
243
+ parameterValue.safetyFilterLevel = request.providerOptions.safetyFilterLevel;
244
+ }
245
+ if (request.providerOptions.personGeneration) {
246
+ parameterValue.personGeneration = request.providerOptions.personGeneration;
247
+ }
248
+ }
249
+ const parameters = helpers.toValue(parameterValue);
250
+ this.log('debug', 'Sending Imagen request', { endpoint, parameters: parameterValue });
251
+ const [response] = await client.predict({
252
+ endpoint,
253
+ instances: [instance],
254
+ parameters,
255
+ });
256
+ const duration = Date.now() - startTime;
257
+ if (!response.predictions || response.predictions.length === 0) {
258
+ throw new base_tti_provider_1.GenerationFailedError(this.providerName, 'No images returned from Imagen API');
259
+ }
260
+ return this.processImagenResponse(response.predictions, helpers, duration);
261
+ }
262
+ catch (error) {
263
+ if (error instanceof base_tti_provider_1.InvalidConfigError || error instanceof base_tti_provider_1.GenerationFailedError) {
264
+ throw error;
265
+ }
266
+ throw this.handleError(error, 'during Imagen API call');
267
+ }
268
+ }
269
+ async getAiplatformClient() {
270
+ if (!this.aiplatformClient) {
271
+ try {
272
+ const { v1, helpers } = await Promise.resolve().then(() => __importStar(require('@google-cloud/aiplatform')));
273
+ const clientOptions = {
274
+ apiEndpoint: `${this.config.region}-aiplatform.googleapis.com`,
275
+ };
276
+ if (this.config.keyFilename) {
277
+ clientOptions.keyFilename = this.config.keyFilename;
278
+ }
279
+ else if (this.config.credentials) {
280
+ clientOptions.credentials = this.config.credentials;
281
+ }
282
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
283
+ this.aiplatformClient = new v1.PredictionServiceClient(clientOptions);
284
+ return {
285
+ client: this.aiplatformClient,
286
+ helpers: helpers,
287
+ };
288
+ }
289
+ catch (error) {
290
+ throw new base_tti_provider_1.InvalidConfigError(this.providerName, `Failed to load @google-cloud/aiplatform. Install it with: npm install @google-cloud/aiplatform`, error);
291
+ }
292
+ }
293
+ const { helpers } = await Promise.resolve().then(() => __importStar(require('@google-cloud/aiplatform')));
294
+ return {
295
+ client: this.aiplatformClient,
296
+ helpers: helpers,
297
+ };
298
+ }
299
+ processImagenResponse(predictions, helpers, duration) {
300
+ const images = [];
301
+ for (const prediction of predictions) {
302
+ const predictionObj = helpers.fromValue(prediction);
303
+ if (predictionObj?.bytesBase64Encoded) {
304
+ images.push({
305
+ base64: predictionObj.bytesBase64Encoded,
306
+ contentType: predictionObj.mimeType || 'image/png',
307
+ });
308
+ }
309
+ }
310
+ if (images.length === 0) {
311
+ throw new base_tti_provider_1.GenerationFailedError(this.providerName, 'No valid images in Imagen response');
312
+ }
313
+ const usage = {
314
+ imagesGenerated: images.length,
315
+ modelId: 'imagen-3',
316
+ };
317
+ return {
318
+ images,
319
+ metadata: {
320
+ provider: this.providerName,
321
+ model: 'imagen-3',
322
+ region: this.lastUsedRegion || this.config.region,
323
+ duration,
324
+ },
325
+ usage,
326
+ };
327
+ }
328
+ // ============================================================
329
+ // PRIVATE: GEMINI FLASH IMAGE IMPLEMENTATION
330
+ // ============================================================
331
+ async generateWithGemini(request, region) {
332
+ const startTime = Date.now();
333
+ const internalModelId = MODEL_ID_MAP['gemini-flash-image'];
334
+ this.lastUsedRegion = region;
335
+ try {
336
+ const client = await this.getGenaiClient(region);
337
+ // Build request parts
338
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
339
+ const parts = [];
340
+ // Add reference images first (for character consistency)
341
+ if ((0, base_tti_provider_1.hasReferenceImages)(request)) {
342
+ for (const ref of request.referenceImages) {
343
+ parts.push({
344
+ inlineData: {
345
+ mimeType: ref.mimeType || 'image/png',
346
+ data: ref.base64,
347
+ },
348
+ });
349
+ }
350
+ // Build character consistency prompt
351
+ const fullPrompt = this.buildCharacterConsistencyPrompt(request.prompt, request.subjectDescription, request.referenceImages.length);
352
+ parts.push({ text: fullPrompt });
353
+ }
354
+ else {
355
+ parts.push({ text: request.prompt });
356
+ }
357
+ const contents = [{ role: 'user', parts }];
358
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
359
+ const config = {
360
+ responseModalities: ['TEXT', 'IMAGE'],
361
+ };
362
+ // Add temperature if provided
363
+ if (request.providerOptions?.temperature !== undefined) {
364
+ config.temperature = request.providerOptions.temperature;
365
+ }
366
+ this.log('debug', 'Sending Gemini request', {
367
+ model: internalModelId,
368
+ region,
369
+ hasReferenceImages: (0, base_tti_provider_1.hasReferenceImages)(request),
370
+ });
371
+ const response = await client.generateContent({
372
+ model: internalModelId,
373
+ contents,
374
+ config,
375
+ });
376
+ const duration = Date.now() - startTime;
377
+ return this.processGeminiResponse(response, duration);
378
+ }
379
+ catch (error) {
380
+ if (error instanceof base_tti_provider_1.InvalidConfigError || error instanceof base_tti_provider_1.GenerationFailedError) {
381
+ throw error;
382
+ }
383
+ throw this.handleError(error, 'during Gemini API call');
384
+ }
385
+ }
386
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
387
+ async getGenaiClient(region) {
388
+ // Recreate client if region changed
389
+ if (!this.genaiClient) {
390
+ try {
391
+ const { GoogleGenAI } = await Promise.resolve().then(() => __importStar(require('@google/genai')));
392
+ // Set environment variables for Vertex AI backend
393
+ process.env.GOOGLE_GENAI_USE_VERTEXAI = 'true';
394
+ process.env.GOOGLE_CLOUD_PROJECT = this.config.projectId;
395
+ process.env.GOOGLE_CLOUD_LOCATION = region;
396
+ this.genaiClient = new GoogleGenAI({
397
+ vertexai: true,
398
+ project: this.config.projectId,
399
+ location: region,
400
+ });
401
+ this.log('debug', 'Initialized @google/genai with Vertex AI backend', {
402
+ project: this.config.projectId,
403
+ location: region,
404
+ });
405
+ }
406
+ catch (error) {
407
+ throw new base_tti_provider_1.InvalidConfigError(this.providerName, `Failed to load @google/genai. Install it with: npm install @google/genai`, error);
408
+ }
409
+ }
410
+ return {
411
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
412
+ generateContent: async (params) => {
413
+ return this.genaiClient.models.generateContent(params);
414
+ },
415
+ };
416
+ }
417
+ buildCharacterConsistencyPrompt(userPrompt, subjectDescription, referenceCount) {
418
+ const referenceText = referenceCount === 1 ? 'the reference image' : `the ${referenceCount} reference images`;
419
+ return `Using ${referenceText} as a reference for the character "${subjectDescription}", generate a new image where: ${userPrompt}
420
+
421
+ IMPORTANT: Maintain exact visual consistency with the character in the reference - same style, colors, proportions, and distinctive features. The character should be immediately recognizable as the same one from the reference.`;
422
+ }
423
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
424
+ processGeminiResponse(response, duration) {
425
+ const images = [];
426
+ const candidates = response?.candidates || response?.response?.candidates;
427
+ if (!candidates || candidates.length === 0) {
428
+ throw new base_tti_provider_1.GenerationFailedError(this.providerName, 'No candidates returned from Gemini API');
429
+ }
430
+ for (const candidate of candidates) {
431
+ const parts = candidate?.content?.parts || [];
432
+ for (const part of parts) {
433
+ if (part.inlineData?.data) {
434
+ images.push({
435
+ base64: part.inlineData.data,
436
+ contentType: part.inlineData.mimeType || 'image/png',
437
+ });
438
+ }
439
+ }
440
+ }
441
+ if (images.length === 0) {
442
+ const firstParts = candidates[0]?.content?.parts || [];
443
+ const partTypes = firstParts.map((p) => {
444
+ if (p.text)
445
+ return `text(${p.text.substring(0, 50)}...)`;
446
+ if (p.inlineData)
447
+ return `inlineData(${p.inlineData.mimeType})`;
448
+ return 'unknown';
449
+ });
450
+ this.log('error', 'No images in Gemini response', {
451
+ candidateCount: candidates.length,
452
+ partTypes,
453
+ });
454
+ throw new base_tti_provider_1.GenerationFailedError(this.providerName, `No images in response. Model returned: ${partTypes.join(', ')}. ` +
455
+ 'Make sure responseModalities includes IMAGE.');
456
+ }
457
+ const usage = {
458
+ imagesGenerated: images.length,
459
+ modelId: 'gemini-flash-image',
460
+ };
461
+ return {
462
+ images,
463
+ metadata: {
464
+ provider: this.providerName,
465
+ model: 'gemini-flash-image',
466
+ region: this.lastUsedRegion || this.config.region,
467
+ duration,
468
+ },
469
+ usage,
470
+ };
471
+ }
472
+ }
473
+ exports.GoogleCloudTTIProvider = GoogleCloudTTIProvider;
@@ -0,0 +1,4 @@
1
+ export * from './base-tti-provider';
2
+ export * from './google-cloud-provider';
3
+ export * from './edenai-provider';
4
+ export * from './ionos-provider';
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ // Base provider, errors, and logging
18
+ __exportStar(require("./base-tti-provider"), exports);
19
+ // Provider implementations
20
+ __exportStar(require("./google-cloud-provider"), exports);
21
+ __exportStar(require("./edenai-provider"), exports);
22
+ __exportStar(require("./ionos-provider"), exports);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * IONOS TTI Provider
3
+ *
4
+ * IONOS Cloud AI service with OpenAI-compatible API.
5
+ *
6
+ * @see https://cloud.ionos.de/
7
+ */
8
+ import { TTIRequest, TTIResponse, ModelInfo } from '../../../types';
9
+ import { BaseTTIProvider } from './base-tti-provider';
10
+ interface IonosConfig {
11
+ apiKey: string;
12
+ apiUrl?: string;
13
+ }
14
+ export declare class IonosProvider extends BaseTTIProvider {
15
+ private config;
16
+ private readonly apiUrl;
17
+ constructor(config?: Partial<IonosConfig>);
18
+ getDisplayName(): string;
19
+ listModels(): ModelInfo[];
20
+ getDefaultModel(): string;
21
+ generate(request: TTIRequest): Promise<TTIResponse>;
22
+ private executeGeneration;
23
+ private aspectRatioToSize;
24
+ private processResponse;
25
+ }
26
+ export {};
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /**
3
+ * IONOS TTI Provider
4
+ *
5
+ * IONOS Cloud AI service with OpenAI-compatible API.
6
+ *
7
+ * @see https://cloud.ionos.de/
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.IonosProvider = void 0;
11
+ const types_1 = require("../../../types");
12
+ const base_tti_provider_1 = require("./base-tti-provider");
13
+ // ============================================================
14
+ // MODEL DEFINITIONS
15
+ // ============================================================
16
+ const IONOS_MODELS = [
17
+ {
18
+ id: 'default',
19
+ displayName: 'IONOS Image Generation',
20
+ capabilities: {
21
+ textToImage: true,
22
+ characterConsistency: false,
23
+ imageEditing: false,
24
+ maxImagesPerRequest: 4,
25
+ },
26
+ pricingUrl: 'https://cloud.ionos.de/preise',
27
+ },
28
+ ];
29
+ // ============================================================
30
+ // PROVIDER IMPLEMENTATION
31
+ // ============================================================
32
+ class IonosProvider extends base_tti_provider_1.BaseTTIProvider {
33
+ constructor(config) {
34
+ super(types_1.TTIProvider.IONOS);
35
+ this.config = {
36
+ apiKey: config?.apiKey || process.env.IONOS_API_KEY || '',
37
+ apiUrl: config?.apiUrl || process.env.IONOS_API_URL,
38
+ };
39
+ const baseUrl = this.config.apiUrl || 'https://api.ionos.cloud/ai/v1';
40
+ this.apiUrl = `${baseUrl.replace(/\/+$/, '')}/images/generations`;
41
+ if (!this.config.apiKey) {
42
+ throw new base_tti_provider_1.InvalidConfigError(this.providerName, 'IONOS API key is required (IONOS_API_KEY)');
43
+ }
44
+ this.log('info', 'IONOS Provider initialized');
45
+ }
46
+ // ============================================================
47
+ // ITTIProvider IMPLEMENTATION
48
+ // ============================================================
49
+ getDisplayName() {
50
+ return 'IONOS Cloud';
51
+ }
52
+ listModels() {
53
+ return IONOS_MODELS;
54
+ }
55
+ getDefaultModel() {
56
+ return 'default';
57
+ }
58
+ async generate(request) {
59
+ this.validateRequest(request);
60
+ return this.executeWithRetry(request, () => this.executeGeneration(request), 'IONOS API call');
61
+ }
62
+ async executeGeneration(request) {
63
+ const startTime = Date.now();
64
+ const body = {
65
+ prompt: request.prompt,
66
+ n: request.n || 1,
67
+ size: request.aspectRatio ? this.aspectRatioToSize(request.aspectRatio) : '1024x1024',
68
+ response_format: 'url',
69
+ model: request.model !== 'default' ? request.model : undefined,
70
+ };
71
+ this.log('debug', 'Generating image with IONOS', {
72
+ size: body.size,
73
+ model: body.model,
74
+ });
75
+ try {
76
+ const response = await fetch(this.apiUrl, {
77
+ method: 'POST',
78
+ headers: {
79
+ Authorization: `Bearer ${this.config.apiKey}`,
80
+ 'Content-Type': 'application/json',
81
+ },
82
+ body: JSON.stringify(body),
83
+ });
84
+ if (!response.ok) {
85
+ const errorText = await response.text();
86
+ throw new Error(`IONOS API error (${response.status}): ${errorText}`);
87
+ }
88
+ const data = (await response.json());
89
+ const duration = Date.now() - startTime;
90
+ return this.processResponse(data, duration);
91
+ }
92
+ catch (error) {
93
+ throw this.handleError(error, 'during IONOS API call');
94
+ }
95
+ }
96
+ // ============================================================
97
+ // PRIVATE METHODS
98
+ // ============================================================
99
+ aspectRatioToSize(aspectRatio) {
100
+ const mapping = {
101
+ '1:1': '1024x1024',
102
+ '16:9': '1792x1024',
103
+ '9:16': '1024x1792',
104
+ '4:3': '1024x768',
105
+ '3:4': '768x1024',
106
+ };
107
+ return mapping[aspectRatio] || '1024x1024';
108
+ }
109
+ processResponse(data, duration) {
110
+ if (!data.data || data.data.length === 0) {
111
+ throw new base_tti_provider_1.GenerationFailedError(this.providerName, 'No images returned in response');
112
+ }
113
+ const images = data.data.map((item) => {
114
+ if (item.b64_json) {
115
+ return { base64: item.b64_json, contentType: 'image/png' };
116
+ }
117
+ return { url: item.url };
118
+ });
119
+ const usage = {
120
+ imagesGenerated: images.length,
121
+ modelId: 'default',
122
+ };
123
+ return {
124
+ images,
125
+ metadata: {
126
+ provider: this.providerName,
127
+ model: 'default',
128
+ duration,
129
+ },
130
+ usage,
131
+ };
132
+ }
133
+ }
134
+ exports.IonosProvider = IonosProvider;