{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 (
handlePriceChange(fieldKey, priceToUnitCost(next))
}
/>
)
})}
)
}
// ---------------------------------------------------------------------------
// Visual editor (list of tiers)
// ---------------------------------------------------------------------------
type VisualEditorProps = {
visualConfig: VisualConfig | null
onChange: (next: VisualConfig) => void
}
function VisualEditor({ visualConfig, onChange }: VisualEditorProps) {
const { t } = useTranslation()
const config = useMemo(
() => normalizeVisualConfig(visualConfig),
[visualConfig]
)
const handleTierChange = (index: number, next: VisualTier) => {
const tiers = [...config.tiers]
tiers[index] = normalizeVisualTier(next)
onChange({ ...config, tiers })
}
const handleAddTier = () => {
const tiers = [...config.tiers]
const lastIndex = tiers.length - 1
// When adding a new fallback, give the previous catch-all tier a default
// upper-bound condition so the expression compiles into a sane two-tier
// shape. Mirrors the classic editor's UX for adding tiers.
if (lastIndex >= 0 && tiers[lastIndex].conditions.length === 0) {
tiers[lastIndex] = normalizeVisualTier({
...tiers[lastIndex],
conditions: [{ var: 'len', op: '<', value: 200000 }],
})
}
tiers.push(
normalizeVisualTier({
label: `tier_${tiers.length + 1}`,
conditions: [],
input_unit_cost: 0,
output_unit_cost: 0,
})
)
onChange({ ...config, tiers })
}
const handleRemoveTier = (index: number) => {
const tiers = config.tiers.filter((_, i) => i !== index)
onChange({ ...config, tiers: tiers.length > 0 ? tiers : config.tiers })
}
const handleAddCondition = (index: number) => {
const tier = config.tiers[index]
if (tier.conditions.length >= 2) return
// Prefer `len` (input length) over `p`/`c` for tier conditions because
// `p` is subject to auto-exclusion when sub-categories like `cr` are
// priced separately, which can misroute long-input requests into shorter
// tiers when cache-hits reduce the effective `p`.
const usedVars = new Set(tier.conditions.map((c) => c.var))
const nextVar: TierConditionInput['var'] = usedVars.has('len') ? 'c' : 'len'
onChange({
...config,
tiers: config.tiers.map((current, i) =>
i === index
? {
...current,
conditions: [
...tier.conditions,
{ var: nextVar, op: '<', value: 200000 },
],
}
: current
),
})
}
return (
{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.'
)}
{config.tiers.map((tier, index) => (
handleTierChange(index, next)}
onRemove={() => handleRemoveTier(index)}
onAddCondition={() => handleAddCondition(index)}
/>
))}
)
}
// ---------------------------------------------------------------------------
// Raw expression editor
// ---------------------------------------------------------------------------
type RawExprEditorProps = {
exprString: string
onChange: (value: string) => void
}
function RawExprEditor({ exprString, onChange }: RawExprEditorProps) {
const { t } = useTranslation()
return (
{BILLING_EXTRA_VARS.map((variable) => {
// BILLING_EXTRA_VARS only contains pricing variables; they are
// guaranteed to have a non-null `field` (the `len` condition-only
// variable is filtered out). Narrow the type here for safety.
if (!variable.field) return null
const stateKey = variable.field.replace(
'Price',
'Tokens'
) as keyof ExtraTokenValues
return (
)
}
// ---------------------------------------------------------------------------
// LLM prompt helper
// ---------------------------------------------------------------------------
const LLM_PROMPT_TEMPLATE = `You are an AI API billing expression design assistant. The user needs help designing a billing expression for an AI API gateway.
## Expression Language
Expressions are based on standard arithmetic with ternary operators.
### Token Variables
Input side:
- p — input token count (for pricing). Automatically excludes sub-categories priced separately (e.g., if cr is used, cache tokens are deducted from p)
- len — total input context length (for condition checks). Not affected by auto-exclusion; always reflects the full input length. Use in tier conditions
- cr — cache-hit (read) token count
- cc — cache-create token count (5-min TTL)
- cc1h — cache-create token count (1-hour TTL, Claude-specific)
- img — image input token count
- ai — audio input token count
Output side:
- c — output token count. Also auto-excludes sub-categories priced separately
- img_o — image output token count
- ao — audio output token count
### p/c Auto-exclusion
p and c are fallback variables representing all tokens not separately priced in the expression. If the expression uses a sub-category variable (e.g., cr), those tokens are deducted from p to avoid double-billing. Unused sub-category tokens remain in p/c at base price.
Important: len is NOT affected by auto-exclusion. Tier conditions should use len instead of p to prevent cache hits from lowering p and misidentifying the tier.
### Built-in Functions
- tier(name, value) — labels the billing tier; must wrap the cost expression
- max(a, b), min(a, b) — maximum/minimum
- ceil(x), floor(x), abs(x) — ceiling, floor, absolute value
- header(name) — reads a request header
- param(path) — reads a request body JSON path (gjson syntax)
- has(source, substr) — substring check
- hour(tz), minute(tz), weekday(tz), month(tz), day(tz) — time functions, tz is a timezone like "Asia/Shanghai"
### Price Coefficients
Numbers in the expression are $/1M tokens prices. For example, p * 2.5 means input $2.50/1M tokens.
## Expression Examples
Simple pricing:
tier("base", p * 2.5 + c * 15)
With cache:
tier("base", p * 2.5 + c * 15 + cr * 0.25)
Multi-tier (use len for conditions):
len <= 200000
? tier("standard", p * 3 + c * 15 + cr * 0.3 + cc * 3.75 + cc1h * 6)
: tier("long_context", p * 6 + c * 22.5 + cr * 0.6 + cc * 7.5 + cc1h * 12)
Image model:
tier("base", p * 2 + c * 8 + img * 2.5)
Multimodal with audio:
tier("base", p * 0.43 + c * 3.06 + img * 0.78 + ai * 3.81 + ao * 15.11)
Three-tier example:
len <= 128000
? tier("standard", p * 1.1 + c * 4.4)
: (len <= 1000000
? tier("medium", p * 2.2 + c * 8.8)
: tier("long", p * 4.4 + c * 17.6))
## Rules
1. Every leaf branch must be wrapped in tier("name", cost_expr)
2. Use English tier names, e.g. "base", "standard", "long_context"
3. Use len for tier conditions (not p), supports <, <=, >, >=
4. Multi-tier uses nested ternary: cond1 ? tier(...) : (cond2 ? tier(...) : tier(...))
5. Price coefficients are the provider's official $/1M tokens prices
6. If cache/image/audio don't need separate pricing, omit those variables; their tokens are included in p/c automatically
Please generate a billing expression based on the model information and pricing requirements provided.`
type LlmPromptHelperProps = {
modelName?: string
}
function LlmPromptHelper({ modelName }: LlmPromptHelperProps) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const prompt = useMemo(() => {
if (modelName) {
return LLM_PROMPT_TEMPLATE + `\n\nCurrent model: ${modelName}`
}
return LLM_PROMPT_TEMPLATE
}, [modelName])
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(prompt)
toast.success(t('Copied to clipboard'))
} catch {
toast.error(t('Failed to copy'))
}
}, [prompt, t])
return (
{t(
'Copy this prompt and send it to an LLM (e.g. ChatGPT / Claude) to help design your billing expression.'
)}