Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
40c1753
feat(ai-gemini): add geminiTextInteractions() adapter for stateful In…
tombeckenham Apr 24, 2026
64a1706
ci: apply automated fixes
autofix-ci[bot] Apr 24, 2026
dae238d
refactor(ai-gemini): surface interactionId via CUSTOM event, drop cor…
tombeckenham Apr 24, 2026
69e27d0
fix(ai-gemini): address CodeRabbit review feedback on Interactions ad…
tombeckenham Apr 24, 2026
f64703c
ci: apply automated fixes
autofix-ci[bot] Apr 24, 2026
7ea0664
fix(ai-gemini): emit RUN_ERROR with spec-compliant flat message/code
tombeckenham Apr 24, 2026
88bba03
feat(examples): wire Gemini Interactions into ts-react-chat, refresh …
tombeckenham Apr 24, 2026
be2cae5
test(ai-gemini): route adapter tests through public core APIs
tombeckenham Apr 24, 2026
4c6fc30
feat(ai-gemini): built-in tools on geminiTextInteractions()
tombeckenham Apr 24, 2026
a0029f3
ci: apply automated fixes
autofix-ci[bot] Apr 24, 2026
2ad5949
ci: apply automated fixes (attempt 2/3)
autofix-ci[bot] Apr 24, 2026
b3e470c
refactor(ai-gemini)!: gate `geminiTextInteractions` behind `/experime…
tombeckenham Apr 27, 2026
a68ad12
chore(ai-gemini): consolidate `geminiTextInteractions` changesets
tombeckenham Apr 27, 2026
958b506
Marked gemini interactions as experimental in example
tombeckenham Apr 27, 2026
34a87ae
test(e2e): wire up stateful-interactions spec for geminiTextInteractions
tombeckenham Apr 29, 2026
eaee03f
Merge pull request #4 from tombeckenham:501-e2e-stateful-interactions
tombeckenham May 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/gemini-text-interactions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@tanstack/ai-gemini': minor
---

feat(ai-gemini): add experimental `geminiTextInteractions()` adapter for Gemini's stateful Interactions API (Beta)

Routes through `client.interactions.create` instead of `client.models.generateContent`, so callers can pass `previous_interaction_id` via `modelOptions` and let the server retain conversation history. On each run, the returned interaction id is surfaced via an AG-UI `CUSTOM` event (`name: 'gemini.interactionId'`) emitted just before `RUN_FINISHED` β€” feed it back on the next turn via `modelOptions.previous_interaction_id`.

Exported from a dedicated `@tanstack/ai-gemini/experimental` subpath so the experimental status is load-bearing in your editor and bundle:

```ts
import { geminiTextInteractions } from '@tanstack/ai-gemini/experimental'
```

Scope: text/chat output with function tools, plus the built-in tools `google_search`, `code_execution`, `url_context`, `file_search`, and `computer_use`. Built-in tool activity is surfaced as AG-UI `CUSTOM` events named `gemini.googleSearchCall` / `gemini.googleSearchResult` (and the matching `codeExecutionCall`/`Result`, `urlContextCall`/`Result`, `fileSearchCall`/`Result` variants), carrying the raw Interactions delta payload. Function-tool `TOOL_CALL_*` events are unchanged, and `finishReason` stays `stop` when only built-in tools ran β€” the core chat loop has nothing to execute.

`google_search_retrieval`, `google_maps`, and `mcp_server` are not supported on this adapter and throw a targeted error explaining the alternative. Image/audio output via Interactions is also not routed through this adapter β€” use `geminiText()`, `geminiImage`, or `geminiSpeech` for those.

Marked `@experimental` β€” the underlying Interactions API is Beta and Google explicitly flags possible breaking changes.
120 changes: 120 additions & 0 deletions docs/adapters/gemini.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,110 @@ const stream = chat({
});
```

## Stateful Conversations β€” Interactions API (Experimental)

Gemini's [Interactions API](https://ai.google.dev/gemini-api/docs/interactions) (currently in Beta) offers server-side conversation state β€” the Gemini equivalent of OpenAI's Responses API. Instead of replaying the full message history on every turn, you pass a `previous_interaction_id` and the server retains the transcript. This also improves cache hit rates for repeated prefixes.

The `geminiTextInteractions` adapter routes through `client.interactions.create` and surfaces the server-assigned interaction id via an AG-UI `CUSTOM` event (`name: 'gemini.interactionId'`) emitted just before `RUN_FINISHED`, so you can chain turns.

> **⚠️ Experimental.** Google marks the Interactions API as Beta and explicitly flags possible breaking changes until it reaches general availability. The adapter is exported from the `@tanstack/ai-gemini/experimental` subpath so the experimental status is load-bearing in your editor and bundle. Text output, function tools, and the built-in tools `google_search`, `code_execution`, `url_context`, `file_search`, and `computer_use` are supported. `google_search_retrieval`, `google_maps`, and `mcp_server` still throw on this adapter β€” use `geminiText()` for those or wait for follow-up work.

### Basic Usage

```typescript
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-2.5-flash"),
messages: [{ role: "user", content: "Hi, my name is Amir." }],
})) {
if (chunk.type === "CUSTOM" && chunk.name === "gemini.interactionId") {
interactionId = (chunk.value as { interactionId?: string }).interactionId;
}
}

// Turn 2: only send the new turn's content β€” the server has the history.
for await (const chunk of chat({
adapter: geminiTextInteractions("gemini-2.5-flash"),
messages: [{ role: "user", content: "What is my name?" }],
modelOptions: {
previous_interaction_id: interactionId,
},
})) {
// ...stream "Your name is Amir." back to the client.
}
```

### How it differs from `geminiText`

| Concern | `geminiText` | `geminiTextInteractions` |
| --- | --- | --- |
| Underlying endpoint | `models:generateContent` | `interactions:create` |
| Conversation state | Stateless β€” send full history each turn | Stateful β€” server retains transcript via `previous_interaction_id` |
| Provider options shape | camelCase (`generationConfig`, `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` (activity surfaced via `CUSTOM` events) |
| Stability | GA | Experimental (Google Beta) |

### Provider Options

The adapter exposes Interactions-specific options on `modelOptions`:

```typescript
import { geminiTextInteractions } from "@tanstack/ai-gemini/experimental";

const stream = chat({
adapter: geminiTextInteractions("gemini-2.5-flash"),
messages,
modelOptions: {
// Stateful chaining β€” passed only on turn 2+.
previous_interaction_id: "int_abc123",

// Persist the interaction server-side (default true). Must be true for
// previous_interaction_id to work on the *next* turn.
store: true,

// Per-request system instruction (interaction-scoped β€” re-specify each turn).
system_instruction: "You are a helpful assistant.",

// snake_case generation config distinct from geminiText's camelCase one.
generation_config: {
thinking_level: "LOW",
thinking_summaries: "auto",
stop_sequences: ["<done>"],
},

response_modalities: ["text"],
},
});
```

### Reading the interaction id

The server's interaction id arrives as an AG-UI `CUSTOM` event emitted just before `RUN_FINISHED`:

```typescript
for await (const chunk of stream) {
if (chunk.type === "CUSTOM" && chunk.name === "gemini.interactionId") {
const id = (chunk.value as { interactionId: string }).interactionId;
// Persist `id` wherever you store per-user conversation pointers β€”
// pass it back on the next turn as `previous_interaction_id`.
}
}
```

### Caveats

- **Tools, `system_instruction`, and `generation_config` are interaction-scoped.** Per Google's docs these are NOT inherited from a prior interaction via `previous_interaction_id` β€” pass them again on every turn you need them.
- `store: false` is incompatible with `previous_interaction_id` (no state to recall) and with `background: true`.
- Retention (as of the time of writing): **55 days on the Paid Tier, 1 day on the Free Tier.** See [Google's Interactions API docs](https://ai.google.dev/gemini-api/docs/interactions) for current retention policy.
- Built-in tools in scope (`google_search`, `code_execution`, `url_context`, `file_search`, `computer_use`) are wired through; activity streams back as AG-UI `CUSTOM` events β€” `gemini.googleSearchCall` / `gemini.googleSearchResult` (and the matching `codeExecutionCall`/`Result`, `urlContextCall`/`Result`, `fileSearchCall`/`Result`) β€” carrying the raw Interactions delta. Function-tool `TOOL_CALL_*` events are unchanged, and `finishReason` stays `stop` when only built-in tools ran.
- `google_search_retrieval`, `google_maps`, and `mcp_server` still throw a targeted error on this adapter. Use `geminiText()` for the first two, or wait for a dedicated follow-up for `mcp_server`.
- Image and audio output via Interactions aren't routed through this adapter yet β€” it's text-only. Use `geminiImage` / `geminiSpeech` for non-text generation for now.

## Model Options

Gemini supports various model-specific options:
Expand Down Expand Up @@ -341,6 +445,22 @@ Creates a Gemini text/chat adapter with an explicit API key.

**Returns:** A Gemini text adapter instance.

### `geminiTextInteractions(model, config?)` (experimental)

Creates a Gemini Interactions API text adapter using environment variables. Backs the stateful conversation pattern via `previous_interaction_id`.

**Returns:** A Gemini Interactions text adapter instance.

### `createGeminiTextInteractions(model, apiKey, config?)` (experimental)

Creates a Gemini Interactions API text adapter with an explicit API key.

- `model` - The model name (e.g. `gemini-2.5-flash`)
- `apiKey` - Your Google API key
- `config.baseURL?` - Custom base URL (optional)

**Returns:** A Gemini Interactions text adapter instance.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### `geminiSummarize(config?)`

Creates a Gemini summarization adapter using environment variables.
Expand Down
99 changes: 49 additions & 50 deletions examples/ts-react-chat/src/lib/model-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type Provider =
| 'openai'
| 'anthropic'
| 'gemini'
| 'gemini-interactions'
| 'ollama'
| 'grok'
| 'groq'
Expand All @@ -15,72 +16,65 @@ export interface ModelOption {

export const MODEL_OPTIONS: Array<ModelOption> = [
// OpenAI
{ provider: 'openai', model: 'gpt-4o', label: 'OpenAI - GPT-4o' },
{ provider: 'openai', model: 'gpt-4o-mini', label: 'OpenAI - GPT-4o Mini' },
{ provider: 'openai', model: 'gpt-5', label: 'OpenAI - GPT-5' },
{ provider: 'openai', model: 'gpt-5.2', label: 'OpenAI - GPT-5.2' },
{ provider: 'openai', model: 'gpt-5.2-pro', label: 'OpenAI - GPT-5.2 Pro' },
{ provider: 'openai', model: 'gpt-5.1', label: 'OpenAI - GPT-5.1' },
{ provider: 'openai', model: 'gpt-5-mini', label: 'OpenAI - GPT-5 Mini' },

// Anthropic
{
provider: 'anthropic',
model: 'claude-sonnet-4-6',
label: 'Anthropic - Claude Sonnet 4.6',
},
{
provider: 'anthropic',
model: 'claude-sonnet-4-5-20250929',
label: 'Anthropic - Claude Sonnet 4.5',
model: 'claude-opus-4-6',
label: 'Anthropic - Claude Opus 4.6',
},
{
provider: 'anthropic',
model: 'claude-opus-4-5-20251101',
label: 'Anthropic - Claude Opus 4.5',
model: 'claude-sonnet-4-6',
label: 'Anthropic - Claude Sonnet 4.6',
},
{
provider: 'anthropic',
model: 'claude-haiku-4-0-20250514',
label: 'Anthropic - Claude Haiku 4.0',
model: 'claude-haiku-4-5',
label: 'Anthropic - Claude Haiku 4.5',
},

// Gemini
// Gemini (stateless `geminiText`)
{
provider: 'gemini',
model: 'gemini-2.0-flash',
label: 'Gemini - 2.0 Flash',
model: 'gemini-3.1-pro-preview',
label: 'Gemini - 3.1 Pro Preview',
},
{
provider: 'gemini',
model: 'gemini-2.5-flash',
label: 'Gemini - 2.5 Flash',
model: 'gemini-3.1-flash-lite-preview',
label: 'Gemini - 3.1 Flash Lite Preview',
},

// Gemini Interactions (stateful, experimental β€” `@tanstack/ai-gemini/experimental`)
{
provider: 'gemini',
model: 'gemini-2.5-pro',
label: 'Gemini - 2.5 Pro',
provider: 'gemini-interactions',
model: 'gemini-3.1-pro-preview',
label: 'Gemini Interactions - 3.1 Pro Preview (experimental)',
},
{
provider: 'gemini-interactions',
model: 'gemini-3.1-flash-lite-preview',
label: 'Gemini Interactions - 3.1 Flash Lite Preview (experimental)',
},

// Openrouter
{
provider: 'openrouter',
model: 'openai/chatgpt-4o-latest',
label: 'Openrouter - ChatGPT 4o Latest',
model: 'openai/gpt-5.2',
label: 'Openrouter - GPT-5.2',
},
{
provider: 'openrouter',
model: 'openai/chatgpt-4o-mini',
label: 'Openrouter - ChatGPT 4o Mini',
model: 'openai/gpt-5-mini',
label: 'Openrouter - GPT-5 Mini',
},

// Ollama
{
provider: 'ollama',
model: 'mistral:7b',
label: 'Ollama - Mistral 7B',
},
{
provider: 'ollama',
model: 'mistral',
label: 'Ollama - Mistral',
},
{
provider: 'ollama',
model: 'gpt-oss:20b',
Expand All @@ -93,15 +87,20 @@ export const MODEL_OPTIONS: Array<ModelOption> = [
},
{
provider: 'ollama',
model: 'smollm',
label: 'Ollama - SmolLM',
model: 'mistral',
label: 'Ollama - Mistral',
},

// Groq
{
provider: 'groq',
model: 'llama-3.3-70b-versatile',
label: 'Groq - Llama 3.3 70B',
model: 'openai/gpt-oss-120b',
label: 'Groq - GPT-OSS 120B',
},
{
provider: 'groq',
model: 'moonshotai/kimi-k2-instruct-0905',
label: 'Groq - Kimi K2 Instruct',
},
{
provider: 'groq',
Expand All @@ -110,30 +109,30 @@ export const MODEL_OPTIONS: Array<ModelOption> = [
},
{
provider: 'groq',
model: 'meta-llama/llama-4-scout-17b-16e-instruct',
label: 'Groq - Llama 4 Scout',
model: 'qwen/qwen3-32b',
label: 'Groq - Qwen3 32B',
},

// Grok
{
provider: 'grok',
model: 'grok-4',
label: 'Grok - Grok 4',
model: 'grok-4.20',
label: 'Grok - Grok 4.20',
},
{
provider: 'grok',
model: 'grok-4-fast-non-reasoning',
label: 'Grok - Grok 4 Fast',
model: 'grok-4-1-fast-reasoning',
label: 'Grok - Grok 4.1 Fast (Reasoning)',
},
{
provider: 'grok',
model: 'grok-3',
label: 'Grok - Grok 3',
model: 'grok-4-1-fast-non-reasoning',
label: 'Grok - Grok 4.1 Fast',
},
{
provider: 'grok',
model: 'grok-3-mini',
label: 'Grok - Grok 3 Mini',
model: 'grok-code-fast-1',
label: 'Grok - Grok Code Fast 1',
},
]

Expand Down
Loading
Loading