Skip to content
Docs

AI Gateway Image-to-Video Generation

Animate a static image into a video. The image you provide becomes the video content itself - you're adding motion to that exact scene.

Every model here also runs as a background job instead of one long-lived request. See asynchronous generation below.

This is different from reference-to-video, where reference images show the model what characters look like, but the video is a completely new scene.

The provider-agnostic frameImages field is the recommended way to pass frames. Each entry pairs an image with a frameType:

  • One first_frame entry animates from that image. This is equivalent to prompt.image, and a first_frame takes priority if you pass both.
  • Adding a last_frame entry transitions between the two frames. Veo, KlingAI, and Seedance honor last_frame; Grok Imagine Video and Wan ignore it with a warning.
frameImages: [
  { image: 'https://example.com/start.png', frameType: 'first_frame' },
  { image: 'https://example.com/end.png', frameType: 'last_frame' },
];

Each provider below also documents its own providerOptions keys (like imageTail or lastFrameImage). Those still work when you omit frameImages.

Google's Veo models support image-to-video generation, animating a starting image into a video.

Browse the latest Veo video models on the AI Gateway Models page.

ParameterTypeRequiredDescription
prompt.imagestringYesURL or base64-encoded image to animate
prompt.textstringNoDescription of the motion or animation
duration4 | 6 | 8NoVideo length in seconds. Defaults to 8
resolutionstringNoResolution ('1280x720' for 720p, '1920x1080' for 1080p). Defaults to 720p
generateAudiobooleanNoGenerate audio alongside the video
providerOptions.vertex.resizeMode'pad' | 'crop'NoHow to resize the image to fit video dimensions. Defaults to 'pad'
providerOptions.vertex.enhancePromptbooleanNoUse Gemini to enhance prompts. Defaults to true
providerOptions.vertex.negativePromptstringNoWhat to discourage in the generated video
providerOptions.vertex.personGeneration'dont_allow' | 'allow_adult' | 'allow_all'NoWhether to allow person generation. Defaults to 'allow_adult'
providerOptions.vertex.pollIntervalMsnumberNoHow often to check task status. Defaults to 5000
providerOptions.vertex.pollTimeoutMsnumberNoMaximum wait time. Defaults to 600000 (10 minutes)
veo-image-to-video.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'google/veo-3.1-generate-001',
  prompt: {
    image: 'https://example.com/landscape.png',
    text: 'Camera slowly pans across the scene as clouds drift by',
  },
  resolution: '1920x1080',
  generateAudio: true,
  providerOptions: {
    vertex: {
      resizeMode: 'crop',
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

Veo can transition between a starting and ending image. Pass both frames through frameImages, tagging one first_frame and one last_frame. Veo animates from the first frame toward the last.

veo-first-last-frame.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'google/veo-3.1-generate-001',
  prompt: '360 pan from the first frame to the last frame',
  frameImages: [
    { image: 'https://example.com/start.png', frameType: 'first_frame' },
    { image: 'https://example.com/end.png', frameType: 'last_frame' },
  ],
  aspectRatio: '16:9',
  resolution: '1280x720',
  duration: 8,
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

KlingAI's image-to-video models animate images with standard or professional quality modes.

Browse the latest KlingAI video models on the AI Gateway Models page.

ParameterTypeRequiredDescription
prompt.imagestring | BufferYesThe image to animate. See image requirements below.
prompt.textstringNoDescription of the motion. Max 2500 characters.
durationnumberNoVideo length in seconds. 5 or 10 for v2.x, 3-15 for v3.0. Defaults to 5.
providerOptions.klingai.mode'std' | 'pro'No'std' for standard quality. 'pro' for professional quality. Defaults to 'std'.
providerOptions.klingai.negativePromptstringNoWhat to avoid in the video. Max 2500 characters.
providerOptions.klingai.cfgScalenumberNoPrompt adherence (0-1). Higher = stricter. Defaults to 0.5. Not supported on v2.x.
generateAudiobooleanNoGenerate audio. Defaults to false. Requires v2.6+.
providerOptions.klingai.voiceListarrayNoVoice IDs for speech. Max 2 voices. Requires v3.0+ with generateAudio: true. Cannot coexist with elementList. See voice generation.
providerOptions.klingai.multiShotbooleanNoEnable multi-shot generation. Requires v3.0+. See multi-shot.
providerOptions.klingai.elementListarrayNoReference elements for element control. Up to 3 elements. Requires v3.0+. Cannot coexist with voiceList.
providerOptions.klingai.watermarkInfoobjectNoSet { enabled: true } to generate watermarked result.
providerOptions.klingai.pollIntervalMsnumberNoHow often to check task status. Defaults to 5000.
providerOptions.klingai.pollTimeoutMsnumberNoMaximum wait time. Defaults to 600000 (10 minutes).

The input image (prompt.image) must meet these requirements:

  • Formats: .jpg, .jpeg, .png
  • File size: 10MB or less
  • Dimensions: Minimum 300px
  • Aspect ratio: Between 1:2.5 and 2.5:1

When using base64 encoding, submit only the raw base64 string without any prefix:

// Correct
const image = 'iVBORw0KGgoAAAANSUhEUgAAAAUA...';
 
// Incorrect - do not include data: prefix
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...';
klingai-image-to-video.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'klingai/kling-v3.0-i2v',
  prompt: {
    image: 'https://example.com/cat.png',
    text: 'The cat slowly turns its head and blinks',
  },
  duration: 5,
  providerOptions: {
    klingai: {
      mode: 'std',
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

Generate a video that transitions between a starting and ending image. The model interpolates the motion between the two frames. Pass both frames through frameImages, tagging one first_frame and one last_frame.

first-last-frame.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const firstFrame = fs.readFileSync('start.png');
const lastFrame = fs.readFileSync('end.png');
 
const result = await generateVideo({
  model: 'klingai/kling-v3.0-i2v',
  prompt: 'Smooth transition between the two scenes',
  frameImages: [
    { image: firstFrame, frameType: 'first_frame' },
    { image: lastFrame, frameType: 'last_frame' },
  ],
  providerOptions: {
    klingai: {
      mode: 'pro',
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

First and last frame is mutually exclusive with these features, which cannot be combined with it:

  • Motion brush (dynamicMasks / staticMask)
  • Camera control (cameraControl)

If you omit frameImages, you can still set the last frame through the legacy providerOptions.klingai.imageTail key (a string or Buffer with the same format requirements as the first frame), with prompt.image as the first frame.

Add speech to your video using voice IDs. Requires v3.0+ models with generateAudio: true.

Reference voices in your prompt using <<<voice_1>>> syntax, where the number matches the order in voiceList:

voice-generation.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'klingai/kling-v3.0-i2v',
  prompt: {
    image: 'https://example.com/person.png',
    text: 'The person<<<voice_1>>> says: "Hello, welcome to my channel"',
  },
  generateAudio: true,
  providerOptions: {
    klingai: {
      mode: 'std',
      voiceList: [{ voice_id: 'your_voice_id' }],
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

You can use up to 2 voices per video. Voice IDs come from KlingAI's voice customization API or system preset voices.

Control camera movement during video generation. This is mutually exclusive with first/last frame and motion brush features.

ParameterTypeRequiredDescription
providerOptions.klingai.cameraControl.typestringYesCamera movement type. See options below.
providerOptions.klingai.cameraControl.configobjectNoMovement configuration. Required when type is 'simple'.

Camera movement types:

TypeDescriptionConfig required
'simple'Basic movement with one axisYes
'down_back'Camera descends and moves backwardNo
'forward_up'Camera moves forward and tilts upNo
'right_turn_forward'Rotate right then move forwardNo
'left_turn_forward'Rotate left then move forwardNo

simple camera configuration options (use only one, set others to 0):

ConfigRangeDescription
horizontal[-10, 10]Camera translation along x-axis. Negative = left.
vertical[-10, 10]Camera translation along y-axis. Negative = down.
pan[-10, 10]Camera rotation around y-axis. Negative = left.
tilt[-10, 10]Camera rotation around x-axis. Negative = down.
roll[-10, 10]Camera rotation around z-axis. Negative = counter-clockwise.
zoom[-10, 10]Focal length change. Negative = narrower FOV.
camera-control.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'klingai/kling-v3.0-i2v',
  prompt: {
    image: 'https://example.com/landscape.png',
    text: 'A serene mountain landscape',
  },
  providerOptions: {
    klingai: {
      mode: 'std',
      cameraControl: {
        type: 'simple',
        config: {
          zoom: 5,
          horizontal: 0,
          vertical: 0,
          pan: 0,
          tilt: 0,
          roll: 0,
        },
      },
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

Control which parts of the image move and how using mask images. This is mutually exclusive with first/last frame and camera control features.

ParameterTypeRequiredDescription
providerOptions.klingai.staticMaskstringNoMask image for areas that should remain static.
providerOptions.klingai.dynamicMasksarrayNoArray of dynamic mask configurations (up to 6).
providerOptions.klingai.dynamicMasks[].maskstringYesMask image for areas that should move.
providerOptions.klingai.dynamicMasks[].trajectoriesarrayYesMotion path coordinates. 2-77 points for 5s video.

Mask requirements:

  • Same format as input image (.jpg, .jpeg, .png)
  • Aspect ratio must match the input image
  • All masks (staticMask and dynamicMasks[].mask) must have identical resolution

Trajectory coordinates use the bottom-left corner of the image as origin. More points create more accurate paths.

motion-brush.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'klingai/kling-v3.0-i2v',
  prompt: {
    image: 'https://example.com/scene.png',
    text: 'A ball bouncing across the scene',
  },
  providerOptions: {
    klingai: {
      mode: 'std',
      dynamicMasks: [
        {
          mask: 'https://example.com/ball-mask.png',
          trajectories: [
            { x: 100, y: 200 },
            { x: 200, y: 300 },
            { x: 300, y: 200 },
            { x: 400, y: 300 },
          ],
        },
      ],
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

Generate videos with multiple storyboard shots, combining a start frame image with per-shot prompts. Requires Kling v3.0+ models.

ParameterTypeRequiredDescription
providerOptions.klingai.multiShotbooleanYesSet to true to enable multi-shot generation
providerOptions.klingai.shotTypestringNoSet to 'customize' for custom shot durations
providerOptions.klingai.multiPromptarrayYesArray of shot configurations
providerOptions.klingai.multiPrompt[].indexnumberYesShot order (starting from 1)
providerOptions.klingai.multiPrompt[].promptstringYesText description for this shot
providerOptions.klingai.multiPrompt[].durationstringYesDuration in seconds for this shot
multi-shot-i2v.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'klingai/kling-v3.0-i2v',
  prompt: {
    image: 'https://example.com/start-frame.png',
    text: '',
  },
  aspectRatio: '16:9',
  duration: 10,
  generateAudio: true,
  providerOptions: {
    klingai: {
      mode: 'pro',
      multiShot: true,
      shotType: 'customize',
      multiPrompt: [
        {
          index: 1,
          prompt: 'The character looks up at the sky.',
          duration: '4',
        },
        {
          index: 2,
          prompt: 'A bird flies across the frame.',
          duration: '3',
        },
        {
          index: 3,
          prompt: 'The character smiles and waves.',
          duration: '3',
        },
      ],
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

Wan offers image-to-video with standard and flash variants. Both support audio generation. Wan requires image URLs (not buffers). Use Vercel Blob to host local images.

Browse the latest Wan video models on the AI Gateway Models page.

Wan does not support first-last-frame interpolation. A last_frame entry in frameImages is ignored with a warning.

ParameterTypeRequiredDescription
promptstring or { image, text }YesText description of the motion or animation
frameImagesArray<{ image, frameType }>NoPass a first_frame entry to animate from an image. URLs only. Takes priority over prompt.image
prompt.imagestringNoURL of the image to animate (URLs only, not buffers). Used when frameImages is omitted
resolutionstringNo'1280x720' or '1920x1080'
durationnumberNo2-15 seconds
generateAudiobooleanNoGenerate audio. Standard models default to true, flash models default to false
providerOptions.alibaba.negativePromptstringNoWhat to avoid in the video. Max 500 characters
providerOptions.alibaba.audioUrlstringNoURL to audio file for audio-video sync (WAV/MP3, 3-30s, max 15MB)
providerOptions.alibaba.watermarkbooleanNoAdd watermark to the video. Defaults to false
providerOptions.alibaba.pollIntervalMsnumberNoHow often to check task status. Defaults to 5000
providerOptions.alibaba.pollTimeoutMsnumberNoMaximum wait time. Defaults to 600000 (10 minutes)
wan-image-to-video.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'alibaba/wan-v2.6-i2v-flash',
  prompt: 'The cat waves hello and smiles',
  frameImages: [
    { image: 'https://example.com/cat.png', frameType: 'first_frame' },
  ],
  duration: 5,
  generateAudio: true,
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

If you omit frameImages, you can still pass the first frame through prompt.image with prompt.text.


Grok Imagine Video (by SpaceXAI) can animate images into videos. The output defaults to the input image's aspect ratio. If you specify aspectRatio, it will override this and stretch the image to the desired ratio.

Browse the latest Grok video models on the AI Gateway Models page.

Grok Imagine Video does not support first-last-frame interpolation. A last_frame entry in frameImages is ignored with a warning. To continue from a video's last frame, use video extension mode instead.

ParameterTypeRequiredDescription
promptstring or { image, text }YesText description of the motion or animation
frameImagesArray<{ image, frameType }>NoPass a first_frame entry to animate from an image. Takes priority over prompt.image
prompt.imagestringNoURL of the image to animate. Used when frameImages is omitted
durationnumberNoVideo length in seconds (1-15)
aspectRatiostringNoOverride the input image's aspect ratio (stretches the image)
providerOptions.spacexai.resolution'480p' | '720p'NoVideo resolution. Defaults to 480p
providerOptions.spacexai.pollIntervalMsnumberNoHow often to check task status. Defaults to 5000
providerOptions.spacexai.pollTimeoutMsnumberNoMaximum wait time. Defaults to 600000 (10 minutes)
grok-image-to-video.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'spacexai/grok-imagine-video',
  prompt: 'The cat slowly turns its head and blinks',
  frameImages: [
    { image: 'https://example.com/cat.png', frameType: 'first_frame' },
  ],
  duration: 5,
  providerOptions: {
    spacexai: {
      pollTimeoutMs: 600000,
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

If you omit frameImages, you can still pass the first frame through prompt.image with optional prompt.text.


ByteDance's Seedance models animate images into videos with support for first-and-last-frame control, multi-reference images, and optional audio generation. All models output MP4 at 24fps. Seedance requires image URLs (not buffers). Use Vercel Blob to host local images.

Browse the latest Seedance video models on the AI Gateway Models page.

ParameterTypeRequiredDescription
prompt.imagestringNoURL of the image to animate. You can pass the first frame through frameImages instead
prompt.textstringNoDescription of the motion or animation
aspectRatiostringNoAspect ratio ('16:9', '4:3', '1:1', '3:4', '9:16', '21:9', 'adaptive'). 'adaptive' uses the input image's aspect ratio
resolutionstringNoResolution ('854x480', '1280x720', '1920x1080'). Seedance 2.5 supports up to 1080p. Lite I2V supports up to 720p
durationnumberNoVideo length in seconds. Seedance 2.5: 4 to 30 seconds. v1.5: 4 to 12 seconds. v1.0: 2 to 12 seconds
providerOptions.bytedance.lastFrameImagestringNoURL of the last frame image. Enables first+last frame mode. See first and last frame
inputReferencesArray<{ data: string; mediaType: string }>NoReference images and videos. Seedance 2.5 accepts up to 30 images and 10 videos across the request. See multi-reference images
providerOptions.bytedance.referenceImagesstring[]NoLegacy alternative to inputReferences for image URLs. Lite I2V supports one to four images
generateAudiobooleanNoGenerate synchronized audio. Seedance v1.5 Pro and the Seedance 2.x series only
providerOptions.bytedance.watermarkbooleanNoAdd a watermark to the video. v1.x only
providerOptions.bytedance.cameraFixedbooleanNoFix the camera position during generation. v1.x only
providerOptions.bytedance.returnLastFramebooleanNoReturn the last frame of the generated video. v1.x only
providerOptions.bytedance.serviceTier'default' | 'flex'No'default' for online inference. 'flex' for offline at 50% cost and higher latency. v1.x only
providerOptions.bytedance.pollIntervalMsnumberNoHow often to check task status. Defaults to 3000
providerOptions.bytedance.pollTimeoutMsnumberNoMaximum wait time. Defaults to 300000 (5 minutes)
seedance-image-to-video.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'bytedance/seedance-2.5',
  prompt: {
    image: 'https://example.com/cat.png',
    text: 'The cat slowly turns its head and blinks',
  },
  duration: 5,
  providerOptions: {
    bytedance: {
      pollTimeoutMs: 600000,
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

Generate a video that transitions smoothly between a starting and ending image. Pass both frames through frameImages, tagging one first_frame and one last_frame. Seedance requires image URLs (not buffers), so host local images on Vercel Blob first.

seedance-first-last-frame.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'bytedance/seedance-2.5',
  prompt: 'Create a 360-degree orbiting camera shot based on this photo',
  frameImages: [
    {
      image: 'https://example.com/first-frame.jpg',
      frameType: 'first_frame',
    },
    { image: 'https://example.com/last-frame.jpg', frameType: 'last_frame' },
  ],
  duration: 5,
  generateAudio: true,
  providerOptions: {
    bytedance: {
      pollTimeoutMs: 600000,
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

If you omit frameImages, you can still set the first frame via prompt.image and the last frame via the legacy providerOptions.bytedance.lastFrameImage key.

Provide reference images that Seedance 2.5 uses to reproduce object shapes, colors, and textures. Seedance 2.5 accepts up to 30 image inputs across the request. Use [Image 1], [Image 2], and so on in your prompt to reference each image.

Pass references through the provider-agnostic inputReferences field, as shown below. The legacy providerOptions.bytedance.referenceImages key also works when you omit inputReferences. Either way, refer to each image with [Image 1], [Image 2], and so on in your prompt. When using inputReferences with URLs, tag each entry with an explicit media type (for example, { data: url, mediaType: 'image/png' }). Seedance treats untyped URL references as images and emits a warning.

For reference-to-video generation with Seedance 2.5, including video references, see Seedance reference-to-video.

ParameterTypeRequiredDescription
inputReferencesArray<{ data: string; mediaType: string }>YesReference images and videos. Tag each URL with an explicit mediaType
seedance-multi-reference.ts
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'node:fs';
 
const result = await generateVideo({
  model: 'bytedance/seedance-2.5',
  prompt:
    'A boy wearing glasses and a blue T-shirt from [Image 1] and a corgi dog from [Image 2], sitting on the lawn from [Image 3], in 3D cartoon style',
  aspectRatio: '16:9',
  duration: 5,
  inputReferences: [
    { data: 'https://example.com/boy.png', mediaType: 'image/png' },
    { data: 'https://example.com/corgi.png', mediaType: 'image/png' },
    { data: 'https://example.com/lawn.png', mediaType: 'image/png' },
  ],
  providerOptions: {
    bytedance: {
      pollTimeoutMs: 600000,
    },
  },
});
 
fs.writeFileSync('output.mp4', result.videos[0].uint8Array);

Video generation can take several minutes. Set pollTimeoutMs to at least 10 minutes (600000ms) for reliable operation.

Pass a webhook factory and the SDK registers your URL with the job instead of polling for it. The factory returns that URL and a received promise it waits on, then it fetches the videos itself.

async-image-to-video.ts
const result = await generateVideo({
  model: gateway.videoModel('klingai/kling-v3.0-i2v'),
  prompt: {
    // Hosted URL, not inline bytes: the start request is capped at 300 KiB.
    image: 'https://example.com/cat.png',
    text: 'The cat stretches and pads across the windowsill',
  },
  duration: 5,
  webhook: async () => ({
    url: callbackUrl,
    received: waitForDelivery(token),
  }),
  poll: { timeoutMs: 10 * 60 * 1000 },
});

The start request AI Gateway persists is capped at 300 KiB, so pass your image as a hosted URL on this flow rather than inline data.

token, callbackUrl, and waitForDelivery come from webhook-driven completion, which also covers the receiver, signature verification, and the payload shape.


Last updated September 8, 2026

Was this helpful?

supported.