[fix] Update API node pricing for multiple providers (#4564)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2025-07-28 23:01:46 -07:00
committed by GitHub
parent 680c09a584
commit b1fc8846a3
2 changed files with 382 additions and 164 deletions

View File

@@ -30,6 +30,25 @@ function safePricingExecution(
}
}
/**
* Helper function to calculate Runway duration-based pricing
* @param node - The LiteGraph node
* @returns Formatted price string
*/
const calculateRunwayDurationPrice = (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
if (!durationWidget) return '$0.05/second'
const duration = Number(durationWidget.value)
// If duration is 0 or NaN, don't fall back to 5 seconds - just use 0
const validDuration = isNaN(duration) ? 5 : duration
const cost = (0.05 * validDuration).toFixed(2)
return `$${cost}/Run`
}
const pixversePricingCalculator = (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration_seconds'
@@ -110,15 +129,27 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
FluxProUltraImageNode: {
displayPrice: '$0.06/Run'
},
FluxProKontextProNode: {
displayPrice: '$0.04/Run'
},
FluxProKontextMaxNode: {
displayPrice: '$0.08/Run'
},
IdeogramV1: {
displayPrice: (node: LGraphNode): string => {
const numImagesWidget = node.widgets?.find(
(w) => w.name === 'num_images'
) as IComboWidget
if (!numImagesWidget) return '$0.06 x num_images/Run'
const turboWidget = node.widgets?.find(
(w) => w.name === 'turbo'
) as IComboWidget
if (!numImagesWidget) return '$0.02-0.06 x num_images/Run'
const numImages = Number(numImagesWidget.value) || 1
const cost = (0.06 * numImages).toFixed(2)
const turbo = String(turboWidget?.value).toLowerCase() === 'true'
const basePrice = turbo ? 0.02 : 0.06
const cost = (basePrice * numImages).toFixed(2)
return `$${cost}/Run`
}
},
@@ -127,10 +158,16 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const numImagesWidget = node.widgets?.find(
(w) => w.name === 'num_images'
) as IComboWidget
if (!numImagesWidget) return '$0.08 x num_images/Run'
const turboWidget = node.widgets?.find(
(w) => w.name === 'turbo'
) as IComboWidget
if (!numImagesWidget) return '$0.05-0.08 x num_images/Run'
const numImages = Number(numImagesWidget.value) || 1
const cost = (0.08 * numImages).toFixed(2)
const turbo = String(turboWidget?.value).toLowerCase() === 'true'
const basePrice = turbo ? 0.05 : 0.08
const cost = (basePrice * numImages).toFixed(2)
return `$${cost}/Run`
}
},
@@ -651,10 +688,10 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
if (duration.includes('5')) {
if (resolution.includes('720p')) return '$0.3/Run'
if (resolution.includes('1080p')) return '~$0.3/Run'
if (resolution.includes('1080p')) return '$0.5/Run'
} else if (duration.includes('10')) {
if (resolution.includes('720p')) return '$0.25/Run'
if (resolution.includes('1080p')) return '$1.0/Run'
if (resolution.includes('720p')) return '$0.4/Run'
if (resolution.includes('1080p')) return '$1.5/Run'
}
return '$0.3/Run'
@@ -678,9 +715,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
if (duration.includes('5')) {
if (resolution.includes('720p')) return '$0.2/Run'
if (resolution.includes('1080p')) return '~$0.45/Run'
if (resolution.includes('1080p')) return '$0.3/Run'
} else if (duration.includes('10')) {
if (resolution.includes('720p')) return '$0.6/Run'
if (resolution.includes('720p')) return '$0.25/Run'
if (resolution.includes('1080p')) return '$1.0/Run'
}
@@ -896,18 +933,11 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
}
const model = String(modelWidget.value)
const aspectRatio = String(aspectRatioWidget.value)
if (model.includes('photon-flash-1')) {
if (aspectRatio.includes('1:1')) return '$0.0045/Run'
if (aspectRatio.includes('16:9')) return '$0.0045/Run'
if (aspectRatio.includes('4:3')) return '$0.0046/Run'
if (aspectRatio.includes('21:9')) return '$0.0047/Run'
return '$0.0019/Run'
} else if (model.includes('photon-1')) {
if (aspectRatio.includes('1:1')) return '$0.0172/Run'
if (aspectRatio.includes('16:9')) return '$0.0172/Run'
if (aspectRatio.includes('4:3')) return '$0.0176/Run'
if (aspectRatio.includes('21:9')) return '$0.0182/Run'
return '$0.0073/Run'
}
return '$0.0172/Run'
@@ -918,31 +948,17 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const modelWidget = node.widgets?.find(
(w) => w.name === 'model'
) as IComboWidget
const aspectRatioWidget = node.widgets?.find(
(w) => w.name === 'aspect_ratio'
) as IComboWidget
if (!modelWidget) {
return '$0.0045-0.0182/Run (varies with model & aspect ratio)'
return '$0.0019-0.0073/Run (varies with model)'
}
const model = String(modelWidget.value)
const aspectRatio = aspectRatioWidget
? String(aspectRatioWidget.value)
: null
if (model.includes('photon-flash-1')) {
if (!aspectRatio) return '$0.0045/Run'
if (aspectRatio.includes('1:1')) return '~$0.0045/Run'
if (aspectRatio.includes('16:9')) return '~$0.0045/Run'
if (aspectRatio.includes('4:3')) return '~$0.0046/Run'
if (aspectRatio.includes('21:9')) return '~$0.0047/Run'
return '$0.0019/Run'
} else if (model.includes('photon-1')) {
if (!aspectRatio) return '$0.0172/Run'
if (aspectRatio.includes('1:1')) return '~$0.0172/Run'
if (aspectRatio.includes('16:9')) return '~$0.0172/Run'
if (aspectRatio.includes('4:3')) return '~$0.0176/Run'
if (aspectRatio.includes('21:9')) return '~$0.0182/Run'
return '$0.0073/Run'
}
return '$0.0172/Run'
@@ -1010,53 +1026,23 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
displayPrice: '$0.08/Run'
},
RunwayImageToVideoNodeGen3a: {
displayPrice: (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
if (!durationWidget) return '$0.05/second'
const duration = Number(durationWidget.value) || 5
const cost = (0.05 * duration).toFixed(2)
return `$${cost}/Run`
}
displayPrice: calculateRunwayDurationPrice
},
RunwayImageToVideoNodeGen4: {
displayPrice: (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
if (!durationWidget) return '$0.05/second'
const duration = Number(durationWidget.value) || 5
const cost = (0.05 * duration).toFixed(2)
return `$${cost}/Run`
}
displayPrice: calculateRunwayDurationPrice
},
RunwayFirstLastFrameNode: {
displayPrice: (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
if (!durationWidget) return '$0.05/second'
const duration = Number(durationWidget.value) || 5
const cost = (0.05 * duration).toFixed(2)
return `$${cost}/Run`
}
displayPrice: calculateRunwayDurationPrice
},
// Rodin nodes - all have the same pricing structure
Rodin3D_Regular: {
displayPrice: '$0.4/Run'
},
Rodin3D_Detail: {
displayPrice: '$1.2/Run'
displayPrice: '$0.4/Run'
},
Rodin3D_Smooth: {
displayPrice: '$1.2/Run'
displayPrice: '$0.4/Run'
},
Rodin3D_Sketch: {
displayPrice: '$0.4/Run'
@@ -1064,60 +1050,113 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
// Tripo nodes - using actual node names from ComfyUI
TripoTextToModelNode: {
displayPrice: (node: LGraphNode): string => {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model' || w.name === 'model_version'
const quadWidget = node.widgets?.find(
(w) => w.name === 'quad'
) as IComboWidget
const styleWidget = node.widgets?.find(
(w) => w.name === 'style'
) as IComboWidget
const textureWidget = node.widgets?.find(
(w) => w.name === 'texture'
) as IComboWidget
const textureQualityWidget = node.widgets?.find(
(w) => w.name === 'texture_quality'
) as IComboWidget
if (!modelWidget)
return '$0.2-0.3/Run (varies with model & texture quality)'
if (!quadWidget || !styleWidget || !textureWidget)
return '$0.1-0.4/Run (varies with quad, style, texture & quality)'
const model = String(modelWidget.value)
const textureQuality = String(textureQualityWidget?.value || 'standard')
const quad = String(quadWidget.value).toLowerCase() === 'true'
const style = String(styleWidget.value).toLowerCase()
const texture = String(textureWidget.value).toLowerCase() === 'true'
const textureQuality = String(
textureQualityWidget?.value || 'standard'
).toLowerCase()
// V2.5 pricing
if (model.includes('v2.5') || model.includes('2.5')) {
return textureQuality.includes('detailed') ? '$0.3/Run' : '$0.2/Run'
}
// V2.0 pricing
else if (model.includes('v2.0') || model.includes('2.0')) {
return textureQuality.includes('detailed') ? '$0.3/Run' : '$0.2/Run'
}
// V1.4 or legacy pricing
else {
return '$0.2/Run'
// Pricing logic based on CSV data
if (style.includes('none')) {
if (!quad) {
if (!texture) return '$0.10/Run'
else return '$0.15/Run'
} else {
if (textureQuality.includes('detailed')) {
if (!texture) return '$0.30/Run'
else return '$0.35/Run'
} else {
if (!texture) return '$0.20/Run'
else return '$0.25/Run'
}
}
} else {
// any style
if (!quad) {
if (!texture) return '$0.15/Run'
else return '$0.20/Run'
} else {
if (textureQuality.includes('detailed')) {
if (!texture) return '$0.35/Run'
else return '$0.40/Run'
} else {
if (!texture) return '$0.25/Run'
else return '$0.30/Run'
}
}
}
}
},
TripoImageToModelNode: {
displayPrice: (node: LGraphNode): string => {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model' || w.name === 'model_version'
const quadWidget = node.widgets?.find(
(w) => w.name === 'quad'
) as IComboWidget
const styleWidget = node.widgets?.find(
(w) => w.name === 'style'
) as IComboWidget
const textureWidget = node.widgets?.find(
(w) => w.name === 'texture'
) as IComboWidget
const textureQualityWidget = node.widgets?.find(
(w) => w.name === 'texture_quality'
) as IComboWidget
if (!modelWidget)
return '$0.3-0.4/Run (varies with model & texture quality)'
if (!quadWidget || !styleWidget || !textureWidget)
return '$0.2-0.5/Run (varies with quad, style, texture & quality)'
const model = String(modelWidget.value)
const textureQuality = String(textureQualityWidget?.value || 'standard')
const quad = String(quadWidget.value).toLowerCase() === 'true'
const style = String(styleWidget.value).toLowerCase()
const texture = String(textureWidget.value).toLowerCase() === 'true'
const textureQuality = String(
textureQualityWidget?.value || 'standard'
).toLowerCase()
// V2.5 and V2.0 have same pricing structure
if (
model.includes('v2.5') ||
model.includes('2.5') ||
model.includes('v2.0') ||
model.includes('2.0')
) {
return textureQuality.includes('detailed') ? '$0.4/Run' : '$0.3/Run'
}
// V1.4 or legacy pricing (image_to_model is always $0.3)
else {
return '$0.3/Run'
// Pricing logic based on CSV data for Image to Model
if (style.includes('none')) {
if (!quad) {
if (!texture) return '$0.20/Run'
else return '$0.25/Run'
} else {
if (textureQuality.includes('detailed')) {
if (!texture) return '$0.40/Run'
else return '$0.45/Run'
} else {
if (!texture) return '$0.30/Run'
else return '$0.35/Run'
}
}
} else {
// any style
if (!quad) {
if (!texture) return '$0.25/Run'
else return '$0.30/Run'
} else {
if (textureQuality.includes('detailed')) {
if (!texture) return '$0.45/Run'
else return '$0.50/Run'
} else {
if (!texture) return '$0.35/Run'
else return '$0.40/Run'
}
}
}
}
},
@@ -1136,6 +1175,68 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
return textureQuality.includes('detailed') ? '$0.2/Run' : '$0.1/Run'
}
},
TripoConvertModelNode: {
displayPrice: '$0.10/Run'
},
TripoRetargetRiggedModelNode: {
displayPrice: '$0.10/Run'
},
TripoMultiviewToModelNode: {
displayPrice: (node: LGraphNode): string => {
const quadWidget = node.widgets?.find(
(w) => w.name === 'quad'
) as IComboWidget
const styleWidget = node.widgets?.find(
(w) => w.name === 'style'
) as IComboWidget
const textureWidget = node.widgets?.find(
(w) => w.name === 'texture'
) as IComboWidget
const textureQualityWidget = node.widgets?.find(
(w) => w.name === 'texture_quality'
) as IComboWidget
if (!quadWidget || !styleWidget || !textureWidget)
return '$0.2-0.5/Run (varies with quad, style, texture & quality)'
const quad = String(quadWidget.value).toLowerCase() === 'true'
const style = String(styleWidget.value).toLowerCase()
const texture = String(textureWidget.value).toLowerCase() === 'true'
const textureQuality = String(
textureQualityWidget?.value || 'standard'
).toLowerCase()
// Pricing logic based on CSV data for Multiview to Model (same as Image to Model)
if (style.includes('none')) {
if (!quad) {
if (!texture) return '$0.20/Run'
else return '$0.25/Run'
} else {
if (textureQuality.includes('detailed')) {
if (!texture) return '$0.40/Run'
else return '$0.45/Run'
} else {
if (!texture) return '$0.30/Run'
else return '$0.35/Run'
}
}
} else {
// any style
if (!quad) {
if (!texture) return '$0.25/Run'
else return '$0.30/Run'
} else {
if (textureQuality.includes('detailed')) {
if (!texture) return '$0.45/Run'
else return '$0.50/Run'
} else {
if (!texture) return '$0.35/Run'
else return '$0.40/Run'
}
}
}
}
},
// Google/Gemini nodes
GeminiNode: {
displayPrice: (node: LGraphNode): string => {
@@ -1151,9 +1252,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
if (model.includes('veo-2.0')) {
return '$0.5/second'
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
return '$0.0035/$0.0008 per 1K tokens'
return '$0.00016/$0.0006 per 1K tokens'
} else if (model.includes('gemini-2.5-flash-preview-04-17')) {
return '$0.0015/$0.0004 per 1K tokens'
return '$0.00125/$0.01 per 1K tokens'
}
// For other Gemini models, show token-based pricing info
return 'Token-based'
@@ -1233,9 +1334,11 @@ export const useNodePricing = () => {
OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size', 'n'],
OpenAIGPTImage1: ['quality', 'n'],
IdeogramV1: ['num_images'],
IdeogramV2: ['num_images'],
IdeogramV1: ['num_images', 'turbo'],
IdeogramV2: ['num_images', 'turbo'],
IdeogramV3: ['rendering_speed', 'num_images'],
FluxProKontextProNode: [],
FluxProKontextMaxNode: [],
VeoVideoGenerationNode: ['duration_seconds'],
LumaVideoNode: ['model', 'resolution', 'duration'],
LumaImageToVideoNode: ['model', 'resolution', 'duration'],
@@ -1269,8 +1372,8 @@ export const useNodePricing = () => {
RunwayImageToVideoNodeGen4: ['duration'],
RunwayFirstLastFrameNode: ['duration'],
// Tripo nodes
TripoTextToModelNode: ['model', 'model_version', 'texture_quality'],
TripoImageToModelNode: ['model', 'model_version', 'texture_quality'],
TripoTextToModelNode: ['quad', 'style', 'texture', 'texture_quality'],
TripoImageToModelNode: ['quad', 'style', 'texture', 'texture_quality'],
TripoTextureNode: ['texture_quality'],
// Google/Gemini nodes
GeminiNode: ['model'],

View File

@@ -603,7 +603,7 @@ describe('useNodePricing', () => {
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.25/Run')
expect(price).toBe('$0.4/Run')
})
it('should return range when widgets are missing', () => {
@@ -771,14 +771,14 @@ describe('useNodePricing', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('IdeogramV1')
expect(widgetNames).toEqual(['num_images'])
expect(widgetNames).toEqual(['num_images', 'turbo'])
})
it('should return correct widget names for IdeogramV2', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('IdeogramV2')
expect(widgetNames).toEqual(['num_images'])
expect(widgetNames).toEqual(['num_images', 'turbo'])
})
it('should return correct widget names for IdeogramV3', () => {
@@ -832,7 +832,7 @@ describe('useNodePricing', () => {
const node = createMockNode('IdeogramV1', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.06 x num_images/Run')
expect(price).toBe('$0.02-0.06 x num_images/Run')
})
it('should fall back to static display when num_images widget is missing for IdeogramV2', () => {
@@ -840,7 +840,7 @@ describe('useNodePricing', () => {
const node = createMockNode('IdeogramV2', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.08 x num_images/Run')
expect(price).toBe('$0.05-0.08 x num_images/Run')
})
it('should handle edge case when num_images value is 1 for IdeogramV1', () => {
@@ -850,7 +850,7 @@ describe('useNodePricing', () => {
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.06/Run') // 0.06 * 1
expect(price).toBe('$0.06/Run') // 0.06 * 1 (turbo=false by default)
})
})
@@ -1036,13 +1036,15 @@ describe('useNodePricing', () => {
'duration'
])
expect(getRelevantWidgetNames('TripoTextToModelNode')).toEqual([
'model',
'model_version',
'quad',
'style',
'texture',
'texture_quality'
])
expect(getRelevantWidgetNames('TripoImageToModelNode')).toEqual([
'model',
'model_version',
'quad',
'style',
'texture',
'texture_quality'
])
})
@@ -1075,6 +1077,26 @@ describe('useNodePricing', () => {
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05/second')
})
it('should handle zero duration for RunwayImageToVideoNodeGen3a', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
{ name: 'duration', value: 0 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.00/Run') // 0.05 * 0 = 0
})
it('should handle NaN duration for RunwayImageToVideoNodeGen3a', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
{ name: 'duration', value: 'invalid' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.25/Run') // Falls back to 5 seconds: 0.05 * 5
})
})
describe('Rodin nodes', () => {
@@ -1091,7 +1113,7 @@ describe('useNodePricing', () => {
const node = createMockNode('Rodin3D_Detail')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.2/Run')
expect(price).toBe('$0.4/Run')
})
it('should return addon price for Rodin3D_Smooth', () => {
@@ -1099,7 +1121,7 @@ describe('useNodePricing', () => {
const node = createMockNode('Rodin3D_Smooth')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.2/Run')
expect(price).toBe('$0.4/Run')
})
})
@@ -1107,44 +1129,53 @@ describe('useNodePricing', () => {
it('should return v2.5 standard pricing for TripoTextToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoTextToModelNode', [
{ name: 'model_version', value: 'v2.5-20250123' },
{ name: 'quad', value: false },
{ name: 'style', value: 'any style' },
{ name: 'texture', value: false },
{ name: 'texture_quality', value: 'standard' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.2/Run')
expect(price).toBe('$0.15/Run') // any style, no quad, no texture
})
it('should return v2.5 detailed pricing for TripoTextToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoTextToModelNode', [
{ name: 'model_version', value: 'v2.5-20250123' },
{ name: 'quad', value: true },
{ name: 'style', value: 'any style' },
{ name: 'texture', value: false },
{ name: 'texture_quality', value: 'detailed' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.3/Run')
expect(price).toBe('$0.35/Run') // any style, quad, no texture, detailed
})
it('should return v2.0 detailed pricing for TripoImageToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoImageToModelNode', [
{ name: 'model_version', value: 'v2.0-20240919' },
{ name: 'quad', value: true },
{ name: 'style', value: 'any style' },
{ name: 'texture', value: false },
{ name: 'texture_quality', value: 'detailed' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.4/Run')
expect(price).toBe('$0.45/Run') // any style, quad, no texture, detailed
})
it('should return legacy pricing for TripoTextToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoTextToModelNode', [
{ name: 'model_version', value: 'v1.4-legacy' }
{ name: 'quad', value: false },
{ name: 'style', value: 'none' },
{ name: 'texture', value: false },
{ name: 'texture_quality', value: 'standard' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.2/Run')
expect(price).toBe('$0.10/Run') // none style, no quad, no texture
})
it('should return static price for TripoRefineNode', () => {
@@ -1160,7 +1191,9 @@ describe('useNodePricing', () => {
const node = createMockNode('TripoTextToModelNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.2-0.3/Run (varies with model & texture quality)')
expect(price).toBe(
'$0.1-0.4/Run (varies with quad, style, texture & quality)'
)
})
it('should return texture-based pricing for TripoTextureNode', () => {
@@ -1176,25 +1209,85 @@ describe('useNodePricing', () => {
expect(getNodeDisplayPrice(detailedNode)).toBe('$0.2/Run')
})
it('should handle various Tripo model version formats', () => {
it('should handle various Tripo parameter combinations', () => {
const { getNodeDisplayPrice } = useNodePricing()
// Test different model version formats
// Test different parameter combinations
const testCases = [
{ model: 'v2.0-20240919', expected: '$0.2/Run' },
{ model: 'v2.5-20250123', expected: '$0.2/Run' },
{ model: 'v1.4', expected: '$0.2/Run' },
{ model: 'unknown-model', expected: '$0.2/Run' }
{ quad: false, style: 'none', texture: false, expected: '$0.10/Run' },
{
quad: false,
style: 'any style',
texture: false,
expected: '$0.15/Run'
},
{ quad: true, style: 'none', texture: false, expected: '$0.20/Run' },
{
quad: true,
style: 'any style',
texture: false,
expected: '$0.25/Run'
}
]
testCases.forEach(({ model, expected }) => {
testCases.forEach(({ quad, style, texture, expected }) => {
const node = createMockNode('TripoTextToModelNode', [
{ name: 'model_version', value: model },
{ name: 'quad', value: quad },
{ name: 'style', value: style },
{ name: 'texture', value: texture },
{ name: 'texture_quality', value: 'standard' }
])
expect(getNodeDisplayPrice(node)).toBe(expected)
})
})
it('should return static price for TripoConvertModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoConvertModelNode')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.10/Run')
})
it('should return static price for TripoRetargetRiggedModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoRetargetRiggedModelNode')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.10/Run')
})
it('should return dynamic pricing for TripoMultiviewToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
// Test basic case - no style, no quad, no texture
const basicNode = createMockNode('TripoMultiviewToModelNode', [
{ name: 'quad', value: false },
{ name: 'style', value: 'none' },
{ name: 'texture', value: false },
{ name: 'texture_quality', value: 'standard' }
])
expect(getNodeDisplayPrice(basicNode)).toBe('$0.20/Run')
// Test high-end case - any style, quad, texture, detailed
const highEndNode = createMockNode('TripoMultiviewToModelNode', [
{ name: 'quad', value: true },
{ name: 'style', value: 'stylized' },
{ name: 'texture', value: true },
{ name: 'texture_quality', value: 'detailed' }
])
expect(getNodeDisplayPrice(highEndNode)).toBe('$0.50/Run')
})
it('should return fallback for TripoMultiviewToModelNode without widgets', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoMultiviewToModelNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.2-0.5/Run (varies with quad, style, texture & quality)'
)
})
})
describe('Gemini and OpenAI Chat nodes', () => {
@@ -1204,11 +1297,11 @@ describe('useNodePricing', () => {
const testCases = [
{
model: 'gemini-2.5-pro-preview-05-06',
expected: '$0.0035/$0.0008 per 1K tokens'
expected: '$0.00016/$0.0006 per 1K tokens'
},
{
model: 'gemini-2.5-flash-preview-04-17',
expected: '$0.0015/$0.0004 per 1K tokens'
expected: '$0.00125/$0.01 per 1K tokens'
},
{ model: 'unknown-gemini-model', expected: 'Token-based' }
]
@@ -1315,7 +1408,7 @@ describe('useNodePricing', () => {
// Test edge cases
const testCases = [
{ duration: 0, expected: '$0.25/Run' }, // Falls back to 5 seconds (0 || 5)
{ duration: 0, expected: '$0.00/Run' }, // Now correctly handles 0 duration
{ duration: 1, expected: '$0.05/Run' },
{ duration: 30, expected: '$1.50/Run' }
]
@@ -1359,8 +1452,8 @@ describe('useNodePricing', () => {
const testCases = [
{ nodeType: 'Rodin3D_Regular', expected: '$0.4/Run' },
{ nodeType: 'Rodin3D_Sketch', expected: '$0.4/Run' },
{ nodeType: 'Rodin3D_Detail', expected: '$1.2/Run' },
{ nodeType: 'Rodin3D_Smooth', expected: '$1.2/Run' }
{ nodeType: 'Rodin3D_Detail', expected: '$0.4/Run' },
{ nodeType: 'Rodin3D_Smooth', expected: '$0.4/Run' }
]
testCases.forEach(({ nodeType, expected }) => {
@@ -1371,24 +1464,42 @@ describe('useNodePricing', () => {
})
describe('Comprehensive Tripo edge case testing', () => {
it('should handle TripoImageToModelNode with various model versions', () => {
it('should handle TripoImageToModelNode with various parameter combinations', () => {
const { getNodeDisplayPrice } = useNodePricing()
const testCases = [
{ model: 'v1.4-legacy', texture: 'standard', expected: '$0.3/Run' },
{ model: 'v2.0-20240919', texture: 'standard', expected: '$0.3/Run' },
{ model: 'v2.0-20240919', texture: 'detailed', expected: '$0.4/Run' },
{ model: 'v2.5-20250123', texture: 'standard', expected: '$0.3/Run' },
{ model: 'v2.5-20250123', texture: 'detailed', expected: '$0.4/Run' }
{ quad: false, style: 'none', texture: false, expected: '$0.20/Run' },
{ quad: false, style: 'none', texture: true, expected: '$0.25/Run' },
{
quad: true,
style: 'any style',
texture: true,
textureQuality: 'detailed',
expected: '$0.50/Run'
},
{
quad: true,
style: 'any style',
texture: false,
textureQuality: 'standard',
expected: '$0.35/Run'
}
]
testCases.forEach(({ model, texture, expected }) => {
const node = createMockNode('TripoImageToModelNode', [
{ name: 'model_version', value: model },
{ name: 'texture_quality', value: texture }
])
expect(getNodeDisplayPrice(node)).toBe(expected)
})
testCases.forEach(
({ quad, style, texture, textureQuality, expected }) => {
const widgets = [
{ name: 'quad', value: quad },
{ name: 'style', value: style },
{ name: 'texture', value: texture }
]
if (textureQuality) {
widgets.push({ name: 'texture_quality', value: textureQuality })
}
const node = createMockNode('TripoImageToModelNode', widgets)
expect(getNodeDisplayPrice(node)).toBe(expected)
}
)
})
it('should return correct fallback for TripoImageToModelNode', () => {
@@ -1396,17 +1507,19 @@ describe('useNodePricing', () => {
const node = createMockNode('TripoImageToModelNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.3-0.4/Run (varies with model & texture quality)')
expect(price).toBe(
'$0.2-0.5/Run (varies with quad, style, texture & quality)'
)
})
it('should handle missing texture quality widget', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoTextToModelNode', [
{ name: 'model_version', value: 'v2.0-20240919' }
])
const node = createMockNode('TripoTextToModelNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.2/Run') // Default to standard texture pricing
expect(price).toBe(
'$0.1-0.4/Run (varies with quad, style, texture & quality)'
)
})
it('should handle missing model version widget', () => {
@@ -1416,7 +1529,9 @@ describe('useNodePricing', () => {
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.2-0.3/Run (varies with model & texture quality)')
expect(price).toBe(
'$0.1-0.4/Run (varies with quad, style, texture & quality)'
)
})
})
})