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 (OpenRouter) if (isProviderAvailable("openrouter") && hasOpenRouterKey) { try { console.log(`[AI Orchestrator] Attempting Primary (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, }); resetProvider("openrouter"); return { imageUrl: result.imageUrl, providerUsed: "openrouter", modelUsed: result.model, durationMs: Date.now() - startTime, fallbackTriggered: false, }; } catch (primaryError: any) { if (isContentSafetyError(primaryError)) { console.warn(`[AI Orchestrator] Primary provider (OpenRouter) rejected image for safety:`, primaryError.message); throw new ContentSafetyError(primaryError.message); } console.warn(`[AI Orchestrator] Primary provider (OpenRouter) failed:`, primaryError.message); warnings.push(`Primary (OpenRouter) failed: ${primaryError.message}`); } } else { warnings.push("Primary provider key (OpenRouter) missing or in cooldown. Skipping to fallback."); } // 2. Try Fallback Provider (Google Gemini) if (isProviderAvailable("gemini") && hasGeminiKey) { try { console.log(`[AI Orchestrator] Attempting Fallback (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: true, warnings, }; } catch (fallbackError: any) { if (isContentSafetyError(fallbackError)) { console.warn(`[AI Orchestrator] Fallback provider (Gemini) rejected image for safety:`, fallbackError.message); throw new ContentSafetyError(fallbackError.message); } console.error(`[AI Orchestrator] Fallback provider (Gemini) failed:`, fallbackError.message); warnings.push(`Fallback (Gemini) failed: ${fallbackError.message}`); } } // If both providers failed or were unavailable, throw descriptive error throw new Error( `AI Generation failed on all available providers. Details: ${warnings.join(" | ")}` ); }