OpenAIVideoSora2 Frontend pricing (#5958)

test: update OpenAIVideoSora2 tests to use `size` instead of
`resolution`

Refactored all OpenAIVideoSora2 test cases in useNodePricing.test.ts to
align with
the updated node logic that replaces the `resolution` widget with
`size`.
Adjusted validation, pricing, and error message expectations
accordingly.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5958-Update-OpenAIVideoSora2-tests-for-new-size-based-pricing-logic-2856d73d365081c9919dd256cce40492)
by [Unito](https://www.unito.io)
This commit is contained in:
Marwan Ahmed
2025-10-08 02:48:15 +03:00
committed by GitHub
parent f62175ed0c
commit 34d5a4523a
2 changed files with 182 additions and 0 deletions

View File

@@ -169,6 +169,74 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
: `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run`
}
// ---- constants ----
const SORA_SIZES = {
BASIC: new Set(['720x1280', '1280x720']),
PRO: new Set(['1024x1792', '1792x1024'])
}
const ALL_SIZES = new Set([...SORA_SIZES.BASIC, ...SORA_SIZES.PRO])
// ---- sora-2 pricing helpers ----
function validateSora2Selection(
modelRaw: string,
duration: number,
sizeRaw: string
): string | undefined {
const model = modelRaw?.toLowerCase() ?? ''
const size = sizeRaw?.toLowerCase() ?? ''
if (!duration || Number.isNaN(duration)) return 'Set duration (4s / 8s / 12s)'
if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'
if (!ALL_SIZES.has(size))
return 'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
if (model.includes('sora-2-pro')) return undefined
if (model.includes('sora-2') && !SORA_SIZES.BASIC.has(size))
return 'sora-2 supports only 720x1280 or 1280x720'
if (!model.includes('sora-2')) return 'Unsupported model'
return undefined
}
function perSecForSora2(modelRaw: string, sizeRaw: string): number {
const model = modelRaw?.toLowerCase() ?? ''
const size = sizeRaw?.toLowerCase() ?? ''
if (model.includes('sora-2-pro')) {
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.3
}
if (model.includes('sora-2')) return 0.1
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.1
}
function formatRunPrice(perSec: number, duration: number) {
return `$${(perSec * duration).toFixed(2)}/Run`
}
// ---- pricing calculator ----
const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => {
const getWidgetValue = (name: string) =>
String(node.widgets?.find((w) => w.name === name)?.value ?? '')
const model = getWidgetValue('model')
const size = getWidgetValue('size')
const duration = Number(
node.widgets?.find((w) => ['duration', 'duration_s'].includes(w.name))
?.value
)
if (!model || !size || !duration) return 'Set model, duration & size'
const validationError = validateSora2Selection(model, duration, size)
if (validationError) return validationError
const perSec = perSecForSora2(model, size)
return formatRunPrice(perSec, duration)
}
/**
* Static pricing data for API nodes, now supporting both strings and functions
*/
@@ -195,6 +263,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
FluxProKontextMaxNode: {
displayPrice: '$0.08/Run'
},
OpenAIVideoSora2: {
displayPrice: sora2PricingCalculator
},
IdeogramV1: {
displayPrice: (node: LGraphNode): string => {
const numImagesWidget = node.widgets?.find(
@@ -1658,6 +1729,7 @@ export const useNodePricing = () => {
MinimaxHailuoVideoNode: ['resolution', 'duration'],
OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size', 'n'],
OpenAIVideoSora2: ['model', 'size', 'duration'],
OpenAIGPTImage1: ['quality', 'n'],
IdeogramV1: ['num_images', 'turbo'],
IdeogramV2: ['num_images', 'turbo'],

View File

@@ -301,7 +301,117 @@ describe('useNodePricing', () => {
expect(price).toBe('$0.04-0.12/Run (varies with size & quality)')
})
})
// ============================== OpenAIVideoSora2 ==============================
describe('dynamic pricing - OpenAIVideoSora2', () => {
it('should require model, duration & size when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [])
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
})
it('should require duration when duration is invalid or zero', () => {
const { getNodeDisplayPrice } = useNodePricing()
const nodeNaN = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 'oops' },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(nodeNaN)).toBe('Set duration (4/8/12)')
const nodeZero = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 0 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(nodeZero)).toBe('Set duration (4/8/12)')
})
it('should require size when size is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 }
])
expect(getNodeDisplayPrice(node)).toBe(
'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'
)
})
it('should compute pricing for sora-2-pro with 1024x1792', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '1024x1792' }
])
expect(getNodeDisplayPrice(node)).toBe('$4.00/Run') // 0.5 * 8
})
it('should compute pricing for sora-2-pro with 720x1280', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 12 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
})
it('should reject unsupported size for sora-2-pro', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '640x640' }
])
expect(getNodeDisplayPrice(node)).toBe(
'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024'
)
})
it('should compute pricing for sora-2 (720x1280 only)', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2' },
{ name: 'duration', value: 10 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(node)).toBe('$1.00/Run') // 0.1 * 10
})
it('should reject non-720 sizes for sora-2', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '1024x1792' }
])
expect(getNodeDisplayPrice(node)).toBe(
'sora-2 supports only 720x1280 or 1280x720'
)
})
it('should accept duration_s alias for duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration_s', value: 4 },
{ name: 'size', value: '1792x1024' }
])
expect(getNodeDisplayPrice(node)).toBe('$2.00/Run') // 0.5 * 4
})
it('should be case-insensitive for model and size', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'SoRa-2-PrO' },
{ name: 'duration', value: 12 },
{ name: 'size', value: '1280x720' }
])
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
})
})
// ============================== MinimaxHailuoVideoNode ==============================
describe('dynamic pricing - MinimaxHailuoVideoNode', () => {
it('should return $0.28 for 6s duration and 768P resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()