The Grok adapter calls xAI chat models on the Responses API. It also calls Grok Imagine for images and video, plus speech, transcription, and realtime voice.
npm i @tanstack/ai-grokpnpm add @tanstack/ai-grokyarn add @tanstack/ai-grokbun add @tanstack/ai-grokPut the key in the environment of your server:
XAI_API_KEY=xai-...grokText and the other short factories read XAI_API_KEY. To pass the key in code, use the create* factory of the same adapter:
import { createGrokText } from "@tanstack/ai-grok";
const adapter = createGrokText("grok-4.7", process.env.XAI_API_KEY!);import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { grokText } from "@tanstack/ai-grok";
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: grokText("grok-4.7"),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
}The client is the same for every provider. Point useChat at your route:
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
});
return (
<form
onSubmit={(event) => {
event.preventDefault();
sendMessage(input);
setInput("");
}}
>
{messages.map((message) => (
<p key={message.id}>
{message.parts
.map((part) => (part.type === "text" ? part.content : ""))
.join("")}
</p>
))}
<input value={input} onChange={(event) => setInput(event.target.value)} />
</form>
);
}For Vue, Solid, Svelte, Angular, and the other frameworks, see Quick Start.
You now have a streaming Grok chat. Tools and Structured Outputs work the same as with every adapter.
import { generateImage } from "@tanstack/ai";
import { grokImage } from "@tanstack/ai-grok";
const result = await generateImage({
adapter: grokImage("grok-imagine-image-2.0"),
prompt: "A futuristic cityscape at sunset",
threadId: "thread-1",
size: "16:9_2k",
modelOptions: { quality: "medium" },
});Grok Imagine makes short clips (1 to 15 seconds, with audio). Video generation is experimental. You start a job, then poll it:
import { generateVideo, getVideoJobStatus } from "@tanstack/ai";
import { grokVideo } from "@tanstack/ai-grok";
const adapter = grokVideo("grok-imagine-video-1.5");
// 1. Create the job
const { jobId } = await generateVideo({
adapter,
prompt: "A red panda balancing on a bamboo stalk in the rain",
threadId: "thread-1",
size: "16:9_720p", // "aspectRatio" or "aspectRatio_resolution"
duration: 5, // integer seconds, 1 to 15
});
// 2. Poll until complete, then read the video URL
let status = await getVideoJobStatus({ adapter, jobId });
while (status.status !== "completed" && status.status !== "failed") {
await new Promise((r) => setTimeout(r, 5000));
status = await getVideoJobStatus({ adapter, jobId });
}
console.log(status.url); // hosted .mp4 URLImage-to-video, references, edits, and sizes are in Video options. For the full flow and the useGenerateVideo hook, see Video Generation.
import { generateSpeech } from "@tanstack/ai";
import { grokSpeech } from "@tanstack/ai-grok";
const result = await generateSpeech({
adapter: grokSpeech("grok-tts"),
text: "Hello from Grok!",
threadId: "thread-1",
voice: "default",
format: "mp3",
});
console.log(result.audio); // Base64-encoded audioimport { generateTranscription } from "@tanstack/ai";
import { grokTranscription } from "@tanstack/ai-grok";
import { audioFile } from "./audio";
const result = await generateTranscription({
adapter: grokTranscription("grok-stt"),
audio: audioFile,
threadId: "thread-1",
});
console.log(result.text);import { summarize } from "@tanstack/ai";
import { grokSummarize } from "@tanstack/ai-grok";
const result = await summarize({
adapter: grokSummarize("grok-4.3"),
text: "Your long text to summarize...",
threadId: "thread-1",
maxLength: 100,
style: "concise", // "concise" | "bullet-points" | "paragraph"
});
console.log(result.summary);xAI runs some tools itself. Import them from @tanstack/ai-grok/tools and add them to tools:
import { chat } from "@tanstack/ai";
import { grokText } from "@tanstack/ai-grok";
import { grokWebSearchTool } from "@tanstack/ai-grok/tools";
const stream = chat({
adapter: grokText("grok-4.3"),
messages: [{ role: "user", content: "What's new in AI this week?" }],
threadId: "thread-1",
tools: [grokWebSearchTool()],
});Supported models: grok-4.3 and grok-build-0.1. For your own tools, use toolDefinition(). See Tools.
Provider options go in modelOptions, with the names of the xAI Responses API. This includes the sampling options temperature, top_p, and max_output_tokens:
import { chat } from "@tanstack/ai";
import { grokText } from "@tanstack/ai-grok";
const stream = chat({
adapter: grokText("grok-4.7"),
messages: [{ role: "user", content: "Hello!" }],
threadId: "thread-1",
modelOptions: {
temperature: 0.7,
top_p: 0.9,
max_output_tokens: 1024,
store: false,
include: ["reasoning.encrypted_content"],
},
});Want Grok to think harder? Set reasoning on chat(). It goes out as reasoning.effort, with a summary so that the thinking text streams:
import { chat } from "@tanstack/ai";
import { grokText } from "@tanstack/ai-grok";
const stream = chat({
adapter: grokText("grok-4.7"),
messages: [{ role: "user", content: "Plan a database migration." }],
threadId: "thread-1",
reasoning: "xhigh",
});| Model | Levels |
|---|---|
| grok-4.7, grok-4.6 | low to xhigh |
| grok-4.5 | low, medium, high |
| grok-4.3 | off, low, medium, high |
| Activity | Factory | Models |
|---|---|---|
| Chat | grokText | grok-4.7, grok-4.6, grok-4.5, grok-4.3, grok-build-0.1 |
| Chat on Vertex | grokVertexText | grok-4.3, grok-4.20-reasoning, grok-4.20-non-reasoning, grok-4.1-fast-reasoning, grok-4.1-fast-non-reasoning |
| Images | grokImage | grok-imagine-image-2.0, grok-imagine-image, grok-imagine-image-quality |
| Video | grokVideo | grok-imagine-video-1.5, grok-imagine-video |
| Speech | grokSpeech | grok-tts |
| Transcription | grokTranscription | grok-stt |
| Realtime voice | grokRealtime | grok-voice-think-fast-2.0, grok-voice-latest, grok-voice-fast-1.0 |
adapter.inputModalities lists the input kinds of the selected model. See Check what a model accepts.
import { createGrokText, type GrokTextConfig } from "@tanstack/ai-grok";
const config: Omit<GrokTextConfig, "apiKey"> = {
baseURL: "https://api.x.ai/v1", // Optional, this is the default
};
const adapter = createGrokText("grok-4.7", process.env.XAI_API_KEY!, config);Use @tanstack/ai-grok/vertex when Grok must run on Vertex AI. That path uses Google Cloud credentials and the regional or global endpoints of Vertex.
npm i @tanstack/ai-grok google-auth-librarypnpm add @tanstack/ai-grok google-auth-libraryyarn add @tanstack/ai-grok google-auth-librarybun add @tanstack/ai-grok google-auth-libraryimport { chat } from "@tanstack/ai";
import { grokVertexText } from "@tanstack/ai-grok/vertex";
const stream = chat({
adapter: grokVertexText("grok-4.3", {
project: "my-project",
location: "global",
}),
messages: [{ role: "user", content: "Hello!" }],
threadId: "thread-1",
});Gemini on Vertex is in @tanstack/ai-vertex.
The grok-imagine models take image prompt parts. The adapter then calls the /v1/images/edits endpoint of xAI.
import { generateImage } from "@tanstack/ai";
import { grokImage } from "@tanstack/ai-grok";
const result = await generateImage({
adapter: grokImage("grok-imagine-image"),
prompt: [
{
type: "text",
content: "Render the product in the style of the second image",
},
{
type: "image",
source: { type: "url", value: "https://example.com/product.png" },
},
{
type: "image",
source: { type: "url", value: "https://example.com/style.png" },
},
],
threadId: "thread-1",
});The servers of xAI fetch URL sources, so the URLs must be public. For a private image, use a data source.
The video size takes an aspectRatio_resolution template. The resolution part is optional.
Add an image prompt part as the first frame. Describe the motion in the text part. The servers of xAI fetch URL sources, so they must be public. For a base64 first frame, use a data source.
import { generateVideo } from "@tanstack/ai";
import { grokVideo } from "@tanstack/ai-grok";
const { jobId } = await generateVideo({
adapter: grokVideo("grok-imagine-video-1.5"),
prompt: [
{
type: "text",
content: "Make the waterfall crash down and slowly pan out the camera",
},
{
type: "image",
source: { type: "url", value: "https://example.com/waterfall-still.png" },
},
],
threadId: "thread-1",
size: "16:9_720p",
duration: 10,
});On grok-imagine-video-1.5, image parts with metadata.role: 'reference' (or 'character') become reference_images. They guide subjects and style, but do not lock the first frame. The prompt text names them <IMAGE_0>, <IMAGE_1>, and so on, in request order.
import { generateVideo } from "@tanstack/ai";
import { grokVideo } from "@tanstack/ai-grok";
const { jobId } = await generateVideo({
adapter: grokVideo("grok-imagine-video-1.5"),
prompt: [
{
type: "text",
content: "<IMAGE_0> walks through a neon-lit alley while <AUDIO_0> narrates",
},
{
type: "image",
source: { type: "url", value: "https://example.com/opening-frame.png" },
metadata: { role: "start_frame" },
},
{
type: "image",
source: { type: "url", value: "https://example.com/character.png" },
metadata: { role: "reference" },
},
],
threadId: "thread-1",
size: "16:9_720p",
modelOptions: {
reference_audios: [{ voice_id: "eve" }],
},
});grok-imagine-video (v1.0) can change or continue a clip. grok-imagine-video-1.5 takes no video input, so the adapter rejects a source-video part or mode on that model. Pass the source clip as a video prompt part and pick the mode with modelOptions.mode:
import { generateVideo } from "@tanstack/ai";
import { grokVideo } from "@tanstack/ai-grok";
const adapter = grokVideo("grok-imagine-video");
// Edit: change the clip in place
const edit = await generateVideo({
adapter,
prompt: [
{ type: "text", content: "Make the sky stormy with distant lightning" },
{
type: "video",
source: { type: "url", value: "https://example.com/clip.mp4" },
},
],
threadId: "thread-1",
modelOptions: { mode: "edit" },
});
// Extend: append 5 more seconds
const extension = await generateVideo({
adapter,
prompt: [
{ type: "text", content: "The camera keeps panning right across the bay" },
{
type: "video",
source: { type: "url", value: "https://example.com/clip.mp4" },
},
],
threadId: "thread-1",
duration: 5, // added seconds, not the total
modelOptions: { mode: "extend" },
});Both return the usual { jobId }. Poll them like any other Grok video job.
When the job is complete, the result has the usage from the xAI API:
Each factory pair has the same shape. The short factory reads XAI_API_KEY from the environment. The create* factory takes an explicit API key. Both take model as the first argument.
| Factories | Creates |
|---|---|
| grokText(model, config?) / createGrokText(model, apiKey, config?) | Text adapter |
| grokSummarize(model, config?) / createGrokSummarize(model, apiKey, config?) | Summarization adapter |
| grokImage(model, config?) / createGrokImage(model, apiKey, config?) | Image adapter |
| grokVideo(model, config?) / createGrokVideo(model, apiKey, config?) | Video adapter for the Grok Imagine video models. Experimental |
| grokSpeech(model, config?) / createGrokSpeech(model, apiKey, config?) | Text-to-speech adapter |
| grokTranscription(model, config?) / createGrokTranscription(model, apiKey, config?) | Speech-to-text adapter |
| grokFiles / createGrokFiles | Files API adapter. See Files API |
| grokRealtime(...) / grokRealtimeToken(...) | Realtime voice adapter and token issuer. See Realtime Voice Chat |
| grokVertexText / grokVertexSummarize | Text and summarize adapters on Vertex. Import from @tanstack/ai-grok/vertex |
config.baseURL sets a custom base URL. adapter.inputModalities lists the input kinds of the selected model. It is undefined for a model that this package does not know.