Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b8c1615
feat(ai): add type-safe tool call events to chat() stream
AlemTuzlak Apr 15, 2026
a2c2f9c
ci: apply automated fixes
autofix-ci[bot] Apr 15, 2026
5a37cb8
feat(ai): make tool call events a discriminated union for per-tool in…
AlemTuzlak Apr 16, 2026
5e3ebd7
ci: apply automated fixes
autofix-ci[bot] Apr 16, 2026
9a1df7e
docs: improve type-safe tool call event documentation
AlemTuzlak Apr 16, 2026
d46cd6d
fix(ai): resolve tsc errors in discriminated union types and fix docs
AlemTuzlak Apr 16, 2026
883fe15
Merge remote-tracking branch 'origin/main' into worktree-serialized-e…
AlemTuzlak Apr 24, 2026
f2aaac0
fix(ai): restore discriminated union narrowing on typed tool-call events
AlemTuzlak Apr 24, 2026
53e7203
Merge branch 'main' into worktree-serialized-exploring-wall
AlemTuzlak May 25, 2026
2b0bd96
fix(ai): address Round 1 CR findings on type-safe stream chunks
AlemTuzlak May 25, 2026
f66ad73
docs: add TaggedCustomEvent reference page
AlemTuzlak May 25, 2026
c42100f
docs(ai): fix TypedStreamChunk JSDoc to match no-typed-tools fallback
AlemTuzlak May 25, 2026
c768cce
refactor(ai): include TTools in streaming-structured chat() return cast
AlemTuzlak May 25, 2026
22d188a
Merge branch 'main' into worktree-serialized-exploring-wall
AlemTuzlak May 26, 2026
3569aa0
feat(ai): narrow stream chunks via Pick<AGUI*Event, …> + EventType li…
AlemTuzlak May 26, 2026
0a9c07e
fix(ai): switch chunk.type from EventType enum literal to plain strin…
AlemTuzlak May 26, 2026
573e413
ci: apply automated fixes
autofix-ci[bot] May 26, 2026
cff54ce
feat(ai): narrow CustomEvent to TaggedCustomEvent even without typed …
AlemTuzlak May 26, 2026
de5f152
docs+feat: update model refs to gpt-5.2 and preserve tool name litera…
AlemTuzlak May 26, 2026
821a3d0
ci: apply automated fixes
autofix-ci[bot] May 26, 2026
c0b0feb
feat(ai): expose typed tool output on TOOL_CALL_END events
AlemTuzlak May 26, 2026
550b102
ci: apply automated fixes
autofix-ci[bot] May 26, 2026
57c46ff
chore(ai): fix lint errors in @tanstack/ai
AlemTuzlak May 26, 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
7 changes: 7 additions & 0 deletions .changeset/svelte-callback-propagation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/ai-svelte': patch
---

fix(ai-svelte): propagate `createChat` callback changes uniformly

`onResponse`, `onChunk`, and `onCustomEvent` were passed as direct references to the underlying `ChatClient`, while `onFinish` and `onError` were wrapped to read from `options.onX?.(...)` at call time. This meant callers who mutated the options object in-place (or invoked `client.updateOptions(...)`) would see their replacement propagate for the latter two but silently miss for the former three. All five user-supplied callbacks now go through the same indirection, matching the React / Preact / Vue / Solid sibling wrappers.
2 changes: 1 addition & 1 deletion docs/adapters/openai.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: "Use OpenAI models with TanStack AI — GPT-4o, GPT-5, DALL-E image
keywords:
- tanstack ai
- openai
- gpt-4o
- gpt-5.2
- gpt-5
- dall-e
- whisper
Expand Down
2 changes: 1 addition & 1 deletion docs/adapters/openrouter.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const stream = chat({
messages,
modelOptions: {
models: [
"openai/gpt-4o",
"openai/gpt-5.2",
"anthropic/claude-3.5-sonnet",
"google/gemini-pro",
],
Expand Down
10 changes: 5 additions & 5 deletions docs/advanced/debug-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const stream = chat({
adapter: openaiText("gpt-4o"),
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Hello" }],
debug: true,
});
Expand All @@ -36,7 +36,7 @@ const stream = chat({
Every internal event now prints to the console with a `[tanstack-ai:<category>]` prefix:

```
[tanstack-ai:request] activity=chat provider=openai model=gpt-4o messages=1 tools=0 stream=true
[tanstack-ai:request] activity=chat provider=openai model=gpt-5.2 messages=1 tools=0 stream=true
[tanstack-ai:agentLoop] run started
[tanstack-ai:provider] provider=openai type=response.output_text.delta
[tanstack-ai:output] type=TEXT_MESSAGE_CONTENT
Expand All @@ -49,7 +49,7 @@ Pass a `DebugConfig` object instead of `true`. Every unspecified category defaul

```typescript
chat({
adapter: openaiText("gpt-4o"),
adapter: openaiText("gpt-5.2"),
messages,
debug: { middleware: false }, // everything except middleware
});
Expand All @@ -59,7 +59,7 @@ If you want to see ONLY a specific set of categories, set the rest to `false` ex

```typescript
chat({
adapter: openaiText("gpt-4o"),
adapter: openaiText("gpt-5.2"),
messages,
debug: {
provider: true,
Expand Down Expand Up @@ -91,7 +91,7 @@ const logger: Logger = {
};

chat({
adapter: openaiText("gpt-4o"),
adapter: openaiText("gpt-5.2"),
messages,
debug: { logger }, // all categories on, piped to pino
});
Expand Down
4 changes: 2 additions & 2 deletions docs/advanced/extend-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const myOpenai = extendAdapter(openaiText, [
])

// Use with original models - full type inference preserved
const gpt4Adapter = myOpenai('gpt-4o')
const gpt4Adapter = myOpenai('gpt-5.2')

// Use with custom models - your custom types are applied
const customAdapter = myOpenai('my-fine-tuned-gpt4')
Expand Down Expand Up @@ -109,7 +109,7 @@ import { openaiText } from '@tanstack/ai-openai'
const myOpenai = extendAdapter(openaiText, [createModel('custom-model', ['text'])])

// ✅ Original models work with their original types
const a1 = myOpenai('gpt-4o')
const a1 = myOpenai('gpt-5.2')

// ✅ Custom models work with your defined types
const a2 = myOpenai('custom-model')
Expand Down
6 changes: 3 additions & 3 deletions docs/advanced/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const logger: ChatMiddleware = {
};

const stream = chat({
adapter: openaiText("gpt-4o"),
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Hello" }],
middleware: [logger],
});
Expand Down Expand Up @@ -444,7 +444,7 @@ Middleware execute in array order. The ordering matters for hooks that pipe or s

```typescript
const stream = chat({
adapter: openaiText("gpt-4o"),
adapter: openaiText("gpt-5.2"),
messages,
middleware: [authMiddleware, loggingMiddleware, cachingMiddleware],
});
Expand Down Expand Up @@ -474,7 +474,7 @@ import { chat } from "@tanstack/ai";
import { toolCacheMiddleware } from "@tanstack/ai/middlewares";

const stream = chat({
adapter: openaiText("gpt-4o"),
adapter: openaiText("gpt-5.2"),
messages,
tools: [weatherTool, stockTool],
middleware: [
Expand Down
8 changes: 4 additions & 4 deletions docs/advanced/otel.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const otel = otelMiddleware({
})

const result = await chat({
adapter: openaiText('gpt-4o'),
adapter: openaiText('gpt-5.2'),
messages: [{ role: 'user', content: 'hi' }],
middleware: [otel],
stream: false,
Expand All @@ -50,11 +50,11 @@ const result = await chat({
### Spans

```text
chat gpt-4o (root, kind: INTERNAL)
├── chat gpt-4o #0 (iteration, kind: CLIENT)
chat gpt-5.2 (root, kind: INTERNAL)
├── chat gpt-5.2 #0 (iteration, kind: CLIENT)
│ ├── execute_tool get_weather
│ └── execute_tool get_time
└── chat gpt-4o #1 (iteration, kind: CLIENT)
└── chat gpt-5.2 #1 (iteration, kind: CLIENT)
```

Iteration spans are numbered (`#0`, `#1`, ...) so distinct iterations of the same chat are easy to pick apart in trace viewers.
Expand Down
2 changes: 1 addition & 1 deletion docs/api/ai-preact.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Extends `ChatClientOptions` from `@tanstack/ai-client`:
- `initialMessages?` - Initial messages array
- `id?` - Unique identifier for this chat instance
- `threadId?` - Thread ID for AG-UI run correlation. Persists across sends; auto-generated if omitted
- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-4o' }`)
- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-5.2' }`)
- `body?` - **Deprecated.** Use `forwardedProps` instead. Still works for backward compatibility; values are merged into `forwardedProps` on the wire
- `onResponse?` - Callback when response is received
- `onChunk?` - Callback when stream chunk is received
Expand Down
2 changes: 1 addition & 1 deletion docs/api/ai-react.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Extends `ChatClientOptions` from `@tanstack/ai-client`:
- `initialMessages?` - Initial messages array
- `id?` - Unique identifier for this chat instance
- `threadId?` - Thread ID for AG-UI run correlation. Persists across sends; auto-generated if omitted
- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-4o' }`)
- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-5.2' }`)
- `body?` - **Deprecated.** Use `forwardedProps` instead. Still works for backward compatibility; values are merged into `forwardedProps` on the wire
- `onResponse?` - Callback when response is received
- `onChunk?` - Callback when stream chunk is received
Expand Down
2 changes: 1 addition & 1 deletion docs/api/ai-solid.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Extends `ChatClientOptions` from `@tanstack/ai-client`:
- `initialMessages?` - Initial messages array
- `id?` - Unique identifier for this chat instance
- `threadId?` - Thread ID for AG-UI run correlation. Persists across sends; auto-generated if omitted
- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-4o' }`)
- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-5.2' }`)
- `body?` - **Deprecated.** Use `forwardedProps` instead. Still works for backward compatibility; values are merged into `forwardedProps` on the wire
- `onResponse?` - Callback when response is received
- `onChunk?` - Callback when stream chunk is received
Expand Down
2 changes: 1 addition & 1 deletion docs/api/ai-svelte.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Extends `ChatClientOptions` from `@tanstack/ai-client` (minus internal state cal
- `initialMessages?` - Initial messages array
- `id?` - Unique identifier for this chat instance
- `threadId?` - Thread ID for AG-UI run correlation. Persists across sends; auto-generated if omitted
- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-4o' }`)
- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-5.2' }`)
- `body?` - **Deprecated.** Use `forwardedProps` instead. Still works for backward compatibility; values are merged into `forwardedProps` on the wire
- `live?` - Enable live subscription mode (subscribes on creation)
- `onResponse?` - Callback when response is received
Expand Down
4 changes: 2 additions & 2 deletions docs/api/ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ import { openaiText } from "@tanstack/ai-openai";
export async function POST(req: Request) {
const params = await chatParamsFromRequest(req);
const stream = chat({
adapter: openaiText("gpt-4o"),
adapter: openaiText("gpt-5.2"),
messages: params.messages,
tools: serverTools,
});
Expand Down Expand Up @@ -246,7 +246,7 @@ import { chat, chatParamsFromRequest, mergeAgentTools } from "@tanstack/ai";

const params = await chatParamsFromRequest(req);
const stream = chat({
adapter: openaiText("gpt-4o"),
adapter: openaiText("gpt-5.2"),
messages: params.messages,
tools: mergeAgentTools(serverTools, params.tools),
});
Expand Down
65 changes: 65 additions & 0 deletions docs/chat/streaming.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,71 @@ TanStack AI implements the [AG-UI Protocol](https://docs.ag-ui.com/introduction)

> **Tip:** Some models expose their internal reasoning as thinking content that streams before the response. See [Thinking & Reasoning](./thinking-content).

### Type-Safe Tool Call Events

When you pass typed tools (defined with `toolDefinition()` and Zod schemas) to `chat()`, the stream chunks automatically carry type information for tool call events. The `toolName` field narrows to the union of your tool name literals, and the `input` field on `TOOL_CALL_END` events is typed as the union of your tool input schemas:

```typescript
import { chat, toolDefinition } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";

const weatherTool = toolDefinition({
name: "get_weather",
description: "Get weather for a location",
inputSchema: z.object({
location: z.string(),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
}),
});

const stream = chat({
adapter: openaiText("gpt-5.2"),
messages,
tools: [weatherTool],
});

for await (const chunk of stream) {
if (chunk.type === "TOOL_CALL_END") {
chunk.toolName; // ✅ typed as "get_weather" (not string)
chunk.input; // ✅ typed as { location: string; unit?: "celsius" | "fahrenheit" } | undefined
}
}
```

Without typed tools, `toolName` defaults to `string` and `input` defaults to `unknown` — the same behavior as before. The type narrowing is automatic when you use `toolDefinition()` with Zod schemas.

When multiple tools are provided, tool call events form a **discriminated union** — checking `toolName` narrows `input` to that specific tool's type:

```typescript
const searchTool = toolDefinition({
name: "search",
description: "Search the web",
inputSchema: z.object({ query: z.string() }),
});

const stream = chat({
adapter: openaiText("gpt-5.2"),
messages,
tools: [weatherTool, searchTool],
});

for await (const chunk of stream) {
if (chunk.type === "TOOL_CALL_END") {
if (chunk.toolName === "get_weather") {
// ✅ input is narrowed to { location: string; unit?: "celsius" | "fahrenheit" }
console.log(`Weather in ${chunk.input?.location}`);
}
if (chunk.toolName === "search") {
// ✅ input is narrowed to { query: string }
console.log(`Searched for: ${chunk.input?.query}`);
}
}
}
```

> **Tip:** The typed stream chunk type is exported as `TypedStreamChunk<TTools>` if you need to annotate variables or function parameters. When used without type arguments, `TypedStreamChunk` is equivalent to `StreamChunk`.

### Thinking Chunks

Thinking/reasoning is represented by AG-UI events `STEP_STARTED` and `STEP_FINISHED`. They stream separately from the final response text:
Expand Down
4 changes: 2 additions & 2 deletions docs/code-mode/code-mode-with-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const { toolsRegistry, systemPrompt, selectedSkills } = await codeModeWithSkills
})

const stream = chat({
adapter: openaiText('gpt-4o'), // strong model for reasoning
adapter: openaiText('gpt-5.2'), // strong model for reasoning
toolRegistry: toolsRegistry,
messages,
systemPrompts: ['You are a helpful assistant.', systemPrompt],
Expand Down Expand Up @@ -189,7 +189,7 @@ const skillsPrompt = createSkillsSystemPrompt({

// 5. Assemble and call chat()
const stream = chat({
adapter: openaiText('gpt-4o'),
adapter: openaiText('gpt-5.2'),
tools: [codeModeTool, ...managementTools, ...skillTools],
messages,
systemPrompts: [BASE_PROMPT, codeModePrompt, skillsPrompt],
Expand Down
2 changes: 1 addition & 1 deletion docs/code-mode/code-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const result = await chat({
adapter: openaiText("gpt-4o"),
adapter: openaiText("gpt-5.2"),
systemPrompts: [
"You are a helpful weather assistant.",
systemPrompt,
Expand Down
12 changes: 6 additions & 6 deletions docs/community-adapters/cencori.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ npm install @cencori/ai-sdk
import { chat } from "@tanstack/ai";
import { cencori } from "@cencori/ai-sdk/tanstack";

const adapter = cencori("gpt-4o");
const adapter = cencori("gpt-5.2");

for await (const chunk of chat({
adapter,
Expand All @@ -49,7 +49,7 @@ const cencori = createCencori({
baseUrl: "https://cencori.com", // Optional
});

const adapter = cencori("gpt-4o");
const adapter = cencori("gpt-5.2");
```

## Streaming
Expand Down Expand Up @@ -79,7 +79,7 @@ for await (const chunk of chat({
import { chat } from "@tanstack/ai";
import { cencori } from "@cencori/ai-sdk/tanstack";

const adapter = cencori("gpt-4o");
const adapter = cencori("gpt-5.2");

for await (const chunk of chat({
adapter,
Expand Down Expand Up @@ -110,7 +110,7 @@ Switch between providers with a single parameter:
import { cencori } from "@cencori/ai-sdk/tanstack";

// OpenAI
const openai = cencori("gpt-4o");
const openai = cencori("gpt-5.2");

// Anthropic
const anthropic = cencori("claude-3-5-sonnet");
Expand All @@ -131,7 +131,7 @@ All responses use the same unified format regardless of provider.

| Provider | Models |
|----------|--------|
| OpenAI | `gpt-5`, `gpt-4o`, `gpt-4o-mini`, `o3`, `o1` |
| OpenAI | `gpt-5`, `gpt-5.2`, `gpt-4o-mini`, `o3`, `o1` |
| Anthropic | `claude-opus-4`, `claude-sonnet-4`, `claude-3-5-sonnet` |
| Google | `gemini-3-pro`, `gemini-2.5-flash`, `gemini-2.0-flash` |
| xAI | `grok-4`, `grok-3` |
Expand Down Expand Up @@ -168,7 +168,7 @@ Creates a Cencori adapter using environment variables.

**Parameters:**

- `model` - Model name (e.g., `"gpt-4o"`, `"claude-3-5-sonnet"`, `"gemini-2.5-flash"`)
- `model` - Model name (e.g., `"gpt-5.2"`, `"claude-3-5-sonnet"`, `"gemini-2.5-flash"`)

**Returns:** A Cencori TanStack AI adapter instance.

Expand Down
8 changes: 4 additions & 4 deletions docs/community-adapters/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ import {
createOpenRouterChat,
} from "@cloudflare/tanstack-ai";

const openai = createOpenAiChat("gpt-4o", {
const openai = createOpenAiChat("gpt-5.2", {
binding: env.AI.gateway("my-gateway-id"),
});

Expand All @@ -214,7 +214,7 @@ const grok = createGrokChat("grok-4", {
binding: env.AI.gateway("my-gateway-id"),
});

const openrouter = createOpenRouterChat("openai/gpt-4o", {
const openrouter = createOpenRouterChat("openai/gpt-5.2", {
binding: env.AI.gateway("my-gateway-id"),
});
```
Expand All @@ -224,7 +224,7 @@ Or use credentials for non-Worker environments:
```typescript
import { createOpenAiChat } from "@cloudflare/tanstack-ai";

const adapter = createOpenAiChat("gpt-4o", {
const adapter = createOpenAiChat("gpt-5.2", {
accountId: "your-account-id",
gatewayId: "your-gateway-id",
cfApiKey: "your-cf-api-key",
Expand All @@ -237,7 +237,7 @@ const adapter = createOpenAiChat("gpt-4o", {
Both binding and credentials modes support cache configuration:

```typescript
const adapter = createOpenAiChat("gpt-4o", {
const adapter = createOpenAiChat("gpt-5.2", {
binding: env.AI.gateway("my-gateway-id"),
skipCache: false,
cacheTtl: 3600,
Expand Down
Loading
Loading