Files
karate_halloween/lib/ai/orchestrator.ts
T

126 lines
4.4 KiB
TypeScript

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<OrchestrationResult> {
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(" | ")}`
);
}