From fca0ea72f7089e45e78bcf31b50ca72b640768da Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:45:55 +0200 Subject: [PATCH] feat(api-nodes): add pricing for new LTXV-2 models (#6307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary For the upcoming LTXV API nodes. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6307-feat-api-nodes-add-pricing-for-new-LTXV-2-models-2986d73d365081db9994deffc219c6c4) by [Unito](https://www.unito.io) --- src/composables/node/useNodePricing.ts | 50 ++++++++++++++++++- .../composables/node/useNodePricing.test.ts | 50 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 26f954145..d1ae22169 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -169,6 +169,46 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => { : `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run` } +const ltxvPricingCalculator = (node: LGraphNode): string => { + const modelWidget = node.widgets?.find( + (w) => w.name === 'model' + ) as IComboWidget + const durationWidget = node.widgets?.find( + (w) => w.name === 'duration' + ) as IComboWidget + const resolutionWidget = node.widgets?.find( + (w) => w.name === 'resolution' + ) as IComboWidget + + const fallback = '$0.04-0.24/second' + if (!modelWidget || !durationWidget || !resolutionWidget) return fallback + + const model = String(modelWidget.value).toLowerCase() + const resolution = String(resolutionWidget.value).toLowerCase() + const seconds = parseFloat(String(durationWidget.value)) + const priceByModel: Record> = { + 'ltx-2 (pro)': { + '1920x1080': 0.06, + '2560x1440': 0.12, + '3840x2160': 0.24 + }, + 'ltx-2 (fast)': { + '1920x1080': 0.04, + '2560x1440': 0.08, + '3840x2160': 0.16 + } + } + + const modelTable = priceByModel[model] + if (!modelTable) return fallback + + const pps = modelTable[resolution] + if (!pps) return fallback + + const cost = (pps * seconds).toFixed(2) + return `$${cost}/Run` +} + // ---- constants ---- const SORA_SIZES = { BASIC: new Set(['720x1280', '1280x720']), @@ -1694,6 +1734,12 @@ const apiNodeCosts: Record = }, WanImageToImageApi: { displayPrice: '$0.03/Run' + }, + LtxvApiTextToVideo: { + displayPrice: ltxvPricingCalculator + }, + LtxvApiImageToVideo: { + displayPrice: ltxvPricingCalculator } } @@ -1796,7 +1842,9 @@ export const useNodePricing = () => { ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'], ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'], WanTextToVideoApi: ['duration', 'size'], - WanImageToVideoApi: ['duration', 'resolution'] + WanImageToVideoApi: ['duration', 'resolution'], + LtxvApiTextToVideo: ['model', 'duration', 'resolution'], + LtxvApiImageToVideo: ['model', '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 36697cbc8..34ff90f83 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -2189,4 +2189,54 @@ describe('useNodePricing', () => { expect(price).toBe('$0.05-0.15/second') }) }) + + describe('dynamic pricing - LtxvApiTextToVideo', () => { + it('should return $0.30 for Pro 1080p 5s', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('LtxvApiTextToVideo', [ + { name: 'model', value: 'LTX-2 (Pro)' }, + { name: 'duration', value: '5' }, + { name: 'resolution', value: '1920x1080' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.30/Run') // 0.06 * 5 + }) + + it('should parse "10s" duration strings', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('LtxvApiTextToVideo', [ + { name: 'model', value: 'LTX-2 (Fast)' }, + { name: 'duration', value: '10' }, + { name: 'resolution', value: '3840x2160' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$1.60/Run') // 0.16 * 10 + }) + + it('should fall back when a required widget is missing (no resolution)', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('LtxvApiTextToVideo', [ + { name: 'model', value: 'LTX-2 (Pro)' }, + { name: 'duration', value: '5' } + // missing resolution + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.04-0.24/second') + }) + + it('should fall back for unknown model', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('LtxvApiTextToVideo', [ + { name: 'model', value: 'LTX-3 (Pro)' }, + { name: 'duration', value: 5 }, + { name: 'resolution', value: '1920x1080' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.04-0.24/second') + }) + }) })