From f086377307dd14b3336ad035c808643b7249f8d7 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Mon, 22 Sep 2025 21:33:00 +0300 Subject: [PATCH] add pricing for new api nodes (#5724) ## Summary Added prices for the new upcoming API nodes. Backport required. --- src/composables/node/useNodePricing.ts | 69 +++++++- .../composables/node/useNodePricing.test.ts | 155 ++++++++++++++++++ 2 files changed, 223 insertions(+), 1 deletion(-) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index e85e6adb6..91f957463 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -1548,6 +1548,71 @@ const apiNodeCosts: Record = }, ByteDanceImageReferenceNode: { displayPrice: byteDanceVideoPricingCalculator + }, + WanTextToVideoApi: { + displayPrice: (node: LGraphNode): string => { + const durationWidget = node.widgets?.find( + (w) => w.name === 'duration' + ) as IComboWidget + const resolutionWidget = node.widgets?.find( + (w) => w.name === 'size' + ) as IComboWidget + + if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second' + + const seconds = parseFloat(String(durationWidget.value)) + const resolutionStr = String(resolutionWidget.value).toLowerCase() + + const resKey = resolutionStr.includes('1080') + ? '1080p' + : resolutionStr.includes('720') + ? '720p' + : resolutionStr.includes('480') + ? '480p' + : resolutionStr.match(/^\s*(\d{3,4}p)/)?.[1] ?? '' + + const pricePerSecond: Record = { + '480p': 0.05, + '720p': 0.1, + '1080p': 0.15 + } + + const pps = pricePerSecond[resKey] + if (isNaN(seconds) || !pps) return '$0.05-0.15/second' + + const cost = (pps * seconds).toFixed(2) + return `$${cost}/Run` + } + }, + WanImageToVideoApi: { + displayPrice: (node: LGraphNode): string => { + const durationWidget = node.widgets?.find( + (w) => w.name === 'duration' + ) as IComboWidget + const resolutionWidget = node.widgets?.find( + (w) => w.name === 'resolution' + ) as IComboWidget + + if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second' + + const seconds = parseFloat(String(durationWidget.value)) + const resolution = String(resolutionWidget.value).trim().toLowerCase() + + const pricePerSecond: Record = { + '480p': 0.05, + '720p': 0.1, + '1080p': 0.15 + } + + const pps = pricePerSecond[resolution] + if (isNaN(seconds) || !pps) return '$0.05-0.15/second' + + const cost = (pps * seconds).toFixed(2) + return `$${cost}/Run` + } + }, + WanTextToImageApi: { + displayPrice: '$0.03/Run' } } @@ -1647,7 +1712,9 @@ export const useNodePricing = () => { ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'], ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'], ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'], - ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'] + ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'], + WanTextToVideoApi: ['duration', 'size'], + WanImageToVideoApi: ['duration', 'resolution'] } return widgetMap[nodeType] || [] } diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index 6cd76cb75..32b18ed68 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -1894,4 +1894,159 @@ describe('useNodePricing', () => { expect(getNodeDisplayPrice(missingDuration)).toBe('Token-based') }) }) + + describe('dynamic pricing - WanTextToVideoApi', () => { + it('should return $1.50 for 10s at 1080p', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanTextToVideoApi', [ + { name: 'duration', value: '10' }, + { name: 'size', value: '1080p: 4:3 (1632x1248)' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$1.50/Run') // 0.15 * 10 + }) + + it('should return $0.50 for 5s at 720p', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanTextToVideoApi', [ + { name: 'duration', value: 5 }, + { name: 'size', value: '720p: 16:9 (1280x720)' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.50/Run') // 0.10 * 5 + }) + + it('should return $0.15 for 3s at 480p', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanTextToVideoApi', [ + { name: 'duration', value: '3' }, + { name: 'size', value: '480p: 1:1 (624x624)' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.15/Run') // 0.05 * 3 + }) + + it('should fall back when widgets are missing', () => { + const { getNodeDisplayPrice } = useNodePricing() + const missingBoth = createMockNode('WanTextToVideoApi', []) + const missingSize = createMockNode('WanTextToVideoApi', [ + { name: 'duration', value: '5' } + ]) + const missingDuration = createMockNode('WanTextToVideoApi', [ + { name: 'size', value: '1080p' } + ]) + + expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second') + expect(getNodeDisplayPrice(missingSize)).toBe('$0.05-0.15/second') + expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second') + }) + + it('should fall back on invalid duration', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanTextToVideoApi', [ + { name: 'duration', value: 'invalid' }, + { name: 'size', value: '1080p' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.05-0.15/second') + }) + + it('should fall back on unknown resolution', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanTextToVideoApi', [ + { name: 'duration', value: '10' }, + { name: 'size', value: '2K' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.05-0.15/second') + }) + }) + + describe('dynamic pricing - WanImageToVideoApi', () => { + it('should return $0.80 for 8s at 720p', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanImageToVideoApi', [ + { name: 'duration', value: 8 }, + { name: 'resolution', value: '720p' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.80/Run') // 0.10 * 8 + }) + + it('should return $0.60 for 12s at 480P', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanImageToVideoApi', [ + { name: 'duration', value: '12' }, + { name: 'resolution', value: '480P' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.60/Run') // 0.05 * 12 + }) + + it('should return $1.50 for 10s at 1080p', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanImageToVideoApi', [ + { name: 'duration', value: '10' }, + { name: 'resolution', value: '1080p' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$1.50/Run') // 0.15 * 10 + }) + + it('should handle "5s" string duration at 1080P', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanImageToVideoApi', [ + { name: 'duration', value: '5s' }, + { name: 'resolution', value: '1080P' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.75/Run') // 0.15 * 5 + }) + + it('should fall back when widgets are missing', () => { + const { getNodeDisplayPrice } = useNodePricing() + const missingBoth = createMockNode('WanImageToVideoApi', []) + const missingRes = createMockNode('WanImageToVideoApi', [ + { name: 'duration', value: '5' } + ]) + const missingDuration = createMockNode('WanImageToVideoApi', [ + { name: 'resolution', value: '1080p' } + ]) + + expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second') + expect(getNodeDisplayPrice(missingRes)).toBe('$0.05-0.15/second') + expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second') + }) + + it('should fall back on invalid duration', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanImageToVideoApi', [ + { name: 'duration', value: 'invalid' }, + { name: 'resolution', value: '720p' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.05-0.15/second') + }) + + it('should fall back on unknown resolution', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('WanImageToVideoApi', [ + { name: 'duration', value: '10' }, + { name: 'resolution', value: 'weird-res' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.05-0.15/second') + }) + }) })