{"schemaVersion":"1.0","type":"Article","types":["Article"],"slug":"a-normalized-request-schema-for-multi-model-image-and-video-generation-1grid","url":"https://zyvop.com/a-normalized-request-schema-for-multi-model-image-and-video-generation-1grid","title":"A Normalized Request Schema for Multi-Model Image and Video Generation","subtitle":null,"tldr":"Working with one generative model is easy enough. Your application collects a prompt, sends a request, waits for a response, and stores the output. The...","keywords":[],"entities":["sarah wilson","ZyVOP"],"keyTakeaways":["Working with one generative model is easy enough.","Your application collects a prompt, sends a request, waits for a response, and stores the output.","The architecture becomes less comfortable when the same product needs to support several image and video models."],"headings":["The Problem Starts With Parameters","Normalize the Intent First","Put Provider Translation at the Boundary","Don't Normalize Everything","Validate Before Sending the Request","Store the Normalized Request","Separate Generation Success From Approval","Model Routing Becomes Easier Later","The Abstraction Should Survive the Next Model","Final Thought"],"outboundLinks":["https://domaxai.com/"],"contentText":"Working with one generative model is easy enough. Your application collects a prompt, sends a request, waits for a response, and stores the output. The architecture becomes less comfortable when the same product needs to support several image and video models. I ran into this question while looking at workflows around DoMax AI, where different image and video generation options can exist inside the same creative environment. The interesting engineering problem isn't adding another model. It's preventing every new model from leaking its own vocabulary into the rest of the application. The Problem Starts With Parameters Imagine two hypothetical video providers. Provider A expects: { prompt: string; image_url?: string; duration: 5 | 10; ratio: \"16:9\" | \"9:16\" }Provider B expects: { text: string; reference?: string; seconds: number; aspect_ratio: string; motion_strength?: number; }They describe roughly the same job, but their contracts are different. The quick solution is usually a conditional: if (provider === \"a\") { // build Provider A payload } if (provider === \"b\") { // build Provider B payload }That works. Then Model C arrives. Then another model supports image-to-video but not text-to-video. Another accepts multiple references. Another exposes a setting that doesn't exist anywhere else. Eventually the UI and application layer start learning too much about individual providers. Normalize the Intent First Instead of treating the provider request as the application's primary data structure, I prefer defining a small internal generation request. For example: type GenerationMode = | \"text-to-image\" | \"image-to-image\" | \"text-to-video\" | \"image-to-video\"; interface GenerationRequest { mode: GenerationMode; prompt: string; references?: { type: \"image\" | \"video\" | \"audio\"; uri: string; }[]; output: { aspectRatio?: string; durationSeconds?: number; }; constraints?: { preserveSubject?: boolean; avoid?: string[]; }; }This is not supposed to describe every capability of every model. It describes what the application wants. That distinction is important. Put Provider Translation at the Boundary Each provider then receives an adapter. interface GenerationAdapter { supports(request: GenerationRequest): boolean; submit( request: GenerationRequest ): Promise&lt;GenerationSubmission&gt;; }A simplified implementation might look like: class ProviderAAdapter implements GenerationAdapter { supports(request: GenerationRequest) { return ( request.mode === \"text-to-video\" || request.mode === \"image-to-video\" ); } async submit(request: GenerationRequest) { const payload = { prompt: request.prompt, image_url: request.references?.find( ref =&gt; ref.type === \"image\" )?.uri, duration: request.output.durationSeconds, ratio: request.output.aspectRatio }; return providerAClient.generate(payload); } }Now the rest of the application doesn't need to know that Provider A calls the field image_url. That detail stays at the integration boundary. Don't Normalize Everything There is a trap here. Once you create a shared schema, it is tempting to make every model look identical. They aren't. Suppose one model supports a unique camera-control parameter. Adding generic fields like this is not necessarily helpful: advancedOption1?: string; advancedOption2?: number; providerSpecific?: Record&lt;string, unknown&gt;;At that point the abstraction stops protecting anything. I prefer a small common contract plus explicit capabilities. interface ModelCapabilities { textToImage: boolean; imageToImage: boolean; textToVideo: boolean; imageToVideo: boolean; maxReferences: number; supportedAspectRatios: string[]; supportedDurations?: number[]; }Provider-specific controls can remain provider-specific. The common schema should normalize the workflow, not pretend the underlying models are identical. Validate Before Sending the Request Once capabilities are explicit, validation becomes straightforward. function validateRequest( request: GenerationRequest, capabilities: ModelCapabilities ): string[] { const errors: string[] = []; if ( request.mode === \"image-to-video\" &amp;&amp; !capabilities.imageToVideo ) { errors.push(\"Model does not support image-to-video.\"); } if ( request.references &amp;&amp; request.references.length &gt; capabilities.maxReferences ) { errors.push(\"Too many reference assets.\"); } if ( request.output.aspectRatio &amp;&amp; !capabilities.supportedAspectRatios.includes( request.output.aspectRatio ) ) { errors.push(\"Unsupported aspect ratio.\"); } return errors; }This is better than waiting for a provider API to reject a request several seconds later. It also gives the frontend enough information to hide settings that don't apply. Store the Normalized Request Another benefit appears after generation. Don't store only this: output-48391.mp4Store the request that created it. A minimal record could contain: interface GenerationRecord { id: string; modelId: string; providerId: string; request: GenerationRequest; providerTaskId?: string; status: | \"queued\" | \"processing\" | \"completed\" | \"failed\"; outputUri?: string; createdAt: string; }Now a generated asset has provenance. You can answer: Which model produced this? Which reference was used? What duration was requested? What prompt generated it? Which generation mode was selected? That becomes particularly useful when someone returns to a project several days later. Separate Generation Success From Approval There is another state I would keep outside the provider response. A provider may report: completedThat means the generation job finished. It does not mean the asset should be published. I would keep review separately: type ReviewStatus = | \"unreviewed\" | \"approved\" | \"rejected\" | \"needs-edit\";Then: interface ReviewedGeneration extends GenerationRecord { reviewStatus: ReviewStatus; reviewNotes?: string; }This matters for visual generation because a technically successful request can still produce an unusable result. A product may change shape. Generated text may be wrong. A video may drift away from its reference. The infrastructure succeeded. The creative task did not. Those are different states. Model Routing Becomes Easier Later Once requests and capabilities are normalized, another possibility appears. The application does not always have to start with a model ID. It could start with a task. For example: function compatibleModels( request: GenerationRequest, models: ModelCapabilities[] ) { return models.filter(model =&gt; validateRequest(request, model).length === 0 ); }That does not automatically tell us which model is \"best.\" But it can eliminate models that cannot satisfy the request. A later routing layer could consider additional information such as latency, cost, historical acceptance rate, or user preference. The architecture becomes: User Intent | Normalized Request | Capability Validation | Model Selection | Provider Adapter | Provider API | Normalized Result | Human ReviewThe important part is that the application owns the workflow. Providers remain replaceable integrations behind it. The Abstraction Should Survive the Next Model A good test for this design is simple. Ask: \"What happens when we add another model next month?\" If adding it requires changes throughout the frontend, generation service, database, review workflow, and analytics layer, the provider abstraction probably isn't strong enough. Ideally, most of the work should be limited to: Register model capabilities. Implement its adapter. Map provider responses into the common result. Expose genuinely unique controls where necessary. Add integration tests. This doesn't make multi-model generation trivial. Different providers will still have different authentication, queues, errors, limits, pricing, moderation behavior, and output formats. But those differences no longer need to spread through the entire application. Final Thought Multi-model AI products are often described in terms of access: how many models can the user reach? From an engineering perspective, I think the more interesting question is: How many parts of the application need to know which provider is actually running the job? The closer that answer gets to \"only the adapter layer,\" the easier the system becomes to evolve. Models will change. Providers will change. Parameters will definitely change. The application's understanding of what the user is trying to create should not have to change with all of them.","contentHash":"sha256:2246f2ab2336fd787389516dc4bd78cdc83920328533749bcf29867848409d5e","authorName":"sarah wilson","authorUrl":"https://zyvop.com/author/sarah","authorSameAs":[],"category":null,"tags":[],"audience":"Software engineers and developers building applications with software development","tone":"Practical and evidence-based engineering guidance","readingTimeMinutes":5,"wordCount":1096,"faqs":null,"primaryTopic":null,"publishedAt":"2026-09-23T08:14:15.110Z","updatedAt":"2026-09-23T08:14:15.110Z","canonicalUrl":"https://zyvop.com/a-normalized-request-schema-for-multi-model-image-and-video-generation-1grid"}