This page has the details of the Gemini adapter. To get a first chat working, start at Google Gemini.
import { createGeminiChat, type GeminiTextConfig } from "@tanstack/ai-gemini";
const config: Omit<GeminiTextConfig, "apiKey"> = {
baseURL: "https://generativelanguage.googleapis.com/v1beta", // Optional
defaultHeaders: { "X-Custom-Header": "value" }, // Optional
};
const adapter = createGeminiChat("gemini-3.8-flash", process.env.GEMINI_API_KEY!, config);Send every request through a gateway, such as Cloudflare AI Gateway or a corporate proxy, with baseURL and defaultHeaders. These two option names are the same on every TanStack AI adapter, so one gateway config works for all of them.
import { createGeminiChat } from "@tanstack/ai-gemini";
const adapter = createGeminiChat("gemini-3.8-flash", process.env.GEMINI_API_KEY!, {
baseURL: "https://gateway.example.com/google-ai-studio",
defaultHeaders: { "cf-aig-authorization": `Bearer ${process.env.GATEWAY_TOKEN}` },
});By default, Gemini samples the video at 1 frame per second. Change the sampling in the part metadata:
// Watch a 30s-90s clip at half a frame per second.
geminiVideoPart(file, { fps: 0.5, startOffset: "30s", endOffset: "90s" });Sampling one time can miss detail in long or fast videos. Set processing: "agentic" to let the model drive. It moves through the timeline and gets frames, transcripts, and audio when it needs them.
geminiVideoPart(file, { processing: "agentic" });Supported models:
With agentic, the adapter sends the request through the Interactions API of Gemini. You give the sampling rate in your prompt ("watch it at 2 fps"), not with fps. The result is deeper but slower, because the model looks at the video again on each turn.
The Interactions API of Gemini keeps the conversation on the server. It is the Gemini equivalent of the OpenAI Responses API, and it is in Beta. You pass a previous_interaction_id, and the server keeps the transcript, so you do not send the full history on every turn. This also gets more cache hits for repeated starts of a prompt.
The geminiTextInteractions adapter calls client.interactions.create. Right before RUN_FINISHED, it sends the interaction id from the server as an AG-UI CUSTOM event (name: 'gemini.interactionId'), so you can chain turns.
Warning
The Interactions API is experimental. Google marks it as Beta and can make breaking changes until it is generally available. Import the adapter from @tanstack/ai-gemini/experimental.
What the adapter supports:
import { chat } from "@tanstack/ai";
import { geminiTextInteractions } from "@tanstack/ai-gemini/experimental";
// Turn 1: introduce yourself, capture the interaction id.
let interactionId: string | undefined;
for await (const chunk of chat({
adapter: geminiTextInteractions("gemini-3.5-flash"),
messages: [{ role: "user", content: "Hi, my name is Amir." }],
threadId: "thread-1",
})) {
if (
chunk.type === "CUSTOM" &&
chunk.name === "gemini.interactionId" &&
chunk.value &&
typeof chunk.value === "object" &&
"interactionId" in chunk.value
) {
interactionId = String(chunk.value.interactionId);
}
}
// Turn 2: only send the new turn. The server has the history.
for await (const chunk of chat({
adapter: geminiTextInteractions("gemini-3.5-flash"),
messages: [{ role: "user", content: "What is my name?" }],
threadId: "thread-1",
modelOptions: {
previous_interaction_id: interactionId,
},
})) {
// ...stream "Your name is Amir." back to the client.
}The Interactions API does not accept a history of many turns without a previous_interaction_id. If a chat client sends [user, assistant, user] to a new interaction, the adapter throws cannot send prior conversation history on a fresh interaction. To make useChat work, keep the id from the server and send it back on the next turn.
Server route (for example, a TanStack Start handler):
import {
chat,
chatParamsFromRequestBody,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { geminiTextInteractions } from "@tanstack/ai-gemini/experimental";
export async function POST({ request }: { request: Request }) {
const params = await chatParamsFromRequestBody(await request.json());
// The client sends previousInteractionId in `forwardedProps`.
const previousInteractionId =
typeof params.forwardedProps.previousInteractionId === "string"
? params.forwardedProps.previousInteractionId
: undefined;
const stream = chat({
adapter: geminiTextInteractions("gemini-3.5-flash"),
messages: params.messages,
threadId: params.threadId,
modelOptions: {
previous_interaction_id: previousInteractionId,
store: true, // required for chaining on the next turn
},
});
return toServerSentEventsResponse(stream);
}React client:
import { useEffect, useMemo, useState } from "react";
import { fetchServerSentEvents, useChat } from "@tanstack/ai-react";
function GeminiChat() {
const [interactionId, setInteractionId] = useState<string | undefined>();
const forwardedProps = useMemo(
() => (interactionId ? { previousInteractionId: interactionId } : {}),
[interactionId],
);
const { messages, setMessages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
forwardedProps,
onCustomEvent: (eventType, data) => {
if (
eventType === "gemini.interactionId" &&
typeof data === "object" &&
data !== null &&
"interactionId" in data
) {
setInteractionId(String(data.interactionId));
}
},
});
// A new provider or model starts a new chain on the server. Clear the id
// AND the local messages together. If you do not, the next turn sends
// many messages with no previous_interaction_id, and the adapter throws.
const [provider, setProvider] = useState("gemini-interactions");
useEffect(() => {
setInteractionId(undefined);
setMessages([]);
}, [provider]);
// ...render messages, call sendMessage(input)
}The full example is in examples/ts-react-chat. See src/routes/index.tsx for the client and src/routes/api.tanchat.ts for the route.
| geminiText | geminiTextInteractions | |
|---|---|---|
| Endpoint | models:generateContent | interactions:create |
| Conversation state | Stateless: send the full history each turn | Stateful: the server keeps the transcript through previous_interaction_id |
| Provider options | camelCase (stopSequences, responseModalities, safetySettings) | snake_case (generation_config, response_modalities, previous_interaction_id) |
| Built-in tools | google_search, code_execution, url_context, file_search, google_maps, google_search_retrieval, computer_use | google_search, code_execution, url_context, file_search, computer_use. Only the first four stream CUSTOM activity events |
| Stability | GA | Experimental (Google Beta) |
The adapter has options for the Interactions API on modelOptions:
import { chat } from "@tanstack/ai";
import { geminiTextInteractions } from "@tanstack/ai-gemini/experimental";
const stream = chat({
adapter: geminiTextInteractions("gemini-3.5-flash"),
messages: [{ role: "user", content: "Hello!" }],
threadId: "thread-1",
// Goes out as generation_config.thinking_level and thinking_summaries.
reasoning: "low",
modelOptions: {
// Chain to the last turn. Pass it only on turn 2 and later.
previous_interaction_id: "int_abc123",
// Keep the interaction on the server (default true). Must be true for
// previous_interaction_id to work on the next turn.
store: true,
// System instruction for this interaction only. Send it again each turn.
system_instruction: "You are a helpful assistant.",
// snake_case generation config, not the camelCase config of geminiText.
generation_config: {
stop_sequences: ["<done>"],
},
response_modalities: ["text"],
},
});The interaction id from the server arrives as an AG-UI CUSTOM event, right before RUN_FINISHED:
for await (const chunk of stream) {
if (
chunk.type === "CUSTOM" &&
chunk.name === "gemini.interactionId" &&
typeof chunk.value === "object" &&
chunk.value !== null &&
"interactionId" in chunk.value
) {
const id = String(chunk.value.interactionId);
// Keep `id` where you store the conversation pointer of each user.
// Pass it back on the next turn as `previous_interaction_id`.
}
}Know these before you ship:
Tools and output:
geminiImage uses the Interactions API. result.id is the interaction id.
You have an image from a Gemini native model. You want to change that image, without a second upload of it. Pass the last id back, and write only the change in prompt:
import { generateImage } from "@tanstack/ai";
import { geminiImage } from "@tanstack/ai-gemini";
const adapter = geminiImage("gemini-3.1-flash-image");
const first = await generateImage({
adapter,
prompt: "A red bicycle on a quiet street",
threadId: "thread-1",
});
const edited = await generateImage({
adapter,
prompt: "Make the bicycle blue",
threadId: "thread-1",
modelOptions: { previous_interaction_id: first.id },
});Keep the interaction, so that the next edit can run:
Also:
The same steps are in Image Generation.
The size of a Gemini image model joins an aspect ratio and a resolution tier:
// Format: "aspectRatio_resolution"
size: "16:9_4K"
size: "1:1_2K"
size: "9:16_1K"Each model has its own set. TypeScript narrows size to the set of the model:
| Model | Aspect ratios | Resolutions |
|---|---|---|
| gemini-3.1-flash-image | 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9, 1:4, 4:1, 1:8, 8:1 | 512, 1K, 2K, 4K |
| gemini-3.1-flash-lite-image | the same 14 as above (see the note) | 1K only |
| gemini-nano-banana-2.1 | the same 14 as above | 1K, 2K, 4K |
| gemini-3-pro-image | 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9 | 1K, 2K, 4K |
Note
For gemini-3.1-flash-lite-image, the four banner ratios (1:4, 4:1, 1:8, 8:1) are partly inferred. The Gemini API guide has no ratio table for this model. The set of 14 comes from the Cloud model page and from the statement in the guide of "a discrete set of 14 aspect ratios". If the API rejects those four, use the 10 standard ratios on this model.
Set image options on modelOptions:
import { generateImage } from "@tanstack/ai";
import { geminiImage, ThinkingLevel } from "@tanstack/ai-gemini";
const result = await generateImage({
adapter: geminiImage("gemini-3.1-flash-image"),
prompt: "A beautiful garden",
threadId: "thread-1",
size: "16:9_4K",
modelOptions: {
thinkingConfig: { thinkingLevel: ThinkingLevel.LOW },
// imageConfig replaces one field from size. The 16:9 ratio from size stays.
// imageConfig accepts only aspectRatio and imageSize.
imageConfig: { imageSize: "2K" },
},
});See Image Generation for the full list of native options.
Gemini names the speakers in the prompt and maps each name to a voice. Pass turns, and the adapter does both for you. It uses the voice name as the speaker label:
import { generateSpeech } from "@tanstack/ai";
import { geminiSpeech } from "@tanstack/ai-gemini";
const result = await generateSpeech({
adapter: geminiSpeech("gemini-3.1-flash-tts-preview"),
turns: [
{ text: "Hey, how is it going?", voice: "Puck" },
{ text: "Not bad, you?", voice: "Kore" },
],
threadId: "thread-1",
});Gemini has tools of its own, next to the function tools that you define. Import them from @tanstack/ai-gemini/tools and pass them into chat({ tools: [...] }).
For the concept, a comparison table, and type gating, see Provider Tools.
Gemini runs Python code in a sandbox and returns the results inline. It takes no arguments. Add it to the tools array.
import { chat } from "@tanstack/ai";
import { geminiText } from "@tanstack/ai-gemini";
import { codeExecutionTool } from "@tanstack/ai-gemini/tools";
const stream = chat({
adapter: geminiText("gemini-3.8-flash"),
messages: [{ role: "user", content: "Calculate the first 10 Fibonacci numbers" }],
threadId: "thread-1",
tools: [codeExecutionTool()],
});Supported models: Gemini 1.5 Pro, Gemini 2.x, Gemini 2.5 and later. See Provider Tools.
Searches the files that you uploaded to the Gemini File API. Pass a FileSearch config object with the stores to search.
import { chat } from "@tanstack/ai";
import { geminiText } from "@tanstack/ai-gemini";
import { fileSearchTool } from "@tanstack/ai-gemini/tools";
const stream = chat({
adapter: geminiText("gemini-3.8-flash"),
messages: [{ role: "user", content: "Find the quarterly revenue figures" }],
threadId: "thread-1",
tools: [
fileSearchTool({
fileSearchStoreNames: ["fileSearchStores/my-file-search-store-123"],
}),
],
});Supported models: Gemini 2.x and later. See Provider Tools.
Gemini searches Google and uses the grounded results in its answer. Pass a GoogleSearch config, or call it with no arguments for the defaults.
import { chat } from "@tanstack/ai";
import { geminiText } from "@tanstack/ai-gemini";
import { googleSearchTool } from "@tanstack/ai-gemini/tools";
const stream = chat({
adapter: geminiText("gemini-3.8-flash"),
messages: [{ role: "user", content: "What's the weather in Tokyo right now?" }],
threadId: "thread-1",
tools: [googleSearchTool()],
});Supported models: Gemini 1.5 Pro, Gemini 2.x, Gemini 2.5. See Provider Tools.
When Google Search returns grounding data:
A Google Search variant for retrieval. It returns ranked passages from the web, with a dynamic retrieval mode that you can set. Pass a GoogleSearchRetrieval config.
import { chat } from "@tanstack/ai";
import { geminiText } from "@tanstack/ai-gemini";
import { googleSearchRetrievalTool } from "@tanstack/ai-gemini/tools";
const stream = chat({
adapter: geminiText("gemini-3.1-pro-preview"),
messages: [{ role: "user", content: "Explain the latest JavaScript proposals" }],
threadId: "thread-1",
tools: [
googleSearchRetrievalTool({
dynamicRetrievalConfig: { mode: "MODE_DYNAMIC", dynamicThreshold: 0.7 },
}),
],
});Supported models: Gemini 1.5 Pro and later. See Provider Tools.
Connects Gemini to Google Maps for questions about places: directions, place search, and geocoding. Pass a GoogleMaps config, or call it with no arguments.
import { chat } from "@tanstack/ai";
import { geminiText } from "@tanstack/ai-gemini";
import { googleMapsTool } from "@tanstack/ai-gemini/tools";
const stream = chat({
adapter: geminiText("gemini-2.5-pro"),
messages: [{ role: "user", content: "Find coffee shops near Union Square, SF" }],
threadId: "thread-1",
tools: [googleMapsTool()],
});Supported models: Gemini 2.5 and later. See Provider Tools.
Gets the content of the URLs in the conversation, so Gemini can use live web pages. It takes no arguments.
import { chat } from "@tanstack/ai";
import { geminiText } from "@tanstack/ai-gemini";
import { urlContextTool } from "@tanstack/ai-gemini/tools";
const stream = chat({
adapter: geminiText("gemini-3.8-flash"),
messages: [{ role: "user", content: "Summarise https://example.com/article" }],
threadId: "thread-1",
tools: [urlContextTool()],
});Supported models: Gemini 2.x and later. See Provider Tools.
Gemini sees a virtual desktop through screenshots. It acts on the desktop with predefined computer-use functions. Give the environment. You can limit the functions with excludedPredefinedFunctions.
import { chat } from "@tanstack/ai";
import { geminiText } from "@tanstack/ai-gemini";
import { computerUseTool } from "@tanstack/ai-gemini/tools";
const stream = chat({
adapter: geminiText("gemini-3.1-pro-preview"),
messages: [{ role: "user", content: "Navigate to example.com in the browser" }],
threadId: "thread-1",
tools: [
computerUseTool({
environment: "browser",
}),
],
});Supported models: Gemini 2.5 and later. See Provider Tools.
Each factory pair has the same shape. The short factory (geminiText, geminiImage, and the others) reads GOOGLE_API_KEY or GEMINI_API_KEY from the environment. The create* factory takes an explicit API key. Both take model as the first argument.
| Factories | Creates |
|---|---|
| geminiText(model, config?) / createGeminiChat(model, apiKey, config?) | Chat adapter |
| geminiTextInteractions(model, config?) / createGeminiTextInteractions(model, apiKey, config?) | Chat adapter on the Interactions API. Experimental. Import from @tanstack/ai-gemini/experimental |
| geminiSummarize(model, config?) / createGeminiSummarize(model, apiKey, config?) | Summarization adapter |
| geminiImage(model, config?) / createGeminiImage(model, apiKey, config?) | Image adapter. See Images |
| geminiSpeech(model, config?) / createGeminiSpeech(model, apiKey, config?) | Text-to-speech adapter. Experimental |
| geminiAudio(model, config?) / createGeminiAudio(model, apiKey, config?) | Lyria music adapter. Experimental |
| geminiVideo(model, config?) / createGeminiVideo(model, apiKey, config?) | Video adapter. Experimental |
| geminiEmbedding(model, config?) / createGeminiEmbedding(model, apiKey, config?) | Embedding adapter |
| geminiFiles / createGeminiFiles | Files API adapter. See Files API |
Chat adapter config:
adapter.inputModalities lists the input kinds of the selected model. It is undefined for a model that this package does not know. See Check what a model accepts.