mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix: require localized interval validation
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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})`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user