Skip to content

Commit 77ca6d5

Browse files
[backport 1.27] add pricing for new api nodes (#5725)
Backport of #5724 to `core/1.27` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5725-backport-1-27-add-pricing-for-new-api-nodes-2766d73d3650819eac1de10bc967deb2) by [Unito](https://www.unito.io) Co-authored-by: Alexander Piskun <[email protected]>
1 parent 12f23da commit 77ca6d5

File tree

2 files changed

+223
-1
lines changed

2 files changed

+223
-1
lines changed

src/composables/node/useNodePricing.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,71 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
15481548
},
15491549
ByteDanceImageReferenceNode: {
15501550
displayPrice: byteDanceVideoPricingCalculator
1551+
},
1552+
WanTextToVideoApi: {
1553+
displayPrice: (node: LGraphNode): string => {
1554+
const durationWidget = node.widgets?.find(
1555+
(w) => w.name === 'duration'
1556+
) as IComboWidget
1557+
const resolutionWidget = node.widgets?.find(
1558+
(w) => w.name === 'size'
1559+
) as IComboWidget
1560+
1561+
if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second'
1562+
1563+
const seconds = parseFloat(String(durationWidget.value))
1564+
const resolutionStr = String(resolutionWidget.value).toLowerCase()
1565+
1566+
const resKey = resolutionStr.includes('1080')
1567+
? '1080p'
1568+
: resolutionStr.includes('720')
1569+
? '720p'
1570+
: resolutionStr.includes('480')
1571+
? '480p'
1572+
: resolutionStr.match(/^\s*(\d{3,4}p)/)?.[1] ?? ''
1573+
1574+
const pricePerSecond: Record<string, number> = {
1575+
'480p': 0.05,
1576+
'720p': 0.1,
1577+
'1080p': 0.15
1578+
}
1579+
1580+
const pps = pricePerSecond[resKey]
1581+
if (isNaN(seconds) || !pps) return '$0.05-0.15/second'
1582+
1583+
const cost = (pps * seconds).toFixed(2)
1584+
return `$${cost}/Run`
1585+
}
1586+
},
1587+
WanImageToVideoApi: {
1588+
displayPrice: (node: LGraphNode): string => {
1589+
const durationWidget = node.widgets?.find(
1590+
(w) => w.name === 'duration'
1591+
) as IComboWidget
1592+
const resolutionWidget = node.widgets?.find(
1593+
(w) => w.name === 'resolution'
1594+
) as IComboWidget
1595+
1596+
if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second'
1597+
1598+
const seconds = parseFloat(String(durationWidget.value))
1599+
const resolution = String(resolutionWidget.value).trim().toLowerCase()
1600+
1601+
const pricePerSecond: Record<string, number> = {
1602+
'480p': 0.05,
1603+
'720p': 0.1,
1604+
'1080p': 0.15
1605+
}
1606+
1607+
const pps = pricePerSecond[resolution]
1608+
if (isNaN(seconds) || !pps) return '$0.05-0.15/second'
1609+
1610+
const cost = (pps * seconds).toFixed(2)
1611+
return `$${cost}/Run`
1612+
}
1613+
},
1614+
WanTextToImageApi: {
1615+
displayPrice: '$0.03/Run'
15511616
}
15521617
}
15531618

@@ -1647,7 +1712,9 @@ export const useNodePricing = () => {
16471712
ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'],
16481713
ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'],
16491714
ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'],
1650-
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution']
1715+
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
1716+
WanTextToVideoApi: ['duration', 'size'],
1717+
WanImageToVideoApi: ['duration', 'resolution']
16511718
}
16521719
return widgetMap[nodeType] || []
16531720
}

tests-ui/tests/composables/node/useNodePricing.test.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1894,4 +1894,159 @@ describe('useNodePricing', () => {
18941894
expect(getNodeDisplayPrice(missingDuration)).toBe('Token-based')
18951895
})
18961896
})
1897+
1898+
describe('dynamic pricing - WanTextToVideoApi', () => {
1899+
it('should return $1.50 for 10s at 1080p', () => {
1900+
const { getNodeDisplayPrice } = useNodePricing()
1901+
const node = createMockNode('WanTextToVideoApi', [
1902+
{ name: 'duration', value: '10' },
1903+
{ name: 'size', value: '1080p: 4:3 (1632x1248)' }
1904+
])
1905+
1906+
const price = getNodeDisplayPrice(node)
1907+
expect(price).toBe('$1.50/Run') // 0.15 * 10
1908+
})
1909+
1910+
it('should return $0.50 for 5s at 720p', () => {
1911+
const { getNodeDisplayPrice } = useNodePricing()
1912+
const node = createMockNode('WanTextToVideoApi', [
1913+
{ name: 'duration', value: 5 },
1914+
{ name: 'size', value: '720p: 16:9 (1280x720)' }
1915+
])
1916+
1917+
const price = getNodeDisplayPrice(node)
1918+
expect(price).toBe('$0.50/Run') // 0.10 * 5
1919+
})
1920+
1921+
it('should return $0.15 for 3s at 480p', () => {
1922+
const { getNodeDisplayPrice } = useNodePricing()
1923+
const node = createMockNode('WanTextToVideoApi', [
1924+
{ name: 'duration', value: '3' },
1925+
{ name: 'size', value: '480p: 1:1 (624x624)' }
1926+
])
1927+
1928+
const price = getNodeDisplayPrice(node)
1929+
expect(price).toBe('$0.15/Run') // 0.05 * 3
1930+
})
1931+
1932+
it('should fall back when widgets are missing', () => {
1933+
const { getNodeDisplayPrice } = useNodePricing()
1934+
const missingBoth = createMockNode('WanTextToVideoApi', [])
1935+
const missingSize = createMockNode('WanTextToVideoApi', [
1936+
{ name: 'duration', value: '5' }
1937+
])
1938+
const missingDuration = createMockNode('WanTextToVideoApi', [
1939+
{ name: 'size', value: '1080p' }
1940+
])
1941+
1942+
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
1943+
expect(getNodeDisplayPrice(missingSize)).toBe('$0.05-0.15/second')
1944+
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
1945+
})
1946+
1947+
it('should fall back on invalid duration', () => {
1948+
const { getNodeDisplayPrice } = useNodePricing()
1949+
const node = createMockNode('WanTextToVideoApi', [
1950+
{ name: 'duration', value: 'invalid' },
1951+
{ name: 'size', value: '1080p' }
1952+
])
1953+
1954+
const price = getNodeDisplayPrice(node)
1955+
expect(price).toBe('$0.05-0.15/second')
1956+
})
1957+
1958+
it('should fall back on unknown resolution', () => {
1959+
const { getNodeDisplayPrice } = useNodePricing()
1960+
const node = createMockNode('WanTextToVideoApi', [
1961+
{ name: 'duration', value: '10' },
1962+
{ name: 'size', value: '2K' }
1963+
])
1964+
1965+
const price = getNodeDisplayPrice(node)
1966+
expect(price).toBe('$0.05-0.15/second')
1967+
})
1968+
})
1969+
1970+
describe('dynamic pricing - WanImageToVideoApi', () => {
1971+
it('should return $0.80 for 8s at 720p', () => {
1972+
const { getNodeDisplayPrice } = useNodePricing()
1973+
const node = createMockNode('WanImageToVideoApi', [
1974+
{ name: 'duration', value: 8 },
1975+
{ name: 'resolution', value: '720p' }
1976+
])
1977+
1978+
const price = getNodeDisplayPrice(node)
1979+
expect(price).toBe('$0.80/Run') // 0.10 * 8
1980+
})
1981+
1982+
it('should return $0.60 for 12s at 480P', () => {
1983+
const { getNodeDisplayPrice } = useNodePricing()
1984+
const node = createMockNode('WanImageToVideoApi', [
1985+
{ name: 'duration', value: '12' },
1986+
{ name: 'resolution', value: '480P' }
1987+
])
1988+
1989+
const price = getNodeDisplayPrice(node)
1990+
expect(price).toBe('$0.60/Run') // 0.05 * 12
1991+
})
1992+
1993+
it('should return $1.50 for 10s at 1080p', () => {
1994+
const { getNodeDisplayPrice } = useNodePricing()
1995+
const node = createMockNode('WanImageToVideoApi', [
1996+
{ name: 'duration', value: '10' },
1997+
{ name: 'resolution', value: '1080p' }
1998+
])
1999+
2000+
const price = getNodeDisplayPrice(node)
2001+
expect(price).toBe('$1.50/Run') // 0.15 * 10
2002+
})
2003+
2004+
it('should handle "5s" string duration at 1080P', () => {
2005+
const { getNodeDisplayPrice } = useNodePricing()
2006+
const node = createMockNode('WanImageToVideoApi', [
2007+
{ name: 'duration', value: '5s' },
2008+
{ name: 'resolution', value: '1080P' }
2009+
])
2010+
2011+
const price = getNodeDisplayPrice(node)
2012+
expect(price).toBe('$0.75/Run') // 0.15 * 5
2013+
})
2014+
2015+
it('should fall back when widgets are missing', () => {
2016+
const { getNodeDisplayPrice } = useNodePricing()
2017+
const missingBoth = createMockNode('WanImageToVideoApi', [])
2018+
const missingRes = createMockNode('WanImageToVideoApi', [
2019+
{ name: 'duration', value: '5' }
2020+
])
2021+
const missingDuration = createMockNode('WanImageToVideoApi', [
2022+
{ name: 'resolution', value: '1080p' }
2023+
])
2024+
2025+
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
2026+
expect(getNodeDisplayPrice(missingRes)).toBe('$0.05-0.15/second')
2027+
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
2028+
})
2029+
2030+
it('should fall back on invalid duration', () => {
2031+
const { getNodeDisplayPrice } = useNodePricing()
2032+
const node = createMockNode('WanImageToVideoApi', [
2033+
{ name: 'duration', value: 'invalid' },
2034+
{ name: 'resolution', value: '720p' }
2035+
])
2036+
2037+
const price = getNodeDisplayPrice(node)
2038+
expect(price).toBe('$0.05-0.15/second')
2039+
})
2040+
2041+
it('should fall back on unknown resolution', () => {
2042+
const { getNodeDisplayPrice } = useNodePricing()
2043+
const node = createMockNode('WanImageToVideoApi', [
2044+
{ name: 'duration', value: '10' },
2045+
{ name: 'resolution', value: 'weird-res' }
2046+
])
2047+
2048+
const price = getNodeDisplayPrice(node)
2049+
expect(price).toBe('$0.05-0.15/second')
2050+
})
2051+
})
18972052
})

0 commit comments

Comments
 (0)