Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/composables/node/useNodePricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,40 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
? minStr
: `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run`
}
const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => {
const modelW = node.widgets?.find((w) => w.name === 'model') as IComboWidget
const durationW = node.widgets?.find(
(w) => w.name === 'duration' || w.name === 'duration_s'
) as IComboWidget
const sizeW = node.widgets?.find((w) => w.name === 'size') as IComboWidget

// precise missing-widget messages (fixes the failing test)
Comment thread
marawan206 marked this conversation as resolved.
Outdated
if (!modelW || !durationW) return 'Set model, duration & size'
if (!sizeW) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'

const model = String(modelW.value ?? '').toLowerCase()
const duration = Number(durationW.value)
const size = String(sizeW.value ?? '').toLowerCase()

if (!duration || Number.isNaN(duration)) return 'Set duration (4/8/12)'
if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'

const SORA720 = ['720x1280', '1280x720']
const SORA_PRO_HIRES = ['1024x1792', '1792x1024']

if (model.includes('sora-2-pro')) {
let perSec: number | null = null
if (SORA_PRO_HIRES.includes(size)) perSec = 0.5
else if (SORA720.includes(size)) perSec = 0.3
Comment thread
marawan206 marked this conversation as resolved.
Outdated
else return 'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024'
return `$${(perSec * duration).toFixed(2)}/Run`
}

// plain sora-2: 720p only
if (!SORA720.includes(size))
return 'sora-2 supports only 720x1280 or 1280x720'
Comment thread
marawan206 marked this conversation as resolved.
Outdated
return `$${(0.1 * duration).toFixed(2)}/Run`
}
/**
* Static pricing data for API nodes, now supporting both strings and functions
*/
Expand All @@ -195,6 +228,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
FluxProKontextMaxNode: {
displayPrice: '$0.08/Run'
},
OpenAIVideoSora2: {
displayPrice: sora2PricingCalculator
},
IdeogramV1: {
displayPrice: (node: LGraphNode): string => {
const numImagesWidget = node.widgets?.find(
Expand Down Expand Up @@ -1589,6 +1625,7 @@ export const useNodePricing = () => {
MinimaxHailuoVideoNode: ['resolution', 'duration'],
OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size', 'n'],
OpenAIVideoSora2: ['model', 'size', 'duration'],
OpenAIGPTImage1: ['quality', 'n'],
IdeogramV1: ['num_images', 'turbo'],
IdeogramV2: ['num_images', 'turbo'],
Expand Down
110 changes: 110 additions & 0 deletions tests-ui/tests/composables/node/useNodePricing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,117 @@ describe('useNodePricing', () => {
expect(price).toBe('$0.04-0.12/Run (varies with size & quality)')
})
})
// ============================== OpenAIVideoSora2 ==============================
describe('dynamic pricing - OpenAIVideoSora2', () => {
it('should require model, duration & size when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [])
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
})

it('should require duration when duration is invalid or zero', () => {
const { getNodeDisplayPrice } = useNodePricing()
const nodeNaN = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 'oops' },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(nodeNaN)).toBe('Set duration (4/8/12)')

const nodeZero = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 0 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(nodeZero)).toBe('Set duration (4/8/12)')
})

it('should require size when size is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 }
])
expect(getNodeDisplayPrice(node)).toBe(
'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'
)
})

it('should compute pricing for sora-2-pro with 1024x1792', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '1024x1792' }
])
expect(getNodeDisplayPrice(node)).toBe('$4.00/Run') // 0.5 * 8
})

it('should compute pricing for sora-2-pro with 720x1280', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 12 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
})

it('should reject unsupported size for sora-2-pro', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '640x640' }
])
expect(getNodeDisplayPrice(node)).toBe(
'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024'
)
})

it('should compute pricing for sora-2 (720x1280 only)', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2' },
{ name: 'duration', value: 10 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(node)).toBe('$1.00/Run') // 0.1 * 10
})

it('should reject non-720 sizes for sora-2', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '1024x1792' }
])
expect(getNodeDisplayPrice(node)).toBe(
'sora-2 supports only 720x1280 or 1280x720'
)
})
it('should accept duration_s alias for duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration_s', value: 4 },
{ name: 'size', value: '1792x1024' }
])
expect(getNodeDisplayPrice(node)).toBe('$2.00/Run') // 0.5 * 4
})

it('should be case-insensitive for model and size', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'SoRa-2-PrO' },
{ name: 'duration', value: 12 },
{ name: 'size', value: '1280x720' }
])
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
})
})

// ============================== MinimaxHailuoVideoNode ==============================
describe('dynamic pricing - MinimaxHailuoVideoNode', () => {
it('should return $0.28 for 6s duration and 768P resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
Expand Down