import { useEffect, useState } from 'react' import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { ChevronDown } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { TieredPricingEditor } from './tiered-pricing-editor' const createModelDialogSchema = (t: (key: string) => string) => z.object({ name: z.string().min(1, t('Model name is required')), price: z.string().optional(), ratio: z.string().optional(), cacheRatio: z.string().optional(), createCacheRatio: z.string().optional(), completionRatio: z.string().optional(), imageRatio: z.string().optional(), audioRatio: z.string().optional(), audioCompletionRatio: z.string().optional(), }) type ModelDialogFormValues = z.infer> type PricingMode = 'per-token' | 'per-request' | 'tiered_expr' type PricingSubMode = 'ratio' | 'price' export type ModelRatioData = { name: string price?: string ratio?: string cacheRatio?: string createCacheRatio?: string completionRatio?: string imageRatio?: string audioRatio?: string audioCompletionRatio?: string billingMode?: 'per-token' | 'per-request' | 'tiered_expr' billingExpr?: string requestRuleExpr?: string } type ModelRatioDialogProps = { open: boolean onOpenChange: (open: boolean) => void onSave: (data: ModelRatioData) => void editData?: ModelRatioData | null } export function ModelRatioDialog({ open, onOpenChange, onSave, editData, }: ModelRatioDialogProps) { const { t } = useTranslation() const [pricingMode, setPricingMode] = useState('per-token') const [pricingSubMode, setPricingSubMode] = useState('ratio') const [advancedOpen, setAdvancedOpen] = useState(false) const [promptPrice, setPromptPrice] = useState('') const [completionPrice, setCompletionPrice] = useState('') const [billingExpr, setBillingExpr] = useState('') const [requestRuleExpr, setRequestRuleExpr] = useState('') const isEditMode = !!editData const form = useForm({ resolver: zodResolver(createModelDialogSchema(t)), defaultValues: { name: '', price: '', ratio: '', cacheRatio: '', createCacheRatio: '', completionRatio: '', imageRatio: '', audioRatio: '', audioCompletionRatio: '', }, }) useEffect(() => { if (editData) { form.reset(editData) // eslint-disable-next-line react-hooks/set-state-in-effect setBillingExpr(editData.billingExpr || '') setRequestRuleExpr(editData.requestRuleExpr || '') if (editData.billingMode === 'tiered_expr') { setPricingMode('tiered_expr') } else if (editData.price && editData.price !== '') { setPricingMode('per-request') } else { setPricingMode('per-token') if (editData.ratio) { const tokenPrice = parseFloat(editData.ratio) * 2 setPromptPrice(tokenPrice.toString()) if (editData.completionRatio) { const compPrice = tokenPrice * parseFloat(editData.completionRatio) setCompletionPrice(compPrice.toString()) } } } } else { form.reset({ name: '', price: '', ratio: '', cacheRatio: '', createCacheRatio: '', completionRatio: '', imageRatio: '', audioRatio: '', audioCompletionRatio: '', }) setPricingMode('per-token') setPricingSubMode('ratio') setPromptPrice('') setCompletionPrice('') setBillingExpr('') setRequestRuleExpr('') setAdvancedOpen(false) } }, [editData, form, open]) const handleSubmit = (values: ModelDialogFormValues) => { // Always pass through every field. The visual editor decides what to // persist based on `billingMode`, and tiered_expr models also keep the // ratio/price values as fallback during multi-instance sync delays // (the backend's ModelPriceHelper checks billing_mode first, so these // fallbacks only kick in when billing_setting hasn't propagated yet). const data: ModelRatioData = { name: values.name, billingMode: pricingMode, price: values.price || '', ratio: values.ratio || '', cacheRatio: values.cacheRatio || '', createCacheRatio: values.createCacheRatio || '', completionRatio: values.completionRatio || '', imageRatio: values.imageRatio || '', audioRatio: values.audioRatio || '', audioCompletionRatio: values.audioCompletionRatio || '', } if (pricingMode === 'tiered_expr') { data.billingExpr = billingExpr data.requestRuleExpr = requestRuleExpr } onSave(data) form.reset() onOpenChange(false) } const validateNumber = (value: string) => { if (value === '') return true return !isNaN(parseFloat(value)) } const handlePromptPriceChange = (value: string) => { setPromptPrice(value) if (value && !isNaN(parseFloat(value))) { const ratio = parseFloat(value) / 2 form.setValue('ratio', ratio.toString()) } else { form.setValue('ratio', '') } } const handleCompletionPriceChange = (value: string) => { setCompletionPrice(value) if ( value && !isNaN(parseFloat(value)) && promptPrice && !isNaN(parseFloat(promptPrice)) && parseFloat(promptPrice) > 0 ) { const completionRatio = parseFloat(value) / parseFloat(promptPrice) form.setValue('completionRatio', completionRatio.toString()) } else { form.setValue('completionRatio', '') } } return ( {isEditMode ? t('Edit model') : t('Add model')} {t('Configure pricing ratios for a specific model.')}
( {t('Model name')} {t('The exact model identifier as used in API requests.')} )} />
setPricingMode(value as PricingMode)} >
{pricingMode === 'tiered_expr' ? ( ) : pricingMode === 'per-request' ? ( ( {t('Fixed price (USD)')} { const value = e.target.value if (validateNumber(value)) { field.onChange(value) } }} /> {t('Cost in USD per request, regardless of tokens used.')} )} /> ) : ( <>
setPricingSubMode(value as PricingSubMode) } >
{pricingSubMode === 'ratio' ? ( <> ( {t('Model ratio')} { const value = e.target.value if (validateNumber(value)) { field.onChange(value) if (value) { setPromptPrice( (parseFloat(value) * 2).toString() ) } else { setPromptPrice('') } } }} /> {field.value && !isNaN(parseFloat(field.value)) ? t( 'Calculated price: ${{price}} per 1M tokens', { price: ( parseFloat(field.value) * 2 ).toFixed(4), } ) : t('Multiplier for prompt tokens.')} )} /> ( {t('Completion ratio')} { const value = e.target.value if (validateNumber(value)) { field.onChange(value) const ratio = form.getValues('ratio') if (value && ratio) { const compPrice = parseFloat(ratio) * 2 * parseFloat(value) setCompletionPrice(compPrice.toString()) } else { setCompletionPrice('') } } }} /> {field.value && !isNaN(parseFloat(field.value)) && promptPrice && !isNaN(parseFloat(promptPrice)) ? t( 'Calculated price: ${{price}} per 1M tokens', { price: ( parseFloat(promptPrice) * parseFloat(field.value) ).toFixed(4), } ) : t('Multiplier for completion tokens.')} )} /> ) : ( <>
handlePromptPriceChange(e.target.value) } />

{promptPrice && !isNaN(parseFloat(promptPrice)) ? t('Calculated ratio: {{ratio}}', { ratio: (parseFloat(promptPrice) / 2).toFixed(4), }) : t('Enter Input price to calculate ratio')}

handleCompletionPriceChange(e.target.value) } />

{completionPrice && !isNaN(parseFloat(completionPrice)) && promptPrice && !isNaN(parseFloat(promptPrice)) && parseFloat(promptPrice) > 0 ? t('Calculated ratio: {{ratio}}', { ratio: ( parseFloat(completionPrice) / parseFloat(promptPrice) ).toFixed(4), }) : t('Enter Completion price to calculate ratio')}

)} ( {t('Cache ratio')} { const value = e.target.value if (validateNumber(value)) { field.onChange(value) } }} /> {t('Discount ratio for cache hits.')} )} /> ( {t('Create cache ratio')} { const value = e.target.value if (validateNumber(value)) { field.onChange(value) } }} /> {t( 'Ratio applied when creating cache entries for supported models.' )} )} /> ( {t('Image ratio')} { const value = e.target.value if (validateNumber(value)) { field.onChange(value) } }} /> {t('Multiplier for image processing.')} )} /> ( {t('Audio ratio')} { const value = e.target.value if (validateNumber(value)) { field.onChange(value) } }} /> {t('Multiplier for audio inputs.')} )} /> ( {t('Audio completion ratio')} { const value = e.target.value if (validateNumber(value)) { field.onChange(value) } }} /> {t('Multiplier for audio outputs.')} )} /> )}
) }