Skip to content

Commit 62be2d7

Browse files
tombeckenhamclaude
andcommitted
feat(examples): add direct-xAI grok video entries to ts-react-media
Adds 'xAI Direct' text-to-video and image-to-video entries to the video generator that use the native grokVideo adapter against xAI's Imagine API (XAI_API_KEY), alongside the existing fal-hosted grok-imagine entries. Polling is now keyed by the UI model id (the direct entries share one adapter model), and the completed card shows the exact USD cost when the adapter reports it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f93ba7d commit 62be2d7

6 files changed

Lines changed: 81 additions & 10 deletions

File tree

examples/ts-react-media/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ FAL_KEY=
55

66
# Get a Google API key at https://aistudio.google.com/apikey
77
GOOGLE_API_KEY=
8+
9+
# Get an xAI API key at https://console.x.ai — used by the "xAI Direct"
10+
# Grok Imagine video models (the other Grok Imagine entries go through fal).
11+
XAI_API_KEY=

examples/ts-react-media/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@tanstack/ai": "workspace:*",
1515
"@tanstack/ai-fal": "workspace:*",
1616
"@tanstack/ai-gemini": "workspace:*",
17+
"@tanstack/ai-grok": "workspace:*",
1718
"@tanstack/react-router": "^1.158.4",
1819
"@tanstack/react-start": "^1.159.0",
1920
"@tanstack/router-plugin": "^1.158.4",

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type JobState =
2121
model: string
2222
progress?: number | undefined
2323
}
24-
| { status: 'completed'; url: string; unitsBilled?: number }
24+
| { status: 'completed'; url: string; unitsBilled?: number; cost?: number }
2525
| { status: 'error'; message: string }
2626

2727
interface VideoGeneratorProps {
@@ -97,6 +97,7 @@ export default function VideoGenerator({
9797
status: 'completed',
9898
url: url,
9999
unitsBilled: urlResult.usage?.unitsBilled,
100+
cost: urlResult.usage?.cost,
100101
},
101102
}))
102103
} else if (status.status === 'processing') {
@@ -164,8 +165,11 @@ export default function VideoGenerator({
164165
},
165166
}))
166167

168+
// Poll keyed by the UI model id, not result.model: the direct-xAI
169+
// entries share one adapter model ('grok-imagine-video'), so
170+
// result.model wouldn't identify the card (or the adapter) uniquely.
167171
const interval = setInterval(() => {
168-
pollStatus(result.jobId, result.model)
172+
pollStatus(result.jobId, modelId)
169173
}, 4000)
170174
pollingRefs.current.set(modelId, interval)
171175
} catch (err) {
@@ -406,12 +410,21 @@ export default function VideoGenerator({
406410
className="w-full h-auto"
407411
/>
408412
</div>
409-
{state.unitsBilled != null && (
413+
{state.cost != null ? (
410414
<p className="text-xs text-gray-500">
411-
Billed {state.unitsBilled} fal unit
412-
{state.unitsBilled === 1 ? '' : 's'} — multiply by the
413-
endpoint unit price for USD cost
415+
Billed ${state.cost.toFixed(3)}
416+
{state.unitsBilled != null
417+
? ` for ${state.unitsBilled} second${state.unitsBilled === 1 ? '' : 's'} of video`
418+
: ''}
414419
</p>
420+
) : (
421+
state.unitsBilled != null && (
422+
<p className="text-xs text-gray-500">
423+
Billed {state.unitsBilled} fal unit
424+
{state.unitsBilled === 1 ? '' : 's'} — multiply by the
425+
endpoint unit price for USD cost
426+
</p>
427+
)
415428
)}
416429
</>
417430
)}

examples/ts-react-media/src/lib/models.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ export const VIDEO_MODELS = [
110110
description: 'xAI animate images to video',
111111
mode: 'image-to-video' as const,
112112
},
113+
{
114+
id: 'grok-imagine-video',
115+
name: 'Grok Imagine Video (xAI Direct, Text-to-Video)',
116+
description: 'xAI Imagine API via the native grokVideo adapter',
117+
mode: 'text-to-video' as const,
118+
},
119+
{
120+
id: 'grok-imagine-video/image-to-video',
121+
name: 'Grok Imagine Video (xAI Direct, Image-to-Video)',
122+
description: 'Animate a starting frame via the native grokVideo adapter',
123+
mode: 'image-to-video' as const,
124+
},
113125
{
114126
id: 'fal-ai/ltx-2.3/text-to-video/fast',
115127
name: 'LTX-2.3 Fast (Text-to-Video)',

examples/ts-react-media/src/lib/server-functions.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { createServerFn } from '@tanstack/react-start'
22
import { falImage, falVideo } from '@tanstack/ai-fal'
33
import { geminiImage } from '@tanstack/ai-gemini'
4+
import { grokVideo } from '@tanstack/ai-grok'
45
import { generateImage, generateVideo, getVideoJobStatus } from '@tanstack/ai'
56

6-
import type { FalModel } from '@tanstack/ai-fal'
77
import type {
88
ImagePart,
99
MediaInputMetadata,
@@ -67,6 +67,21 @@ function asImageToVideoPrompt(
6767
return narrowed
6868
}
6969

70+
/**
71+
* Resolves the video adapter for a UI model id. The native grok-imagine
72+
* entries hit xAI's Imagine API directly via the `grokVideo` adapter
73+
* (XAI_API_KEY); everything else is a fal-hosted model.
74+
*/
75+
function videoAdapterForModel(model: string) {
76+
if (
77+
model === 'grok-imagine-video' ||
78+
model === 'grok-imagine-video/image-to-video'
79+
) {
80+
return grokVideo('grok-imagine-video')
81+
}
82+
return falVideo(model)
83+
}
84+
7085
export const generateImageFn = createServerFn({ method: 'POST' })
7186
.inputValidator((data: { prompt: MediaPrompt; model: string }) => {
7287
if (!hasPromptContent(data.prompt)) throw new Error('Prompt is required')
@@ -214,6 +229,18 @@ export const createVideoJobFn = createServerFn({ method: 'POST' })
214229
},
215230
})
216231
}
232+
case 'grok-imagine-video': {
233+
// Direct xAI Imagine API (XAI_API_KEY) — no fal in between. Sizing is
234+
// an "aspectRatio_resolution" template; durations are 1-15 integer
235+
// seconds. Completed jobs report usage.unitsBilled (billed seconds)
236+
// and usage.cost (exact USD).
237+
return generateVideo({
238+
adapter: grokVideo('grok-imagine-video'),
239+
prompt: asTextPrompt(data.prompt),
240+
size: '16:9_720p',
241+
duration: 5,
242+
})
243+
}
217244
case 'fal-ai/ltx-2.3/text-to-video/fast': {
218245
return generateVideo({
219246
adapter: falVideo('fal-ai/ltx-2.3/text-to-video/fast'),
@@ -252,6 +279,17 @@ export const createVideoJobFn = createServerFn({ method: 'POST' })
252279
},
253280
})
254281
}
282+
case 'grok-imagine-video/image-to-video': {
283+
// Direct xAI Imagine API. The starting frame is supplied as an image
284+
// prompt part (asImageToVideoPrompt requires one); the grokVideo
285+
// adapter forwards it to the Imagine endpoint as the start frame.
286+
return generateVideo({
287+
adapter: grokVideo('grok-imagine-video'),
288+
prompt: asImageToVideoPrompt(data.prompt),
289+
size: '16:9_720p',
290+
duration: 5,
291+
})
292+
}
255293
case 'fal-ai/ltx-2.3/image-to-video/fast': {
256294
return generateVideo({
257295
adapter: falVideo('fal-ai/ltx-2.3/image-to-video/fast'),
@@ -265,9 +303,9 @@ export const createVideoJobFn = createServerFn({ method: 'POST' })
265303
})
266304

267305
export const getVideoStatusFn = createServerFn({ method: 'GET' })
268-
.inputValidator((data: { jobId: string; model: FalModel }) => data)
306+
.inputValidator((data: { jobId: string; model: string }) => data)
269307
.handler(async ({ data }) => {
270-
const adapter = falVideo(data.model)
308+
const adapter = videoAdapterForModel(data.model)
271309
return await getVideoJobStatus({
272310
adapter,
273311
jobId: data.jobId,
@@ -277,7 +315,7 @@ export const getVideoStatusFn = createServerFn({ method: 'GET' })
277315
export const getVideoUrlFn = createServerFn({ method: 'GET' })
278316
.inputValidator((data: { jobId: string; model: string }) => data)
279317
.handler(async ({ data }) => {
280-
const adapter = falVideo(data.model)
318+
const adapter = videoAdapterForModel(data.model)
281319
return await getVideoJobStatus({
282320
adapter,
283321
jobId: data.jobId,

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)