Skip to content

Commit 34d5a45

Browse files
authored
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)
1 parent f62175e commit 34d5a45

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed

src/composables/node/useNodePricing.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,74 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
169169
: `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run`
170170
}
171171

172+
// ---- constants ----
173+
const SORA_SIZES = {
174+
BASIC: new Set(['720x1280', '1280x720']),
175+
PRO: new Set(['1024x1792', '1792x1024'])
176+
}
177+
const ALL_SIZES = new Set([...SORA_SIZES.BASIC, ...SORA_SIZES.PRO])
178+
179+
// ---- sora-2 pricing helpers ----
180+
function validateSora2Selection(
181+
modelRaw: string,
182+
duration: number,
183+
sizeRaw: string
184+
): string | undefined {
185+
const model = modelRaw?.toLowerCase() ?? ''
186+
const size = sizeRaw?.toLowerCase() ?? ''
187+
188+
if (!duration || Number.isNaN(duration)) return 'Set duration (4s / 8s / 12s)'
189+
if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'
190+
if (!ALL_SIZES.has(size))
191+
return 'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
192+
193+
if (model.includes('sora-2-pro')) return undefined
194+
195+
if (model.includes('sora-2') && !SORA_SIZES.BASIC.has(size))
196+
return 'sora-2 supports only 720x1280 or 1280x720'
197+
198+
if (!model.includes('sora-2')) return 'Unsupported model'
199+
200+
return undefined
201+
}
202+
203+
function perSecForSora2(modelRaw: string, sizeRaw: string): number {
204+
const model = modelRaw?.toLowerCase() ?? ''
205+
const size = sizeRaw?.toLowerCase() ?? ''
206+
207+
if (model.includes('sora-2-pro')) {
208+
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.3
209+
}
210+
if (model.includes('sora-2')) return 0.1
211+
212+
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.1
213+
}
214+
215+
function formatRunPrice(perSec: number, duration: number) {
216+
return `$${(perSec * duration).toFixed(2)}/Run`
217+
}
218+
219+
// ---- pricing calculator ----
220+
const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => {
221+
const getWidgetValue = (name: string) =>
222+
String(node.widgets?.find((w) => w.name === name)?.value ?? '')
223+
224+
const model = getWidgetValue('model')
225+
const size = getWidgetValue('size')
226+
const duration = Number(
227+
node.widgets?.find((w) => ['duration', 'duration_s'].includes(w.name))
228+
?.value
229+
)
230+
231+
if (!model || !size || !duration) return 'Set model, duration & size'
232+
233+
const validationError = validateSora2Selection(model, duration, size)
234+
if (validationError) return validationError
235+
236+
const perSec = perSecForSora2(model, size)
237+
return formatRunPrice(perSec, duration)
238+
}
239+
172240
/**
173241
* Static pricing data for API nodes, now supporting both strings and functions
174242
*/
@@ -195,6 +263,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
195263
FluxProKontextMaxNode: {
196264
displayPrice: '$0.08/Run'
197265
},
266+
OpenAIVideoSora2: {
267+
displayPrice: sora2PricingCalculator
268+
},
198269
IdeogramV1: {
199270
displayPrice: (node: LGraphNode): string => {
200271
const numImagesWidget = node.widgets?.find(
@@ -1658,6 +1729,7 @@ export const useNodePricing = () => {
16581729
MinimaxHailuoVideoNode: ['resolution', 'duration'],
16591730
OpenAIDalle3: ['size', 'quality'],
16601731
OpenAIDalle2: ['size', 'n'],
1732+
OpenAIVideoSora2: ['model', 'size', 'duration'],
16611733
OpenAIGPTImage1: ['quality', 'n'],
16621734
IdeogramV1: ['num_images', 'turbo'],
16631735
IdeogramV2: ['num_images', 'turbo'],

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

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,117 @@ describe('useNodePricing', () => {
301301
expect(price).toBe('$0.04-0.12/Run (varies with size & quality)')
302302
})
303303
})
304+
// ============================== OpenAIVideoSora2 ==============================
305+
describe('dynamic pricing - OpenAIVideoSora2', () => {
306+
it('should require model, duration & size when widgets are missing', () => {
307+
const { getNodeDisplayPrice } = useNodePricing()
308+
const node = createMockNode('OpenAIVideoSora2', [])
309+
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
310+
})
311+
312+
it('should require duration when duration is invalid or zero', () => {
313+
const { getNodeDisplayPrice } = useNodePricing()
314+
const nodeNaN = createMockNode('OpenAIVideoSora2', [
315+
{ name: 'model', value: 'sora-2-pro' },
316+
{ name: 'duration', value: 'oops' },
317+
{ name: 'size', value: '720x1280' }
318+
])
319+
expect(getNodeDisplayPrice(nodeNaN)).toBe('Set duration (4/8/12)')
320+
321+
const nodeZero = createMockNode('OpenAIVideoSora2', [
322+
{ name: 'model', value: 'sora-2-pro' },
323+
{ name: 'duration', value: 0 },
324+
{ name: 'size', value: '720x1280' }
325+
])
326+
expect(getNodeDisplayPrice(nodeZero)).toBe('Set duration (4/8/12)')
327+
})
328+
329+
it('should require size when size is missing', () => {
330+
const { getNodeDisplayPrice } = useNodePricing()
331+
const node = createMockNode('OpenAIVideoSora2', [
332+
{ name: 'model', value: 'sora-2-pro' },
333+
{ name: 'duration', value: 8 }
334+
])
335+
expect(getNodeDisplayPrice(node)).toBe(
336+
'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'
337+
)
338+
})
339+
340+
it('should compute pricing for sora-2-pro with 1024x1792', () => {
341+
const { getNodeDisplayPrice } = useNodePricing()
342+
const node = createMockNode('OpenAIVideoSora2', [
343+
{ name: 'model', value: 'sora-2-pro' },
344+
{ name: 'duration', value: 8 },
345+
{ name: 'size', value: '1024x1792' }
346+
])
347+
expect(getNodeDisplayPrice(node)).toBe('$4.00/Run') // 0.5 * 8
348+
})
349+
350+
it('should compute pricing for sora-2-pro with 720x1280', () => {
351+
const { getNodeDisplayPrice } = useNodePricing()
352+
const node = createMockNode('OpenAIVideoSora2', [
353+
{ name: 'model', value: 'sora-2-pro' },
354+
{ name: 'duration', value: 12 },
355+
{ name: 'size', value: '720x1280' }
356+
])
357+
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
358+
})
359+
360+
it('should reject unsupported size for sora-2-pro', () => {
361+
const { getNodeDisplayPrice } = useNodePricing()
362+
const node = createMockNode('OpenAIVideoSora2', [
363+
{ name: 'model', value: 'sora-2-pro' },
364+
{ name: 'duration', value: 8 },
365+
{ name: 'size', value: '640x640' }
366+
])
367+
expect(getNodeDisplayPrice(node)).toBe(
368+
'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024'
369+
)
370+
})
371+
372+
it('should compute pricing for sora-2 (720x1280 only)', () => {
373+
const { getNodeDisplayPrice } = useNodePricing()
374+
const node = createMockNode('OpenAIVideoSora2', [
375+
{ name: 'model', value: 'sora-2' },
376+
{ name: 'duration', value: 10 },
377+
{ name: 'size', value: '720x1280' }
378+
])
379+
expect(getNodeDisplayPrice(node)).toBe('$1.00/Run') // 0.1 * 10
380+
})
381+
382+
it('should reject non-720 sizes for sora-2', () => {
383+
const { getNodeDisplayPrice } = useNodePricing()
384+
const node = createMockNode('OpenAIVideoSora2', [
385+
{ name: 'model', value: 'sora-2' },
386+
{ name: 'duration', value: 8 },
387+
{ name: 'size', value: '1024x1792' }
388+
])
389+
expect(getNodeDisplayPrice(node)).toBe(
390+
'sora-2 supports only 720x1280 or 1280x720'
391+
)
392+
})
393+
it('should accept duration_s alias for duration', () => {
394+
const { getNodeDisplayPrice } = useNodePricing()
395+
const node = createMockNode('OpenAIVideoSora2', [
396+
{ name: 'model', value: 'sora-2-pro' },
397+
{ name: 'duration_s', value: 4 },
398+
{ name: 'size', value: '1792x1024' }
399+
])
400+
expect(getNodeDisplayPrice(node)).toBe('$2.00/Run') // 0.5 * 4
401+
})
402+
403+
it('should be case-insensitive for model and size', () => {
404+
const { getNodeDisplayPrice } = useNodePricing()
405+
const node = createMockNode('OpenAIVideoSora2', [
406+
{ name: 'model', value: 'SoRa-2-PrO' },
407+
{ name: 'duration', value: 12 },
408+
{ name: 'size', value: '1280x720' }
409+
])
410+
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
411+
})
412+
})
304413

414+
// ============================== MinimaxHailuoVideoNode ==============================
305415
describe('dynamic pricing - MinimaxHailuoVideoNode', () => {
306416
it('should return $0.28 for 6s duration and 768P resolution', () => {
307417
const { getNodeDisplayPrice } = useNodePricing()

0 commit comments

Comments
 (0)