[fix] Add dynamic pricing for API nodes with quantity parameters (#4362)

This commit is contained in:
Christian Byrne
2025-07-05 20:50:33 -07:00
committed by GitHub
parent 0c4339f652
commit 191b4574b9
2 changed files with 230 additions and 31 deletions

View File

@@ -275,30 +275,33 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const modelWidget = node.widgets?.find(
(w) => w.name === 'model_name'
) as IComboWidget
const nWidget = node.widgets?.find(
(w) => w.name === 'n'
) as IComboWidget
if (!modelWidget)
return '$0.0035-0.028/Run (varies with modality & model)'
return '$0.0035-0.028 x n/Run (varies with modality & model)'
const model = String(modelWidget.value)
const n = Number(nWidget?.value) || 1
let basePrice = 0.014 // default
if (modality.includes('text to image')) {
if (model.includes('kling-v1')) {
return '$0.0035/Run'
} else if (
model.includes('kling-v1-5') ||
model.includes('kling-v2')
) {
return '$0.014/Run'
if (model.includes('kling-v1-5') || model.includes('kling-v2')) {
basePrice = 0.014
} else if (model.includes('kling-v1')) {
basePrice = 0.0035
}
} else if (modality.includes('image to image')) {
if (model.includes('kling-v1')) {
return '$0.0035/Run'
} else if (model.includes('kling-v1-5')) {
return '$0.028/Run'
if (model.includes('kling-v1-5')) {
basePrice = 0.028
} else if (model.includes('kling-v1')) {
basePrice = 0.0035
}
}
return '$0.014/Run'
const totalCost = (basePrice * n).toFixed(4)
return `$${totalCost}/Run`
}
},
KlingLipSyncAudioToVideoNode: {
@@ -523,19 +526,26 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const sizeWidget = node.widgets?.find(
(w) => w.name === 'size'
) as IComboWidget
const nWidget = node.widgets?.find(
(w) => w.name === 'n'
) as IComboWidget
if (!sizeWidget) return '$0.016-0.02/Run (varies with size)'
if (!sizeWidget) return '$0.016-0.02 x n/Run (varies with size & n)'
const size = String(sizeWidget.value)
const n = Number(nWidget?.value) || 1
let basePrice = 0.02 // default
if (size.includes('1024x1024')) {
return '$0.02/Run'
basePrice = 0.02
} else if (size.includes('512x512')) {
return '$0.018/Run'
basePrice = 0.018
} else if (size.includes('256x256')) {
return '$0.016/Run'
basePrice = 0.016
}
return '$0.02/Run'
const totalCost = (basePrice * n).toFixed(3)
return `$${totalCost}/Run`
}
},
OpenAIDalle3: {
@@ -570,19 +580,30 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const qualityWidget = node.widgets?.find(
(w) => w.name === 'quality'
) as IComboWidget
const nWidget = node.widgets?.find(
(w) => w.name === 'n'
) as IComboWidget
if (!qualityWidget) return '$0.011-0.30/Run (varies with quality)'
if (!qualityWidget)
return '$0.011-0.30 x n/Run (varies with quality & n)'
const quality = String(qualityWidget.value)
const n = Number(nWidget?.value) || 1
let basePriceRange = '$0.046-0.07' // default medium
if (quality.includes('high')) {
return '$0.167-0.30/Run'
basePriceRange = '$0.167-0.30'
} else if (quality.includes('medium')) {
return '$0.046-0.07/Run'
basePriceRange = '$0.046-0.07'
} else if (quality.includes('low')) {
return '$0.011-0.02/Run'
basePriceRange = '$0.011-0.02'
}
return '$0.046-0.07/Run'
if (n === 1) {
return `${basePriceRange}/Run`
} else {
return `${basePriceRange} x ${n}/Run`
}
}
},
PikaImageToVideoNode2_2: {
@@ -717,6 +738,42 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
RecraftCrispUpscaleNode: {
displayPrice: '$0.004/Run'
},
RecraftGenerateColorFromImageNode: {
displayPrice: (node: LGraphNode): string => {
const nWidget = node.widgets?.find(
(w) => w.name === 'n'
) as IComboWidget
if (!nWidget) return '$0.04 x n/Run'
const n = Number(nWidget.value) || 1
const cost = (0.04 * n).toFixed(2)
return `$${cost}/Run`
}
},
RecraftGenerateImageNode: {
displayPrice: (node: LGraphNode): string => {
const nWidget = node.widgets?.find(
(w) => w.name === 'n'
) as IComboWidget
if (!nWidget) return '$0.04 x n/Run'
const n = Number(nWidget.value) || 1
const cost = (0.04 * n).toFixed(2)
return `$${cost}/Run`
}
},
RecraftGenerateVectorImageNode: {
displayPrice: (node: LGraphNode): string => {
const nWidget = node.widgets?.find(
(w) => w.name === 'n'
) as IComboWidget
if (!nWidget) return '$0.08 x n/Run'
const n = Number(nWidget.value) || 1
const cost = (0.08 * n).toFixed(2)
return `$${cost}/Run`
}
},
RecraftImageInpaintingNode: {
displayPrice: (node: LGraphNode): string => {
const nWidget = node.widgets?.find(
@@ -772,7 +829,16 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
}
},
RecraftVectorizeImageNode: {
displayPrice: '$0.01/Run'
displayPrice: (node: LGraphNode): string => {
const nWidget = node.widgets?.find(
(w) => w.name === 'n'
) as IComboWidget
if (!nWidget) return '$0.01 x n/Run'
const n = Number(nWidget.value) || 1
const cost = (0.01 * n).toFixed(2)
return `$${cost}/Run`
}
},
StabilityStableImageSD_3_5Node: {
displayPrice: (node: LGraphNode): string => {
@@ -915,13 +981,13 @@ export const useNodePricing = () => {
const widgetMap: Record<string, string[]> = {
KlingTextToVideoNode: ['mode', 'model_name', 'duration'],
KlingImage2VideoNode: ['mode', 'model_name', 'duration'],
KlingImageGenerationNode: ['modality', 'model_name'],
KlingImageGenerationNode: ['modality', 'model_name', 'n'],
KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'],
KlingSingleImageVideoEffectNode: ['effect_scene'],
KlingStartEndFrameNode: ['mode', 'model_name', 'duration'],
OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size'],
OpenAIGPTImage1: ['quality'],
OpenAIDalle2: ['size', 'n'],
OpenAIGPTImage1: ['quality', 'n'],
IdeogramV1: ['num_images'],
IdeogramV2: ['num_images'],
IdeogramV3: ['rendering_speed', 'num_images'],
@@ -945,7 +1011,11 @@ export const useNodePricing = () => {
RecraftTextToImageNode: ['n'],
RecraftImageToImageNode: ['n'],
RecraftImageInpaintingNode: ['n'],
RecraftTextToVectorNode: ['n']
RecraftTextToVectorNode: ['n'],
RecraftVectorizeImageNode: ['n'],
RecraftGenerateColorFromImageNode: ['n'],
RecraftGenerateImageNode: ['n'],
RecraftGenerateVectorImageNode: ['n']
}
return widgetMap[nodeType] || []
}

View File

@@ -227,7 +227,7 @@ describe('useNodePricing', () => {
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.02/Run')
expect(price).toBe('$0.020/Run')
})
it('should return $0.018 for 512x512 size', () => {
@@ -255,7 +255,7 @@ describe('useNodePricing', () => {
const node = createMockNode('OpenAIDalle2', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.016-0.02/Run (varies with size)')
expect(price).toBe('$0.016-0.02 x n/Run (varies with size & n)')
})
})
@@ -295,7 +295,7 @@ describe('useNodePricing', () => {
const node = createMockNode('OpenAIGPTImage1', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.011-0.30/Run (varies with quality)')
expect(price).toBe('$0.011-0.30 x n/Run (varies with quality & n)')
})
})
@@ -894,4 +894,133 @@ describe('useNodePricing', () => {
})
})
})
describe('OpenAI nodes dynamic pricing with n parameter', () => {
it('should calculate dynamic pricing for OpenAIDalle2 based on size and n', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle2', [
{ name: 'size', value: '1024x1024' },
{ name: 'n', value: 3 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.060/Run') // 0.02 * 3
})
it('should calculate dynamic pricing for OpenAIGPTImage1 based on quality and n', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIGPTImage1', [
{ name: 'quality', value: 'low' },
{ name: 'n', value: 2 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.011-0.02 x 2/Run')
})
it('should fall back to static display when n widget is missing for OpenAIDalle2', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle2', [
{ name: 'size', value: '512x512' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.018/Run') // n defaults to 1
})
})
describe('KlingImageGenerationNode dynamic pricing with n parameter', () => {
it('should calculate dynamic pricing for text-to-image with kling-v1', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingImageGenerationNode', [
{ name: 'model_name', value: 'kling-v1' },
{ name: 'n', value: 4 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.0140/Run') // 0.0035 * 4
})
it('should calculate dynamic pricing for text-to-image with kling-v1-5', () => {
const { getNodeDisplayPrice } = useNodePricing()
// Mock node without image input (text-to-image mode)
const node = createMockNode('KlingImageGenerationNode', [
{ name: 'model_name', value: 'kling-v1-5' },
{ name: 'n', value: 2 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.0280/Run') // For kling-v1-5 text-to-image: 0.014 * 2
})
it('should fall back to static display when model widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingImageGenerationNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.0035-0.028 x n/Run (varies with modality & model)')
})
})
describe('New Recraft nodes dynamic pricing', () => {
it('should calculate dynamic pricing for RecraftGenerateImageNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RecraftGenerateImageNode', [
{ name: 'n', value: 3 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.12/Run') // 0.04 * 3
})
it('should calculate dynamic pricing for RecraftVectorizeImageNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RecraftVectorizeImageNode', [
{ name: 'n', value: 5 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05/Run') // 0.01 * 5
})
it('should calculate dynamic pricing for RecraftGenerateVectorImageNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RecraftGenerateVectorImageNode', [
{ name: 'n', value: 2 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.16/Run') // 0.08 * 2
})
})
describe('Widget names for reactive updates', () => {
it('should include n parameter for OpenAI nodes', () => {
const { getRelevantWidgetNames } = useNodePricing()
expect(getRelevantWidgetNames('OpenAIDalle2')).toEqual(['size', 'n'])
expect(getRelevantWidgetNames('OpenAIGPTImage1')).toEqual([
'quality',
'n'
])
})
it('should include n parameter for Kling and new Recraft nodes', () => {
const { getRelevantWidgetNames } = useNodePricing()
expect(getRelevantWidgetNames('KlingImageGenerationNode')).toEqual([
'modality',
'model_name',
'n'
])
expect(getRelevantWidgetNames('RecraftVectorizeImageNode')).toEqual(['n'])
expect(getRelevantWidgetNames('RecraftGenerateImageNode')).toEqual(['n'])
expect(getRelevantWidgetNames('RecraftGenerateVectorImageNode')).toEqual([
'n'
])
expect(
getRelevantWidgetNames('RecraftGenerateColorFromImageNode')
).toEqual(['n'])
})
})
})