'use client'; import React, { useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { useQuery } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from '@/components/ui/command'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Switch } from '@/components/ui/switch'; import { Loader2, Plus } from 'lucide-react'; import { toast } from 'sonner'; import { AdminService, type AdminModel } from '@/lib/api/services/admin'; const listFromString = (value: string): string[] => value .split(',') .map((item) => item.trim()) .filter((item) => item.length > 0); const listToString = (value: string[] | undefined | null): string => value && value.length > 0 ? value.join(', ') : ''; const FormSchema = z.object({ id: z.string().min(1, 'Model ID is required'), name: z.string().min(1, 'Name is required'), description: z.string().default(''), context_length: z.coerce.number().min(0).default(8192), modality: z.string().min(1, 'Modality is required'), input_modalities_raw: z.string().default(''), output_modalities_raw: z.string().default(''), tokenizer: z.string().default(''), instruct_type: z.string().default(''), canonical_slug: z.string().default(''), alias_ids_raw: z.string().default(''), upstream_provider_id: z.string().default(''), input_cost: z.coerce.number().min(0).default(0), output_cost: z.coerce.number().min(0).default(0), request_cost: z.coerce.number().min(0).default(0), image_cost: z.coerce.number().min(0).default(0), web_search_cost: z.coerce.number().min(0).default(0), internal_reasoning_cost: z.coerce.number().min(0).default(0), max_prompt_cost: z.coerce.number().min(0).default(0), max_completion_cost: z.coerce.number().min(0).default(0), max_cost: z.coerce.number().min(0).default(0), per_request_limits_raw: z.string().default(''), top_provider_context_length: z.coerce.number().min(0).optional(), top_provider_max_completion_tokens: z.coerce.number().min(0).optional(), top_provider_is_moderated: z.boolean().default(false), enabled: z.boolean().default(true), }); type FormData = z.output; export interface AddProviderModelDialogProps { providerId: number; isOpen: boolean; onClose: () => void; onSuccess: () => void; initialData?: AdminModel | null; mode?: 'create' | 'edit' | 'override'; } export function AddProviderModelDialog({ providerId, isOpen, onClose, onSuccess, initialData, mode = 'create', }: AddProviderModelDialogProps) { const [isSubmitting, setIsSubmitting] = useState(false); const [isPresetOpen, setIsPresetOpen] = useState(false); const [selectedPresetLabel, setSelectedPresetLabel] = useState('Select a preset'); const form = useForm({ resolver: zodResolver(FormSchema) as never, defaultValues: { id: '', name: '', description: '', context_length: 8192, modality: 'text', input_modalities_raw: 'text', output_modalities_raw: 'text', tokenizer: '', instruct_type: '', canonical_slug: '', alias_ids_raw: '', upstream_provider_id: '', input_cost: 0, output_cost: 0, request_cost: 0, image_cost: 0, web_search_cost: 0, internal_reasoning_cost: 0, max_prompt_cost: 0, max_completion_cost: 0, max_cost: 0, per_request_limits_raw: '', top_provider_context_length: undefined, top_provider_max_completion_tokens: undefined, top_provider_is_moderated: false, enabled: true, }, }); const isOverride = useMemo(() => mode === 'override', [mode]); const isEdit = useMemo(() => mode === 'edit', [mode]); const { data: presets = [], isLoading: isLoadingPresets } = useQuery({ queryKey: ['openrouter-presets'], queryFn: () => AdminService.getOpenRouterPresets(), staleTime: 10 * 60 * 1000, refetchOnWindowFocus: false, }); useEffect(() => { if (initialData) { const architecture = initialData.architecture as Record; const pricing = initialData.pricing as Record; const topProvider = initialData.top_provider as Record< string, unknown > | null; form.reset({ id: initialData.id, name: initialData.name, description: initialData.description, context_length: initialData.context_length, modality: typeof architecture?.modality === 'string' ? architecture.modality : 'text', input_modalities_raw: listToString( (architecture?.input_modalities as string[]) || [] ), output_modalities_raw: listToString( (architecture?.output_modalities as string[]) || [] ), tokenizer: typeof architecture?.tokenizer === 'string' ? architecture.tokenizer : '', instruct_type: typeof architecture?.instruct_type === 'string' ? architecture.instruct_type : '', canonical_slug: initialData.canonical_slug || '', alias_ids_raw: listToString(initialData.alias_ids), upstream_provider_id: typeof initialData.upstream_provider_id === 'string' ? initialData.upstream_provider_id : initialData.upstream_provider_id?.toString() || '', input_cost: pricing?.prompt ?? 0, output_cost: pricing?.completion ?? 0, request_cost: pricing?.request ?? 0, image_cost: pricing?.image ?? 0, web_search_cost: pricing?.web_search ?? 0, internal_reasoning_cost: pricing?.internal_reasoning ?? 0, max_prompt_cost: pricing?.max_prompt_cost ?? 0, max_completion_cost: pricing?.max_completion_cost ?? 0, max_cost: pricing?.max_cost ?? 0, per_request_limits_raw: initialData.per_request_limits ? JSON.stringify(initialData.per_request_limits, null, 2) : '', top_provider_context_length: typeof topProvider?.context_length === 'number' ? topProvider.context_length : undefined, top_provider_max_completion_tokens: typeof topProvider?.max_completion_tokens === 'number' ? topProvider.max_completion_tokens : undefined, top_provider_is_moderated: typeof topProvider?.is_moderated === 'boolean' ? topProvider.is_moderated : false, enabled: initialData.enabled, }); } else { form.reset({ id: '', name: '', description: '', context_length: 8192, modality: 'text', input_modalities_raw: 'text', output_modalities_raw: 'text', tokenizer: '', instruct_type: '', canonical_slug: '', alias_ids_raw: '', upstream_provider_id: '', input_cost: 0, output_cost: 0, request_cost: 0, image_cost: 0, web_search_cost: 0, internal_reasoning_cost: 0, max_prompt_cost: 0, max_completion_cost: 0, max_cost: 0, per_request_limits_raw: '', top_provider_context_length: undefined, top_provider_max_completion_tokens: undefined, top_provider_is_moderated: false, enabled: true, }); } }, [initialData, form, isOpen]); const applyModelToForm = (model: AdminModel) => { setSelectedPresetLabel(`${model.id} — ${model.name}`); const architecture = model.architecture as Record; const pricing = model.pricing as Record; const topProvider = model.top_provider as Record | null; if (!isOverride) { form.setValue('id', model.id); } form.setValue('name', model.name); form.setValue('description', model.description || ''); form.setValue('context_length', model.context_length); form.setValue( 'modality', typeof architecture?.modality === 'string' ? architecture.modality : 'text' ); form.setValue( 'input_modalities_raw', listToString((architecture?.input_modalities as string[]) || []) ); form.setValue( 'output_modalities_raw', listToString((architecture?.output_modalities as string[]) || []) ); form.setValue( 'tokenizer', typeof architecture?.tokenizer === 'string' ? architecture.tokenizer : '' ); form.setValue( 'instruct_type', typeof architecture?.instruct_type === 'string' ? architecture.instruct_type : '' ); form.setValue('canonical_slug', model.canonical_slug || ''); form.setValue('alias_ids_raw', listToString(model.alias_ids)); form.setValue( 'upstream_provider_id', typeof model.upstream_provider_id === 'string' ? model.upstream_provider_id : model.upstream_provider_id?.toString() || '' ); form.setValue('input_cost', pricing?.prompt ?? 0); form.setValue('output_cost', pricing?.completion ?? 0); form.setValue('request_cost', pricing?.request ?? 0); form.setValue('image_cost', pricing?.image ?? 0); form.setValue('web_search_cost', pricing?.web_search ?? 0); form.setValue('internal_reasoning_cost', pricing?.internal_reasoning ?? 0); form.setValue('max_prompt_cost', pricing?.max_prompt_cost ?? 0); form.setValue('max_completion_cost', pricing?.max_completion_cost ?? 0); form.setValue('max_cost', pricing?.max_cost ?? 0); form.setValue( 'per_request_limits_raw', model.per_request_limits ? JSON.stringify(model.per_request_limits, null, 2) : '' ); form.setValue( 'top_provider_context_length', typeof topProvider?.context_length === 'number' ? topProvider.context_length : undefined ); form.setValue( 'top_provider_max_completion_tokens', typeof topProvider?.max_completion_tokens === 'number' ? topProvider.max_completion_tokens : undefined ); form.setValue( 'top_provider_is_moderated', typeof topProvider?.is_moderated === 'boolean' ? topProvider.is_moderated : false ); form.setValue('enabled', model.enabled); }; const onSubmit = async (data: FormData) => { setIsSubmitting(true); try { let perRequestLimits: Record | null = null; if ( data.per_request_limits_raw && data.per_request_limits_raw.trim().length ) { try { perRequestLimits = JSON.parse(data.per_request_limits_raw); } catch { toast.error('Per-request limits must be valid JSON'); setIsSubmitting(false); return; } } const adminModel: AdminModel = { id: data.id, name: data.name, description: data.description || '', created: Math.floor(Date.now() / 1000), context_length: data.context_length, architecture: { modality: data.modality, input_modalities: listFromString( data.input_modalities_raw || data.modality ), output_modalities: listFromString( data.output_modalities_raw || data.modality ), tokenizer: data.tokenizer || '', instruct_type: data.instruct_type?.trim() || null, }, pricing: { prompt: data.input_cost, completion: data.output_cost, request: data.request_cost, image: data.image_cost, web_search: data.web_search_cost, internal_reasoning: data.internal_reasoning_cost, max_prompt_cost: data.max_prompt_cost, max_completion_cost: data.max_completion_cost, max_cost: data.max_cost, }, per_request_limits: perRequestLimits, top_provider: data.top_provider_context_length || data.top_provider_max_completion_tokens || data.top_provider_is_moderated ? { context_length: data.top_provider_context_length ?? null, max_completion_tokens: data.top_provider_max_completion_tokens ?? null, is_moderated: data.top_provider_is_moderated, } : null, upstream_provider_id: data.upstream_provider_id?.trim().length ? data.upstream_provider_id.trim() : providerId, canonical_slug: data.canonical_slug?.trim() || null, alias_ids: listFromString(data.alias_ids_raw || ''), enabled: data.enabled, }; if (isEdit) { await AdminService.updateProviderModel(providerId, data.id, adminModel); toast.success('Model updated successfully'); } else { await AdminService.createProviderModel(providerId, adminModel); toast.success( isOverride ? 'Model override created' : 'Model created successfully' ); } onSuccess(); onClose(); } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Unknown error saving model'; toast.error(`Failed to save model: ${message}`); } finally { setIsSubmitting(false); } }; const title = isEdit ? 'Edit Model' : isOverride ? 'Override Model' : 'Add Custom Model'; const description = isOverride ? 'Create a custom override for this upstream model' : 'Add a new model configuration for this provider'; return ( !open && onClose()}> {title} {description} {!isEdit && (
Presets
e.preventDefault()} > e.stopPropagation()} > {isLoadingPresets ? ( Loading presets... ) : presets.length === 0 ? ( No presets available. ) : ( {presets.map((preset) => ( { applyModelToForm(preset); setIsPresetOpen(false); }} >
{preset.id} {preset.name}
))}
)}
{isOverride ? 'Apply pricing and settings from a preset model (keeping the model ID unchanged).' : 'Prefill fields from a preset model definition, then adjust as needed.'}
)}
( Model ID * Unique identifier for the model )} /> ( Display Name * )} />
( Description