import { BoothConfig, BoothStyle } from "@/types/booth"; import { generateWithGemini } from "./gemini"; import { generateWithOpenRouter } from "./openrouter"; import { isProviderAvailable, triggerCooldown, resetProvider, } from "./circuit-breaker"; import { ContentSafetyError, isContentSafetyError } from "./safety"; export interface OrchestrationRequest { config: BoothConfig; style: BoothStyle; imageBase64: string; mimeType?: string; } export interface OrchestrationResult { imageUrl: string; providerUsed: "gemini" | "openrouter" | "demo_mock"; modelUsed: string; durationMs: number; fallbackTriggered: boolean; warnings?: string[]; } export async function orchestrateGeneration( request: OrchestrationRequest ): Promise { const startTime = Date.now(); const { config, style, imageBase64 } = request; const mimeType = request.mimeType || "image/jpeg"; const warnings: string[] = []; const hasGeminiKey = Boolean(process.env.GEMINI_API_KEY); const hasOpenRouterKey = Boolean(process.env.OPENROUTER_API_KEY); // Demo / Offline Mode fallback when no API keys are provided yet if (!hasGeminiKey && !hasOpenRouterKey) { console.log("[AI Orchestrator] No API keys configured. Using Demo Mode sample image."); // Simulate generation delay (2 seconds) await new Promise((r) => setTimeout(r, 2000)); return { imageUrl: style.sampleResult || style.thumbnail || "/assets/styles/sample_clay_pink.png", providerUsed: "demo_mock", modelUsed: "demo-simulator", durationMs: Date.now() - startTime, fallbackTriggered: false, warnings: ["Demo Mode active: Set GEMINI_API_KEY or OPENROUTER_API_KEY in .env.local to enable live AI generation."], }; } // 1. Try Primary Provider (Google Gemini) if (isProviderAvailable("gemini") && hasGeminiKey) { try { console.log(`[AI Orchestrator] Attempting Primary (Gemini: ${config.ai.primary.model})...`); const result = await generateWithGemini({ model: config.ai.primary.model, prompt: style.prompt, negativePrompt: style.negativePrompt, imageBase64, mimeType, timeoutMs: config.ai.primary.timeoutMs, enableSfwCheck: config.ai.enableSfwCheck, }); resetProvider("gemini"); return { imageUrl: result.imageUrl, providerUsed: "gemini", modelUsed: result.model, durationMs: Date.now() - startTime, fallbackTriggered: false, }; } catch (primaryError: any) { // Content safety rejections are caused by user input, NOT provider downtime if (isContentSafetyError(primaryError)) { console.warn(`[AI Orchestrator] Primary provider (Gemini) rejected image for safety:`, primaryError.message); throw new ContentSafetyError(primaryError.message); } console.warn(`[AI Orchestrator] Primary provider (Gemini) failed:`, primaryError.message); warnings.push(`Primary failed: ${primaryError.message}`); } } else { warnings.push("Primary provider key missing. Skipping to backup."); } // 2. Try Backup Provider (OpenRouter) if (isProviderAvailable("openrouter") && hasOpenRouterKey) { try { console.log(`[AI Orchestrator] Attempting Backup (OpenRouter: ${config.ai.backup.model})...`); const result = await generateWithOpenRouter({ model: config.ai.backup.model, prompt: style.prompt, negativePrompt: style.negativePrompt, imageBase64, mimeType, timeoutMs: config.ai.backup.timeoutMs, }); return { imageUrl: result.imageUrl, providerUsed: "openrouter", modelUsed: result.model, durationMs: Date.now() - startTime, fallbackTriggered: true, warnings, }; } catch (backupError: any) { if (isContentSafetyError(backupError)) { console.warn(`[AI Orchestrator] Backup provider (OpenRouter) rejected image for safety:`, backupError.message); throw new ContentSafetyError(backupError.message); } console.error(`[AI Orchestrator] Backup provider (OpenRouter) failed:`, backupError.message); warnings.push(`Backup failed: ${backupError.message}`); } } // If both providers failed or were unavailable, throw descriptive error throw new Error( `AI Generation failed on all available providers. Details: ${warnings.join(" | ")}` ); }