feat: add Seedance 2.0 models

This commit is contained in:
saturn
2026-04-02 19:16:00 +08:00
parent 9703714b69
commit 71ef6ff818
21 changed files with 811 additions and 38 deletions

View File

@@ -5,6 +5,7 @@ import {
calcLipSync,
calcText,
calcVideo,
calcVideoByTokens,
calcVoice,
calcVoiceDesign,
} from '@/lib/billing/cost'
@@ -87,6 +88,37 @@ describe('billing/cost', () => {
})).toThrow('Unsupported video capability pricing')
})
it('estimates Seedance 2.0 video pricing from official token formula', () => {
const cost = calcVideo('doubao-seedance-2-0-260128', '720p', 1, {
resolution: '720p',
duration: 5,
aspectRatio: '16:9',
containsVideoInput: false,
})
expect(cost).toBeCloseTo(4.968, 8)
})
it('applies Seedance 2.0 video-input token floor for quoted pricing', () => {
const cost = calcVideo('doubao-seedance-2-0-fast-260128', '720p', 1, {
resolution: '720p',
duration: 5,
aspectRatio: '16:9',
containsVideoInput: true,
inputVideoSeconds: 2,
})
expect(cost).toBeCloseTo(4.2768, 8)
})
it('settles Seedance 2.0 videos from exact usage tokens', () => {
const cost = calcVideoByTokens('doubao-seedance-2-0-260128', 120_000, {
containsVideoInput: false,
})
expect(cost).toBeCloseTo(5.52, 8)
})
it('supports minimax capability-aware video pricing', () => {
const hailuoNormal = calcVideo('minimax-hailuo-2.3', '768p', 1, {
generationMode: 'normal',

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { calcText, calcVoice } from '@/lib/billing/cost'
import { calcText, calcVideo, calcVoice } from '@/lib/billing/cost'
import type { TaskBillingInfo } from '@/lib/task/types'
const ledgerMock = vi.hoisted(() => ({
@@ -230,6 +230,34 @@ describe('billing/service', () => {
}
}
function buildSeedance2VideoTaskInfo(
overrides: Partial<Extract<TaskBillingInfo, { billable: true }>> = {},
): Extract<TaskBillingInfo, { billable: true }> {
return {
billable: true,
source: 'task',
taskType: 'video_panel',
apiType: 'video',
model: 'doubao-seedance-2-0-260128',
quantity: 1,
unit: 'video',
maxFrozenCost: calcVideo('doubao-seedance-2-0-260128', '720p', 1, {
resolution: '720p',
duration: 5,
aspectRatio: '16:9',
containsVideoInput: false,
}),
action: 'video_panel_generate',
metadata: {
resolution: '720p',
duration: 5,
aspectRatio: '16:9',
containsVideoInput: false,
},
...overrides,
}
}
it('prepareTaskBilling handles OFF/SHADOW/ENFORCE paths', async () => {
modeMock.getBillingMode.mockResolvedValueOnce('OFF')
const off = await prepareTaskBilling({
@@ -468,6 +496,25 @@ describe('billing/service', () => {
expect((settled as Extract<TaskBillingInfo, { billable: true }>).chargedCost).toBeCloseTo(calcVoice(50), 8)
})
it('settleTaskBilling charges Seedance 2.0 videos from exact usage tokens', async () => {
ledgerMock.confirmChargeWithRecord.mockResolvedValueOnce(true)
const settled = await settleTaskBilling({
id: 'task_seedance2_actual_tokens',
userId: 'u1',
projectId: 'p1',
billingInfo: buildSeedance2VideoTaskInfo({
modeSnapshot: 'ENFORCE',
freezeId: 'freeze_seedance2_actual_tokens',
}),
}, {
result: { actualVideoTokens: 120_000 },
})
expect(ledgerMock.increasePendingFreezeAmount).toHaveBeenCalledTimes(1)
expect((settled as Extract<TaskBillingInfo, { billable: true }>).chargedCost).toBeCloseTo(5.52, 8)
})
it('settleTaskBilling keeps quoted charge when text usage has no token counts', async () => {
const quoted = calcText('anthropic/claude-sonnet-4', 500, 500)
const textBillingInfo: Extract<TaskBillingInfo, { billable: true }> = {