Provider-agnostic Text-to-Image middleware with GDPR compliance and character consistency support. Currently supports Google Cloud (Imagen 3, Imagen 4, Gemini Flash Image, Gemini 3 Pro Image), Eden AI, and IONOS. Features EU data residency via Vertex AI, automatic region fallback, retry logic, and comprehensive error handling.
Table of Contents
- Multi-Provider Architecture: Unified API for all TTI providers
- Google Cloud (Recommended): Imagen 3, Imagen 4, Gemini Flash Image & Gemini 3 Pro Image with EU data residency
- Eden AI: Aggregator with access to OpenAI, Stability AI, Replicate (experimental)
- IONOS: German cloud provider with OpenAI-compatible API (experimental)
- Character Consistency: Generate consistent characters across multiple images (perfect for children's book illustrations)
- GDPR/DSGVO Compliance: Built-in EU region support with automatic fallback
- Region Rotation: Opt-in region rotation on quota errors (429) for Google Cloud — rotate through regions instead of retrying the same exhausted region
- Retry Logic: Exponential backoff with jitter for transient errors (429, 408, 5xx, timeouts)
- TypeScript First: Full type safety with comprehensive interfaces
- Logging Control: Configurable log levels via environment or API
- Debug Logging: Markdown file logging for debugging prompts and responses
- Error Handling: Typed error classes for precise error handling
Install from npm:
npm install @loonylabs/tti-middleware
# For Google Cloud provider (recommended):
npm install @google-cloud/aiplatform @google/genaiOr install directly from GitHub:
npm install github:loonylabs-dev/tti-middlewareimport { TTIService, GoogleCloudTTIProvider, TTIProvider } from '@loonylabs/tti-middleware';
// Create service and register provider
const service = new TTIService();
service.registerProvider(new GoogleCloudTTIProvider({
projectId: process.env.GOOGLE_CLOUD_PROJECT,
region: 'europe-west4', // EU region for GDPR
}));
// Generate an image
const result = await service.generate({
prompt: 'A futuristic city with flying cars, cyberpunk style',
model: 'imagen-3',
});
console.log('Image generated:', result.images[0].base64?.substring(0, 50) + '...');
console.log('Duration:', result.metadata.duration, 'ms');Using Character Consistency
Generate consistent characters across multiple images:
// 1. Create the initial character
const character = await service.generate({
prompt: 'A cute cartoon bear with a red hat and blue scarf, watercolor style',
model: 'gemini-flash-image', // Only this model supports character consistency!
});
// 2. Generate new scenes with the same character (Structured Mode)
const scene = await service.generate({
prompt: 'dancing happily in the rain, jumping in puddles',
model: 'gemini-flash-image',
referenceImages: [{
base64: character.images[0].base64!,
mimeType: 'image/png',
}],
subjectDescription: 'cute cartoon bear with red hat and blue scarf',
});
// 3. Or use Index-Based Mode for multiple characters
const multiCharScene = await service.generate({
prompt: 'The FIRST reference image character meets the SECOND reference image character',
model: 'gemini-flash-image',
referenceImages: [
{ base64: character1.images[0].base64!, mimeType: 'image/png' },
{ base64: character2.images[0].base64!, mimeType: 'image/png' },
],
// subjectDescription omitted = Index-Based Mode
});Important: Character consistency is only supported by gemini-flash-image model!
Switching Providers
// Use Google Cloud (recommended for EU)
const googleResult = await service.generate({
prompt: 'A mountain landscape',
model: 'imagen-3',
}, TTIProvider.GOOGLE_CLOUD);
// Use Eden AI (experimental)
const edenResult = await service.generate({
prompt: 'A mountain landscape',
model: 'openai', // Uses DALL-E via Eden AI
}, TTIProvider.EDENAI);
// Use IONOS (experimental)
const ionosResult = await service.generate({
prompt: 'A mountain landscape',
}, TTIProvider.IONOS);Required Dependencies
- Node.js 18+
- TypeScript 5.3+
- Google Cloud SDK (optional, for Google Cloud provider)
For Google Cloud provider:
npm install @google-cloud/aiplatform @google/genaiEnvironment Setup
Create a .env file in your project root:
# Default provider
TTI_DEFAULT_PROVIDER=google-cloud
# Logging level (debug, info, warn, error, silent)
TTI_LOG_LEVEL=info
# Google Cloud (recommended for EU/GDPR)
GOOGLE_CLOUD_PROJECT=your-project-id
GOOGLE_APPLICATION_CREDENTIALS=./service-account.json
GOOGLE_CLOUD_REGION=europe-west4 # Recommended for Gemini
# Eden AI (experimental)
EDENAI_API_KEY=your-api-key
# IONOS (experimental)
IONOS_API_KEY=your-api-key
IONOS_API_URL=https://api.ionos.cloud/ai/v1| Model | ID | Character Consistency | EU Regions | Max Images |
|---|---|---|---|---|
| Imagen 3 | imagen-3 |
No | All EU regions | 4 |
| Imagen 4 | imagen-4 |
No | All EU regions | 4 |
| Imagen 4 Fast | imagen-4-fast |
No | All EU regions | 4 |
| Imagen 4 Ultra | imagen-4-ultra |
No | All EU regions | 4 |
| Gemini Flash Image | gemini-flash-image |
Yes | europe-west1, europe-west4, europe-north1 | 1 |
| Gemini 3 Pro Image | gemini-pro-image |
No | global endpoint (auto-routed) |
1 |
Important:
gemini-flash-imageis NOT available ineurope-west3(Frankfurt) - auto-fallback to EU alternativegemini-pro-imagerequires theglobalVertex AI endpoint - the middleware handles this automatically
| Model | ID | Notes |
|---|---|---|
| OpenAI DALL-E | openai |
Via Eden AI aggregator |
| Stability AI | stabilityai |
Via Eden AI aggregator |
| Replicate | replicate |
Via Eden AI aggregator |
| Model | ID | Notes |
|---|---|---|
| Default | default |
OpenAI-compatible API |
| Region | Location | Imagen 3 | Imagen 4 | Gemini Flash Image | Gemini Pro Image |
|---|---|---|---|---|---|
europe-west1 |
Belgium | Yes | Yes | Yes | global endpoint |
europe-west3 |
Frankfurt | Yes | Yes | No | global endpoint |
europe-west4 |
Netherlands | Yes | Yes | Yes (Recommended) | global endpoint |
europe-north1 |
Finland | Yes | Yes | Yes | global endpoint |
europe-west9 |
Paris | Yes | Yes | No | global endpoint |
Generate consistent characters across multiple images - perfect for children's book illustrations.
Best for scenes with a single consistent character:
// Step 1: Create a character
const bear = await service.generate({
prompt: 'A cute cartoon bear with a red hat, watercolor style',
model: 'gemini-flash-image',
});
// Step 2: Use in different scenes
const scenes = ['playing in the park', 'reading a book', 'eating honey'];
for (const scene of scenes) {
const result = await service.generate({
prompt: scene,
model: 'gemini-flash-image',
referenceImages: [{ base64: bear.images[0].base64!, mimeType: 'image/png' }],
subjectDescription: 'cute cartoon bear with red hat', // Required in structured mode
});
// Save result...
}Best for scenes with multiple distinct characters. Reference images directly in your prompt by their position:
// Load two different character references
const cowboy1 = await loadImage('cowboy1.png');
const cowboy2 = await loadImage('cowboy2.png');
// Reference each image by index in the prompt
const duelScene = await service.generate({
prompt: `Generate a cinematic wide shot of a western duel.
- The character on the LEFT should look exactly like the person in the FIRST reference image.
- The character on the RIGHT should look exactly like the person in the SECOND reference image.
They are standing in a dusty street at high noon.`,
model: 'gemini-flash-image',
referenceImages: [
{ base64: cowboy1, mimeType: 'image/png' },
{ base64: cowboy2, mimeType: 'image/png' },
],
// subjectDescription intentionally omitted for index-based mode
aspectRatio: '16:9',
});Reference keywords: Use "FIRST reference image", "SECOND reference image" or "Image 1", "Image 2" etc.
| Mode | subjectDescription |
Use Case |
|---|---|---|
| Structured | Required | Single character across scenes |
| Index-Based | Omitted | Multiple characters in one scene |
- Model must be
gemini-flash-image(only model supporting character consistency)
| Provider | DPA | GDPR | EU Data Residency | Document |
|---|---|---|---|---|
| Google Cloud | Yes | Yes | Yes | CDPA |
| Eden AI | Yes | Depends* | Depends* | Privacy Policy |
| IONOS | Yes | Yes | Yes | AGB |
*Eden AI is an aggregator - compliance depends on the underlying provider.
- Customer data is NOT used for training AI models
- Data stays in configured region (e.g.,
europe-west4) - Zero data retention option available
- Vertex AI Privacy Whitepaper
Checking EU Region Status
import { GoogleCloudTTIProvider } from '@loonylabs/tti-middleware';
const provider = new GoogleCloudTTIProvider({
projectId: 'my-project',
region: 'europe-west4',
});
console.log('Is EU region:', provider.isEURegion()); // true
console.log('Current region:', provider.getRegion()); // 'europe-west4'class TTIService {
registerProvider(provider: BaseTTIProvider): void;
generate(request: TTIRequest, provider?: TTIProvider): Promise<TTIResponse>;
getProvider(name: TTIProvider): BaseTTIProvider | undefined;
listAllModels(): Array<{ provider: TTIProvider; models: ModelInfo[] }>;
}interface TTIRequest {
prompt: string;
model?: string; // 'imagen-3', 'imagen-4', 'gemini-flash-image', 'gemini-pro-image', etc.
n?: number; // Number of images (default: 1)
aspectRatio?: string; // '1:1', '16:9', '4:3', etc.
// Character consistency
referenceImages?: TTIReferenceImage[];
subjectDescription?: string;
// Retry configuration
retry?: boolean | RetryOptions; // true (default), false, or custom config
providerOptions?: Record<string, unknown>;
}interface TTIResponse {
images: TTIImage[];
metadata: {
provider: string;
model: string;
region?: string;
duration: number;
};
usage: {
imagesGenerated: number;
modelId: string;
};
billing?: { // Only if provider returns costs
cost: number;
currency: string;
source: 'provider' | 'estimated';
};
}Region Rotation (Google Cloud)
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:
const provider = new GoogleCloudTTIProvider({
projectId: 'my-project',
region: 'europe-west4',
regionRotation: {
regions: ['europe-west4', 'europe-west1', 'europe-north1', 'europe-central2'],
fallback: 'global',
alwaysTryFallback: true, // Default: one bonus attempt on fallback after budget exhausted
},
});Key behavior:
maxRetriesis the total budget across all regions (no multiplier)- Only quota errors (429, Resource Exhausted) trigger rotation — server errors (500, 503) retry the same region
alwaysTryFallback: true(default): one bonus attempt on fallback even if retry budget is exhausted- Without
regionRotation: existing behavior unchanged
Retry Configuration
Automatic retry with exponential backoff and jitter for transient errors (429, 408, 5xx, network timeouts). Follows Google Cloud best practices.
// Default: 3 retries, exponential backoff (1s → 2s → 4s), jitter enabled
const result = await service.generate({
prompt: 'A sunset over mountains',
model: 'imagen-3',
// retry: true (default)
});
// Custom retry configuration
const result = await service.generate({
prompt: 'A sunset over mountains',
model: 'imagen-3',
retry: {
maxRetries: 5,
delayMs: 1000,
backoffMultiplier: 2.0, // 1s, 2s, 4s, 8s, 16s
maxDelayMs: 30000, // Cap at 30s
jitter: true, // Randomize to prevent thundering herd
},
});
// Disable retry
const result = await service.generate({
prompt: 'A sunset over mountains',
model: 'imagen-3',
retry: false,
});Retryable errors: 429, 408, 500, 502, 503, 504, timeouts, ECONNRESET, ECONNREFUSED, socket hang up Not retried: 400, 401, 403, and other client errors
| Option | Default | Description |
|---|---|---|
maxRetries |
3 | Maximum retry attempts |
delayMs |
1000 | Base delay between retries (ms) |
backoffMultiplier |
2.0 | Exponential multiplier per attempt |
maxDelayMs |
30000 | Maximum delay cap (ms) |
jitter |
true | Randomize delay to prevent thundering herd |
Logging Configuration
Control logging via environment variable or API:
import { setLogLevel } from '@loonylabs/tti-middleware';
// Set log level programmatically
setLogLevel('warn'); // Only show warnings and errors
// Or via environment variable
// TTI_LOG_LEVEL=errorAvailable levels: debug, info, warn, error, silent
Debug Logging (Markdown Files)
Log all TTI requests and responses to markdown files for debugging:
import { TTIDebugger } from '@loonylabs/tti-middleware';
// Enable via environment variable
// DEBUG_TTI_REQUESTS=true
// Or programmatically
TTIDebugger.setEnabled(true);
TTIDebugger.setLogsDir('./logs/tti/requests');
// Configure all options at once
TTIDebugger.configure({
enabled: true,
logsDir: './logs/tti/requests',
consoleLog: true, // Also log to console
includeBase64: false, // Exclude base64 data (default)
});Log file contents:
- Provider, model, and region
- Full prompt text
- Subject description (for character consistency)
- Reference image metadata
- Response data (duration, image count)
- Errors with full details
Use case: Debug why character consistency isn't working by inspecting exactly what prompt and subjectDescription are being sent to the API.
Error Handling
Typed error classes for precise error handling:
import {
TTIError,
InvalidConfigError,
QuotaExceededError,
ProviderUnavailableError,
GenerationFailedError,
NetworkError,
CapabilityNotSupportedError,
} from '@loonylabs/tti-middleware';
try {
const result = await service.generate({ prompt: 'test' });
} catch (error) {
if (error instanceof QuotaExceededError) {
console.log('Rate limit hit, try again later');
} else if (error instanceof CapabilityNotSupportedError) {
console.log('Model does not support this feature');
} else if (error instanceof TTIError) {
console.log(`TTI Error [${error.code}]: ${error.message}`);
}
}# Run all tests
npm test
# Unit tests only (123 tests, >95% coverage)
npm run test:unit
# Unit tests with watch mode
npm run test:unit:watch
# Unit tests with coverage report
npm run test:unit:coverage
# Integration tests (requires TTI_INTEGRATION_TESTS=true)
npm run test:integration
# CI/CD mode (unit tests only, in band)
npm run test:ci
# Manual test scripts
npm run test:manual:google-cloudIntegration tests make real API calls. They are skipped by default.
# Enable and run integration tests
TTI_INTEGRATION_TESTS=true npm run test:integrationPrerequisites:
GOOGLE_CLOUD_PROJECTenvironment variableGOOGLE_APPLICATION_CREDENTIALSpointing to service account JSON
- Getting Started - Detailed setup guide
- Google Cloud Provider - Imagen 3 & Gemini Flash Image
- GDPR/Compliance - Data processing agreements
- Testing Guide - Unit & integration tests
- CHANGELOG - Release notes
We welcome contributions! Please ensure:
-
Tests: Add tests for new features
-
Linting: Run
npm run lintbefore committing -
Conventions: Follow the existing project structure
-
Fork the repository
-
Create your feature branch (
git checkout -b feature/amazing-feature) -
Commit your changes (
git commit -m 'Add some amazing feature') -
Push to the branch (
git push origin feature/amazing-feature) -
Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.