Spaces:
Runtime error
Runtime error
| '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<typeof SmartRouterInputSchema>; | |
| // 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<typeof SmartRouterOutputSchema>; | |
| /** | |
| * 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<SmartRouterOutput> { | |
| // 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, | |
| }; | |
| } | |