From 1ee8565332940bee964e68efb9ecd6b9872b0b2e Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Mon, 22 Jun 2026 19:03:40 +0200 Subject: [PATCH 01/12] ci(docs): type-check doc code samples with kiira Add kiira to type-check the TS/TSX/JS/JSX code fences in /docs against the real package APIs, and wire it into the CI quality gate so doc examples can't drift from the SDK. - Add kiira devDependency + kiira.config.ts: checks docs/**/*.md, excludes the auto-generated docs/reference/**, enables the JSX runtime per framework, and provides an `ambient` fixture for illustrative messages/message values. No tsconfig.docs.json. - Add `test:kiira` script; include it in the test:pr/test:ci nx target lists, nx.includedScripts, and an nx cache target (nx.json). - knip: ignore kiira.config.ts (consumed by the kiira binary, not imported). - Fix the type errors kiira surfaced across 81 docs: add missing imports, group continuation fences, import arbitrary placeholders from relative modules, and fix real API drift (e.g. toolDefinition now requires `description`, ContentPartUrlSource uses `value`, chat passes tools via `tools`/modelOptions). Genuinely un-checkable fences (competitor SDKs, uninstalled community adapters, framework route-registration boilerplate) are marked `ignore`. --- docs/adapters/anthropic.md | 14 +- docs/adapters/elevenlabs.md | 11 +- docs/adapters/fal.md | 20 +- docs/adapters/gemini.md | 40 ++-- docs/adapters/grok.md | 14 +- docs/adapters/groq.md | 9 +- docs/adapters/ollama.md | 15 +- docs/adapters/openai-compatible.md | 8 +- docs/adapters/openai.md | 37 ++-- docs/adapters/openrouter.md | 30 ++- docs/advanced/built-in-middleware.md | 24 +- docs/advanced/debug-logging.md | 33 ++- docs/advanced/extend-adapter.md | 10 +- docs/advanced/middleware.md | 75 +++++-- docs/advanced/multimodal-content.md | 18 +- docs/advanced/otel.md | 8 +- docs/advanced/per-model-type-safety.md | 5 +- docs/advanced/runtime-adapter-switching.md | 46 ++-- docs/advanced/runtime-context.md | 27 ++- docs/advanced/tree-shaking.md | 8 +- docs/advanced/typed-options.md | 14 +- docs/api/ai-angular.md | 24 +- docs/api/ai-client.md | 72 ++++-- docs/api/ai-preact.md | 19 +- docs/api/ai-react.md | 20 +- docs/api/ai-solid.md | 85 +++---- docs/api/ai-svelte.md | 10 +- docs/api/ai-vue.md | 16 +- docs/api/ai.md | 201 ++++++++++------- docs/architecture/approval-flow-processing.md | 49 ++-- docs/chat/agentic-cycle.md | 16 +- docs/chat/connection-adapters.md | 34 +-- docs/chat/persistence.md | 14 +- docs/chat/streaming.md | 22 +- docs/chat/thinking-content.md | 13 +- docs/code-mode/client-integration.md | 10 +- docs/code-mode/code-mode-isolates.md | 2 + docs/code-mode/code-mode-with-skills.md | 17 +- docs/code-mode/code-mode.md | 13 +- docs/community-adapters/cencori.md | 10 +- docs/community-adapters/cloudflare.md | 22 +- docs/community-adapters/decart.md | 18 +- docs/community-adapters/guide.md | 8 +- docs/community-adapters/mynth.md | 12 +- docs/community-adapters/soniox.md | 18 +- docs/comparison/vercel-ai-sdk.md | 32 ++- docs/getting-started/devtools.md | 2 +- docs/getting-started/overview.md | 2 + docs/getting-started/quick-start-angular.md | 13 +- .../quick-start-react-native.md | 2 +- docs/getting-started/quick-start-server.md | 6 +- docs/getting-started/quick-start-svelte.md | 2 +- docs/getting-started/quick-start-vue.md | 2 +- docs/getting-started/quick-start.md | 4 +- docs/media/audio-generation.md | 15 +- docs/media/generation-hooks.md | 5 +- docs/media/generations.md | 64 +++--- docs/media/image-generation.md | 70 ++++-- docs/media/realtime-chat.md | 64 ++++-- docs/media/text-to-speech.md | 43 ++-- docs/media/transcription.md | 41 +++- docs/media/video-generation.md | 48 ++-- docs/migration/ag-ui-compliance.md | 32 ++- docs/migration/migration-from-vercel-ai.md | 209 +++++++++++------- docs/migration/migration.md | 30 +-- .../sampling-options-to-model-options.md | 40 +++- docs/structured-outputs/multi-turn.md | 10 +- docs/structured-outputs/one-shot.md | 43 +++- docs/structured-outputs/overview.md | 3 +- docs/structured-outputs/streaming.md | 6 +- docs/structured-outputs/with-tools.md | 10 +- docs/tools/client-tools.md | 27 ++- docs/tools/lazy-tool-discovery.md | 35 +-- docs/tools/mcp-managed.md | 39 +++- docs/tools/mcp-manual.md | 39 +++- docs/tools/mcp.md | 84 +++++-- docs/tools/provider-tools.md | 2 +- docs/tools/server-tools.md | 12 +- docs/tools/tool-approval.md | 10 +- docs/tools/tool-architecture.md | 15 +- docs/tools/tools.md | 44 +++- kiira.config.ts | 41 ++++ knip.json | 1 + nx.json | 8 + package.json | 7 +- pnpm-lock.yaml | 20 ++ 86 files changed, 1656 insertions(+), 717 deletions(-) create mode 100644 kiira.config.ts diff --git a/docs/adapters/anthropic.md b/docs/adapters/anthropic.md index c2fbe48b6..c04da21bd 100644 --- a/docs/adapters/anthropic.md +++ b/docs/adapters/anthropic.md @@ -82,7 +82,7 @@ export async function POST(request: Request) { ## Example: With Tools -```typescript +```typescript fixture=ambient import { chat, toolDefinition } from "@tanstack/ai"; import { anthropicText } from "@tanstack/ai-anthropic"; import { z } from "zod"; @@ -111,7 +111,10 @@ const stream = chat({ Anthropic supports various provider-specific options. Sampling parameters live here too — `temperature`, `top_p`, and `max_tokens` — rather than as root-level props on `chat()`: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; + const stream = chat({ adapter: anthropicText("claude-sonnet-4-6"), messages, @@ -131,7 +134,7 @@ const stream = chat({ Enable extended thinking with a token budget. This allows Claude to show its reasoning process, which is streamed as `thinking` chunks: -```typescript +```typescript ignore modelOptions: { thinking: { type: "enabled", @@ -147,6 +150,9 @@ modelOptions: { Cache prompts for better performance and reduced costs: ```typescript +import { chat } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; + const stream = chat({ adapter: anthropicText("claude-sonnet-4-6"), messages: [ @@ -242,7 +248,7 @@ import { anthropicText } from "@tanstack/ai-anthropic"; import { webSearchTool } from "@tanstack/ai-anthropic/tools"; const stream = chat({ - adapter: anthropicText("claude-opus-4.8"), + adapter: anthropicText("claude-opus-4-7"), messages: [{ role: "user", content: "What's new in AI this week?" }], tools: [ webSearchTool({ diff --git a/docs/adapters/elevenlabs.md b/docs/adapters/elevenlabs.md index c560b4e71..3a1e57378 100644 --- a/docs/adapters/elevenlabs.md +++ b/docs/adapters/elevenlabs.md @@ -40,7 +40,7 @@ npm install @tanstack/ai @tanstack/ai-client The server generates a **signed WebSocket URL** so your API key never reaches the client. The signed URL is valid for 30 minutes. -```typescript +```typescript group=elevenlabs-1 import { realtimeToken } from '@tanstack/ai' import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs' @@ -60,7 +60,7 @@ export async function POST() { You can override agent settings at token generation time without changing your dashboard configuration: -```typescript +```typescript group=elevenlabs-1 const token = await realtimeToken({ adapter: elevenlabsRealtimeToken({ agentId: process.env.ELEVENLABS_AGENT_ID!, @@ -78,7 +78,7 @@ const token = await realtimeToken({ ### React (useRealtimeChat) -```tsx +```tsx group=elevenlabs-2 import { useRealtimeChat } from '@tanstack/ai-react' import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs' @@ -154,7 +154,7 @@ await client.connect() ElevenLabs supports client-side tools that execute in the browser. Define tools using the standard `toolDefinition()` API: -```typescript +```typescript group=elevenlabs-2 import { toolDefinition } from '@tanstack/ai' import { z } from 'zod' @@ -228,7 +228,7 @@ ElevenLabs and OpenAI take different approaches to realtime voice: The ElevenLabs adapter provides audio visualization data through the same interface as other realtime adapters: -```typescript +```typescript group=elevenlabs-2 const { inputLevel, // 0-1 normalized microphone volume outputLevel, // 0-1 normalized speaker volume @@ -305,6 +305,7 @@ Transcribe audio with `elevenlabsTranscription`: ```typescript import { generateTranscription } from "@tanstack/ai"; import { elevenlabsTranscription } from "@tanstack/ai-elevenlabs"; +import { audioFile } from "./audio"; const result = await generateTranscription({ adapter: elevenlabsTranscription("scribe_v1"), diff --git a/docs/adapters/fal.md b/docs/adapters/fal.md index cc32c634d..be26d5ac0 100644 --- a/docs/adapters/fal.md +++ b/docs/adapters/fal.md @@ -28,12 +28,14 @@ npm install @tanstack/ai-fal The fal adapter provides full type safety when you pass the model ID as a **string literal**. This gives you autocomplete for `size` and `modelOptions` specific to that model. Always use string literals — not variables — when creating adapters: ```typescript +import { falImage } from "@tanstack/ai-fal"; + // Good — full type safety and autocomplete const adapter = falImage("fal-ai/z-image/turbo"); // Bad — no type inference for model-specific options const modelId = "fal-ai/z-image/turbo"; -const adapter = falImage(modelId); +const adapterNoTypeInference = falImage(modelId); ``` You can also pass any string for new models that fal.ai hasn't provided types for yet — you just won't get type safety on those endpoints. @@ -141,7 +143,7 @@ The fal adapter supports a flexible `size` paramater that maps either to `image_ | aspect ratio & resolution | `"16:9_4K"` | `aspect_ratio: "16:9"`, `resolution: "4K"` | | aspect ratio only | `"16:9"` | `aspect_ratio: "16:9"` | -```typescript +```typescript ignore // Aspect ratio only size: "16:9" @@ -177,7 +179,7 @@ const adapter = falVideo("fal-ai/kling-video/v2.6/pro/text-to-video"); const job = await generateVideo({ adapter, prompt: "A timelapse of a flower blooming", - size: "16:9_1080p", + size: "16:9", modelOptions: { duration: "5", }, @@ -234,6 +236,9 @@ console.log(result.contentType); // e.g. "audio/wav" Google's newest TTS model (`fal-ai/gemini-3.1-flash-tts`) supports 80+ languages and introduces **granular audio tags** for expressive control — you can embed speaker tags and style cues directly in the text. ```typescript +import { generateSpeech } from "@tanstack/ai"; +import { falSpeech } from "@tanstack/ai-fal"; + const result = await generateSpeech({ adapter: falSpeech("fal-ai/gemini-3.1-flash-tts"), text: "[warm, enthusiastic] Welcome to TanStack AI! [pause] Let's build something great.", @@ -246,6 +251,9 @@ const result = await generateSpeech({ ### ElevenLabs v3 ```typescript +import { generateSpeech } from "@tanstack/ai"; +import { falSpeech } from "@tanstack/ai-fal"; + const result = await generateSpeech({ adapter: falSpeech("fal-ai/elevenlabs/tts/eleven-v3"), text: "Welcome to TanStack AI.", @@ -283,7 +291,7 @@ for (const segment of result.segments ?? []) { Music and sound-effect generation uses `falAudio()` with the `generateAudio()` activity. Unlike TTS, the result is returned as a URL in `result.audio.url` (you can fetch it yourself if you need raw bytes). -```typescript +```typescript group=fal import { generateAudio } from "@tanstack/ai"; import { falAudio } from "@tanstack/ai-fal"; @@ -296,7 +304,7 @@ const music = await generateAudio({ console.log(music.audio.url); ``` -```typescript +```typescript group=fal // DiffRhythm with explicit lyrics const lyrical = await generateAudio({ adapter: falAudio("fal-ai/diffrhythm"), @@ -307,7 +315,7 @@ const lyrical = await generateAudio({ }); ``` -```typescript +```typescript group=fal // Sound effects const sfx = await generateAudio({ adapter: falAudio("fal-ai/elevenlabs/sound-effects/v2"), diff --git a/docs/adapters/gemini.md b/docs/adapters/gemini.md index 42b766553..5164a49f7 100644 --- a/docs/adapters/gemini.md +++ b/docs/adapters/gemini.md @@ -58,7 +58,9 @@ const stream = chat({ import { createGeminiChat, type GeminiTextConfig } from "@tanstack/ai-gemini"; const config: Omit = { - baseURL: "https://generativelanguage.googleapis.com/v1beta", // Optional + httpOptions: { + baseUrl: "https://generativelanguage.googleapis.com/v1beta", // Optional + }, }; const adapter = createGeminiChat("gemini-3.1-pro-preview", process.env.GEMINI_API_KEY!, config); @@ -85,7 +87,7 @@ export async function POST(request: Request) { ## Example: With Tools -```typescript +```typescript fixture=ambient import { chat, toolDefinition } from "@tanstack/ai"; import { geminiText } from "@tanstack/ai-gemini"; import { z } from "zod"; @@ -120,7 +122,7 @@ The `geminiTextInteractions` adapter routes through `client.interactions.create` ### Basic Usage -```typescript +```typescript ignore import { chat } from "@tanstack/ai"; import { geminiTextInteractions, @@ -252,7 +254,8 @@ The full working example is in [`examples/ts-react-chat`](https://github.com/Tan The adapter exposes Interactions-specific options on `modelOptions`: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; import { geminiTextInteractions } from "@tanstack/ai-gemini/experimental"; const stream = chat({ @@ -271,7 +274,7 @@ const stream = chat({ // snake_case generation config distinct from geminiText's camelCase one. generation_config: { - thinking_level: "LOW", + thinking_level: "low", thinking_summaries: "auto", stop_sequences: [""], }, @@ -285,7 +288,7 @@ const stream = chat({ The server's interaction id arrives as an AG-UI `CUSTOM` event emitted just before `RUN_FINISHED`: -```typescript +```typescript ignore for await (const chunk of stream) { if (chunk.type === "CUSTOM" && chunk.name === "gemini.interactionId") { const id = (chunk.value as { interactionId: string }).interactionId; @@ -309,7 +312,10 @@ for await (const chunk of stream) { Gemini supports various model-specific options. Sampling parameters live here too — `temperature`, `topP`, and `maxOutputTokens` — rather than as root-level props on `chat()`: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; + const stream = chat({ adapter: geminiText("gemini-3.1-pro-preview"), messages, @@ -329,7 +335,7 @@ const stream = chat({ Enable thinking for models that support it: -```typescript +```typescript ignore modelOptions: { thinking: { includeThoughts: true, @@ -341,7 +347,7 @@ modelOptions: { Configure structured output format: -```typescript +```typescript ignore modelOptions: { responseMimeType: "application/json", } @@ -413,7 +419,7 @@ console.log(result.images); Gemini native image models use a template literal size format combining aspect ratio and resolution tier: -```typescript +```typescript ignore // Format: "aspectRatio_resolution" size: "16:9_4K" size: "1:1_2K" @@ -438,6 +444,9 @@ Imagen models use WIDTHxHEIGHT format, which maps to aspect ratios internally: Alternatively, you can specify the aspect ratio directly in Model Options: ```typescript +import { generateImage } from "@tanstack/ai"; +import { geminiImage } from "@tanstack/ai-gemini"; + const result = await generateImage({ adapter: geminiImage("imagen-4.0-generate-001"), prompt: "A landscape photo", @@ -449,7 +458,10 @@ const result = await generateImage({ ### Image Model Options -```typescript +```typescript ignore +import { generateImage } from "@tanstack/ai"; +import { geminiImage } from "@tanstack/ai-gemini"; + const result = await generateImage({ adapter: geminiImage("imagen-4.0-generate-001"), prompt: "...", @@ -642,7 +654,7 @@ A retrieval-augmented variant of Google Search that returns ranked passages from the web with configurable dynamic retrieval mode. Pass an optional `GoogleSearchRetrieval` config. -```typescript +```typescript ignore import { chat } from "@tanstack/ai"; import { geminiText } from "@tanstack/ai-gemini"; import { googleSearchRetrievalTool } from "@tanstack/ai-gemini/tools"; @@ -672,7 +684,7 @@ import { geminiText } from "@tanstack/ai-gemini"; import { googleMapsTool } from "@tanstack/ai-gemini/tools"; const stream = chat({ - adapter: geminiText("gemini-3.1-pro-preview"), + adapter: geminiText("gemini-2.5-pro"), messages: [{ role: "user", content: "Find coffee shops near Union Square, SF" }], tools: [googleMapsTool()], }); @@ -705,7 +717,7 @@ Allows Gemini to observe a virtual desktop via screenshots and interact with it using predefined computer-use functions. Provide the `environment` and optionally restrict callable functions via `excludedPredefinedFunctions`. -```typescript +```typescript ignore import { chat } from "@tanstack/ai"; import { geminiText } from "@tanstack/ai-gemini"; import { computerUseTool } from "@tanstack/ai-gemini/tools"; diff --git a/docs/adapters/grok.md b/docs/adapters/grok.md index 5b4e043be..99b2ba1bf 100644 --- a/docs/adapters/grok.md +++ b/docs/adapters/grok.md @@ -78,7 +78,7 @@ export async function POST(request: Request) { ## Example: With Tools -```typescript +```typescript fixture=ambient import { chat, toolDefinition } from "@tanstack/ai"; import { grokText } from "@tanstack/ai-grok"; import { z } from "zod"; @@ -107,7 +107,10 @@ const stream = chat({ Grok supports xAI Responses API options. Sampling parameters live here too — `temperature`, `top_p`, and `max_output_tokens` — rather than as root-level props on `chat()`: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; +import { grokText } from "@tanstack/ai-grok"; + const stream = chat({ adapter: grokText("grok-build-0.1"), messages, @@ -163,6 +166,9 @@ are aspect-ratio sized — `size` takes an `aspectRatio_resolution` template like `"16:9_2k"` (the `_2k` suffix is optional): ```typescript +import { generateImage } from "@tanstack/ai"; +import { grokImage } from "@tanstack/ai-grok"; + const result = await generateImage({ adapter: grokImage("grok-imagine-image"), prompt: "A futuristic cityscape at sunset", @@ -179,6 +185,9 @@ there is no in-prompt referencing syntax; write the prompt naturally and your text is sent verbatim: ```typescript +import { generateImage } from "@tanstack/ai"; +import { grokImage } from "@tanstack/ai-grok"; + const result = await generateImage({ adapter: grokImage("grok-imagine-image"), prompt: [ @@ -228,6 +237,7 @@ Transcribe audio with Grok STT: ```typescript import { generateTranscription } from "@tanstack/ai"; import { grokTranscription } from "@tanstack/ai-grok"; +import { audioFile } from "./audio"; const result = await generateTranscription({ adapter: grokTranscription("grok-stt"), diff --git a/docs/adapters/groq.md b/docs/adapters/groq.md index 232dd4606..aef802040 100644 --- a/docs/adapters/groq.md +++ b/docs/adapters/groq.md @@ -82,7 +82,7 @@ export async function POST(request: Request) { ## Example: With Tools ```typescript -import { chat, toolDefinition } from "@tanstack/ai"; +import { chat, toolDefinition, type ModelMessage } from "@tanstack/ai"; import { groqText } from "@tanstack/ai-groq"; import { z } from "zod"; @@ -99,7 +99,7 @@ const searchDatabase = searchDatabaseDef.server(async ({ query }) => { return { results: [] }; }); -const messages = [{ role: "user", content: "Search for something" }]; +const messages: Array = [{ role: "user", content: "Search for something" }]; const stream = chat({ adapter: groqText("llama-3.3-70b-versatile"), @@ -113,6 +113,9 @@ const stream = chat({ Groq supports various provider-specific options. Sampling parameters live here too — `temperature`, `top_p`, and `max_completion_tokens` (Groq's token-limit key) — rather than as root-level props on `chat()`: ```typescript +import { chat } from "@tanstack/ai"; +import { groqText } from "@tanstack/ai-groq"; + const stream = chat({ adapter: groqText("llama-3.3-70b-versatile"), messages: [{ role: "user", content: "Hello!" }], @@ -130,7 +133,7 @@ const stream = chat({ Enable reasoning for models that support it (e.g., `openai/gpt-oss-120b`, `qwen/qwen3-32b`). This allows the model to show its reasoning process, which is streamed as `thinking` chunks: -```typescript +```typescript ignore modelOptions: { reasoning_effort: "medium", // "none" | "default" | "low" | "medium" | "high" } diff --git a/docs/adapters/ollama.md b/docs/adapters/ollama.md index d7e6ec18e..bbb3ea656 100644 --- a/docs/adapters/ollama.md +++ b/docs/adapters/ollama.md @@ -102,7 +102,7 @@ export async function POST(request: Request) { ## Example: With Tools -```typescript +```typescript fixture=ambient import { chat, toolDefinition } from "@tanstack/ai"; import { ollamaText } from "@tanstack/ai-ollama"; import { z } from "zod"; @@ -133,9 +133,12 @@ const stream = chat({ Ollama supports various provider-specific options. Unlike the other providers, Ollama nests its sampling and runner parameters inside an `options` object **within** `modelOptions` — `temperature`, `top_p`, and `num_predict` (the token-limit key) all live under `modelOptions.options`: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; +import { ollamaText } from "@tanstack/ai-ollama"; + const stream = chat({ - adapter: ollamaText("llama3"), + adapter: ollamaText("llama3:latest"), messages, modelOptions: { options: { @@ -157,7 +160,7 @@ const stream = chat({ All sampling and runner parameters are nested under `modelOptions.options`: -```typescript +```typescript ignore modelOptions: { options: { // Sampling @@ -195,7 +198,7 @@ modelOptions: { Summarize long text content locally: -```typescript +```typescript ignore import { summarize } from "@tanstack/ai"; import { ollamaSummarize } from "@tanstack/ai-ollama"; @@ -241,6 +244,8 @@ The server runs on `http://localhost:11434` by default. ## Running on a Remote Server ```typescript +import { createOllamaChat } from "@tanstack/ai-ollama"; + const adapter = createOllamaChat("llama3", "http://your-server:11434"); ``` diff --git a/docs/adapters/openai-compatible.md b/docs/adapters/openai-compatible.md index 133ea9255..7d70d7a56 100644 --- a/docs/adapters/openai-compatible.md +++ b/docs/adapters/openai-compatible.md @@ -101,6 +101,8 @@ const provider = openaiCompatible({ `openaiCompatible` accepts every OpenAI SDK `ClientOptions` field besides `apiKey`/`baseURL` (which are required and promoted to the top level). The most useful are `defaultHeaders` and `defaultQuery`, for providers that need extra auth or routing parameters: ```typescript +import { openaiCompatible } from "@tanstack/ai-openai/compatible"; + const provider = openaiCompatible({ baseURL: "https://api.example.com/v1", apiKey: process.env.EXAMPLE_API_KEY!, @@ -115,6 +117,8 @@ const provider = openaiCompatible({ By default the adapter targets the **Chat Completions** API (`/chat/completions`) — the surface virtually every compatible provider implements. For the rare provider that also implements OpenAI's **Responses** API (e.g. Azure OpenAI), opt in with `api: "responses"`: ```typescript +import { openaiCompatible } from "@tanstack/ai-openai/compatible"; + const provider = openaiCompatible({ baseURL: "https://my-resource.openai.azure.com/openai/v1", apiKey: process.env.AZURE_OPENAI_API_KEY!, @@ -150,7 +154,7 @@ Any provider implementing the OpenAI Chat Completions API works. Common ones are Point the adapter at any local OpenAI-compatible server. The API key is usually a placeholder: -```typescript +```typescript group=openai-compatible import { openaiCompatible } from "@tanstack/ai-openai/compatible"; // LM Studio @@ -184,7 +188,7 @@ const ollama = openaiCompatible({ Azure uses a resource-scoped URL and a separate API-version. Use the `/openai/v1` endpoint with `defaultQuery` for the version and `defaultHeaders` for the `api-key` header: -```typescript +```typescript group=openai-compatible const azure = openaiCompatible({ name: "azure", baseURL: "https://YOUR_RESOURCE.openai.azure.com/openai/v1", diff --git a/docs/adapters/openai.md b/docs/adapters/openai.md index dbf852ca2..39f4e562b 100644 --- a/docs/adapters/openai.md +++ b/docs/adapters/openai.md @@ -68,8 +68,7 @@ With an explicit API key: import { chat } from "@tanstack/ai"; import { createOpenaiChatCompletions } from "@tanstack/ai-openai"; -const adapter = createOpenaiChatCompletions("gpt-5.2", { - apiKey: process.env.OPENAI_API_KEY!, +const adapter = createOpenaiChatCompletions("gpt-5.2", process.env.OPENAI_API_KEY!, { // organization, baseURL, headers — all optional }); @@ -130,7 +129,7 @@ export async function POST(request: Request) { ## Example: With Tools -```typescript +```typescript fixture=ambient import { chat, toolDefinition } from "@tanstack/ai"; import { openaiText } from "@tanstack/ai-openai"; import { z } from "zod"; @@ -159,7 +158,10 @@ const stream = chat({ OpenAI supports various provider-specific options. Sampling parameters live here too — `temperature`, `top_p`, and `max_output_tokens` (the Responses API token-limit key) — rather than as root-level props on `chat()`: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; + const stream = chat({ adapter: openaiText("gpt-5.2"), messages, @@ -167,9 +169,6 @@ const stream = chat({ temperature: 0.7, max_output_tokens: 1000, top_p: 0.9, - frequency_penalty: 0.5, - presence_penalty: 0.5, - stop: ["END"], }, }); ``` @@ -180,7 +179,7 @@ const stream = chat({ Enable reasoning for models that support it (e.g., GPT-5, O3). This allows the model to show its reasoning process, which is streamed as `thinking` chunks: -```typescript +```typescript ignore modelOptions: { reasoning: { effort: "medium", // "none" | "minimal" | "low" | "medium" | "high" @@ -230,12 +229,14 @@ console.log(result.images); ### Image Model Options ```typescript +import { generateImage } from "@tanstack/ai"; +import { openaiImage } from "@tanstack/ai-openai"; + const result = await generateImage({ adapter: openaiImage("gpt-image-1"), prompt: "...", modelOptions: { - quality: "hd", // "standard" | "hd" - style: "natural", // "natural" | "vivid" + quality: "high", // "high" | "medium" | "low" | "auto" }, }); ``` @@ -266,11 +267,14 @@ Available voices: `alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`, `ash`, `b ### TTS Model Options ```typescript +import { generateSpeech } from "@tanstack/ai"; +import { openaiSpeech } from "@tanstack/ai-openai"; + const result = await generateSpeech({ adapter: openaiSpeech("tts-1-hd"), text: "High quality speech", modelOptions: { - speed: 1.0, // 0.25 to 4.0 + instructions: "Speak slowly and clearly.", // voice instructions (not supported by tts-1/tts-1-hd) }, }); ``` @@ -282,6 +286,7 @@ Transcribe audio to text: ```typescript import { generateTranscription } from "@tanstack/ai"; import { openaiTranscription } from "@tanstack/ai-openai"; +import { audioFile } from "./audio"; const result = await generateTranscription({ adapter: openaiTranscription("whisper-1"), @@ -295,18 +300,20 @@ console.log(result.text); // Transcribed text ### Transcription Model Options ```typescript +import { generateTranscription } from "@tanstack/ai"; +import { openaiTranscription } from "@tanstack/ai-openai"; +import { audioFile } from "./audio"; + const result = await generateTranscription({ adapter: openaiTranscription("whisper-1"), audio: audioFile, modelOptions: { - response_format: "verbose_json", // Get timestamps temperature: 0, - prompt: "Technical terms: API, SDK", }, }); -// Access segments with timestamps -console.log(result.segments); +// Access the transcribed text +console.log(result.text); ``` ## Environment Variables diff --git a/docs/adapters/openrouter.md b/docs/adapters/openrouter.md index cd8984511..913eb42de 100644 --- a/docs/adapters/openrouter.md +++ b/docs/adapters/openrouter.md @@ -52,7 +52,7 @@ const adapter = createOpenRouterText( OpenRouter provides access to 300+ models from various providers. Models use the format `provider/model-name`: -```typescript +```text model: "openai/gpt-5.1" model: "anthropic/claude-sonnet-4.5" model: "google/gemini-3.1-pro-preview" @@ -82,7 +82,7 @@ export async function POST(request: Request) { ## Example: With Tools -```typescript +```typescript fixture=ambient import { chat, toolDefinition } from "@tanstack/ai"; import { openRouterText } from "@tanstack/ai-openrouter"; import { z } from "zod"; @@ -120,15 +120,18 @@ OPENROUTER_API_KEY=sk-or-... OpenRouter can automatically route requests to the best available provider: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; +import { openRouterText } from "@tanstack/ai-openrouter"; + const stream = chat({ adapter: openRouterText("openrouter/auto"), messages, modelOptions: { models: [ - "openai/gpt-4o", - "anthropic/claude-3.5-sonnet", - "google/gemini-pro", + "openai/gpt-5.5", + "anthropic/claude-sonnet-4.5", + "google/gemini-3.1-pro-preview", ], }, }); @@ -138,9 +141,12 @@ const stream = chat({ OpenRouter supports various provider-specific options. Sampling parameters live here too — `temperature`, `topP`, and `maxCompletionTokens` (OpenRouter's token-limit key for the chat adapter) — rather than as root-level props on `chat()`: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; +import { openRouterText } from "@tanstack/ai-openrouter"; + const stream = chat({ - adapter: openRouterText("openai/gpt-5"), + adapter: openRouterText("anthropic/claude-sonnet-4.5"), messages, modelOptions: { temperature: 0.7, @@ -199,14 +205,18 @@ OpenRouter's [Usage Accounting](https://openrouter.ai/docs/use-cases/usage-accou docs for the meaning and units of these fields. ```typescript -import { chat } from "@tanstack/ai"; +import { chat, type RunFinishedEvent, type StreamChunk } from "@tanstack/ai"; import { openRouterText } from "@tanstack/ai-openrouter"; +function isRunFinished(chunk: StreamChunk): chunk is RunFinishedEvent { + return "finishReason" in chunk; +} + for await (const chunk of chat({ adapter: openRouterText("openai/gpt-5"), messages: [{ role: "user", content: "Hello!" }], })) { - if (chunk.type === "RUN_FINISHED") { + if (isRunFinished(chunk)) { console.log("cost:", chunk.usage?.cost); console.log("breakdown:", chunk.usage?.costDetails); } diff --git a/docs/advanced/built-in-middleware.md b/docs/advanced/built-in-middleware.md index ecc600759..2714af9af 100644 --- a/docs/advanced/built-in-middleware.md +++ b/docs/advanced/built-in-middleware.md @@ -27,9 +27,11 @@ TanStack AI ships ready-made middleware so you don't have to hand-roll the commo Caches tool call results based on tool name and arguments. When a tool is called with the same name and arguments as a previous call, the cached result is returned immediately without re-executing the tool. -```typescript +```typescript fixture=ambient import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; import { toolCacheMiddleware } from "@tanstack/ai/middlewares"; +import { weatherTool, stockTool } from "./tools"; const stream = chat({ adapter: openaiText("gpt-5.5"), @@ -65,6 +67,8 @@ const stream = chat({ **Custom key function** — useful when you want to ignore certain arguments: ```typescript +import { toolCacheMiddleware } from "@tanstack/ai/middlewares"; + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } @@ -87,8 +91,10 @@ By default the cache lives in-memory and is scoped to a single `toolCacheMiddlew The storage interface: ```typescript +import { type ToolCacheEntry, type ToolCacheStorage } from "@tanstack/ai/middlewares"; + // Implement this interface (exported from `@tanstack/ai/middlewares`): -interface ToolCacheStorage { +interface MyStorage extends ToolCacheStorage { getItem: (key: string) => ToolCacheEntry | undefined | Promise; setItem: (key: string, value: ToolCacheEntry) => void | Promise; deleteItem: (key: string) => void | Promise; @@ -101,7 +107,7 @@ All methods may return a `Promise` for async backends. The middleware handles TT **Redis example:** -```typescript +```typescript ignore import { createClient } from "redis"; import { toolCacheMiddleware, type ToolCacheStorage } from "@tanstack/ai/middlewares"; @@ -131,6 +137,11 @@ const stream = chat({ **Sharing a cache across requests:** ```typescript +import { chat, toServerSentEventsResponse } from "@tanstack/ai"; +import { toolCacheMiddleware, type ToolCacheStorage } from "@tanstack/ai/middlewares"; +import { globalCache, app, adapter } from "./server"; +import { weatherTool } from "./tools"; + // Create storage once, reuse across chat() calls const sharedStorage: ToolCacheStorage = { getItem: (key) => globalCache.get(key), @@ -139,7 +150,7 @@ const sharedStorage: ToolCacheStorage = { }; // Both requests share the same cache -app.post("/api/chat", async (req) => { +app.post("/api/chat", async (req: { body: { messages: unknown[] } }) => { const stream = chat({ adapter, messages: req.body.messages, @@ -154,8 +165,9 @@ app.post("/api/chat", async (req) => { Filters or transforms streamed text content as it flows through `onChunk`. Use it to redact sensitive data (SSNs, emails, API keys), enforce a profanity filter, or rewrite text on the fly. Rules are applied to `TEXT_MESSAGE_CONTENT` chunks; all other chunk types pass through untouched. -```typescript +```typescript fixture=ambient import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; import { contentGuardMiddleware } from "@tanstack/ai/middlewares"; const stream = chat({ @@ -201,7 +213,7 @@ const stream = chat({ Emits vendor-neutral OpenTelemetry traces and metrics for every `chat()` call — a root span per call, a child span per agent-loop iteration, and a grandchild span per tool execution, all tagged with [GenAI semantic-convention attributes](https://opentelemetry.io/docs/specs/semconv/gen-ai/). -```typescript +```typescript ignore import { chat } from "@tanstack/ai"; import { otelMiddleware } from "@tanstack/ai/middlewares/otel"; import { trace, metrics } from "@opentelemetry/api"; diff --git a/docs/advanced/debug-logging.md b/docs/advanced/debug-logging.md index ec850c010..f937c5681 100644 --- a/docs/advanced/debug-logging.md +++ b/docs/advanced/debug-logging.md @@ -47,7 +47,10 @@ Every internal event now prints to the console with a `[tanstack-ai:]` Pass a `DebugConfig` object instead of `true`. Every unspecified category defaults to `true`, so toggle by setting specific flags to `false`: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; + chat({ adapter: openaiText("gpt-4o"), messages, @@ -57,7 +60,10 @@ chat({ If you want to see ONLY a specific set of categories, set the rest to `false` explicitly. Errors default to `true` — keep them on unless you really want total silence: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; + chat({ adapter: openaiText("gpt-4o"), messages, @@ -78,7 +84,7 @@ chat({ Pass a `Logger` implementation and all debug output flows through it instead of `console`: -```typescript +```typescript ignore import type { Logger } from "@tanstack/ai"; import pino from "pino"; @@ -109,7 +115,7 @@ If your `Logger` implementation throws — a cyclic-meta `JSON.stringify`, a tra If you need to know when your own logger is failing, guard inside your implementation: -```typescript +```typescript ignore const logger: Logger = { debug: (msg, meta) => { try { @@ -140,7 +146,10 @@ const logger: Logger = { Errors flow through the logger unconditionally — even when you omit `debug`: -```typescript +```typescript fixture=ambient +import { chat } from "@tanstack/ai"; +import { adapter } from "./server"; + chat({ adapter, messages }); // still prints [tanstack-ai:errors] ... on failure ``` @@ -151,6 +160,20 @@ To fully silence (including errors), set `debug: false` or `debug: { errors: fal The same `debug` option works on every activity: ```typescript +import { + summarize, + generateImage, + generateSpeech, + generateAudio, + generateTranscription, + type Logger, +} from "@tanstack/ai"; +import { adapter } from "./server"; +import { logger } from "./logger"; + +declare const text: string; +declare const audio: File; + summarize({ adapter, text, debug: true }); generateImage({ adapter, prompt: "a cat", debug: { logger } }); generateSpeech({ adapter, text, debug: { request: true } }); diff --git a/docs/advanced/extend-adapter.md b/docs/advanced/extend-adapter.md index 1e1abaa94..85b9b602d 100644 --- a/docs/advanced/extend-adapter.md +++ b/docs/advanced/extend-adapter.md @@ -53,7 +53,7 @@ const stream = chat({ The `createModel` function provides a clean way to define custom models with full type inference: -```typescript +```typescript group=extend-adapter import { createModel } from '@tanstack/ai' // Arguments define name and input modalities @@ -72,7 +72,7 @@ A custom model definition (`ExtendedModelDef`) has the required properties `name The positional form takes a model name and an `input` array specifying which content types your model supports: -```typescript +```typescript group=extend-adapter const models = [ createModel('text-only-model', ['text']), createModel('multimodal-model', ['text', 'image', 'audio']), @@ -113,6 +113,8 @@ const reasoner = createModel('my-reasoner', { import { createModel, extendAdapter } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' +const customModels = [createModel('my-fine-tuned-gpt4', ['text', 'image'])] as const + const myOpenai = extendAdapter(openaiText, customModels) // Config parameter is preserved @@ -126,7 +128,7 @@ const adapter = myOpenai('my-fine-tuned-gpt4', { The extended adapter provides full type safety: -```typescript +```typescript ignore import { extendAdapter, createModel } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' @@ -190,7 +192,7 @@ const adapter = proxyAdapter('llama-3.1-70b', { Adding type safety for your fine-tuned models: ```typescript -import { createModel, extendAdapter } from '@tanstack/ai' +import { chat, createModel, extendAdapter } from '@tanstack/ai' import { anthropicText } from '@tanstack/ai-anthropic' const fineTunedModels = [ diff --git a/docs/advanced/middleware.md b/docs/advanced/middleware.md index fab12f6e6..5ef2cdb9e 100644 --- a/docs/advanced/middleware.md +++ b/docs/advanced/middleware.md @@ -113,9 +113,11 @@ Called once during `init` (startup) and once per iteration during `beforeModel` Return a **partial** config object with only the fields you want to change — they are shallow-merged with the current config automatically. No need to spread the existing config. ```typescript +import { type ChatMiddleware, type ChatMiddlewareConfig } from "@tanstack/ai"; + const dynamicTemperature: ChatMiddleware = { name: "dynamic-temperature", - onConfig: (ctx, config) => { + onConfig: (ctx, config): void | Partial => { if (ctx.phase === "init") { // Add a system prompt at startup — only systemPrompts is overwritten return { @@ -169,6 +171,9 @@ Called once at the start of the final structured-output adapter call — only wh Return a **partial** `StructuredOutputMiddlewareConfig` with only the fields you want to change — they are shallow-merged with the current config. Return `void` to pass through. ```typescript +import { type ChatMiddleware } from "@tanstack/ai"; +import { sharedDefs } from "./defs"; + const injectDefs: ChatMiddleware = { name: "inject-defs", onStructuredOutputConfig: (_ctx, config) => { @@ -205,6 +210,8 @@ When multiple middleware define `onStructuredOutputConfig`, the config is **pipe Called once after the initial `onConfig` completes. Use it for setup tasks like initializing timers or logging. ```typescript +import { type ChatMiddleware } from "@tanstack/ai"; + const timer: ChatMiddleware = { name: "timer", onStart: (ctx) => { @@ -217,7 +224,9 @@ const timer: ChatMiddleware = { Called for every chunk streamed from the adapter. You can observe, transform, expand, or drop chunks. -```typescript +```typescript ignore +import { type ChatMiddleware } from "@tanstack/ai"; + const redactor: ChatMiddleware = { name: "redactor", onChunk: (ctx, chunk) => { @@ -268,7 +277,9 @@ How you distinguish them depends on which finalization path the adapter takes: - **Separate-finalization adapters** (the legacy path — adapters that don't declare `supportsCombinedToolsAndSchema()`): `ctx.phase === 'structuredOutput'` during the finalization call. Discriminate on the phase. - **Native-combined adapters** (modern OpenAI Chat Completions / Responses, Claude 4.5+, Gemini 3.x, Grok 4.x — see issue #605): the schema-constrained JSON is produced on the model's natural final turn, so **`ctx.phase` stays `'modelStream'`** — the `'structuredOutput'` phase never fires. Discriminate on the CUSTOM event name (`structured-output.start` / `structured-output.complete`) instead. -```typescript +```typescript ignore +import { type ChatMiddleware } from "@tanstack/ai"; + const redactStructuredOutput: ChatMiddleware = { name: "redact-structured-output", onChunk: (ctx, chunk) => { @@ -306,16 +317,22 @@ const redactStructuredOutput: ChatMiddleware = { Called before each tool executes. The first middleware that returns a non-void decision short-circuits — remaining middleware are skipped for that tool call. ```typescript +import { type ChatMiddleware, type BeforeToolCallDecision } from "@tanstack/ai"; + +function isRecord(v: unknown): v is Record { + return typeof v === "object" && v !== null; +} + const guard: ChatMiddleware = { name: "guard", - onBeforeToolCall: (ctx, hookCtx) => { + onBeforeToolCall: (ctx, hookCtx): BeforeToolCallDecision => { // Block dangerous tools if (hookCtx.toolName === "deleteDatabase") { return { type: "abort", reason: "Dangerous operation blocked" }; } // Validate and transform arguments - if (hookCtx.toolName === "search" && !hookCtx.args.limit) { + if (hookCtx.toolName === "search" && isRecord(hookCtx.args) && !hookCtx.args.limit) { return { type: "transformArgs", args: { ...hookCtx.args, limit: 10 }, @@ -349,6 +366,8 @@ The `hookCtx` provides: Called after each tool execution (or skip). All middleware run — there is no short-circuiting. ```typescript +import { type ChatMiddleware } from "@tanstack/ai"; + const toolLogger: ChatMiddleware = { name: "tool-logger", onAfterToolCall: (ctx, info) => { @@ -379,6 +398,8 @@ The `info` object provides: Called once per model iteration when the `RUN_FINISHED` chunk includes usage data. Receives the usage object directly. ```typescript +import { type ChatMiddleware } from "@tanstack/ai"; + const usageTracker: ChatMiddleware = { name: "usage-tracker", onUsage: (ctx, usage) => { @@ -419,6 +440,8 @@ Exactly **one** terminal hook fires per `chat()` invocation. They are mutually e > To aggregate usage across the whole run, accumulate from `onUsage` callbacks rather than relying on `info.usage`. ```typescript +import { type ChatMiddleware } from "@tanstack/ai"; + const terminal: ChatMiddleware = { name: "terminal", onFinish: (ctx, info) => { @@ -468,8 +491,9 @@ Every hook receives a `ChatMiddlewareContext` as its first argument. It provides `ChatMiddleware` accepts a context generic. This lets reusable middleware declared outside `chat()` access the same typed runtime context as your tools. -```typescript +```typescript fixture=ambient import { chat, type ChatMiddleware } from "@tanstack/ai"; +import { adapter, session, audit } from "./server"; type AppContext = { userId: string; @@ -510,6 +534,8 @@ Runtime context is process-local application state. It is separate from AG-UI `R Call `ctx.abort()` to gracefully stop the run. This triggers the `onAbort` terminal hook: ```typescript +import { type ChatMiddleware } from "@tanstack/ai"; + const timeout: ChatMiddleware = { name: "timeout", onChunk: (ctx) => { @@ -525,6 +551,8 @@ const timeout: ChatMiddleware = { Use `ctx.defer()` to register promises that run after the terminal hook without blocking the stream: ```typescript +import { type ChatMiddleware } from "@tanstack/ai"; + const analytics: ChatMiddleware = { name: "analytics", onFinish: (ctx, info) => { @@ -546,7 +574,11 @@ const analytics: ChatMiddleware = { Middleware execute in array order. The ordering matters for hooks that pipe or short-circuit: -```typescript +```typescript fixture=ambient +import { chat, type ChatMiddleware } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { authMiddleware, loggingMiddleware, cachingMiddleware } from "./middleware"; + const stream = chat({ adapter: openaiText("gpt-5.5"), messages, @@ -619,10 +651,11 @@ Three array fields on a middleware declare its capability contract. Each is a `R Author middleware with `defineChatMiddleware` — it sharpens the `requires` / `provides` tuple types so the coverage check and builder can read them precisely. Here a **provider** sets up a counter in `setup`, and a **consumer** reads it in a hook: -```typescript +```typescript group=capability-example import { chat, createCapability, + createChatMiddleware, defineChatMiddleware, } from "@tanstack/ai"; import { openaiText } from "@tanstack/ai-openai"; @@ -665,16 +698,13 @@ If you drop `withCounter` from the array, `chat()` reports a compile-time error `createChatMiddleware()` builds the array through chained `.use()` calls and enforces **provider-before-consumer ordering at compile time**: each `.use()` requires that the middleware's `requires` are already covered by capabilities provided by earlier `.use()` calls. -```typescript -import { chat, createChatMiddleware } from "@tanstack/ai"; -import { openaiText } from "@tanstack/ai-openai"; - +```typescript group=capability-example const middleware = createChatMiddleware() .use(withCounter) // provides "counter" .use(countsChunks) // requires "counter" — OK, already provided above .build(); -const stream = chat({ +const stream2 = chat({ adapter: openaiText("gpt-5.5"), messages: [{ role: "user", content: "Hello" }], middleware, @@ -712,11 +742,13 @@ See [Built-in Middleware](./built-in-middleware) for full options and examples f Limit the number of tool calls per request: ```typescript +import { type ChatMiddleware, type BeforeToolCallDecision } from "@tanstack/ai"; + function rateLimitMiddleware(maxCalls: number): ChatMiddleware { let toolCallCount = 0; return { name: "rate-limit", - onBeforeToolCall: (ctx, hookCtx) => { + onBeforeToolCall: (ctx, hookCtx): BeforeToolCallDecision => { toolCallCount++; if (toolCallCount > maxCalls) { return { @@ -734,6 +766,9 @@ function rateLimitMiddleware(maxCalls: number): ChatMiddleware { Log every action for compliance: ```typescript +import { type ChatMiddleware } from "@tanstack/ai"; +import { db } from "./db"; + const auditTrail: ChatMiddleware = { name: "audit-trail", onStart: (ctx) => { @@ -776,9 +811,11 @@ const auditTrail: ChatMiddleware = { Expose different tools at different stages of the agent loop: ```typescript +import { type ChatMiddleware, type ChatMiddlewareConfig } from "@tanstack/ai"; + const toolSwapper: ChatMiddleware = { name: "tool-swapper", - onConfig: (ctx, config) => { + onConfig: (ctx, config): void | Partial => { if (ctx.phase !== "beforeModel") return; if (ctx.iteration === 0) { @@ -796,7 +833,10 @@ const toolSwapper: ChatMiddleware = { Drop or transform chunks before they reach the consumer: -```typescript +```typescript ignore +import { type ChatMiddleware } from "@tanstack/ai"; +import { containsProfanity } from "./filters"; + const contentFilter: ChatMiddleware = { name: "content-filter", onChunk: (ctx, chunk) => { @@ -813,6 +853,9 @@ const contentFilter: ChatMiddleware = { ### Error Recovery with Retry Logging ```typescript +import { type ChatMiddleware } from "@tanstack/ai"; +import { alertService } from "./services"; + const errorRecovery: ChatMiddleware = { name: "error-recovery", onError: (ctx, info) => { diff --git a/docs/advanced/multimodal-content.md b/docs/advanced/multimodal-content.md index e42067a05..d697f9908 100644 --- a/docs/advanced/multimodal-content.md +++ b/docs/advanced/multimodal-content.md @@ -98,6 +98,7 @@ OpenAI supports images and audio in their vision and audio models: ```typescript import { openaiText } from '@tanstack/ai-openai' +import { imageBase64 } from './data' const adapter = openaiText('gpt-5.5') @@ -125,6 +126,7 @@ Anthropic's Claude models support images and PDF documents: ```typescript import { anthropicText } from '@tanstack/ai-anthropic' +import { imageBase64, pdfBase64 } from './data' const adapter = anthropicText('claude-sonnet-4-6') @@ -164,6 +166,7 @@ Google's Gemini models support a wide range of modalities: ```typescript import { geminiText } from '@tanstack/ai-gemini' +import { imageBase64 } from './data' const adapter = geminiText('gemini-3-flash-preview') @@ -189,6 +192,7 @@ Ollama supports images in compatible models: ```typescript import { ollamaText } from '@tanstack/ai-ollama' +import { imageBase64 } from './data' // `ollamaText(model)` takes a model name. The host is read from the // `OLLAMA_HOST` environment variable (defaults to http://localhost:11434). @@ -299,7 +303,7 @@ import type { GeminiImageMetadata } from '@tanstack/ai-gemini' When receiving messages from external sources (like `request.json()`), the data is typed as `any`. TanStack AI does not ship a runtime message validator — define a schema with your preferred Standard-Schema library (Zod, Valibot, ArkType, …) and parse the body before handing it to `chat()`. -```typescript +```typescript ignore import { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' import { z } from 'zod' @@ -351,7 +355,7 @@ When using the `ChatClient` from `@tanstack/ai-client`, you can send multimodal The `sendMessage` method accepts either a simple string or a `MultimodalContent` object: -```typescript +```typescript group=multimodal-content import { ChatClient, fetchServerSentEvents } from '@tanstack/ai-client' const client = new ChatClient({ @@ -377,7 +381,7 @@ await client.sendMessage({ You can provide a custom ID for the message: -```typescript +```typescript group=multimodal-content await client.sendMessage({ content: 'Hello!', id: 'custom-message-id-123' @@ -388,14 +392,14 @@ await client.sendMessage({ The second parameter allows you to pass additional `forwardedProps` for that specific request. These are shallow-merged with the client's base `forwardedProps` configuration, with per-message values taking priority: -```typescript -const client = new ChatClient({ +```typescript group=multimodal-content +const client2 = new ChatClient({ connection: fetchServerSentEvents('/api/chat'), forwardedProps: { model: 'gpt-5' }, // Base forwarded props }) // Override model for this specific message -await client.sendMessage('Analyze this complex problem', { +await client2.sendMessage('Analyze this complex problem', { model: 'gpt-5', temperature: 0.2, }) @@ -463,7 +467,7 @@ function ChatWithFileUpload() { reader.onload = () => { const result = reader.result as string // Remove data URL prefix (e.g., "data:image/png;base64,") - resolve(result.split(',')[1]) + resolve(result.split(',')[1]!) } reader.readAsDataURL(file) }) diff --git a/docs/advanced/otel.md b/docs/advanced/otel.md index 2eadc86f5..e1249de64 100644 --- a/docs/advanced/otel.md +++ b/docs/advanced/otel.md @@ -109,7 +109,7 @@ By default, only metadata lands on spans. To record prompt and completion conten Pass a `redact` function to strip PII before anything is recorded: -```ts +```ts ignore otelMiddleware({ tracer, captureContent: true, @@ -133,7 +133,7 @@ All four extensions are optional. Each wraps user code in try/catch — a thrown Override default span names. `info.kind` is `'chat' | 'iteration' | 'tool'`. -```ts +```ts ignore otelMiddleware({ tracer, spanNameFormatter: (info) => @@ -145,7 +145,7 @@ otelMiddleware({ Add custom attributes to every span. Fires once per span. -```ts +```ts ignore otelMiddleware({ tracer, attributeEnricher: () => ({ @@ -162,7 +162,7 @@ Mutate `SpanOptions` immediately before `tracer.startSpan(...)`. Useful for addi Fires just before every `span.end()`. Common uses: record custom events, emit per-tool metrics via your own `Meter`. -```ts +```ts ignore const toolDuration = meter.createHistogram('tool.duration') otelMiddleware({ tracer, diff --git a/docs/advanced/per-model-type-safety.md b/docs/advanced/per-model-type-safety.md index a7ed8e2d3..6e9d92c41 100644 --- a/docs/advanced/per-model-type-safety.md +++ b/docs/advanced/per-model-type-safety.md @@ -54,7 +54,10 @@ const validCall = chat({ ### ❌ Incorrect Usage -```typescript +```typescript ignore +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; + // ❌ gpt-4-turbo does NOT support structured outputs - `text` is rejected const invalidCall = chat({ adapter: openaiText("gpt-4-turbo"), diff --git a/docs/advanced/runtime-adapter-switching.md b/docs/advanced/runtime-adapter-switching.md index c26044f3e..1eef7f2b2 100644 --- a/docs/advanced/runtime-adapter-switching.md +++ b/docs/advanced/runtime-adapter-switching.md @@ -33,14 +33,16 @@ const adapters = { openai: () => openaiText('gpt-5.5'), // ✅ Autocomplete! } -// In your request handler: -const body = await request.json() -const provider: Provider = body.forwardedProps?.provider || 'openai' - -const stream = chat({ - adapter: adapters[provider](), - messages: body.messages, -}) +async function handleRequest(request: Request) { + // In your request handler: + const body = await request.json() + const provider: Provider = body.forwardedProps?.provider || 'openai' + + const stream = chat({ + adapter: adapters[provider](), + messages: body.messages, + }) +} ``` ## Why This Works @@ -48,9 +50,11 @@ const stream = chat({ Each adapter factory function accepts a model name as its first argument and returns a fully typed adapter: ```typescript +import { openaiText, OpenAITextAdapter } from '@tanstack/ai-openai' + // These are equivalent: const adapter1 = openaiText('gpt-5.5') -const adapter2 = new OpenAITextAdapter({ apiKey: process.env.OPENAI_API_KEY }, 'gpt-5.5') +const adapter2 = new OpenAITextAdapter({ apiKey: process.env.OPENAI_API_KEY! }, 'gpt-5.5') // The model is stored on the adapter console.log(adapter1.model) // 'gpt-5.5' @@ -66,7 +70,7 @@ When you pass an adapter to `chat()`, it uses the model from `adapter.model`. Th Here's a complete example showing a multi-provider chat API: -```typescript +```typescript ignore import { createFileRoute } from '@tanstack/react-router' import { chat, maxIterations, toServerSentEventsResponse } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' @@ -119,7 +123,10 @@ import { generateImage } from '@tanstack/ai' import { openaiImage } from '@tanstack/ai-openai' import { geminiImage } from '@tanstack/ai-gemini' -const imageAdapters = { +type ImageProvider = 'openai' | 'gemini' +declare const provider: ImageProvider + +const imageAdapters: Record ReturnType> = { openai: () => openaiImage('gpt-image-1'), gemini: () => geminiImage('gemini-3.1-flash-image-preview'), } @@ -141,7 +148,11 @@ import { summarize } from '@tanstack/ai' import { openaiSummarize } from '@tanstack/ai-openai' import { anthropicSummarize } from '@tanstack/ai-anthropic' -const summarizeAdapters = { +type SummarizeProvider = 'openai' | 'anthropic' +declare const provider: SummarizeProvider +declare const longDocument: string + +const summarizeAdapters: Record ReturnType> = { openai: () => openaiSummarize('gpt-5.4-mini'), anthropic: () => anthropicSummarize('claude-sonnet-4-6'), } @@ -161,7 +172,7 @@ If you have existing code using switch statements, here's how to migrate: ### Before -```typescript +```typescript ignore let adapter let model @@ -186,7 +197,14 @@ const stream = chat({ ### After -```typescript +```typescript fixture=ambient +import { chat } from '@tanstack/ai' +import { anthropicText } from '@tanstack/ai-anthropic' +import { openaiText } from '@tanstack/ai-openai' + +type AfterProvider = 'openai' | 'anthropic' +declare const provider: AfterProvider + const adapters = { anthropic: () => anthropicText('claude-sonnet-4-6'), openai: () => openaiText('gpt-5.5'), diff --git a/docs/advanced/runtime-context.md b/docs/advanced/runtime-context.md index 2a7b52d70..a4b889041 100644 --- a/docs/advanced/runtime-context.md +++ b/docs/advanced/runtime-context.md @@ -28,8 +28,11 @@ The source of truth is: This means the context value is the implementation detail you provide at runtime, while tools and middleware are the contract. TanStack AI infers the required context from every typed tool and middleware in the call, merges those requirements, and checks your `context` option against the result. -```typescript -import { chat, toolDefinition, type ChatMiddleware } from "@tanstack/ai"; +```typescript fixture=ambient +import { chat, toolDefinition, type ChatMiddleware, type AnyTextAdapter } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; + +declare const adapter: AnyTextAdapter; type UserContext = { userId: string; @@ -117,6 +120,7 @@ import { } from "@tanstack/ai"; import { openaiText } from "@tanstack/ai-openai"; import { z } from "zod"; +import { requireUser, db } from "./auth"; type AppContext = { userId: string; @@ -177,7 +181,7 @@ When any tool or middleware in a `chat()` call declares a concrete context type, Client runtime context is local to `ChatClient` and framework hooks. It is passed to client tool implementations and is not serialized to the server. -```typescript +```typescript group=runtime-context import { createChatClientOptions, clientTools } from "@tanstack/ai-client"; import { useChat, fetchServerSentEvents } from "@tanstack/ai-react"; import { toolDefinition } from "@tanstack/ai"; @@ -213,7 +217,9 @@ Use client context for local dependencies only. Do not put values there expectin To send serializable client data to the server, use `forwardedProps`, validate it in your route, and explicitly map it into the server runtime context. -```typescript +```typescript group=runtime-context +declare const selectedTenantId: string; +declare const clientRuntimeContext: ClientContext; // Client useChat({ connection: fetchServerSentEvents("/api/chat"), @@ -230,7 +236,13 @@ import { chat, chatParamsFromRequest, toServerSentEventsResponse, + type AnyTextAdapter, + type AnyTool, } from "@tanstack/ai"; +import { requireUser } from "./auth"; + +declare const adapter: AnyTextAdapter; +declare const tools: AnyTool[]; type AppContext = { userId: string; @@ -269,6 +281,13 @@ AG-UI also defines `RunAgentInput.context`, usually as protocol-level context en TanStack AI does not automatically copy AG-UI `params.aguiContext` into runtime context. If you want to use AG-UI context values, validate and map them yourself. `params.context` is a deprecated alias of `params.aguiContext` kept for backward compatibility. ```typescript +import { chat, chatParamsFromRequest, type AnyTextAdapter, type AnyTool } from "@tanstack/ai"; +import { buildRuntimeContextFrom } from "./context"; + +declare const request: Request; +declare const adapter: AnyTextAdapter; +declare const tools: AnyTool[]; + const params = await chatParamsFromRequest(request); const stream = chat({ diff --git a/docs/advanced/tree-shaking.md b/docs/advanced/tree-shaking.md index 0018ca9ce..e6135869d 100644 --- a/docs/advanced/tree-shaking.md +++ b/docs/advanced/tree-shaking.md @@ -167,7 +167,7 @@ Each activity is in its own module, so bundlers can eliminate unused ones. The tree-shakeable design doesn't sacrifice type safety. Each adapter provides full type safety for its supported models: -```ts +```ts ignore import { openaiText, type OpenAIChatModel } from '@tanstack/ai-openai' const adapter = openaiText('gpt-5.5') @@ -254,7 +254,7 @@ Modern bundlers (Vite, Webpack, Rollup, esbuild) can easily eliminate unused cod packages. See [Quick Start: React Native](../getting-started/quick-start-react-native) for the server-only provider boundary and mobile transport setup. -```ts +```ts group=tree-shaking // ✅ Good - Only imports chat import { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' @@ -277,7 +277,9 @@ Each adapter type implements a specific interface: All adapters have a `kind` property that indicates their type: -```ts +```ts group=tree-shaking +import { openaiSummarize } from '@tanstack/ai-openai' + const chatAdapter = openaiText('gpt-5.5') console.log(chatAdapter.kind) // 'text' diff --git a/docs/advanced/typed-options.md b/docs/advanced/typed-options.md index 7ff2d4d8f..41c658829 100644 --- a/docs/advanced/typed-options.md +++ b/docs/advanced/typed-options.md @@ -22,7 +22,7 @@ You have a `chat()` (or `generateImage()`, `generateSpeech()`, …) configuratio Every activity in `@tanstack/ai` ships a paired `createXxxOptions` helper that takes the exact same options object as the activity itself and returns it unchanged — at runtime it's the identity function. The point is **type inference**: the returned object carries the adapter's full type, so when you spread it into the activity, TypeScript still narrows `modelOptions`, content modalities, and `outputSchema` to the adapter you chose. -```typescript +```typescript fixture=ambient import { chat, createChatOptions } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' @@ -78,9 +78,11 @@ Suppose you have several routes that all hit the same model with the same provid import { createChatOptions, toolDefinition } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' import { z } from 'zod' +import { db } from './db' const lookupOrderDef = toolDefinition({ name: 'lookupOrder', + description: 'Look up a customer order by ID', inputSchema: z.object({ orderId: z.string() }), }) @@ -98,7 +100,7 @@ export const supportChatOptions = createChatOptions({ }) ``` -```typescript +```typescript ignore // routes/api/support/chat.ts import { chat, toServerSentEventsResponse } from '@tanstack/ai' import { supportChatOptions } from '@/lib/ai/chat-options' @@ -110,7 +112,7 @@ export async function POST(request: Request) { } ``` -```typescript +```typescript ignore // routes/api/support/draft-reply.ts — same adapter+tools, different schema import { chat } from '@tanstack/ai' import { supportChatOptions } from '@/lib/ai/chat-options' @@ -138,14 +140,12 @@ import { openaiImage } from '@tanstack/ai-openai' const heroImageOptions = createImageOptions({ adapter: openaiImage('gpt-image-1'), + prompt: 'A glass sphere refracting a sunset over a calm sea', size: '1536x1024', numberOfImages: 1, }) -const result = await generateImage({ - ...heroImageOptions, - prompt: 'A glass sphere refracting a sunset over a calm sea', -}) +const result = await generateImage(heroImageOptions) ``` The same pattern works for `createVideoOptions`, `createSpeechOptions`, `createTranscriptionOptions`, `createAudioOptions`, and `createSummarizeOptions` — the adapter is captured in the typed options object and every downstream call is narrowed to it. diff --git a/docs/api/ai-angular.md b/docs/api/ai-angular.md index 70cb5e246..6a136a18d 100644 --- a/docs/api/ai-angular.md +++ b/docs/api/ai-angular.md @@ -71,6 +71,8 @@ Extends `ChatClientOptions` from `@tanstack/ai-client` (minus internal state cal **Reactive options** (`body`, `forwardedProps`, `context`, `live`) accept a `ReactiveOption`, which is one of: ```typescript +import type { Signal } from "@angular/core"; + type ReactiveOption = T | Signal | (() => T); ``` @@ -81,6 +83,16 @@ A plain value becomes a constant; a `Signal` is read directly; a zero-arg getter ### Returns ```typescript +import type { Signal } from "@angular/core"; +import type { + UIMessage, + MultimodalContent, + DeepPartial, +} from "@tanstack/ai-angular"; +import type { ModelMessage, InferSchemaType } from "@tanstack/ai/client"; +import type { ChatClientState, ConnectionStatus } from "@tanstack/ai-client"; +type TSchema = any; + interface InjectChatResult { messages: Signal; sendMessage: (content: string | MultimodalContent) => Promise; @@ -244,12 +256,12 @@ import { updateUIDef, saveToStorageDef } from "./tool-definitions"; }) export class TypedChatComponent { // Create client implementations - private updateUI = updateUIDef.client((input) => { + private updateUI = updateUIDef.client((input: any) => { // input is fully typed! return { success: true }; }); - private saveToStorage = saveToStorageDef.client((input) => { + private saveToStorage = saveToStorageDef.client((input: any) => { localStorage.setItem(input.key, input.value); return { saved: true }; }); @@ -471,13 +483,17 @@ All generation injectables automatically clean up via `DestroyRef.onDestroy`. Angular's DI system requires that `inject()` is called during component construction. Every `inject*` function in this package calls `inject()` internally. Valid call sites: ```typescript +import { runInInjectionContext, Injector } from "@angular/core"; +import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular"; +declare const injector: Injector; + // Field initializer (recommended) export class MyComponent { chat = injectChat({ connection: fetchServerSentEvents("/api/chat") }); } // Constructor -export class MyComponent { +export class MyComponentAlt { chat: ReturnType; constructor() { this.chat = injectChat({ connection: fetchServerSentEvents("/api/chat") }); @@ -500,6 +516,8 @@ import { createChatClientOptions, type InferChatMessages, } from "@tanstack/ai-client"; +import { fetchServerSentEvents } from "@tanstack/ai-angular"; +import { tool1, tool2 } from "./tools"; // Create typed tools array (no 'as const' needed!) const tools = clientTools(tool1, tool2); diff --git a/docs/api/ai-client.md b/docs/api/ai-client.md index a812faf5a..652cb6bf4 100644 --- a/docs/api/ai-client.md +++ b/docs/api/ai-client.md @@ -25,11 +25,12 @@ npm install @tanstack/ai-client The main client class for managing chat state. -```typescript +```typescript group=chatclient import { ChatClient, clientTools, fetchServerSentEvents, + type UIMessage, } from "@tanstack/ai-client"; import { myClientTool } from "./tools"; @@ -37,7 +38,7 @@ const client = new ChatClient({ connection: fetchServerSentEvents("/api/chat"), initialMessages: [], tools: clientTools(myClientTool), - onMessagesChange: (messages) => { + onMessagesChange: (messages: UIMessage[]) => { console.log("Messages updated:", messages); }, }); @@ -68,7 +69,7 @@ const client = new ChatClient({ Sends a user message and gets a response. -```typescript +```typescript group=chatclient await client.sendMessage("Hello!"); ``` @@ -76,7 +77,7 @@ await client.sendMessage("Hello!"); Appends a message to the conversation. -```typescript +```typescript group=chatclient await client.append({ role: "user", content: "Additional context", @@ -87,7 +88,7 @@ await client.append({ Reloads the last assistant message. -```typescript +```typescript group=chatclient await client.reload(); ``` @@ -95,7 +96,7 @@ await client.reload(); Stops the current response generation. -```typescript +```typescript group=chatclient client.stop(); ``` @@ -103,7 +104,7 @@ client.stop(); Clears all messages. -```typescript +```typescript group=chatclient client.clear(); ``` @@ -111,7 +112,8 @@ client.clear(); Manually sets the messages array. -```typescript +```typescript group=chatclient +const newMessages: UIMessage[] = []; client.setMessagesManually([...newMessages]); ``` @@ -119,7 +121,7 @@ client.setMessagesManually([...newMessages]); Adds the result of a client-side tool execution. -```typescript +```typescript group=chatclient await client.addToolResult({ toolCallId: "call_123", tool: "toolName", @@ -132,7 +134,7 @@ await client.addToolResult({ Responds to a tool approval request. -```typescript +```typescript group=chatclient await client.addToolApprovalResponse({ id: "approval_123", approved: true, @@ -242,7 +244,7 @@ override static adapter `body` values. Creates a custom connection adapter. -```typescript +```typescript ignore import { stream } from "@tanstack/ai-client"; const adapter = stream(async (messages, data, signal) => { @@ -265,16 +267,23 @@ const adapter = stream(async (messages, data, signal) => { Creates a typed array of client tools with proper type inference. This eliminates the need for `as const` when defining tool arrays and enables proper discriminated union type narrowing. ```typescript -import { clientTools } from "@tanstack/ai-client"; +import { + clientTools, + createChatClientOptions, + fetchServerSentEvents, + type UIMessage, +} from "@tanstack/ai-client"; import { myTool1, myTool2 } from "./tools"; +const messages: UIMessage[] = []; + // Create client implementations -const tool1Client = myTool1.client((input) => { +const tool1Client = myTool1.client((input: unknown) => { // Implementation return { result: "..." }; }); -const tool2Client = myTool2.client((input) => { +const tool2Client = myTool2.client((input: unknown) => { // Implementation return { result: "..." }; }); @@ -305,7 +314,13 @@ messages.forEach((message) => { Helper function to create typed chat client options with proper type inference. ```typescript -import { createChatClientOptions, clientTools } from "@tanstack/ai-client"; +import { + createChatClientOptions, + clientTools, + fetchServerSentEvents, + type InferChatMessages, +} from "@tanstack/ai-client"; +import { tool1, tool2 } from "./tools"; const tools = clientTools(tool1, tool2); @@ -321,11 +336,18 @@ type ChatMessages = InferChatMessages; `createChatClientOptions` also preserves typed client runtime context: ```typescript +import { + createChatClientOptions, + clientTools, + fetchServerSentEvents, +} from "@tanstack/ai-client"; +import { projectTool, runProjectAction } from "./tools"; + type ClientContext = { activeProjectId: string; }; -const tool = projectTool.client((input, ctx) => { +const tool = projectTool.client((input: unknown, ctx: { context: ClientContext }) => { return runProjectAction(ctx.context.activeProjectId, input); }); @@ -344,7 +366,7 @@ Client runtime context is local to the client instance. Use `forwardedProps` for ### `UIMessage` -```typescript +```typescript ignore interface UIMessage { id: string; role: "user" | "assistant"; @@ -355,7 +377,7 @@ interface UIMessage { ### `MessagePart` -```typescript +```typescript ignore type MessagePart = TextPart | ThinkingPart | ToolCallPart | ToolResultPart; ``` @@ -383,7 +405,7 @@ Thinking parts represent the model's internal reasoning process. They are typica ### `ToolCallPart` -```typescript +```typescript ignore interface ToolCallPart { type: "tool-call"; id: string; @@ -400,7 +422,7 @@ When using typed tools with `clientTools()` and `createChatClientOptions()`, the ### `ToolResultPart` -```typescript +```typescript ignore interface ToolResultPart { type: "tool-result"; toolCallId: string; @@ -412,7 +434,7 @@ interface ToolResultPart { ### `ToolCallState` -```typescript +```typescript ignore type ToolCallState = | "awaiting-input" | "input-streaming" @@ -424,7 +446,7 @@ type ToolCallState = ### `ToolResultState` -```typescript +```typescript ignore type ToolResultState = | "streaming" | "complete" @@ -436,7 +458,11 @@ type ToolResultState = Configure stream processing with chunk strategies: ```typescript -import { ImmediateStrategy, fetchServerSentEvents } from "@tanstack/ai-client"; +import { + ChatClient, + ImmediateStrategy, + fetchServerSentEvents, +} from "@tanstack/ai-client"; const client = new ChatClient({ connection: fetchServerSentEvents("/api/chat"), diff --git a/docs/api/ai-preact.md b/docs/api/ai-preact.md index 8951e8e71..80500981c 100644 --- a/docs/api/ai-preact.md +++ b/docs/api/ai-preact.md @@ -31,10 +31,13 @@ import { createChatClientOptions, type InferChatMessages } from "@tanstack/ai-client"; +import { updateUIDef } from "./tool-definitions"; +import { useState } from "preact/hooks"; function ChatComponent() { + const [, setNotification] = useState(null); // Create client tool implementations - const updateUI = updateUIDef.client((input) => { + const updateUI = updateUIDef.client((input: any) => { setNotification(input.message); return { success: true }; }); @@ -80,6 +83,9 @@ Extends `ChatClientOptions` from `@tanstack/ai-client`: ### Returns ```typescript +import type { UIMessage } from "@tanstack/ai-preact"; +import type { ModelMessage } from "@tanstack/ai/client"; + interface UseChatReturn { messages: UIMessage[]; sendMessage: (content: string) => Promise; @@ -130,7 +136,7 @@ export function Chat() { connection: fetchServerSentEvents("/api/chat"), }); - const handleSubmit = (e) => { + const handleSubmit = (e: Event) => { e.preventDefault(); if (input.trim() && !isLoading) { sendMessage(input); @@ -241,16 +247,16 @@ import { updateUIDef, saveToStorageDef } from "./tool-definitions"; import { useState } from "preact/hooks"; export function ChatWithClientTools() { - const [notification, setNotification] = useState(null); + const [notification, setNotification] = useState<{ message: string; type: string } | null>(null); // Create client implementations - const updateUI = updateUIDef.client((input) => { + const updateUI = updateUIDef.client((input: any) => { // ✅ input is fully typed! setNotification({ message: input.message, type: input.type }); return { success: true }; }); - const saveToStorage = saveToStorageDef.client((input) => { + const saveToStorage = saveToStorageDef.client((input: any) => { localStorage.setItem(input.key, input.value); return { saved: true }; }); @@ -271,6 +277,7 @@ export function ChatWithClientTools() { // ✅ part.input and part.output are fully typed! return
Tool executed: {part.name}
; } + return null; }) )} @@ -288,6 +295,8 @@ import { createChatClientOptions, type InferChatMessages } from "@tanstack/ai-client"; +import { fetchServerSentEvents } from "@tanstack/ai-preact"; +import { tool1, tool2 } from "./tools"; // Create typed tools array (no 'as const' needed!) const tools = clientTools(tool1, tool2); diff --git a/docs/api/ai-react.md b/docs/api/ai-react.md index e8ef2a62c..9cdd866a4 100644 --- a/docs/api/ai-react.md +++ b/docs/api/ai-react.md @@ -37,10 +37,14 @@ import { createChatClientOptions, type InferChatMessages } from "@tanstack/ai-client"; +import { updateUIDef } from "./tool-definitions"; +import { useState } from "react"; function ChatComponent() { + const [notification, setNotification] = useState(null); + // Create client tool implementations - const updateUI = updateUIDef.client((input) => { + const updateUI = updateUIDef.client((input: { message: string }) => { setNotification(input.message); return { success: true }; }); @@ -86,6 +90,9 @@ Extends `ChatClientOptions` from `@tanstack/ai-client`: ### Returns ```typescript +import type { UIMessage } from "@tanstack/ai-react"; +import type { ModelMessage } from "@tanstack/ai"; + interface UseChatReturn { messages: UIMessage[]; sendMessage: (content: string) => Promise; @@ -266,16 +273,16 @@ import { updateUIDef, saveToStorageDef } from "./tool-definitions"; import { useState } from "react"; export function ChatWithClientTools() { - const [notification, setNotification] = useState(null); + const [notification, setNotification] = useState<{ message: string; type: string } | null>(null); // Create client implementations - const updateUI = updateUIDef.client((input) => { + const updateUI = updateUIDef.client((input: { message: string; type: string }) => { // ✅ input is fully typed! setNotification({ message: input.message, type: input.type }); return { success: true }; }); - const saveToStorage = saveToStorageDef.client((input) => { + const saveToStorage = saveToStorageDef.client((input: { key: string; value: string }) => { localStorage.setItem(input.key, input.value); return { saved: true }; }); @@ -294,8 +301,9 @@ export function ChatWithClientTools() { message.parts.map((part) => { if (part.type === "tool-call" && part.name === "updateUI") { // ✅ part.input and part.output are fully typed! - return
Tool executed: {part.name}
; + return
Tool executed: {part.name}
; } + return null; }) )} @@ -311,8 +319,10 @@ Helper to create typed chat options (re-exported from `@tanstack/ai-client`). import { clientTools, createChatClientOptions, + fetchServerSentEvents, type InferChatMessages } from "@tanstack/ai-client"; +import { tool1, tool2 } from "./tools"; // Create typed tools array (no 'as const' needed!) const tools = clientTools(tool1, tool2); diff --git a/docs/api/ai-solid.md b/docs/api/ai-solid.md index 20c49f089..3b0937250 100644 --- a/docs/api/ai-solid.md +++ b/docs/api/ai-solid.md @@ -32,10 +32,13 @@ import { createChatClientOptions, type InferChatMessages } from "@tanstack/ai-client"; +import { updateUIDef } from "./tool-definitions"; +import { createSignal } from "solid-js"; function ChatComponent() { + const [, setNotification] = createSignal(null); // Create client tool implementations - const updateUI = updateUIDef.client((input) => { + const updateUI = updateUIDef.client((input: any) => { setNotification(input.message); return { success: true }; }); @@ -81,6 +84,10 @@ Extends `ChatClientOptions` from `@tanstack/ai-client`: ### Returns ```typescript +import type { Accessor } from "solid-js"; +import type { UIMessage } from "@tanstack/ai-solid"; +import type { ModelMessage } from "@tanstack/ai/client"; + interface UseChatReturn { messages: Accessor; sendMessage: (content: string) => Promise; @@ -198,39 +205,40 @@ export function ChatWithApproval() { {(message) => ( - {(part) => ( - -
-

Approve: {part.name}

- - -
-
- )} + {(part) => { + if ( + part.type === "tool-call" && + part.state === "approval-requested" && + part.approval + ) { + return ( +
+

Approve: {part.name}

+ + +
+ ); + } + return null; + }}
)}
@@ -252,16 +260,16 @@ import { updateUIDef, saveToStorageDef } from "./tool-definitions"; import { createSignal, For } from "solid-js"; export function ChatWithClientTools() { - const [notification, setNotification] = createSignal(null); + const [notification, setNotification] = createSignal<{ message: string; type: string } | null>(null); // Create client implementations - const updateUI = updateUIDef.client((input) => { + const updateUI = updateUIDef.client((input: any) => { // ✅ input is fully typed! setNotification({ message: input.message, type: input.type }); return { success: true }; }); - const saveToStorage = saveToStorageDef.client((input) => { + const saveToStorage = saveToStorageDef.client((input: any) => { localStorage.setItem(input.key, input.value); return { saved: true }; }); @@ -284,6 +292,7 @@ export function ChatWithClientTools() { // ✅ part.input and part.output are fully typed! return
Tool executed: {part.name}
; } + return null; }} )} @@ -303,6 +312,8 @@ import { createChatClientOptions, type InferChatMessages } from "@tanstack/ai-client"; +import { fetchServerSentEvents } from "@tanstack/ai-solid"; +import { tool1, tool2 } from "./tools"; // Create typed tools array (no 'as const' needed!) const tools = clientTools(tool1, tool2); diff --git a/docs/api/ai-svelte.md b/docs/api/ai-svelte.md index e4dbb2865..62e8737ca 100644 --- a/docs/api/ai-svelte.md +++ b/docs/api/ai-svelte.md @@ -32,9 +32,12 @@ import { createChatClientOptions, type InferChatMessages, } from "@tanstack/ai-client"; +import { updateUIDef } from "./tool-definitions"; // In