feat(default): reorganize system settings pricing UI

Refine the default system settings structure and model pricing editor so pricing configuration is easier to scan and edit.
This commit is contained in:
CaIon
2026-05-06 16:24:06 +08:00
parent 9acf5fecae
commit 0f9f094a48
62 changed files with 3655 additions and 2343 deletions
@@ -10,7 +10,7 @@ import {
type InputHTMLAttributes,
type MouseEvent as ReactMouseEvent,
} from 'react'
import { Copy, Plus, Trash2 } from 'lucide-react'
import { ChevronDown, Copy, Plus, Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
@@ -31,6 +31,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea'
import {
BILLING_EXTRA_VARS,
@@ -38,6 +39,10 @@ import {
MATCH_CONTAINS,
MATCH_EQ,
MATCH_EXISTS,
MATCH_GT,
MATCH_GTE,
MATCH_LT,
MATCH_LTE,
MATCH_RANGE,
SOURCE_HEADER,
SOURCE_PARAM,
@@ -48,7 +53,6 @@ import {
createEmptyCondition,
createEmptyRuleGroup,
createEmptyTimeCondition,
createEmptyTimeRuleGroup,
getRequestRuleMatchOptions,
splitBillingExprAndRequestRules,
tryParseRequestRuleExpr,
@@ -77,11 +81,20 @@ import {
} from '@/features/pricing/lib/tier-expr'
const PRICE_SUFFIX = '$/1M tokens'
const CACHE_PRICE_VARS = BILLING_EXTRA_VARS.filter(
(variable) => variable.group === 'cache'
)
const MEDIA_PRICE_VARS = BILLING_EXTRA_VARS.filter(
(variable) => variable.group === 'media'
)
const VAR_OPTIONS: { value: TierConditionInput['var']; label: string }[] = [
{ value: 'len', label: 'len (input length)' },
{ value: 'p', label: 'p (input)' },
{ value: 'c', label: 'c (output)' },
const CONDITION_INPUT_OPTIONS: {
value: TierConditionInput['var']
labelKey: string
}[] = [
{ value: 'len', labelKey: 'Full input length' },
{ value: 'p', labelKey: 'Billable input tokens' },
{ value: 'c', labelKey: 'Billable output tokens' },
]
const OPS: TierConditionInput['op'][] = ['<', '<=', '>', '>=']
@@ -394,6 +407,11 @@ type ConditionRowProps = {
}
function ConditionRow({ condition, onChange, onRemove }: ConditionRowProps) {
const { t } = useTranslation()
const currentInputOption = CONDITION_INPUT_OPTIONS.find(
(option) => option.value === condition.var
)
return (
<div className='flex items-center gap-2'>
<Select
@@ -403,12 +421,16 @@ function ConditionRow({ condition, onChange, onRemove }: ConditionRowProps) {
}
>
<SelectTrigger className='w-32' size='sm'>
<SelectValue />
<SelectValue>
{currentInputOption
? t(currentInputOption.labelKey)
: condition.var}
</SelectValue>
</SelectTrigger>
<SelectContent>
{VAR_OPTIONS.map((option) => (
{CONDITION_INPUT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
{t(option.labelKey)}
</SelectItem>
))}
</SelectContent>
@@ -462,31 +484,19 @@ type PriceFieldProps = {
hint?: string
value: number
onChange: (next: number) => void
showSuffix?: boolean
}
function PriceField({
label,
hint,
value,
onChange,
showSuffix = true,
}: PriceFieldProps) {
function PriceField({ label, hint, value, onChange }: PriceFieldProps) {
return (
<div className='space-y-1'>
<Label className='text-xs'>{label}</Label>
<div className='flex items-center gap-2'>
<DraftNumberInput
min={0}
step={0.01}
value={Number.isFinite(value) ? value : 0}
onValueChange={onChange}
className='w-32'
/>
{showSuffix && (
<span className='text-muted-foreground text-xs'>{PRICE_SUFFIX}</span>
)}
</div>
<div className='w-36 space-y-0.5'>
<Label className='text-muted-foreground text-xs'>{label}</Label>
<DraftNumberInput
min={0}
step={0.01}
value={Number.isFinite(value) ? value : 0}
onValueChange={onChange}
className='h-8 w-full'
/>
{hint && <p className='text-muted-foreground text-xs'>{hint}</p>}
</div>
)
@@ -547,21 +557,49 @@ function VisualTierCard({
const inputUnitPrice = unitCostToPrice(tier.input_unit_cost)
const outputUnitPrice = unitCostToPrice(tier.output_unit_cost)
const hasMediaPricing = MEDIA_PRICE_VARS.some((variable) => {
const fieldKey = variable.tierField as keyof VisualTier
return unitCostToPrice((tier[fieldKey] as number | undefined) ?? 0) > 0
})
const [mediaOpen, setMediaOpen] = useState(hasMediaPricing)
useEffect(() => {
if (hasMediaPricing) setMediaOpen(true)
}, [hasMediaPricing])
const renderPriceVariable = (
variable: (typeof BILLING_EXTRA_VARS)[number]
) => {
const fieldKey = variable.tierField as keyof VisualTier
const value = unitCostToPrice((tier[fieldKey] as number | undefined) ?? 0)
return (
<PriceField
key={variable.key}
label={t(variable.label)}
value={value}
onChange={(next) => handlePriceChange(fieldKey, priceToUnitCost(next))}
/>
)
}
return (
<div className='bg-muted/30 space-y-3 rounded-md border p-3'>
<div className='flex items-center justify-between gap-2'>
<div className='space-y-3 rounded-lg border p-3'>
<div className='flex flex-wrap items-center justify-between gap-2'>
<div className='flex items-center gap-2'>
<Badge variant='outline'>
{t('Tier')} {index + 1} / {total}
</Badge>
{tier.conditions.length === 0 && (
<Badge variant='secondary'>{t('Fallback tier')}</Badge>
)}
<Input
value={tier.label}
onChange={(event) =>
onChange({ ...tier, label: event.target.value })
}
placeholder={t('Tier name')}
className='h-8 w-40'
className='h-7 w-36'
/>
</div>
<Button
@@ -575,9 +613,10 @@ function VisualTierCard({
</Button>
</div>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label className='text-xs'>{t('Conditions (AND)')}</Label>
{/* Conditions */}
<div className='space-y-1.5'>
<div className='flex h-7 items-center justify-between'>
<Label className='text-xs font-medium'>{t('Tier conditions')}</Label>
<Button
variant='ghost'
size='sm'
@@ -605,67 +644,90 @@ function VisualTierCard({
)}
</div>
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
<PriceField
label={t('Input price')}
hint={`${inputUnitPrice} × p`}
value={inputUnitPrice}
onChange={(value) =>
handlePriceChange('input_unit_cost', priceToUnitCost(value))
}
/>
<PriceField
label={t('Output price')}
hint={`${outputUnitPrice} × c`}
value={outputUnitPrice}
onChange={(value) =>
handlePriceChange('output_unit_cost', priceToUnitCost(value))
}
/>
<div className='space-y-2'>
<div className='flex items-center justify-between gap-3'>
<Label className='text-sm font-semibold'>{t('Token prices')}</Label>
<span className='bg-muted text-muted-foreground rounded-md px-2 py-1 text-xs'>
{PRICE_SUFFIX}
</span>
</div>
<div className='space-y-3'>
<div className='flex flex-wrap gap-x-4 gap-y-2'>
<PriceField
label={t('Input price')}
value={inputUnitPrice}
onChange={(value) =>
handlePriceChange('input_unit_cost', priceToUnitCost(value))
}
/>
<PriceField
label={t('Output price')}
value={outputUnitPrice}
onChange={(value) =>
handlePriceChange('output_unit_cost', priceToUnitCost(value))
}
/>
</div>
<div className='space-y-2'>
<div className='flex h-7 items-center'>
<Tabs
value={cacheMode}
onValueChange={(value) =>
value !== null && handleCacheModeChange(value as CacheMode)
}
>
<TabsList className='h-8'>
<TabsTrigger
value={CACHE_MODE_GENERIC}
className='px-2 text-xs'
>
{t('Generic cache')}
</TabsTrigger>
<TabsTrigger
value={CACHE_MODE_TIMED}
className='px-2 text-xs'
>
{t('Time-sliced cache (Claude)')}
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className='flex flex-wrap gap-x-4 gap-y-2'>
{CACHE_PRICE_VARS.map((variable) => {
if (variable.key === 'cc1h' && cacheMode !== CACHE_MODE_TIMED) {
return null
}
return renderPriceVariable(variable)
})}
</div>
</div>
</div>
</div>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label className='text-xs'>{t('Cache mode')}</Label>
<Select
value={cacheMode}
onValueChange={(value) => handleCacheModeChange(value as CacheMode)}
>
<SelectTrigger className='w-44' size='sm'>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={CACHE_MODE_GENERIC}>
{t('Generic cache')}
</SelectItem>
<SelectItem value={CACHE_MODE_TIMED}>
{t('Timed cache (1h)')}
</SelectItem>
</SelectContent>
</Select>
</div>
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
{BILLING_EXTRA_VARS.map((variable) => {
if (variable.key === 'cc1h' && cacheMode !== CACHE_MODE_TIMED) {
return null
}
const fieldKey = variable.tierField as keyof VisualTier
const value = unitCostToPrice(
(tier[fieldKey] as number | undefined) ?? 0
)
return (
<PriceField
key={variable.key}
label={variable.label}
hint={`${value} × ${variable.key}`}
value={value}
onChange={(next) =>
handlePriceChange(fieldKey, priceToUnitCost(next))
}
/>
)
})}
</div>
{/* Media prices */}
<div className='space-y-1.5'>
<Button
type='button'
variant='ghost'
size='sm'
className='h-7 px-2 text-xs'
onClick={() => setMediaOpen((prev) => !prev)}
>
<ChevronDown
className={cn(
'mr-1 h-3 w-3 transition-transform',
mediaOpen && 'rotate-180'
)}
/>
{t('Media pricing')}
</Button>
{mediaOpen && (
<div className='flex flex-wrap gap-x-4 gap-y-2'>
{MEDIA_PRICE_VARS.map(renderPriceVariable)}
</div>
)}
</div>
</div>
)
@@ -747,14 +809,12 @@ function VisualEditor({ visualConfig, onChange }: VisualEditorProps) {
}
return (
<div className='space-y-3'>
<Alert>
<AlertDescription className='text-xs'>
{t(
'Each tier supports 0~2 conditions (over len, p, c); the last tier is the catch-all without conditions. Use len (full input length, including cache hits) for tier conditions to avoid mis-routing when cache hits reduce p.'
)}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<p className='text-muted-foreground text-xs'>
{t(
'Each tier supports up to 2 conditions. The last tier without conditions is the fallback.'
)}
</p>
{config.tiers.map((tier, index) => (
<VisualTierCard
key={index}
@@ -766,7 +826,12 @@ function VisualEditor({ visualConfig, onChange }: VisualEditorProps) {
onAddCondition={() => handleAddCondition(index)}
/>
))}
<Button variant='outline' size='sm' onClick={handleAddTier}>
<Button
variant='outline'
size='sm'
className='h-9 w-36 justify-center'
onClick={handleAddTier}
>
<Plus className='mr-2 h-4 w-4' />
{t('Add tier')}
</Button>
@@ -832,6 +897,50 @@ function RuleConditionRow({
}: RuleConditionRowProps) {
const { t } = useTranslation()
const matchOptions = getRequestRuleMatchOptions(condition.source)
const getMatchLabel = (mode: string) => {
switch (mode) {
case MATCH_EQ:
return t('Equals')
case MATCH_CONTAINS:
return t('Contains')
case MATCH_EXISTS:
return t('Exists')
case MATCH_GT:
return t('Greater than')
case MATCH_GTE:
return t('Greater than or equal')
case MATCH_LT:
return t('Less than')
case MATCH_LTE:
return t('Less than or equal')
case MATCH_RANGE:
return t('Overnight range')
default:
return mode
}
}
const getTimeFuncLabel = (timeFunc: TimeFunc) => {
switch (timeFunc) {
case 'hour':
return t('Hour of day')
case 'minute':
return t('Minute')
case 'weekday':
return t('Weekday')
case 'month':
return t('Month number')
case 'day':
return t('Day of month')
default:
return timeFunc
}
}
const sourceLabel =
condition.source === SOURCE_PARAM
? t('Body param')
: condition.source === SOURCE_HEADER
? t('Header')
: t('Time')
const handleSourceChange = (source: string) => {
if (source === SOURCE_TIME) {
@@ -857,12 +966,12 @@ function RuleConditionRow({
}
>
<SelectTrigger className='w-32' size='sm'>
<SelectValue />
<SelectValue>{getTimeFuncLabel(timeCond.timeFunc)}</SelectValue>
</SelectTrigger>
<SelectContent>
{TIME_FUNCS.map((fn) => (
<SelectItem key={fn} value={fn}>
{fn}
{getTimeFuncLabel(fn)}
</SelectItem>
))}
</SelectContent>
@@ -874,7 +983,10 @@ function RuleConditionRow({
}
>
<SelectTrigger className='w-56' size='sm'>
<SelectValue />
<SelectValue>
{COMMON_TIMEZONES.find((tz) => tz.value === timeCond.timezone)
?.label ?? timeCond.timezone}
</SelectValue>
</SelectTrigger>
<SelectContent>
{COMMON_TIMEZONES.map((tz) => (
@@ -889,12 +1001,12 @@ function RuleConditionRow({
onValueChange={(v) => v !== null && handleModeChange(v)}
>
<SelectTrigger className='w-32' size='sm'>
<SelectValue />
<SelectValue>{getMatchLabel(timeCond.mode)}</SelectValue>
</SelectTrigger>
<SelectContent>
{matchOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{t(option.labelKey)}
{getMatchLabel(option.value)}
</SelectItem>
))}
</SelectContent>
@@ -906,7 +1018,7 @@ function RuleConditionRow({
onValueChange={(value) =>
onChange({ ...timeCond, rangeStart: String(value) })
}
placeholder='start'
placeholder={t('Start')}
className='w-20'
/>
<span className='text-muted-foreground text-xs'>~</span>
@@ -915,7 +1027,7 @@ function RuleConditionRow({
onValueChange={(value) =>
onChange({ ...timeCond, rangeEnd: String(value) })
}
placeholder='end'
placeholder={t('End')}
className='w-20'
/>
</>
@@ -925,7 +1037,7 @@ function RuleConditionRow({
onValueChange={(value) =>
onChange({ ...timeCond, value: String(value) })
}
placeholder='value'
placeholder={t('Value')}
className='w-24'
/>
)}
@@ -947,12 +1059,12 @@ function RuleConditionRow({
onValueChange={(v) => v !== null && handleModeChange(v)}
>
<SelectTrigger className='w-32' size='sm'>
<SelectValue />
<SelectValue>{getMatchLabel(phCond.mode)}</SelectValue>
</SelectTrigger>
<SelectContent>
{matchOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{t(option.labelKey)}
{getMatchLabel(option.value)}
</SelectItem>
))}
</SelectContent>
@@ -977,7 +1089,7 @@ function RuleConditionRow({
onValueChange={(v) => v !== null && handleSourceChange(v)}
>
<SelectTrigger className='w-28' size='sm'>
<SelectValue />
<SelectValue>{sourceLabel}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value={SOURCE_PARAM}>{t('Body param')}</SelectItem>
@@ -1214,7 +1326,7 @@ function CostEstimator({ effectiveExpr }: EstimatorProps) {
</div>
<div className='grid grid-cols-2 gap-3'>
<div className='space-y-1'>
<Label className='text-xs'>{t('Input tokens')} (p)</Label>
<Label className='text-xs'>{t('Input tokens')}</Label>
<DraftNumberInput
min={0}
value={promptTokens}
@@ -1222,7 +1334,7 @@ function CostEstimator({ effectiveExpr }: EstimatorProps) {
/>
</div>
<div className='space-y-1'>
<Label className='text-xs'>{t('Output tokens')} (c)</Label>
<Label className='text-xs'>{t('Output tokens')}</Label>
<DraftNumberInput
min={0}
value={completionTokens}
@@ -1243,9 +1355,7 @@ function CostEstimator({ effectiveExpr }: EstimatorProps) {
) as keyof ExtraTokenValues
return (
<div key={variable.key} className='space-y-1'>
<Label className='text-xs'>
{variable.shortLabel} ({variable.key})
</Label>
<Label className='text-xs'>{t(variable.shortLabel)}</Label>
<DraftNumberInput
min={0}
value={extras[stateKey]}
@@ -1612,7 +1722,7 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
<div className='flex-1'>
<PresetSection applyPreset={applyPreset} />
</div>
<LlmPromptHelper modelName={modelName} />
{editorMode === 'raw' && <LlmPromptHelper modelName={modelName} />}
</div>
<div className='bg-muted/30 space-y-3 rounded-md border p-3'>
@@ -1665,34 +1775,20 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
}
/>
))}
<div className='flex flex-wrap gap-2'>
<Button
variant='outline'
size='sm'
onClick={() =>
handleRuleGroupsChange([
...requestRuleGroups,
createEmptyRuleGroup(),
])
}
>
<Plus className='mr-2 h-4 w-4' />
{t('Add rule group')}
</Button>
<Button
variant='ghost'
size='sm'
onClick={() =>
handleRuleGroupsChange([
...requestRuleGroups,
createEmptyTimeRuleGroup(),
])
}
>
<Plus className='mr-2 h-4 w-4' />
{t('Add time rule group')}
</Button>
</div>
<Button
variant='outline'
size='sm'
className='h-9 w-36 justify-center'
onClick={() =>
handleRuleGroupsChange([
...requestRuleGroups,
createEmptyRuleGroup(),
])
}
>
<Plus className='mr-2 h-4 w-4' />
{t('Add rule group')}
</Button>
</>
)}
</div>