/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState, } from 'react' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { AlertTriangle, Save } from 'lucide-react' import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' import { Alert, AlertDescription } from '@/components/ui/alert' import { Button } from '@/components/ui/button' import { Field, FieldDescription, FieldGroup, FieldLabel, } from '@/components/ui/field' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { InputGroup, InputGroupAddon, InputGroupInput, } from '@/components/ui/input-group' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from '@/components/ui/sheet' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { sideDrawerContentClassName } from '@/components/drawer-layout' import { EMPTY_LANE_ENABLED, EMPTY_LANE_PRICES, buildPreviewRows, createInitialLaneState, createModelPricingSchema, hasValue, laneConfigs, numericDraftRegex, ratioFieldByLane, toNumberOrNull, type LaneKey, type ModelPricingFormValues, type ModelRatioData, type PricingMode, } from './model-pricing-core' import { PriceInput, PriceLane } from './model-pricing-inputs' import { formatPricingNumber } from './pricing-format' import { TieredPricingEditor } from './tiered-pricing-editor' export type { ModelRatioData } from './model-pricing-core' type ModelPricingSheetProps = { open: boolean onOpenChange: (open: boolean) => void editData?: ModelRatioData | null onSave?: () => void | Promise isSaving?: boolean } type ModelPricingEditorPanelProps = Omit< ModelPricingSheetProps, 'open' | 'onOpenChange' > & { className?: string } export type ModelPricingEditorPanelHandle = { commitDraft: () => Promise } export const ModelPricingSheet = forwardRef< ModelPricingEditorPanelHandle, ModelPricingSheetProps >(function ModelPricingSheet( { open, onOpenChange, editData, onSave, isSaving }, ref ) { const { t } = useTranslation() const title = editData ? t('Edit model pricing') : t('Add model pricing') const description = editData?.name || t('New model') return ( {title} {description} ) }) export const ModelPricingEditorPanel = forwardRef< ModelPricingEditorPanelHandle, ModelPricingEditorPanelProps >(function ModelPricingEditorPanel( { editData, className, onSave, isSaving }, ref ) { const { t } = useTranslation() const [pricingMode, setPricingMode] = useState('per-token') const [promptPrice, setPromptPrice] = useState('') const [lanePrices, setLanePrices] = useState>({ ...EMPTY_LANE_PRICES, }) const [laneEnabled, setLaneEnabled] = useState>({ ...EMPTY_LANE_ENABLED, }) const [billingExpr, setBillingExpr] = useState('') const [requestRuleExpr, setRequestRuleExpr] = useState('') const isEditMode = !!editData const form = useForm({ resolver: zodResolver(createModelPricingSchema(t)), defaultValues: { name: '', price: '', ratio: '', cacheRatio: '', createCacheRatio: '', completionRatio: '', imageRatio: '', audioRatio: '', audioCompletionRatio: '', }, }) useEffect(() => { const nextLaneState = createInitialLaneState(editData) if (editData) { form.reset({ name: editData.name, price: editData.price || '', ratio: editData.ratio || '', cacheRatio: editData.cacheRatio || '', createCacheRatio: editData.createCacheRatio || '', completionRatio: editData.completionRatio || '', imageRatio: editData.imageRatio || '', audioRatio: editData.audioRatio || '', audioCompletionRatio: editData.audioCompletionRatio || '', }) setPricingMode( editData.billingMode === 'tiered_expr' ? 'tiered_expr' : editData.price ? 'per-request' : 'per-token' ) setBillingExpr(editData.billingExpr || '') setRequestRuleExpr(editData.requestRuleExpr || '') } else { form.reset({ name: '', price: '', ratio: '', cacheRatio: '', createCacheRatio: '', completionRatio: '', imageRatio: '', audioRatio: '', audioCompletionRatio: '', }) setPricingMode('per-token') setBillingExpr('') setRequestRuleExpr('') } setPromptPrice(nextLaneState.promptPrice) setLanePrices(nextLaneState.prices) setLaneEnabled(nextLaneState.enabled) }, [editData, form]) const setFormValue = (field: keyof ModelPricingFormValues, value: string) => { form.setValue(field, value, { shouldDirty: true, shouldValidate: true, }) } const deriveLaneRatio = ( lane: LaneKey, price: string, nextPromptPrice = promptPrice, nextLanePrices = lanePrices ) => { const priceNumber = toNumberOrNull(price) if (priceNumber === null) return '' if (lane === 'audioOutput') { const audioInputPrice = toNumberOrNull(nextLanePrices.audioInput) if (audioInputPrice === null || audioInputPrice === 0) return '' return formatPricingNumber(priceNumber / audioInputPrice) } const inputPrice = toNumberOrNull(nextPromptPrice) if (inputPrice === null || inputPrice === 0) return '' return formatPricingNumber(priceNumber / inputPrice) } const syncLaneRatios = ( nextPromptPrice = promptPrice, nextLanePrices = lanePrices, nextLaneEnabled = laneEnabled ) => { const inputPrice = toNumberOrNull(nextPromptPrice) setFormValue( 'ratio', inputPrice !== null ? formatPricingNumber(inputPrice / 2) : '' ) laneConfigs.forEach(({ key }) => { const ratioField = ratioFieldByLane[key] if (!nextLaneEnabled[key]) { setFormValue(ratioField, '') return } setFormValue( ratioField, deriveLaneRatio( key, nextLanePrices[key], nextPromptPrice, nextLanePrices ) ) }) } const handlePromptPriceChange = (value: string) => { if (!numericDraftRegex.test(value)) return setPromptPrice(value) syncLaneRatios(value, lanePrices, laneEnabled) } const handleLanePriceChange = (lane: LaneKey, value: string) => { if (!numericDraftRegex.test(value)) return const nextLanePrices = { ...lanePrices, [lane]: value } setLanePrices(nextLanePrices) if (laneEnabled[lane]) { setFormValue( ratioFieldByLane[lane], deriveLaneRatio(lane, value, promptPrice, nextLanePrices) ) } if (lane === 'audioInput' && laneEnabled.audioOutput) { setFormValue( 'audioCompletionRatio', deriveLaneRatio( 'audioOutput', nextLanePrices.audioOutput, promptPrice, nextLanePrices ) ) } } const handleLaneToggle = (lane: LaneKey, checked: boolean) => { const nextEnabled = { ...laneEnabled, [lane]: checked } let nextPrices = lanePrices if (!checked) { nextPrices = { ...nextPrices, [lane]: '' } setFormValue(ratioFieldByLane[lane], '') if (lane === 'audioInput') { nextEnabled.audioOutput = false nextPrices.audioOutput = '' setFormValue('audioCompletionRatio', '') } } setLaneEnabled(nextEnabled) setLanePrices(nextPrices) if (checked) { setFormValue( ratioFieldByLane[lane], deriveLaneRatio(lane, nextPrices[lane], promptPrice, nextPrices) ) } } const handleModeChange = (value: string) => { const nextMode = value as PricingMode setPricingMode(nextMode) if (nextMode === 'tiered_expr' && !billingExpr) { setBillingExpr('tier("base", p * 0 + c * 0)') } } const watchedValues = form.watch() const previewRows = useMemo( () => buildPreviewRows( watchedValues, pricingMode, billingExpr, requestRuleExpr, promptPrice, lanePrices, laneEnabled, t ), [ billingExpr, laneEnabled, lanePrices, pricingMode, promptPrice, requestRuleExpr, t, watchedValues, ] ) const warnings = useMemo(() => { const nextWarnings: string[] = [] const hasConflict = !!editData?.price && [ editData.ratio, editData.completionRatio, editData.cacheRatio, editData.createCacheRatio, editData.imageRatio, editData.audioRatio, editData.audioCompletionRatio, ].some(hasValue) if (hasConflict) { nextWarnings.push( t( 'This model has both fixed-price and token-price settings. Saving the current mode will rewrite the conflicting fields.' ) ) } if ( pricingMode === 'per-token' && toNumberOrNull(promptPrice) === null && laneConfigs.some( ({ key }) => laneEnabled[key] && hasValue(lanePrices[key]) ) ) { nextWarnings.push( t('Input price is required before saving dependent prices.') ) } if ( pricingMode === 'per-token' && laneEnabled.audioOutput && !hasValue(lanePrices.audioInput) ) { nextWarnings.push(t('Audio output price requires an audio input price.')) } return nextWarnings }, [editData, laneEnabled, lanePrices, pricingMode, promptPrice, t]) const validatePricingValues = useCallback(() => { if ( pricingMode === 'per-token' && toNumberOrNull(promptPrice) === null && laneConfigs.some( ({ key }) => laneEnabled[key] && hasValue(lanePrices[key]) ) ) { form.setError('ratio', { message: t('Input price is required before saving dependent prices.'), }) return false } if ( pricingMode === 'per-token' && laneEnabled.audioOutput && !hasValue(lanePrices.audioInput) ) { form.setError('audioRatio', { message: t('Audio output price requires an audio input price.'), }) return false } return true }, [form, laneEnabled, lanePrices, pricingMode, promptPrice, t]) const buildSubmitData = useCallback( (values: ModelPricingFormValues) => { const data: ModelRatioData = { name: values.name.trim(), 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 } return data }, [billingExpr, pricingMode, requestRuleExpr] ) useImperativeHandle( ref, () => ({ commitDraft: async () => { const isValid = await form.trigger() if (!isValid || !validatePricingValues()) return null return buildSubmitData(form.getValues()) }, }), [form, validatePricingValues, buildSubmitData] ) const showActions = Boolean(onSave) return (

{isEditMode ? t('Edit model pricing') : t('Add model pricing')}

event.preventDefault()} className='flex min-h-0 flex-1 flex-col' autoComplete='off' >
{warnings.length > 0 && (
{warnings.map((warning) => ( {warning} ))}
)} ( {t('Model name')} {t( 'The exact model identifier as used in API requests.' )} )} /> {t('Per-token')} {t('Per-request')} {t('Expression')} {t('Input price')} {t('USD price per 1M input tokens.')}
{laneConfigs.map((lane) => { const disabled = lane.key === 'audioOutput' && (!laneEnabled.audioInput || !hasValue(lanePrices.audioInput)) return ( handleLaneToggle(lane.key, checked) } onChange={(value) => handleLanePriceChange(lane.key, value) } /> ) })}
( {t('Fixed price')} $ { const value = event.target.value if (numericDraftRegex.test(value)) { field.onChange(value) } }} /> {t('per request')} {t( 'Cost in USD per request, regardless of tokens used.' )} )} />
{showActions && (
{onSave && ( )}
)}
) })