diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 36aaff5a7..a7bdcd309 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -919,6 +919,33 @@ const apiNodeCosts: Record = return `$${price.toFixed(2)}/Run` } }, + Veo3VideoGenerationNode: { + displayPrice: (node: LGraphNode): string => { + const modelWidget = node.widgets?.find( + (w) => w.name === 'model' + ) as IComboWidget + const generateAudioWidget = node.widgets?.find( + (w) => w.name === 'generate_audio' + ) as IComboWidget + + if (!modelWidget || !generateAudioWidget) { + return '$2.00-6.00/Run (varies with model & audio generation)' + } + + const model = String(modelWidget.value) + const generateAudio = + String(generateAudioWidget.value).toLowerCase() === 'true' + + if (model.includes('veo-3.0-fast-generate-001')) { + return generateAudio ? '$3.20/Run' : '$2.00/Run' + } else if (model.includes('veo-3.0-generate-001')) { + return generateAudio ? '$6.00/Run' : '$4.00/Run' + } + + // Default fallback + return '$2.00-6.00/Run' + } + }, LumaImageNode: { displayPrice: (node: LGraphNode): string => { const modelWidget = node.widgets?.find( @@ -1340,6 +1367,7 @@ export const useNodePricing = () => { FluxProKontextProNode: [], FluxProKontextMaxNode: [], VeoVideoGenerationNode: ['duration_seconds'], + Veo3VideoGenerationNode: ['model', 'generate_audio'], LumaVideoNode: ['model', 'resolution', 'duration'], LumaImageToVideoNode: ['model', 'resolution', 'duration'], LumaImageNode: ['model', 'aspect_ratio'], diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index 67f78d7d3..1633c79d1 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -393,6 +393,86 @@ describe('useNodePricing', () => { }) }) + describe('dynamic pricing - Veo3VideoGenerationNode', () => { + it('should return $2.00 for veo-3.0-fast-generate-001 without audio', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-fast-generate-001' }, + { name: 'generate_audio', value: false } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$2.00/Run') + }) + + it('should return $3.20 for veo-3.0-fast-generate-001 with audio', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-fast-generate-001' }, + { name: 'generate_audio', value: true } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$3.20/Run') + }) + + it('should return $4.00 for veo-3.0-generate-001 without audio', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-generate-001' }, + { name: 'generate_audio', value: false } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$4.00/Run') + }) + + it('should return $6.00 for veo-3.0-generate-001 with audio', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-generate-001' }, + { name: 'generate_audio', value: true } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$6.00/Run') + }) + + it('should return range when widgets are missing', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', []) + + const price = getNodeDisplayPrice(node) + expect(price).toBe( + '$2.00-6.00/Run (varies with model & audio generation)' + ) + }) + + it('should return range when only model widget is present', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-generate-001' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe( + '$2.00-6.00/Run (varies with model & audio generation)' + ) + }) + + it('should return range when only generate_audio widget is present', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'generate_audio', value: true } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe( + '$2.00-6.00/Run (varies with model & audio generation)' + ) + }) + }) + describe('dynamic pricing - LumaVideoNode', () => { it('should return $2.19 for ray-flash-2 4K 5s', () => { const { getNodeDisplayPrice } = useNodePricing() @@ -736,6 +816,13 @@ describe('useNodePricing', () => { expect(widgetNames).toEqual(['duration_seconds']) }) + it('should return correct widget names for Veo3VideoGenerationNode', () => { + const { getRelevantWidgetNames } = useNodePricing() + + const widgetNames = getRelevantWidgetNames('Veo3VideoGenerationNode') + expect(widgetNames).toEqual(['model', 'generate_audio']) + }) + it('should return correct widget names for LumaVideoNode', () => { const { getRelevantWidgetNames } = useNodePricing()