diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 3f32413403..36aaff5a79 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -30,6 +30,25 @@ function safePricingExecution( } } +/** + * Helper function to calculate Runway duration-based pricing + * @param node - The LiteGraph node + * @returns Formatted price string + */ +const calculateRunwayDurationPrice = (node: LGraphNode): string => { + const durationWidget = node.widgets?.find( + (w) => w.name === 'duration' + ) as IComboWidget + + if (!durationWidget) return '$0.05/second' + + const duration = Number(durationWidget.value) + // If duration is 0 or NaN, don't fall back to 5 seconds - just use 0 + const validDuration = isNaN(duration) ? 5 : duration + const cost = (0.05 * validDuration).toFixed(2) + return `$${cost}/Run` +} + const pixversePricingCalculator = (node: LGraphNode): string => { const durationWidget = node.widgets?.find( (w) => w.name === 'duration_seconds' @@ -110,15 +129,27 @@ const apiNodeCosts: Record = FluxProUltraImageNode: { displayPrice: '$0.06/Run' }, + FluxProKontextProNode: { + displayPrice: '$0.04/Run' + }, + FluxProKontextMaxNode: { + displayPrice: '$0.08/Run' + }, IdeogramV1: { 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 turboWidget = node.widgets?.find( + (w) => w.name === 'turbo' + ) as IComboWidget + + if (!numImagesWidget) return '$0.02-0.06 x num_images/Run' const numImages = Number(numImagesWidget.value) || 1 - const cost = (0.06 * numImages).toFixed(2) + const turbo = String(turboWidget?.value).toLowerCase() === 'true' + const basePrice = turbo ? 0.02 : 0.06 + const cost = (basePrice * numImages).toFixed(2) return `$${cost}/Run` } }, @@ -127,10 +158,16 @@ const apiNodeCosts: Record = const numImagesWidget = node.widgets?.find( (w) => w.name === 'num_images' ) as IComboWidget - if (!numImagesWidget) return '$0.08 x num_images/Run' + const turboWidget = node.widgets?.find( + (w) => w.name === 'turbo' + ) as IComboWidget + + if (!numImagesWidget) return '$0.05-0.08 x num_images/Run' const numImages = Number(numImagesWidget.value) || 1 - const cost = (0.08 * numImages).toFixed(2) + const turbo = String(turboWidget?.value).toLowerCase() === 'true' + const basePrice = turbo ? 0.05 : 0.08 + const cost = (basePrice * numImages).toFixed(2) return `$${cost}/Run` } }, @@ -651,10 +688,10 @@ const apiNodeCosts: Record = if (duration.includes('5')) { if (resolution.includes('720p')) return '$0.3/Run' - if (resolution.includes('1080p')) return '~$0.3/Run' + if (resolution.includes('1080p')) return '$0.5/Run' } else if (duration.includes('10')) { - if (resolution.includes('720p')) return '$0.25/Run' - if (resolution.includes('1080p')) return '$1.0/Run' + if (resolution.includes('720p')) return '$0.4/Run' + if (resolution.includes('1080p')) return '$1.5/Run' } return '$0.3/Run' @@ -678,9 +715,9 @@ const apiNodeCosts: Record = if (duration.includes('5')) { if (resolution.includes('720p')) return '$0.2/Run' - if (resolution.includes('1080p')) return '~$0.45/Run' + if (resolution.includes('1080p')) return '$0.3/Run' } else if (duration.includes('10')) { - if (resolution.includes('720p')) return '$0.6/Run' + if (resolution.includes('720p')) return '$0.25/Run' if (resolution.includes('1080p')) return '$1.0/Run' } @@ -896,18 +933,11 @@ const apiNodeCosts: Record = } const model = String(modelWidget.value) - const aspectRatio = String(aspectRatioWidget.value) if (model.includes('photon-flash-1')) { - if (aspectRatio.includes('1:1')) return '$0.0045/Run' - if (aspectRatio.includes('16:9')) return '$0.0045/Run' - if (aspectRatio.includes('4:3')) return '$0.0046/Run' - if (aspectRatio.includes('21:9')) return '$0.0047/Run' + return '$0.0019/Run' } else if (model.includes('photon-1')) { - if (aspectRatio.includes('1:1')) return '$0.0172/Run' - if (aspectRatio.includes('16:9')) return '$0.0172/Run' - if (aspectRatio.includes('4:3')) return '$0.0176/Run' - if (aspectRatio.includes('21:9')) return '$0.0182/Run' + return '$0.0073/Run' } return '$0.0172/Run' @@ -918,31 +948,17 @@ const apiNodeCosts: Record = const modelWidget = node.widgets?.find( (w) => w.name === 'model' ) as IComboWidget - const aspectRatioWidget = node.widgets?.find( - (w) => w.name === 'aspect_ratio' - ) as IComboWidget if (!modelWidget) { - return '$0.0045-0.0182/Run (varies with model & aspect ratio)' + return '$0.0019-0.0073/Run (varies with model)' } const model = String(modelWidget.value) - const aspectRatio = aspectRatioWidget - ? String(aspectRatioWidget.value) - : null if (model.includes('photon-flash-1')) { - if (!aspectRatio) return '$0.0045/Run' - if (aspectRatio.includes('1:1')) return '~$0.0045/Run' - if (aspectRatio.includes('16:9')) return '~$0.0045/Run' - if (aspectRatio.includes('4:3')) return '~$0.0046/Run' - if (aspectRatio.includes('21:9')) return '~$0.0047/Run' + return '$0.0019/Run' } else if (model.includes('photon-1')) { - if (!aspectRatio) return '$0.0172/Run' - if (aspectRatio.includes('1:1')) return '~$0.0172/Run' - if (aspectRatio.includes('16:9')) return '~$0.0172/Run' - if (aspectRatio.includes('4:3')) return '~$0.0176/Run' - if (aspectRatio.includes('21:9')) return '~$0.0182/Run' + return '$0.0073/Run' } return '$0.0172/Run' @@ -1010,53 +1026,23 @@ const apiNodeCosts: Record = displayPrice: '$0.08/Run' }, RunwayImageToVideoNodeGen3a: { - displayPrice: (node: LGraphNode): string => { - const durationWidget = node.widgets?.find( - (w) => w.name === 'duration' - ) as IComboWidget - - if (!durationWidget) return '$0.05/second' - - const duration = Number(durationWidget.value) || 5 - const cost = (0.05 * duration).toFixed(2) - return `$${cost}/Run` - } + displayPrice: calculateRunwayDurationPrice }, RunwayImageToVideoNodeGen4: { - displayPrice: (node: LGraphNode): string => { - const durationWidget = node.widgets?.find( - (w) => w.name === 'duration' - ) as IComboWidget - - if (!durationWidget) return '$0.05/second' - - const duration = Number(durationWidget.value) || 5 - const cost = (0.05 * duration).toFixed(2) - return `$${cost}/Run` - } + displayPrice: calculateRunwayDurationPrice }, RunwayFirstLastFrameNode: { - displayPrice: (node: LGraphNode): string => { - const durationWidget = node.widgets?.find( - (w) => w.name === 'duration' - ) as IComboWidget - - if (!durationWidget) return '$0.05/second' - - const duration = Number(durationWidget.value) || 5 - const cost = (0.05 * duration).toFixed(2) - return `$${cost}/Run` - } + displayPrice: calculateRunwayDurationPrice }, // Rodin nodes - all have the same pricing structure Rodin3D_Regular: { displayPrice: '$0.4/Run' }, Rodin3D_Detail: { - displayPrice: '$1.2/Run' + displayPrice: '$0.4/Run' }, Rodin3D_Smooth: { - displayPrice: '$1.2/Run' + displayPrice: '$0.4/Run' }, Rodin3D_Sketch: { displayPrice: '$0.4/Run' @@ -1064,60 +1050,113 @@ const apiNodeCosts: Record = // Tripo nodes - using actual node names from ComfyUI TripoTextToModelNode: { displayPrice: (node: LGraphNode): string => { - const modelWidget = node.widgets?.find( - (w) => w.name === 'model' || w.name === 'model_version' + const quadWidget = node.widgets?.find( + (w) => w.name === 'quad' + ) as IComboWidget + const styleWidget = node.widgets?.find( + (w) => w.name === 'style' + ) as IComboWidget + const textureWidget = node.widgets?.find( + (w) => w.name === 'texture' ) as IComboWidget const textureQualityWidget = node.widgets?.find( (w) => w.name === 'texture_quality' ) as IComboWidget - if (!modelWidget) - return '$0.2-0.3/Run (varies with model & texture quality)' - - const model = String(modelWidget.value) - const textureQuality = String(textureQualityWidget?.value || 'standard') - - // V2.5 pricing - if (model.includes('v2.5') || model.includes('2.5')) { - return textureQuality.includes('detailed') ? '$0.3/Run' : '$0.2/Run' - } - // V2.0 pricing - else if (model.includes('v2.0') || model.includes('2.0')) { - return textureQuality.includes('detailed') ? '$0.3/Run' : '$0.2/Run' - } - // V1.4 or legacy pricing - else { - return '$0.2/Run' + if (!quadWidget || !styleWidget || !textureWidget) + return '$0.1-0.4/Run (varies with quad, style, texture & quality)' + + const quad = String(quadWidget.value).toLowerCase() === 'true' + const style = String(styleWidget.value).toLowerCase() + const texture = String(textureWidget.value).toLowerCase() === 'true' + const textureQuality = String( + textureQualityWidget?.value || 'standard' + ).toLowerCase() + + // Pricing logic based on CSV data + if (style.includes('none')) { + if (!quad) { + if (!texture) return '$0.10/Run' + else return '$0.15/Run' + } else { + if (textureQuality.includes('detailed')) { + if (!texture) return '$0.30/Run' + else return '$0.35/Run' + } else { + if (!texture) return '$0.20/Run' + else return '$0.25/Run' + } + } + } else { + // any style + if (!quad) { + if (!texture) return '$0.15/Run' + else return '$0.20/Run' + } else { + if (textureQuality.includes('detailed')) { + if (!texture) return '$0.35/Run' + else return '$0.40/Run' + } else { + if (!texture) return '$0.25/Run' + else return '$0.30/Run' + } + } } } }, TripoImageToModelNode: { displayPrice: (node: LGraphNode): string => { - const modelWidget = node.widgets?.find( - (w) => w.name === 'model' || w.name === 'model_version' + const quadWidget = node.widgets?.find( + (w) => w.name === 'quad' + ) as IComboWidget + const styleWidget = node.widgets?.find( + (w) => w.name === 'style' + ) as IComboWidget + const textureWidget = node.widgets?.find( + (w) => w.name === 'texture' ) as IComboWidget const textureQualityWidget = node.widgets?.find( (w) => w.name === 'texture_quality' ) as IComboWidget - if (!modelWidget) - return '$0.3-0.4/Run (varies with model & texture quality)' - - const model = String(modelWidget.value) - const textureQuality = String(textureQualityWidget?.value || 'standard') - - // V2.5 and V2.0 have same pricing structure - if ( - model.includes('v2.5') || - model.includes('2.5') || - model.includes('v2.0') || - model.includes('2.0') - ) { - return textureQuality.includes('detailed') ? '$0.4/Run' : '$0.3/Run' - } - // V1.4 or legacy pricing (image_to_model is always $0.3) - else { - return '$0.3/Run' + if (!quadWidget || !styleWidget || !textureWidget) + return '$0.2-0.5/Run (varies with quad, style, texture & quality)' + + const quad = String(quadWidget.value).toLowerCase() === 'true' + const style = String(styleWidget.value).toLowerCase() + const texture = String(textureWidget.value).toLowerCase() === 'true' + const textureQuality = String( + textureQualityWidget?.value || 'standard' + ).toLowerCase() + + // Pricing logic based on CSV data for Image to Model + if (style.includes('none')) { + if (!quad) { + if (!texture) return '$0.20/Run' + else return '$0.25/Run' + } else { + if (textureQuality.includes('detailed')) { + if (!texture) return '$0.40/Run' + else return '$0.45/Run' + } else { + if (!texture) return '$0.30/Run' + else return '$0.35/Run' + } + } + } else { + // any style + if (!quad) { + if (!texture) return '$0.25/Run' + else return '$0.30/Run' + } else { + if (textureQuality.includes('detailed')) { + if (!texture) return '$0.45/Run' + else return '$0.50/Run' + } else { + if (!texture) return '$0.35/Run' + else return '$0.40/Run' + } + } } } }, @@ -1136,6 +1175,68 @@ const apiNodeCosts: Record = return textureQuality.includes('detailed') ? '$0.2/Run' : '$0.1/Run' } }, + TripoConvertModelNode: { + displayPrice: '$0.10/Run' + }, + TripoRetargetRiggedModelNode: { + displayPrice: '$0.10/Run' + }, + TripoMultiviewToModelNode: { + displayPrice: (node: LGraphNode): string => { + const quadWidget = node.widgets?.find( + (w) => w.name === 'quad' + ) as IComboWidget + const styleWidget = node.widgets?.find( + (w) => w.name === 'style' + ) as IComboWidget + const textureWidget = node.widgets?.find( + (w) => w.name === 'texture' + ) as IComboWidget + const textureQualityWidget = node.widgets?.find( + (w) => w.name === 'texture_quality' + ) as IComboWidget + + if (!quadWidget || !styleWidget || !textureWidget) + return '$0.2-0.5/Run (varies with quad, style, texture & quality)' + + const quad = String(quadWidget.value).toLowerCase() === 'true' + const style = String(styleWidget.value).toLowerCase() + const texture = String(textureWidget.value).toLowerCase() === 'true' + const textureQuality = String( + textureQualityWidget?.value || 'standard' + ).toLowerCase() + + // Pricing logic based on CSV data for Multiview to Model (same as Image to Model) + if (style.includes('none')) { + if (!quad) { + if (!texture) return '$0.20/Run' + else return '$0.25/Run' + } else { + if (textureQuality.includes('detailed')) { + if (!texture) return '$0.40/Run' + else return '$0.45/Run' + } else { + if (!texture) return '$0.30/Run' + else return '$0.35/Run' + } + } + } else { + // any style + if (!quad) { + if (!texture) return '$0.25/Run' + else return '$0.30/Run' + } else { + if (textureQuality.includes('detailed')) { + if (!texture) return '$0.45/Run' + else return '$0.50/Run' + } else { + if (!texture) return '$0.35/Run' + else return '$0.40/Run' + } + } + } + } + }, // Google/Gemini nodes GeminiNode: { displayPrice: (node: LGraphNode): string => { @@ -1151,9 +1252,9 @@ const apiNodeCosts: Record = if (model.includes('veo-2.0')) { return '$0.5/second' } else if (model.includes('gemini-2.5-pro-preview-05-06')) { - return '$0.0035/$0.0008 per 1K tokens' + return '$0.00016/$0.0006 per 1K tokens' } else if (model.includes('gemini-2.5-flash-preview-04-17')) { - return '$0.0015/$0.0004 per 1K tokens' + return '$0.00125/$0.01 per 1K tokens' } // For other Gemini models, show token-based pricing info return 'Token-based' @@ -1233,9 +1334,11 @@ export const useNodePricing = () => { OpenAIDalle3: ['size', 'quality'], OpenAIDalle2: ['size', 'n'], OpenAIGPTImage1: ['quality', 'n'], - IdeogramV1: ['num_images'], - IdeogramV2: ['num_images'], + IdeogramV1: ['num_images', 'turbo'], + IdeogramV2: ['num_images', 'turbo'], IdeogramV3: ['rendering_speed', 'num_images'], + FluxProKontextProNode: [], + FluxProKontextMaxNode: [], VeoVideoGenerationNode: ['duration_seconds'], LumaVideoNode: ['model', 'resolution', 'duration'], LumaImageToVideoNode: ['model', 'resolution', 'duration'], @@ -1269,8 +1372,8 @@ export const useNodePricing = () => { RunwayImageToVideoNodeGen4: ['duration'], RunwayFirstLastFrameNode: ['duration'], // Tripo nodes - TripoTextToModelNode: ['model', 'model_version', 'texture_quality'], - TripoImageToModelNode: ['model', 'model_version', 'texture_quality'], + TripoTextToModelNode: ['quad', 'style', 'texture', 'texture_quality'], + TripoImageToModelNode: ['quad', 'style', 'texture', 'texture_quality'], TripoTextureNode: ['texture_quality'], // Google/Gemini nodes GeminiNode: ['model'], diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index 15240ee7db..67f78d7d38 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -603,7 +603,7 @@ describe('useNodePricing', () => { ]) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.25/Run') + expect(price).toBe('$0.4/Run') }) it('should return range when widgets are missing', () => { @@ -771,14 +771,14 @@ describe('useNodePricing', () => { const { getRelevantWidgetNames } = useNodePricing() const widgetNames = getRelevantWidgetNames('IdeogramV1') - expect(widgetNames).toEqual(['num_images']) + expect(widgetNames).toEqual(['num_images', 'turbo']) }) it('should return correct widget names for IdeogramV2', () => { const { getRelevantWidgetNames } = useNodePricing() const widgetNames = getRelevantWidgetNames('IdeogramV2') - expect(widgetNames).toEqual(['num_images']) + expect(widgetNames).toEqual(['num_images', 'turbo']) }) it('should return correct widget names for IdeogramV3', () => { @@ -832,7 +832,7 @@ describe('useNodePricing', () => { const node = createMockNode('IdeogramV1', []) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.06 x num_images/Run') + expect(price).toBe('$0.02-0.06 x num_images/Run') }) it('should fall back to static display when num_images widget is missing for IdeogramV2', () => { @@ -840,7 +840,7 @@ describe('useNodePricing', () => { const node = createMockNode('IdeogramV2', []) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.08 x num_images/Run') + expect(price).toBe('$0.05-0.08 x num_images/Run') }) it('should handle edge case when num_images value is 1 for IdeogramV1', () => { @@ -850,7 +850,7 @@ describe('useNodePricing', () => { ]) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.06/Run') // 0.06 * 1 + expect(price).toBe('$0.06/Run') // 0.06 * 1 (turbo=false by default) }) }) @@ -1036,13 +1036,15 @@ describe('useNodePricing', () => { 'duration' ]) expect(getRelevantWidgetNames('TripoTextToModelNode')).toEqual([ - 'model', - 'model_version', + 'quad', + 'style', + 'texture', 'texture_quality' ]) expect(getRelevantWidgetNames('TripoImageToModelNode')).toEqual([ - 'model', - 'model_version', + 'quad', + 'style', + 'texture', 'texture_quality' ]) }) @@ -1075,6 +1077,26 @@ describe('useNodePricing', () => { const price = getNodeDisplayPrice(node) expect(price).toBe('$0.05/second') }) + + it('should handle zero duration for RunwayImageToVideoNodeGen3a', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('RunwayImageToVideoNodeGen3a', [ + { name: 'duration', value: 0 } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.00/Run') // 0.05 * 0 = 0 + }) + + it('should handle NaN duration for RunwayImageToVideoNodeGen3a', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('RunwayImageToVideoNodeGen3a', [ + { name: 'duration', value: 'invalid' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.25/Run') // Falls back to 5 seconds: 0.05 * 5 + }) }) describe('Rodin nodes', () => { @@ -1091,7 +1113,7 @@ describe('useNodePricing', () => { const node = createMockNode('Rodin3D_Detail') const price = getNodeDisplayPrice(node) - expect(price).toBe('$1.2/Run') + expect(price).toBe('$0.4/Run') }) it('should return addon price for Rodin3D_Smooth', () => { @@ -1099,7 +1121,7 @@ describe('useNodePricing', () => { const node = createMockNode('Rodin3D_Smooth') const price = getNodeDisplayPrice(node) - expect(price).toBe('$1.2/Run') + expect(price).toBe('$0.4/Run') }) }) @@ -1107,44 +1129,53 @@ describe('useNodePricing', () => { it('should return v2.5 standard pricing for TripoTextToModelNode', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('TripoTextToModelNode', [ - { name: 'model_version', value: 'v2.5-20250123' }, + { name: 'quad', value: false }, + { name: 'style', value: 'any style' }, + { name: 'texture', value: false }, { name: 'texture_quality', value: 'standard' } ]) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.2/Run') + expect(price).toBe('$0.15/Run') // any style, no quad, no texture }) it('should return v2.5 detailed pricing for TripoTextToModelNode', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('TripoTextToModelNode', [ - { name: 'model_version', value: 'v2.5-20250123' }, + { name: 'quad', value: true }, + { name: 'style', value: 'any style' }, + { name: 'texture', value: false }, { name: 'texture_quality', value: 'detailed' } ]) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.3/Run') + expect(price).toBe('$0.35/Run') // any style, quad, no texture, detailed }) it('should return v2.0 detailed pricing for TripoImageToModelNode', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('TripoImageToModelNode', [ - { name: 'model_version', value: 'v2.0-20240919' }, + { name: 'quad', value: true }, + { name: 'style', value: 'any style' }, + { name: 'texture', value: false }, { name: 'texture_quality', value: 'detailed' } ]) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.4/Run') + expect(price).toBe('$0.45/Run') // any style, quad, no texture, detailed }) it('should return legacy pricing for TripoTextToModelNode', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('TripoTextToModelNode', [ - { name: 'model_version', value: 'v1.4-legacy' } + { name: 'quad', value: false }, + { name: 'style', value: 'none' }, + { name: 'texture', value: false }, + { name: 'texture_quality', value: 'standard' } ]) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.2/Run') + expect(price).toBe('$0.10/Run') // none style, no quad, no texture }) it('should return static price for TripoRefineNode', () => { @@ -1160,7 +1191,9 @@ describe('useNodePricing', () => { const node = createMockNode('TripoTextToModelNode', []) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.2-0.3/Run (varies with model & texture quality)') + expect(price).toBe( + '$0.1-0.4/Run (varies with quad, style, texture & quality)' + ) }) it('should return texture-based pricing for TripoTextureNode', () => { @@ -1176,25 +1209,85 @@ describe('useNodePricing', () => { expect(getNodeDisplayPrice(detailedNode)).toBe('$0.2/Run') }) - it('should handle various Tripo model version formats', () => { + it('should handle various Tripo parameter combinations', () => { const { getNodeDisplayPrice } = useNodePricing() - // Test different model version formats + // Test different parameter combinations const testCases = [ - { model: 'v2.0-20240919', expected: '$0.2/Run' }, - { model: 'v2.5-20250123', expected: '$0.2/Run' }, - { model: 'v1.4', expected: '$0.2/Run' }, - { model: 'unknown-model', expected: '$0.2/Run' } + { quad: false, style: 'none', texture: false, expected: '$0.10/Run' }, + { + quad: false, + style: 'any style', + texture: false, + expected: '$0.15/Run' + }, + { quad: true, style: 'none', texture: false, expected: '$0.20/Run' }, + { + quad: true, + style: 'any style', + texture: false, + expected: '$0.25/Run' + } ] - testCases.forEach(({ model, expected }) => { + testCases.forEach(({ quad, style, texture, expected }) => { const node = createMockNode('TripoTextToModelNode', [ - { name: 'model_version', value: model }, + { name: 'quad', value: quad }, + { name: 'style', value: style }, + { name: 'texture', value: texture }, { name: 'texture_quality', value: 'standard' } ]) expect(getNodeDisplayPrice(node)).toBe(expected) }) }) + + it('should return static price for TripoConvertModelNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('TripoConvertModelNode') + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.10/Run') + }) + + it('should return static price for TripoRetargetRiggedModelNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('TripoRetargetRiggedModelNode') + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.10/Run') + }) + + it('should return dynamic pricing for TripoMultiviewToModelNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + + // Test basic case - no style, no quad, no texture + const basicNode = createMockNode('TripoMultiviewToModelNode', [ + { name: 'quad', value: false }, + { name: 'style', value: 'none' }, + { name: 'texture', value: false }, + { name: 'texture_quality', value: 'standard' } + ]) + expect(getNodeDisplayPrice(basicNode)).toBe('$0.20/Run') + + // Test high-end case - any style, quad, texture, detailed + const highEndNode = createMockNode('TripoMultiviewToModelNode', [ + { name: 'quad', value: true }, + { name: 'style', value: 'stylized' }, + { name: 'texture', value: true }, + { name: 'texture_quality', value: 'detailed' } + ]) + expect(getNodeDisplayPrice(highEndNode)).toBe('$0.50/Run') + }) + + it('should return fallback for TripoMultiviewToModelNode without widgets', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('TripoMultiviewToModelNode', []) + + const price = getNodeDisplayPrice(node) + expect(price).toBe( + '$0.2-0.5/Run (varies with quad, style, texture & quality)' + ) + }) }) describe('Gemini and OpenAI Chat nodes', () => { @@ -1204,11 +1297,11 @@ describe('useNodePricing', () => { const testCases = [ { model: 'gemini-2.5-pro-preview-05-06', - expected: '$0.0035/$0.0008 per 1K tokens' + expected: '$0.00016/$0.0006 per 1K tokens' }, { model: 'gemini-2.5-flash-preview-04-17', - expected: '$0.0015/$0.0004 per 1K tokens' + expected: '$0.00125/$0.01 per 1K tokens' }, { model: 'unknown-gemini-model', expected: 'Token-based' } ] @@ -1315,7 +1408,7 @@ describe('useNodePricing', () => { // Test edge cases const testCases = [ - { duration: 0, expected: '$0.25/Run' }, // Falls back to 5 seconds (0 || 5) + { duration: 0, expected: '$0.00/Run' }, // Now correctly handles 0 duration { duration: 1, expected: '$0.05/Run' }, { duration: 30, expected: '$1.50/Run' } ] @@ -1359,8 +1452,8 @@ describe('useNodePricing', () => { const testCases = [ { nodeType: 'Rodin3D_Regular', expected: '$0.4/Run' }, { nodeType: 'Rodin3D_Sketch', expected: '$0.4/Run' }, - { nodeType: 'Rodin3D_Detail', expected: '$1.2/Run' }, - { nodeType: 'Rodin3D_Smooth', expected: '$1.2/Run' } + { nodeType: 'Rodin3D_Detail', expected: '$0.4/Run' }, + { nodeType: 'Rodin3D_Smooth', expected: '$0.4/Run' } ] testCases.forEach(({ nodeType, expected }) => { @@ -1371,24 +1464,42 @@ describe('useNodePricing', () => { }) describe('Comprehensive Tripo edge case testing', () => { - it('should handle TripoImageToModelNode with various model versions', () => { + it('should handle TripoImageToModelNode with various parameter combinations', () => { const { getNodeDisplayPrice } = useNodePricing() const testCases = [ - { model: 'v1.4-legacy', texture: 'standard', expected: '$0.3/Run' }, - { model: 'v2.0-20240919', texture: 'standard', expected: '$0.3/Run' }, - { model: 'v2.0-20240919', texture: 'detailed', expected: '$0.4/Run' }, - { model: 'v2.5-20250123', texture: 'standard', expected: '$0.3/Run' }, - { model: 'v2.5-20250123', texture: 'detailed', expected: '$0.4/Run' } + { quad: false, style: 'none', texture: false, expected: '$0.20/Run' }, + { quad: false, style: 'none', texture: true, expected: '$0.25/Run' }, + { + quad: true, + style: 'any style', + texture: true, + textureQuality: 'detailed', + expected: '$0.50/Run' + }, + { + quad: true, + style: 'any style', + texture: false, + textureQuality: 'standard', + expected: '$0.35/Run' + } ] - testCases.forEach(({ model, texture, expected }) => { - const node = createMockNode('TripoImageToModelNode', [ - { name: 'model_version', value: model }, - { name: 'texture_quality', value: texture } - ]) - expect(getNodeDisplayPrice(node)).toBe(expected) - }) + testCases.forEach( + ({ quad, style, texture, textureQuality, expected }) => { + const widgets = [ + { name: 'quad', value: quad }, + { name: 'style', value: style }, + { name: 'texture', value: texture } + ] + if (textureQuality) { + widgets.push({ name: 'texture_quality', value: textureQuality }) + } + const node = createMockNode('TripoImageToModelNode', widgets) + expect(getNodeDisplayPrice(node)).toBe(expected) + } + ) }) it('should return correct fallback for TripoImageToModelNode', () => { @@ -1396,17 +1507,19 @@ describe('useNodePricing', () => { const node = createMockNode('TripoImageToModelNode', []) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.3-0.4/Run (varies with model & texture quality)') + expect(price).toBe( + '$0.2-0.5/Run (varies with quad, style, texture & quality)' + ) }) it('should handle missing texture quality widget', () => { const { getNodeDisplayPrice } = useNodePricing() - const node = createMockNode('TripoTextToModelNode', [ - { name: 'model_version', value: 'v2.0-20240919' } - ]) + const node = createMockNode('TripoTextToModelNode', []) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.2/Run') // Default to standard texture pricing + expect(price).toBe( + '$0.1-0.4/Run (varies with quad, style, texture & quality)' + ) }) it('should handle missing model version widget', () => { @@ -1416,7 +1529,9 @@ describe('useNodePricing', () => { ]) const price = getNodeDisplayPrice(node) - expect(price).toBe('$0.2-0.3/Run (varies with model & texture quality)') + expect(price).toBe( + '$0.1-0.4/Run (varies with quad, style, texture & quality)' + ) }) }) })