diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 59a961938..a2cd014d8 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -303,6 +303,46 @@ const apiNodeCosts: Record = FluxProKontextMaxNode: { displayPrice: '$0.08/Run' }, + Flux2ProImageNode: { + displayPrice: (node: LGraphNode): string => { + const widthW = node.widgets?.find( + (w) => w.name === 'width' + ) as IComboWidget + const heightW = node.widgets?.find( + (w) => w.name === 'height' + ) as IComboWidget + + const w = Number(widthW?.value) + const h = Number(heightW?.value) + if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) { + // global min/max for this node given schema bounds (1MP..4MP output) + return '$0.03–$0.15/Run' + } + + // Is the 'images' input connected? + const imagesInput = node.inputs?.find( + (i) => i.name === 'images' + ) as INodeInputSlot + const hasRefs = + typeof imagesInput?.link !== 'undefined' && imagesInput.link != null + + // Output cost: ceil((w*h)/MP); first MP $0.03, each additional $0.015 + const MP = 1024 * 1024 + const outMP = Math.max(1, Math.floor((w * h + MP - 1) / MP)) + const outputCost = 0.03 + 0.015 * Math.max(outMP - 1, 0) + + if (hasRefs) { + // Unknown ref count/size on the frontend: + // min extra is $0.015, max extra is $0.120 (8 MP cap / 8 refs) + const minTotal = outputCost + 0.015 + const maxTotal = outputCost + 0.12 + return `~$${parseFloat(minTotal.toFixed(3))}–$${parseFloat(maxTotal.toFixed(3))}/Run` + } + + // Precise text-to-image price + return `$${parseFloat(outputCost.toFixed(3))}/Run` + } + }, OpenAIVideoSora2: { displayPrice: sora2PricingCalculator }, @@ -1809,6 +1849,7 @@ export const useNodePricing = () => { IdeogramV3: ['rendering_speed', 'num_images', 'character_image'], FluxProKontextProNode: [], FluxProKontextMaxNode: [], + Flux2ProImageNode: ['width', 'height', 'images'], VeoVideoGenerationNode: ['duration_seconds'], Veo3VideoGenerationNode: ['model', 'generate_audio'], LumaVideoNode: ['model', 'resolution', 'duration'], diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index 9049ed378..dad9e878a 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -94,6 +94,42 @@ describe('useNodePricing', () => { }) }) + describe('dynamic pricing - Flux2ProImageNode', () => { + it('should return precise price for text-to-image 1024x1024 (no refs)', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Flux2ProImageNode', [ + { name: 'width', value: 1024 }, + { name: 'height', value: 1024 } + ]) + + // 1024x1024 => 1 MP => $0.03 + expect(getNodeDisplayPrice(node)).toBe('$0.03/Run') + }) + + it('should return minimum estimate when refs are connected (1024x1024)', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode( + 'Flux2ProImageNode', + [ + { name: 'width', value: 1024 }, + { name: 'height', value: 1024 } + ], + true, + // connect the 'images' input + [{ name: 'images', connected: true }] + ) + + // 1024x1024 => 1 MP output = $0.03, min input add = $0.015 => ~$0.045 min + expect(getNodeDisplayPrice(node)).toBe('~$0.045–$0.15/Run') + }) + + it('should show fallback when width/height are missing', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Flux2ProImageNode', []) + expect(getNodeDisplayPrice(node)).toBe('$0.03–$0.15/Run') + }) + }) + describe('dynamic pricing - KlingTextToVideoNode', () => { it('should return high price for kling-v2-1-master model', () => { const { getNodeDisplayPrice } = useNodePricing()