-
Notifications
You must be signed in to change notification settings - Fork 15
fix: normalize video duration per model rules via Zod schemas #194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+145
−28
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| /** | ||
| * Per-model provider input validation schemas. | ||
| * | ||
| * Each model that has provider-specific input constraints (duration format, | ||
| * allowed ranges, type coercion) gets a Zod schema here. The schemas use | ||
| * `.transform()` to auto-fix invalid inputs — rounding floats, clamping to | ||
| * valid ranges, and converting types (e.g. number → string for Kling v3). | ||
| * | ||
| * Usage: | ||
| * const fixed = normalizeProviderInput("kling-v3", { duration: 2.34 }); | ||
| * // → { duration: "3" } (rounded to 2, clamped to min 3, stringified) | ||
| * | ||
| * NOTE: This file is kept in sync with gateway/packages/schemas/src/model-rules.ts. | ||
| * When adding new model rules, update both files. | ||
| */ | ||
|
|
||
| import { z } from "zod"; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Duration schema builders | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** Duration as string integer clamped to [min, max]. Accepts number, outputs string. */ | ||
| function stringIntDuration(min: number, max: number, defaultVal: number) { | ||
| return z | ||
| .number() | ||
| .optional() | ||
| .transform((v) => | ||
| String(Math.max(min, Math.min(max, Math.round(v ?? defaultVal)))), | ||
| ); | ||
| } | ||
|
|
||
| /** Duration snapped to nearest allowed value. Accepts number, outputs number. */ | ||
| function enumDuration(allowed: number[], defaultVal: number) { | ||
| return z | ||
| .number() | ||
| .optional() | ||
| .transform((v) => { | ||
| const raw = v ?? defaultVal; | ||
| return allowed.reduce((prev, curr) => | ||
| Math.abs(curr - raw) < Math.abs(prev - raw) ? curr : prev, | ||
| ); | ||
| }); | ||
| } | ||
|
|
||
| /** Duration as integer clamped to [min, max]. Accepts number, outputs number. */ | ||
| function rangeDuration(min: number, max: number, defaultVal: number) { | ||
| return z | ||
| .number() | ||
| .optional() | ||
| .transform((v) => | ||
| Math.max(min, Math.min(max, Math.round(v ?? defaultVal))), | ||
| ); | ||
| } | ||
|
|
||
| /** Passthrough duration rounded to integer. */ | ||
| function intDuration(defaultVal: number) { | ||
| return z | ||
| .number() | ||
| .optional() | ||
| .transform((v) => Math.round(v ?? defaultVal)); | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Per-model provider input schemas | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const ModelDurationRules: Record<string, z.ZodType> = { | ||
| // Kling O3 (v3): fal expects string integer "3"–"15" | ||
| "kling-v3": z.object({ duration: stringIntDuration(3, 15, 5) }), | ||
| "kling-v3-standard": z.object({ duration: stringIntDuration(3, 15, 5) }), | ||
|
|
||
| // Kling v2.6: same rules as v3 | ||
| "kling-v2.6": z.object({ duration: stringIntDuration(3, 15, 5) }), | ||
|
|
||
| // Kling legacy: exactly 5 or 10 | ||
| "kling-v2.5": z.object({ duration: enumDuration([5, 10], 5) }), | ||
| "kling-v2.1": z.object({ duration: enumDuration([5, 10], 5) }), | ||
| "kling-v2": z.object({ duration: enumDuration([5, 10], 5) }), | ||
|
|
||
| // Wan: 5 or 10 | ||
| "wan-2.5": z.object({ duration: enumDuration([5, 10], 5) }), | ||
| "wan-2.5-preview": z.object({ duration: enumDuration([5, 10], 5) }), | ||
|
|
||
| // Minimax: round to integer | ||
| minimax: z.object({ duration: intDuration(5) }), | ||
|
|
||
| // Grok Imagine: integer 1–15 | ||
| "grok-imagine": z.object({ duration: rangeDuration(1, 15, 6) }), | ||
|
|
||
| // Sora 2: only 4, 8, 12, 16, 20 | ||
| "sora-2": z.object({ duration: enumDuration([4, 8, 12, 16, 20], 4) }), | ||
| "sora-2-pro": z.object({ duration: enumDuration([4, 8, 12, 16, 20], 4) }), | ||
|
|
||
| // Seedance (piapi): 5, 10, or 15 | ||
| "seedance-2-preview": z.object({ duration: enumDuration([5, 10, 15], 5) }), | ||
| "seedance-2-fast-preview": z.object({ | ||
| duration: enumDuration([5, 10, 15], 5), | ||
| }), | ||
| }; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Public API | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * Normalize provider input for a given model. | ||
| * | ||
| * Validates and transforms fields (currently `duration`) to match what the | ||
| * provider API expects — correct type, clamped to valid range, rounded to | ||
| * integer. | ||
| * | ||
| * - Unknown models: input returned as-is (passthrough). | ||
| * - Parse failures: input returned as-is (defensive — never throws). | ||
| */ | ||
| export function normalizeProviderInput( | ||
| model: string, | ||
| input: Record<string, unknown>, | ||
| ): Record<string, unknown> { | ||
| const schema = ModelDurationRules[model]; | ||
| if (!schema) return input; | ||
|
|
||
| const result = schema.safeParse({ duration: input.duration }); | ||
| if (!result.success) return input; | ||
|
|
||
| return { ...input, ...(result.data as Record<string, unknown>) }; | ||
| } | ||
|
|
||
| export { ModelDurationRules }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
silent normalization loses observability from previous implementation
the old openai.ts and fal.ts code pushed warnings when duration was invalid or clamped (e.g.,
"Duration ${duration}s not supported. Using ${validSeconds}s"). this new approach silently transforms values without any indication to the caller.if someone passes
duration: 2.5to kling-v3, it silently becomes"3"with no warning. might want to consider returning warnings alongside the normalized input for cases where values were adjusted.💡 possible approach to add observability
🤖 Prompt for AI Agents