From 29e1f55bb151b60bc19d43336258b1cfd0a33d6b Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Tue, 8 Jul 2025 01:57:22 -0700 Subject: [PATCH] [Hotfix] Cherry-pick pricing fixes to core/1.23 (#4383) --- src/composables/node/useNodePricing.ts | 229 ++++++++++++++--- .../composables/node/useNodePricing.test.ts | 232 +++++++++++++++++- 2 files changed, 421 insertions(+), 40 deletions(-) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 3e8891fd8..5e1d5f070 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -111,30 +111,55 @@ const apiNodeCosts: Record = displayPrice: '$0.06/Run' }, IdeogramV1: { - displayPrice: '$0.06/Run' + displayPrice: (node: LGraphNode): string => { + const numImagesWidget = node.widgets?.find( + (w) => w.name === 'num_images' + ) as IComboWidget + if (!numImagesWidget) return '$0.06 x num_images/Run' + + const numImages = Number(numImagesWidget.value) || 1 + const cost = (0.06 * numImages).toFixed(2) + return `$${cost}/Run` + } }, IdeogramV2: { - displayPrice: '$0.08/Run' + displayPrice: (node: LGraphNode): string => { + const numImagesWidget = node.widgets?.find( + (w) => w.name === 'num_images' + ) as IComboWidget + if (!numImagesWidget) return '$0.08 x num_images/Run' + + const numImages = Number(numImagesWidget.value) || 1 + const cost = (0.08 * numImages).toFixed(2) + return `$${cost}/Run` + } }, IdeogramV3: { displayPrice: (node: LGraphNode): string => { const renderingSpeedWidget = node.widgets?.find( (w) => w.name === 'rendering_speed' ) as IComboWidget + const numImagesWidget = node.widgets?.find( + (w) => w.name === 'num_images' + ) as IComboWidget if (!renderingSpeedWidget) - return '$0.03-0.08/Run (varies with rendering speed)' + return '$0.03-0.08 x num_images/Run (varies with rendering speed & num_images)' + + const numImages = Number(numImagesWidget?.value) || 1 + let basePrice = 0.06 // default balanced price const renderingSpeed = String(renderingSpeedWidget.value) if (renderingSpeed.toLowerCase().includes('quality')) { - return '$0.08/Run' + basePrice = 0.08 } else if (renderingSpeed.toLowerCase().includes('balanced')) { - return '$0.06/Run' + basePrice = 0.06 } else if (renderingSpeed.toLowerCase().includes('turbo')) { - return '$0.03/Run' + basePrice = 0.03 } - return '$0.06/Run' + const totalCost = (basePrice * numImages).toFixed(2) + return `$${totalCost}/Run` } }, KlingCameraControlI2VNode: { @@ -250,30 +275,33 @@ const apiNodeCosts: Record = const modelWidget = node.widgets?.find( (w) => w.name === 'model_name' ) as IComboWidget + const nWidget = node.widgets?.find( + (w) => w.name === 'n' + ) as IComboWidget if (!modelWidget) - return '$0.0035-0.028/Run (varies with modality & model)' + return '$0.0035-0.028 x n/Run (varies with modality & model)' const model = String(modelWidget.value) + const n = Number(nWidget?.value) || 1 + let basePrice = 0.014 // default if (modality.includes('text to image')) { - if (model.includes('kling-v1')) { - return '$0.0035/Run' - } else if ( - model.includes('kling-v1-5') || - model.includes('kling-v2') - ) { - return '$0.014/Run' + if (model.includes('kling-v1-5') || model.includes('kling-v2')) { + basePrice = 0.014 + } else if (model.includes('kling-v1')) { + basePrice = 0.0035 } } else if (modality.includes('image to image')) { - if (model.includes('kling-v1')) { - return '$0.0035/Run' - } else if (model.includes('kling-v1-5')) { - return '$0.028/Run' + if (model.includes('kling-v1-5')) { + basePrice = 0.028 + } else if (model.includes('kling-v1')) { + basePrice = 0.0035 } } - return '$0.014/Run' + const totalCost = (basePrice * n).toFixed(4) + return `$${totalCost}/Run` } }, KlingLipSyncAudioToVideoNode: { @@ -498,19 +526,26 @@ const apiNodeCosts: Record = const sizeWidget = node.widgets?.find( (w) => w.name === 'size' ) as IComboWidget + const nWidget = node.widgets?.find( + (w) => w.name === 'n' + ) as IComboWidget - if (!sizeWidget) return '$0.016-0.02/Run (varies with size)' + if (!sizeWidget) return '$0.016-0.02 x n/Run (varies with size & n)' const size = String(sizeWidget.value) + const n = Number(nWidget?.value) || 1 + let basePrice = 0.02 // default + if (size.includes('1024x1024')) { - return '$0.02/Run' + basePrice = 0.02 } else if (size.includes('512x512')) { - return '$0.018/Run' + basePrice = 0.018 } else if (size.includes('256x256')) { - return '$0.016/Run' + basePrice = 0.016 } - return '$0.02/Run' + const totalCost = (basePrice * n).toFixed(3) + return `$${totalCost}/Run` } }, OpenAIDalle3: { @@ -545,19 +580,30 @@ const apiNodeCosts: Record = const qualityWidget = node.widgets?.find( (w) => w.name === 'quality' ) as IComboWidget + const nWidget = node.widgets?.find( + (w) => w.name === 'n' + ) as IComboWidget - if (!qualityWidget) return '$0.011-0.30/Run (varies with quality)' + if (!qualityWidget) + return '$0.011-0.30 x n/Run (varies with quality & n)' const quality = String(qualityWidget.value) + const n = Number(nWidget?.value) || 1 + let basePriceRange = '$0.046-0.07' // default medium + if (quality.includes('high')) { - return '$0.167-0.30/Run' + basePriceRange = '$0.167-0.30' } else if (quality.includes('medium')) { - return '$0.046-0.07/Run' + basePriceRange = '$0.046-0.07' } else if (quality.includes('low')) { - return '$0.011-0.02/Run' + basePriceRange = '$0.011-0.02' } - return '$0.046-0.07/Run' + if (n === 1) { + return `${basePriceRange}/Run` + } else { + return `${basePriceRange} x ${n}/Run` + } } }, PikaImageToVideoNode2_2: { @@ -692,6 +738,42 @@ const apiNodeCosts: Record = RecraftCrispUpscaleNode: { displayPrice: '$0.004/Run' }, + RecraftGenerateColorFromImageNode: { + displayPrice: (node: LGraphNode): string => { + const nWidget = node.widgets?.find( + (w) => w.name === 'n' + ) as IComboWidget + if (!nWidget) return '$0.04 x n/Run' + + const n = Number(nWidget.value) || 1 + const cost = (0.04 * n).toFixed(2) + return `$${cost}/Run` + } + }, + RecraftGenerateImageNode: { + displayPrice: (node: LGraphNode): string => { + const nWidget = node.widgets?.find( + (w) => w.name === 'n' + ) as IComboWidget + if (!nWidget) return '$0.04 x n/Run' + + const n = Number(nWidget.value) || 1 + const cost = (0.04 * n).toFixed(2) + return `$${cost}/Run` + } + }, + RecraftGenerateVectorImageNode: { + displayPrice: (node: LGraphNode): string => { + const nWidget = node.widgets?.find( + (w) => w.name === 'n' + ) as IComboWidget + if (!nWidget) return '$0.08 x n/Run' + + const n = Number(nWidget.value) || 1 + const cost = (0.08 * n).toFixed(2) + return `$${cost}/Run` + } + }, RecraftImageInpaintingNode: { displayPrice: (node: LGraphNode): string => { const nWidget = node.widgets?.find( @@ -747,7 +829,16 @@ const apiNodeCosts: Record = } }, RecraftVectorizeImageNode: { - displayPrice: '$0.01/Run' + displayPrice: (node: LGraphNode): string => { + const nWidget = node.widgets?.find( + (w) => w.name === 'n' + ) as IComboWidget + if (!nWidget) return '$0.01 x n/Run' + + const n = Number(nWidget.value) || 1 + const cost = (0.01 * n).toFixed(2) + return `$${cost}/Run` + } }, StabilityStableImageSD_3_5Node: { displayPrice: (node: LGraphNode): string => { @@ -856,6 +947,63 @@ const apiNodeCosts: Record = return '$0.0172/Run' } + }, + MoonvalleyTxt2VideoNode: { + displayPrice: (node: LGraphNode): string => { + const lengthWidget = node.widgets?.find( + (w) => w.name === 'length' + ) as IComboWidget + + // If no length widget exists, default to 5s pricing + if (!lengthWidget) return '$1.50/Run' + + const length = String(lengthWidget.value) + if (length === '5s') { + return '$1.50/Run' + } else if (length === '10s') { + return '$3.00/Run' + } + + return '$1.50/Run' + } + }, + MoonvalleyImg2VideoNode: { + displayPrice: (node: LGraphNode): string => { + const lengthWidget = node.widgets?.find( + (w) => w.name === 'length' + ) as IComboWidget + + // If no length widget exists, default to 5s pricing + if (!lengthWidget) return '$1.50/Run' + + const length = String(lengthWidget.value) + if (length === '5s') { + return '$1.50/Run' + } else if (length === '10s') { + return '$3.00/Run' + } + + return '$1.50/Run' + } + }, + MoonvalleyVideo2VideoNode: { + displayPrice: (node: LGraphNode): string => { + const lengthWidget = node.widgets?.find( + (w) => w.name === 'length' + ) as IComboWidget + + // If no length widget exists, default to 5s pricing + if (!lengthWidget) return '$2.25/Run' + + const length = String(lengthWidget.value) + if (length === '5s') { + return '$2.25/Run' + } else if (length === '10s') { + return '$4.00/Run' + } + + return '$2.25/Run' + } } } @@ -890,14 +1038,16 @@ export const useNodePricing = () => { const widgetMap: Record = { KlingTextToVideoNode: ['mode', 'model_name', 'duration'], KlingImage2VideoNode: ['mode', 'model_name', 'duration'], - KlingImageGenerationNode: ['modality', 'model_name'], + KlingImageGenerationNode: ['modality', 'model_name', 'n'], KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'], KlingSingleImageVideoEffectNode: ['effect_scene'], KlingStartEndFrameNode: ['mode', 'model_name', 'duration'], OpenAIDalle3: ['size', 'quality'], - OpenAIDalle2: ['size'], - OpenAIGPTImage1: ['quality'], - IdeogramV3: ['rendering_speed'], + OpenAIDalle2: ['size', 'n'], + OpenAIGPTImage1: ['quality', 'n'], + IdeogramV1: ['num_images'], + IdeogramV2: ['num_images'], + IdeogramV3: ['rendering_speed', 'num_images'], VeoVideoGenerationNode: ['duration_seconds'], LumaVideoNode: ['model', 'resolution', 'duration'], LumaImageToVideoNode: ['model', 'resolution', 'duration'], @@ -918,7 +1068,14 @@ export const useNodePricing = () => { RecraftTextToImageNode: ['n'], RecraftImageToImageNode: ['n'], RecraftImageInpaintingNode: ['n'], - RecraftTextToVectorNode: ['n'] + RecraftTextToVectorNode: ['n'], + RecraftVectorizeImageNode: ['n'], + RecraftGenerateColorFromImageNode: ['n'], + RecraftGenerateImageNode: ['n'], + RecraftGenerateVectorImageNode: ['n'], + MoonvalleyTxt2VideoNode: ['length'], + MoonvalleyImg2VideoNode: ['length'], + MoonvalleyVideo2VideoNode: ['length'] } return widgetMap[nodeType] || [] } diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index 70ae5c621..bd16e18e8 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -227,7 +227,7 @@ describe('useNodePricing', () => { ]) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.02/Run') + expect(price).toBe('$0.020/Run') }) it('should return $0.018 for 512x512 size', () => { @@ -255,7 +255,7 @@ describe('useNodePricing', () => { const node = createMockNode('OpenAIDalle2', []) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.016-0.02/Run (varies with size)') + expect(price).toBe('$0.016-0.02 x n/Run (varies with size & n)') }) }) @@ -295,7 +295,7 @@ describe('useNodePricing', () => { const node = createMockNode('OpenAIGPTImage1', []) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.011-0.30/Run (varies with quality)') + expect(price).toBe('$0.011-0.30 x n/Run (varies with quality & n)') }) }) @@ -335,7 +335,31 @@ describe('useNodePricing', () => { const node = createMockNode('IdeogramV3', []) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.03-0.08/Run (varies with rendering speed)') + expect(price).toBe( + '$0.03-0.08 x num_images/Run (varies with rendering speed & num_images)' + ) + }) + + it('should multiply price by num_images for Quality rendering speed', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('IdeogramV3', [ + { name: 'rendering_speed', value: 'Quality' }, + { name: 'num_images', value: 3 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.24/Run') // 0.08 * 3 + }) + + it('should multiply price by num_images for Turbo rendering speed', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('IdeogramV3', [ + { name: 'rendering_speed', value: 'Turbo' }, + { name: 'num_images', value: 5 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.15/Run') // 0.03 * 5 }) }) @@ -742,6 +766,29 @@ describe('useNodePricing', () => { expect(widgetNames).toEqual([]) }) + describe('Ideogram nodes with num_images parameter', () => { + it('should return correct widget names for IdeogramV1', () => { + const { getRelevantWidgetNames } = useNodePricing() + + const widgetNames = getRelevantWidgetNames('IdeogramV1') + expect(widgetNames).toEqual(['num_images']) + }) + + it('should return correct widget names for IdeogramV2', () => { + const { getRelevantWidgetNames } = useNodePricing() + + const widgetNames = getRelevantWidgetNames('IdeogramV2') + expect(widgetNames).toEqual(['num_images']) + }) + + it('should return correct widget names for IdeogramV3', () => { + const { getRelevantWidgetNames } = useNodePricing() + + const widgetNames = getRelevantWidgetNames('IdeogramV3') + expect(widgetNames).toEqual(['rendering_speed', 'num_images']) + }) + }) + describe('Recraft nodes with n parameter', () => { it('should return correct widget names for RecraftTextToImageNode', () => { const { getRelevantWidgetNames } = useNodePricing() @@ -759,6 +806,54 @@ describe('useNodePricing', () => { }) }) + describe('Ideogram nodes dynamic pricing', () => { + it('should calculate dynamic pricing for IdeogramV1 based on num_images value', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('IdeogramV1', [ + { name: 'num_images', value: 3 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.18/Run') // 0.06 * 3 + }) + + it('should calculate dynamic pricing for IdeogramV2 based on num_images value', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('IdeogramV2', [ + { name: 'num_images', value: 4 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.32/Run') // 0.08 * 4 + }) + + it('should fall back to static display when num_images widget is missing for IdeogramV1', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('IdeogramV1', []) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.06 x num_images/Run') + }) + + it('should fall back to static display when num_images widget is missing for IdeogramV2', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('IdeogramV2', []) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.08 x num_images/Run') + }) + + it('should handle edge case when num_images value is 1 for IdeogramV1', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('IdeogramV1', [ + { name: 'num_images', value: 1 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.06/Run') // 0.06 * 1 + }) + }) + describe('Recraft nodes dynamic pricing', () => { it('should calculate dynamic pricing for RecraftTextToImageNode based on n value', () => { const { getNodeDisplayPrice } = useNodePricing() @@ -799,4 +894,133 @@ describe('useNodePricing', () => { }) }) }) + + describe('OpenAI nodes dynamic pricing with n parameter', () => { + it('should calculate dynamic pricing for OpenAIDalle2 based on size and n', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIDalle2', [ + { name: 'size', value: '1024x1024' }, + { name: 'n', value: 3 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.060/Run') // 0.02 * 3 + }) + + it('should calculate dynamic pricing for OpenAIGPTImage1 based on quality and n', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIGPTImage1', [ + { name: 'quality', value: 'low' }, + { name: 'n', value: 2 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.011-0.02 x 2/Run') + }) + + it('should fall back to static display when n widget is missing for OpenAIDalle2', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIDalle2', [ + { name: 'size', value: '512x512' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.018/Run') // n defaults to 1 + }) + }) + + describe('KlingImageGenerationNode dynamic pricing with n parameter', () => { + it('should calculate dynamic pricing for text-to-image with kling-v1', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('KlingImageGenerationNode', [ + { name: 'model_name', value: 'kling-v1' }, + { name: 'n', value: 4 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.0140/Run') // 0.0035 * 4 + }) + + it('should calculate dynamic pricing for text-to-image with kling-v1-5', () => { + const { getNodeDisplayPrice } = useNodePricing() + // Mock node without image input (text-to-image mode) + const node = createMockNode('KlingImageGenerationNode', [ + { name: 'model_name', value: 'kling-v1-5' }, + { name: 'n', value: 2 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.0280/Run') // For kling-v1-5 text-to-image: 0.014 * 2 + }) + + it('should fall back to static display when model widget is missing', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('KlingImageGenerationNode', []) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.0035-0.028 x n/Run (varies with modality & model)') + }) + }) + + describe('New Recraft nodes dynamic pricing', () => { + it('should calculate dynamic pricing for RecraftGenerateImageNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('RecraftGenerateImageNode', [ + { name: 'n', value: 3 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.12/Run') // 0.04 * 3 + }) + + it('should calculate dynamic pricing for RecraftVectorizeImageNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('RecraftVectorizeImageNode', [ + { name: 'n', value: 5 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.05/Run') // 0.01 * 5 + }) + + it('should calculate dynamic pricing for RecraftGenerateVectorImageNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('RecraftGenerateVectorImageNode', [ + { name: 'n', value: 2 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.16/Run') // 0.08 * 2 + }) + }) + + describe('Widget names for reactive updates', () => { + it('should include n parameter for OpenAI nodes', () => { + const { getRelevantWidgetNames } = useNodePricing() + + expect(getRelevantWidgetNames('OpenAIDalle2')).toEqual(['size', 'n']) + expect(getRelevantWidgetNames('OpenAIGPTImage1')).toEqual([ + 'quality', + 'n' + ]) + }) + + it('should include n parameter for Kling and new Recraft nodes', () => { + const { getRelevantWidgetNames } = useNodePricing() + + expect(getRelevantWidgetNames('KlingImageGenerationNode')).toEqual([ + 'modality', + 'model_name', + 'n' + ]) + expect(getRelevantWidgetNames('RecraftVectorizeImageNode')).toEqual(['n']) + expect(getRelevantWidgetNames('RecraftGenerateImageNode')).toEqual(['n']) + expect(getRelevantWidgetNames('RecraftGenerateVectorImageNode')).toEqual([ + 'n' + ]) + expect( + getRelevantWidgetNames('RecraftGenerateColorFromImageNode') + ).toEqual(['n']) + }) + }) })