diff --git a/frontend/src/components/admin/channel/__tests__/types.spec.ts b/frontend/src/components/admin/channel/__tests__/types.spec.ts index 6594baa73f..04758012f1 100644 --- a/frontend/src/components/admin/channel/__tests__/types.spec.ts +++ b/frontend/src/components/admin/channel/__tests__/types.spec.ts @@ -16,6 +16,10 @@ function makeInterval(over: Partial): IntervalFormEntry { } } +function t(key: string, params?: Record): 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') }) }) }) diff --git a/frontend/src/components/admin/channel/types.ts b/frontend/src/components/admin/channel/types.ts index e270cedfce..bda497a172 100644 --- a/frontend/src/components/admin/channel/types.ts +++ b/frontend/src/components/admin/channel/types.ts @@ -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, - 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})`, ) } }