fix: require localized interval validation

This commit is contained in:
Heatherm Huang
2026-06-30 22:31:21 +08:00
parent 41bb15d302
commit 1158cf31bc
2 changed files with 30 additions and 33 deletions
@@ -16,6 +16,10 @@ function makeInterval(over: Partial<IntervalFormEntry>): IntervalFormEntry {
}
}
function t(key: string, params?: Record<string, unknown>): string {
return `${key}${params ? ` ${JSON.stringify(params)}` : ''}`
}
describe('validateIntervals', () => {
describe('token mode', () => {
it('rejects unbounded interval that is not last', () => {
@@ -23,7 +27,7 @@ describe('validateIntervals', () => {
makeInterval({ min_tokens: 0, max_tokens: null, input_price: 1, output_price: 1 }),
makeInterval({ min_tokens: 200000, max_tokens: 500000, input_price: 2, output_price: 2 }),
]
expect(validateIntervals(intervals, 'token')).toMatch(/unbounded interval/)
expect(validateIntervals(intervals, 'token', t)).toContain('unboundedLast')
})
it('accepts unbounded interval at the end', () => {
@@ -31,7 +35,7 @@ describe('validateIntervals', () => {
makeInterval({ min_tokens: 0, max_tokens: 200000, input_price: 1, output_price: 1 }),
makeInterval({ min_tokens: 200000, max_tokens: null, input_price: 2, output_price: 2 }),
]
expect(validateIntervals(intervals, 'token')).toBeNull()
expect(validateIntervals(intervals, 'token', t)).toBeNull()
})
it('rejects overlapping intervals', () => {
@@ -39,15 +43,15 @@ describe('validateIntervals', () => {
makeInterval({ min_tokens: 0, max_tokens: 250000, input_price: 1, output_price: 1 }),
makeInterval({ min_tokens: 200000, max_tokens: 500000, input_price: 2, output_price: 2 }),
]
expect(validateIntervals(intervals, 'token')).toMatch(/overlap/)
expect(validateIntervals(intervals, 'token', t)).toContain('overlap')
})
it('defaults mode to token when omitted', () => {
it('rejects unbounded interval in token mode', () => {
const intervals: IntervalFormEntry[] = [
makeInterval({ min_tokens: 0, max_tokens: null, input_price: 1, output_price: 1 }),
makeInterval({ min_tokens: 100, max_tokens: 200, input_price: 2, output_price: 2 }),
]
expect(validateIntervals(intervals)).toMatch(/unbounded interval/)
expect(validateIntervals(intervals, 'token', t)).toContain('unboundedLast')
})
})
@@ -58,22 +62,22 @@ describe('validateIntervals', () => {
makeInterval({ tier_label: '2K', per_request_price: 0.06 }),
makeInterval({ tier_label: '4K', per_request_price: 0.08 }),
]
expect(validateIntervals(intervals, 'image')).toBeNull()
expect(validateIntervals(intervals, 'per_request')).toBeNull()
expect(validateIntervals(intervals, 'image', t)).toBeNull()
expect(validateIntervals(intervals, 'per_request', t)).toBeNull()
})
it('still rejects negative prices', () => {
const intervals: IntervalFormEntry[] = [
makeInterval({ tier_label: '1K', per_request_price: -1 }),
]
expect(validateIntervals(intervals, 'image')).toMatch(/cannot be negative/)
expect(validateIntervals(intervals, 'image', t)).toContain('negativePrice')
})
it('still rejects max <= min on a single tier', () => {
const intervals: IntervalFormEntry[] = [
makeInterval({ tier_label: '1K', min_tokens: 100, max_tokens: 50, per_request_price: 0.04 }),
]
expect(validateIntervals(intervals, 'image')).toMatch(/must be greater/)
expect(validateIntervals(intervals, 'image', t)).toContain('maxGreaterThanMin')
})
})
})
+17 -24
View File
@@ -126,8 +126,8 @@ export function findModelConflict(models: string[]): [string, string] | null {
*/
export function validateIntervals(
intervals: IntervalFormEntry[],
mode: BillingMode = 'token',
t?: TranslateFn,
mode: BillingMode,
t: TranslateFn,
): string | null {
if (!intervals || intervals.length === 0) return null
@@ -145,26 +145,24 @@ export function validateIntervals(
}
function intervalValidationMessage(
t: TranslateFn | undefined,
t: TranslateFn,
key: string,
params: Record<string, unknown>,
fallback: string,
): string {
return t ? t(`admin.channels.intervalValidation.${key}`, params) : fallback
return t(`admin.channels.intervalValidation.${key}`, params)
}
function intervalPriceLabel(t: TranslateFn | undefined, key: string, fallback: string): string {
return t ? t(`admin.channels.intervalValidation.price.${key}`) : fallback
function intervalPriceLabel(t: TranslateFn, key: string): string {
return t(`admin.channels.intervalValidation.price.${key}`)
}
function validateSingleInterval(iv: IntervalFormEntry, idx: number, t?: TranslateFn): string | null {
function validateSingleInterval(iv: IntervalFormEntry, idx: number, t: TranslateFn): string | null {
const index = idx + 1
if (iv.min_tokens < 0) {
return intervalValidationMessage(
t,
'negativeMin',
{ index, value: iv.min_tokens },
`Interval #${index}: minimum token count (${iv.min_tokens}) cannot be negative`,
)
}
if (iv.max_tokens != null) {
@@ -173,7 +171,6 @@ function validateSingleInterval(iv: IntervalFormEntry, idx: number, t?: Translat
t,
'maxPositive',
{ index, value: iv.max_tokens },
`Interval #${index}: maximum token count (${iv.max_tokens}) must be greater than 0`,
)
}
if (iv.max_tokens <= iv.min_tokens) {
@@ -181,37 +178,35 @@ function validateSingleInterval(iv: IntervalFormEntry, idx: number, t?: Translat
t,
'maxGreaterThanMin',
{ index, max: iv.max_tokens, min: iv.min_tokens },
`Interval #${index}: maximum token count (${iv.max_tokens}) must be greater than minimum token count (${iv.min_tokens})`,
)
}
}
return validateIntervalPrices(iv, idx, t)
}
function validateIntervalPrices(iv: IntervalFormEntry, idx: number, t?: TranslateFn): string | null {
function validateIntervalPrices(iv: IntervalFormEntry, idx: number, t: TranslateFn): string | null {
const index = idx + 1
const prices: [string, string, number | string | null][] = [
['inputPrice', 'input price', iv.input_price],
['outputPrice', 'output price', iv.output_price],
['cacheWritePrice', 'cache write price', iv.cache_write_price],
['cacheReadPrice', 'cache read price', iv.cache_read_price],
['perRequestPrice', 'per-request price', iv.per_request_price],
const prices: [string, number | string | null][] = [
['inputPrice', iv.input_price],
['outputPrice', iv.output_price],
['cacheWritePrice', iv.cache_write_price],
['cacheReadPrice', iv.cache_read_price],
['perRequestPrice', iv.per_request_price],
]
for (const [key, fallbackName, val] of prices) {
for (const [key, val] of prices) {
if (val != null && val !== '' && Number(val) < 0) {
const field = intervalPriceLabel(t, key, fallbackName)
const field = intervalPriceLabel(t, key)
return intervalValidationMessage(
t,
'negativePrice',
{ index, field },
`Interval #${index}: ${field} cannot be negative`,
)
}
}
return null
}
function checkIntervalOverlap(sorted: IntervalFormEntry[], t?: TranslateFn): string | null {
function checkIntervalOverlap(sorted: IntervalFormEntry[], t: TranslateFn): string | null {
for (let i = 0; i < sorted.length; i++) {
// 无上限区间必须是最后一个
if (sorted[i].max_tokens == null && i < sorted.length - 1) {
@@ -219,7 +214,6 @@ function checkIntervalOverlap(sorted: IntervalFormEntry[], t?: TranslateFn): str
t,
'unboundedLast',
{ index: i + 1 },
`Interval #${i + 1}: an unbounded interval (empty maximum token count) must be last`,
)
}
if (i === 0) continue
@@ -231,7 +225,6 @@ function checkIntervalOverlap(sorted: IntervalFormEntry[], t?: TranslateFn): str
t,
'overlap',
{ previousIndex: i, currentIndex: i + 1, previousMax: prevMax, currentMin: sorted[i].min_tokens },
`Intervals #${i} and #${i + 1} overlap: previous upper bound (${prevMax}) is greater than current lower bound (${sorted[i].min_tokens})`,
)
}
}