'use server'; /** * @fileOverview Smart routing flow for automatically selecting the best image generation provider. * * This flow: * 1. Checks which API keys are valid/present (from client or env) * 2. Uses keyword matching to select the best provider for the prompt * 3. Falls back to available providers when some are unavailable * 4. Returns the selected provider along with available options * * NOTE: This version uses keyword matching only (no AI/Genkit) to support * client-only API key mode without requiring server-side env configuration. */ import { z } from 'zod'; import { type ImageProvider, PROVIDER_CONFIGS } from '@/lib/api-config'; import { getAvailableProviders } from '@/lib/api-validation'; // Shared provider enum for consistent typing const ProviderEnum = z.enum(['openai', 'google', 'qwen']); // Client API keys schema const ClientApiKeysSchema = z.object({ openai: z.string().optional(), google: z.string().optional(), qwen: z.string().optional(), }).optional(); // Input schema for the smart router const SmartRouterInputSchema = z.object({ prompt: z.string().describe('The user prompt for image generation.'), smartRoutingEnabled: z.boolean().describe('Whether smart routing is enabled.'), preferredProvider: ProviderEnum.optional().describe('User-preferred provider when smart routing is disabled.'), clientApiKeys: ClientApiKeysSchema.describe('API keys provided by user from client-side.'), }); export type SmartRouterInput = z.infer; // Output schema for the smart router const SmartRouterOutputSchema = z.object({ selectedProvider: ProviderEnum.describe('The selected image generation provider.'), availableProviders: z.array(ProviderEnum).describe('List of available providers.'), reason: z.string().describe('Reason for the selection.'), }); export type SmartRouterOutput = z.infer; /** * Check if a provider has a valid key from client */ function hasValidClientKey(provider: ImageProvider, clientKeys?: { openai?: string; google?: string; qwen?: string }): boolean { const clientKey = clientKeys?.[provider]; return !!(clientKey && clientKey.trim().length > 0); } /** * Keyword-based provider selection */ function selectProviderByKeywords( prompt: string, availableProviders: ('openai' | 'google' | 'qwen')[] ): { provider: 'openai' | 'google' | 'qwen'; reason: string } { const lowerPrompt = prompt.toLowerCase(); // Check for photorealistic keywords -> OpenAI const photorealisticKeywords = [ 'photo', 'photograph', 'realistic', 'professional', 'headshot', 'product', 'portrait', 'camera', 'hdr', 'stock photo', 'documentary' ]; if (availableProviders.includes('openai') && photorealisticKeywords.some(kw => lowerPrompt.includes(kw))) { return { provider: 'openai', reason: 'Detected photorealistic content - using OpenAI DALL-E for best results.', }; } // Check for artistic keywords -> Qwen const artisticKeywords = [ 'artistic', 'fantasy', 'abstract', 'stylized', 'watercolor', 'painting', 'illustration', 'anime', 'digital art', 'concept art', 'cartoon', 'manga', 'sketch', 'oil painting', 'impressionist', 'surreal', 'dreamy', 'magical', 'ethereal' ]; if (availableProviders.includes('qwen') && artisticKeywords.some(kw => lowerPrompt.includes(kw))) { return { provider: 'qwen', reason: 'Detected artistic/creative content - using Qwen for stylized results.', }; } // Default to Google for general purpose if (availableProviders.includes('google')) { return { provider: 'google', reason: 'Using Google Gemini for general purpose image generation.', }; } // Final fallback - use first available return { provider: availableProviders[0], reason: `Using ${PROVIDER_CONFIGS[availableProviders[0]].displayName} as default.`, }; } /** * Smart router for selecting the best image generation provider. * Uses keyword matching for selection (no external AI calls). */ export async function smartSelectProvider(input: SmartRouterInput): Promise { // 1. Get available providers from environment const envAvailableProviders = await getAvailableProviders(); // 2. Check client-provided keys const clientKeys = input.clientApiKeys; const allProviders: ImageProvider[] = ['openai', 'google', 'qwen']; // Combine: provider is available if it has env key OR valid client key const availableProviders = allProviders.filter(provider => envAvailableProviders.includes(provider) || hasValidClientKey(provider, clientKeys) ); // 3. Handle case where no providers are available if (availableProviders.length === 0) { return { selectedProvider: 'google', // Default, but won't work availableProviders: [], reason: 'No API keys are configured. Please add at least one API key in Settings.', }; } // Cast to the enum type for Zod compatibility const typedAvailableProviders = availableProviders as ('openai' | 'google' | 'qwen')[]; // 4. If only one provider is available, use it if (typedAvailableProviders.length === 1) { const provider = typedAvailableProviders[0]; return { selectedProvider: provider, availableProviders: typedAvailableProviders, reason: `Only ${PROVIDER_CONFIGS[provider].displayName} is available.`, }; } // 5. If smart routing is disabled and user has a preference if (!input.smartRoutingEnabled && input.preferredProvider) { // Check if preferred provider is available if (typedAvailableProviders.includes(input.preferredProvider)) { return { selectedProvider: input.preferredProvider, availableProviders: typedAvailableProviders, reason: `User selected ${PROVIDER_CONFIGS[input.preferredProvider].displayName}.`, }; } else { // Preferred provider not available, fall back to first available const fallback = typedAvailableProviders[0]; const preferredConfig = input.preferredProvider ? PROVIDER_CONFIGS[input.preferredProvider] : null; const preferredDisplayName = preferredConfig?.displayName || input.preferredProvider || 'Selected provider'; return { selectedProvider: fallback, availableProviders: typedAvailableProviders, reason: `${preferredDisplayName} is not available. Falling back to ${PROVIDER_CONFIGS[fallback].displayName}.`, }; } } // 6. Smart routing enabled - use keyword matching to select appropriate provider const selection = selectProviderByKeywords(input.prompt, typedAvailableProviders); return { selectedProvider: selection.provider, availableProviders: typedAvailableProviders, reason: selection.reason, }; }