mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +00:00
[fix] Update API node pricing for multiple providers (#4564)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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'],
|
||||
|
||||
@@ -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)'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user