Skip to content

Commit de5fbb5

Browse files
tombeckenhamclaude
andauthored
feat(ai-gemini): Gemini Omni Flash video generation via the Interactions API (#886)
* feat(ai-gemini): support Gemini Omni Flash video generation via the Interactions API Add gemini-omni-flash-preview to the Gemini video adapter. Omni only serves the Interactions API (generateContent rejects it with 400), so the adapter now routes by model: Veo models keep the :predictLongRunning operations flow, while Omni creates a background interaction with response_modalities: ['video'], polls it by id, and returns the inline base64 MP4 as a data: URL (Files-API URI delivery passes through). Usage maps from output_tokens_by_modality, size maps onto response_format.aspect_ratio, and modelOptions.previous_interaction_id chains conversational video edits. - model-meta: GEMINI_OMNI_FLASH_PREVIEW ($0.10/sec video+audio output) + GEMINI_INTERACTIONS_VIDEO_MODELS - provider options: GeminiOmniVideoProviderOptions derived from the SDK's CreateModelInteractionParamsNonStreaming; per-model input modalities (Omni accepts image+video parts) and fixed 10s duration - @google/genai floor bumped to ^2.10.0 for the interactions surface - 17 new unit tests; new interactions-video E2E feature backed by a dedicated aimock mount (native interactions text handling untouched) - docs/media/video-generation.md + media-generation skill updates Verified live against the Gemini API: background job completed in ~45s and returned a valid MP4 with video-modality usage; the SDK's typed interactions.create works with Step-list input, so no raw REST fallback is needed. Closes #871 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(examples): Gemini Omni Flash with all inputs in ts-react-media Add gemini-omni-flash-preview (text-to-video + image-to-video) to the ts-react-media example, exercising every Omni input: text prompts, a start image, an attached reference/edit video clip (Omni-only — never sent to other providers), and conversational editing that chains a new prompt onto a completed generation via previous_interaction_id. Also fixes a latent core type bug this surfaced: generateVideo / getVideoJobStatus constrained adapters as VideoAdapter<string, any, any, any>, leaving the duration generic at its Record<string, number> default — any adapter with a narrowed per-model duration union (Omni's 10, Veo's 4|6|8) failed assignability under strict function-type contravariance. All video-activity constraints now span all six VideoAdapter generics. Verified live: Omni edit chaining (previous_interaction_id) against the real Gemini API returned an edited 10s MP4; example dev server boots and type-checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ai-gemini): support Omni Flash 3-10s clip durations The issue's live verification concluded Omni clips were a fixed 10 seconds, but response_format.duration is a real request field — just undocumented. Verified against the live API: it takes a "<seconds>s" string, accepts any value in the 3-10s range including fractional seconds (a 3s request returns a 3.008s MP4 per ffprobe), rejects out-of-range values with explicit minimum/maximum errors, and defaults to 10s when omitted. Omni's duration is now typed number with availableDurations() = { kind: 'range', min: 3, max: 10, unit: 'seconds' } and snapDuration clamping into it; the adapter maps the generateVideo duration option onto response_format.duration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai-gemini): address PR review findings for Omni Flash video - Reject out-of-range Omni durations at job creation with a clear local error instead of silently passing them to the live API - Map requires_action interactions to a failed status so polling can't spin until timeout (reachable via previous_interaction_id chaining) - Surface failed job statuses in the ts-react-media example instead of polling forever on a pending spinner - Add a compile-time regression test guarding the generateVideo VideoAdapter generic-arity fix, plus unit tests for duration rejection, fractional pass-through, and requires_action mapping - Fix stale doc/comment claims: Veo 2/3 model lists, "fixed 10s" clips, "clamped" duration wording, and content-block ordering (images, then videos, then text) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(examples): Omni Studio conversational session view in ts-react-media Add a chat-style session tab built around Gemini Omni Flash's conversational editing: - Timeline of turns where each completed clip auto-becomes the continue-from target; "Continue from here" branches the session from any earlier clip via previous_interaction_id - Multi-reference image tray (Omni supports ~6), single reference clip slot labeled with the 3s API limit, and a prompt-steered audio direction field - Per-turn duration slider (3-10s) doubling as a $0.10/s cost estimate, aspect toggle, and task-mode pin mapped to generation_config.video_config.task - Duration is locked and omitted for edit-shaped requests (continuing a clip, task=edit, or attached clip without a reference pin) — Omni rejects durations that don't match the source clip's length - Failed turns get an "Edit & retry" button that restores the prompt, attachments, and settings to the composer and removes the dead turn createVideoJobFn gains omniOptions (duration / aspectRatio / task) to carry the new controls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 2665085 commit de5fbb5

31 files changed

Lines changed: 2231 additions & 100 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/ai-gemini': minor
3+
---
4+
5+
Add Gemini Omni Flash (`gemini-omni-flash-preview`) video generation via the Interactions API. Omni only serves the Interactions API (`generateContent` rejects it), so the video adapter now routes by model: Veo models keep the `:predictLongRunning` operations flow, while `geminiVideo('gemini-omni-flash-preview')` creates a background interaction with `response_modalities: ['video']`, polls it by id, and returns the inline base64 MP4 as a `data:` URL (Files-API URI delivery passes through). Usage is mapped from the interaction's `output_tokens_by_modality`. Image and video prompt parts are sent as interaction content blocks, and `modelOptions.previous_interaction_id` chains a new prompt onto a prior Omni generation for conversational video editing. The top-level `size` option maps onto `response_format.aspect_ratio` (`'16:9' | '9:16'`) and `duration` onto `response_format.duration` — any value in the 3–10 second range (fractional seconds included, verified against the live API), defaulting to a 10-second clip when omitted. Raises the `@google/genai` floor to `^2.10.0` for the Interactions API surface.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/ai': patch
3+
---
4+
5+
Fix `generateVideo` / `getVideoJobStatus` rejecting video adapters that declare a narrowed per-model duration union (e.g. Gemini's `4 | 6 | 8` for Veo or `10` for Omni Flash) at the type level. The activity's `TAdapter extends VideoAdapter<string, any, any, any>` constraints left the input-modality and duration generics at their defaults, so `duration?: number` failed contravariance against the adapter's literal union. All video-activity constraints and helper conditionals now span all six `VideoAdapter` generics.

docs/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@
294294
"label": "Video Generation",
295295
"to": "media/video-generation",
296296
"addedAt": "2026-04-15",
297-
"updatedAt": "2026-07-01"
297+
"updatedAt": "2026-07-02"
298298
},
299299
{
300300
"label": "Generation Hooks",

docs/media/video-generation.md

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
title: Video Generation
33
id: video-generation
44
order: 6
5-
description: "Generate video from text prompts with OpenAI Sora, Google Veo, xAI Grok Imagine, or fal.ai using TanStack AI's experimental generateVideo() jobs/polling API."
5+
description: "Generate video from text prompts with OpenAI Sora, Google Veo, Gemini Omni Flash, xAI Grok Imagine, or fal.ai using TanStack AI's experimental generateVideo() jobs/polling API."
66
keywords:
77
- tanstack ai
88
- video generation
99
- sora
1010
- veo
11+
- omni flash
12+
- interactions api
1113
- gemini
1214
- grok imagine
1315
- fal
@@ -40,7 +42,7 @@ TanStack AI provides experimental support for video generation through dedicated
4042

4143
Currently supported:
4244
- **OpenAI**: Sora-2 and Sora-2-Pro models (when available)
43-
- **Google Gemini**: Veo 3.1, Veo 3, and Veo 2 models (via the long-running operations API)
45+
- **Google Gemini**: Veo 3.1 models (via the long-running operations API), and Gemini Omni Flash (via the Interactions API)
4446
- **Grok (xAI)**: grok-imagine-video (text-to-video + image-to-video) and grok-imagine-video-1.5 (image-to-video only) models
4547
- **fal.ai**: MiniMax, Luma, Kling, Hunyuan, and other hosted video models
4648

@@ -569,6 +571,85 @@ Adapters that haven't declared a per-model duration map keep the plain
569571
> Files API and requires your API key to download (send it as an
570572
> `x-goog-api-key` header or `key` query parameter).
571573
574+
### Gemini Omni Flash (Interactions API) Model Options
575+
576+
Gemini Omni Flash (`gemini-omni-flash-preview`) is Google's multimodal
577+
video-generation model with conversational editing. It only serves the
578+
[Interactions API](https://ai.google.dev/gemini-api/docs/omni) — the same
579+
`geminiVideo()` adapter routes it automatically: `generateVideo` creates a
580+
background interaction, `getVideoJobStatus` polls it by id, and the
581+
finished clip comes back **inline as a `data:video/mp4;base64,…` URL**
582+
(when Google delivers by reference instead, the Files API URI passes
583+
through and needs your API key to download, like Veo).
584+
585+
Clips are 720p at 24 FPS, and `duration` accepts any value in the **3–10
586+
second** range (fractional seconds included), defaulting to 10 seconds when
587+
omitted. `availableDurations()` reports
588+
`{ kind: 'range', min: 3, max: 10, unit: 'seconds' }`; out-of-range
589+
`duration` values are rejected at job creation, and `snapDuration(n)` snaps
590+
raw seconds into the range (clamping to its bounds and rounding to whole
591+
seconds). The `size` option maps onto the interaction's output aspect
592+
ratio:
593+
594+
```typescript ignore
595+
import { generateVideo, getVideoJobStatus } from '@tanstack/ai'
596+
import { geminiVideo } from '@tanstack/ai-gemini'
597+
598+
const adapter = geminiVideo('gemini-omni-flash-preview')
599+
600+
const { jobId } = await generateVideo({
601+
adapter,
602+
prompt: 'A woman playing violin outdoors at golden hour',
603+
size: '9:16', // aspect ratio: '16:9' (default) or '9:16'
604+
duration: 6, // 3-10 seconds; omit for the 10s default
605+
})
606+
607+
const status = await getVideoJobStatus({ adapter, jobId })
608+
// status.url → 'data:video/mp4;base64,…' once completed
609+
```
610+
611+
Image and video prompt parts are sent to the interaction as content blocks
612+
— grouped as images, then videos, then the text prompt (Omni doesn't use
613+
Veo's `metadata.role` routing) — so you can condition the generation on
614+
stills or short reference clips. `data` sources
615+
are sent inline as base64; `url` sources pass through as-is — the adapter
616+
never downloads them, so use Gemini Files API URIs (upload large media via
617+
the Files API first).
618+
619+
#### Conversational video editing
620+
621+
Omni's headline capability is iterative refinement: pass the interaction id
622+
of a prior generation (its `jobId`) as
623+
`modelOptions.previous_interaction_id` and describe the change — the model
624+
edits the video while preserving everything you didn't mention:
625+
626+
```typescript ignore
627+
import { generateVideo } from '@tanstack/ai'
628+
import { geminiVideo } from '@tanstack/ai-gemini'
629+
630+
const adapter = geminiVideo('gemini-omni-flash-preview')
631+
632+
// Turn 1: generate
633+
const first = await generateVideo({
634+
adapter,
635+
prompt: 'A woman playing violin outdoors at golden hour',
636+
})
637+
638+
// …poll first.jobId to completion, then…
639+
640+
// Turn 2: edit the result conversationally
641+
const second = await generateVideo({
642+
adapter,
643+
prompt: 'Make the violin invisible',
644+
modelOptions: { previous_interaction_id: first.jobId },
645+
})
646+
```
647+
648+
`modelOptions` also passes through the Interactions API's request fields
649+
(e.g. `generation_config.video_config.task` to pin
650+
`'text_to_video' | 'image_to_video' | 'reference_to_video' | 'edit'`
651+
instead of letting the model infer the task mode).
652+
572653
### Grok (xAI Imagine) Model Options
573654

574655
Based on the [xAI video generation API](https://docs.x.ai/docs/guides/video-generations). Two models are available: `grok-imagine-video` (v1.0) supports **text-to-video and image-to-video**, while `grok-imagine-video-1.5` is **image-to-video only** (a text-only prompt is rejected by the API; the adapter throws a clear error pointing you at `grok-imagine-video`). Both are aspect-ratio sized — the generic `size` option takes an `aspectRatio_resolution` template (like the Grok Imagine image models), and clips can be 1–15 seconds long.

examples/ts-react-media/src/components/ImageGenerator.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import type { MediaPrompt } from '@tanstack/ai/client'
66
import { generateImageFn } from '@/lib/server-functions'
77
import { getRandomImagePrompt } from '@/lib/prompts'
88
import { IMAGE_MODELS } from '@/lib/models'
9-
import { readImageFile, toImagePart } from '@/lib/media'
10-
import type { AttachedImage } from '@/lib/media'
9+
import { readMediaFile, toImagePart } from '@/lib/media'
10+
import type { AttachedMedia } from '@/lib/media'
1111

1212
interface ImageGeneratorProps {
1313
onImageGenerated?: (imageUrl: string) => void
@@ -36,7 +36,7 @@ export default function ImageGenerator({
3636
const [selectedModel, setSelectedModel] = useState<string>('all')
3737
const [isLoading, setIsLoading] = useState(false)
3838
const [results, setResults] = useState<Record<string, ModelResult>>({})
39-
const [images, setImages] = useState<Array<AttachedImage>>([])
39+
const [images, setImages] = useState<Array<AttachedMedia>>([])
4040
const fileInputRef = useRef<HTMLInputElement>(null)
4141

4242
const currentModel = IMAGE_MODELS.find((m) => m.id === selectedModel)
@@ -56,7 +56,7 @@ export default function ImageGenerator({
5656
const files = Array.from(e.target.files ?? [])
5757
if (fileInputRef.current) fileInputRef.current.value = ''
5858
if (files.length === 0) return
59-
const attached = await Promise.all(files.map((file) => readImageFile(file)))
59+
const attached = await Promise.all(files.map((file) => readMediaFile(file)))
6060
setImages((prev) => [...prev, ...attached])
6161
}
6262

0 commit comments

Comments
 (0)