@tanstack/ai 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/activities/chat/index.d.ts +7 -0
- package/dist/esm/activities/chat/index.js +86 -20
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/mcp/manager.d.ts +25 -0
- package/dist/esm/activities/chat/mcp/manager.js +71 -0
- package/dist/esm/activities/chat/mcp/manager.js.map +1 -0
- package/dist/esm/activities/chat/mcp/types.d.ts +56 -0
- package/dist/esm/activities/chat/messages.js +1 -1
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.js +20 -8
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +6 -0
- package/dist/esm/activities/chat/stream/processor.js +18 -3
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +2 -1
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +2 -1
- package/dist/esm/activities/generateVideo/index.js +12 -2
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/extend-adapter.d.ts +22 -6
- package/dist/esm/extend-adapter.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logger/console-logger.d.ts +18 -0
- package/dist/esm/logger/console-logger.js +64 -8
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/types.d.ts +4 -4
- package/dist/esm/realtime/index.d.ts +1 -3
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/types.d.ts +13 -1
- package/package.json +2 -2
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -2
- package/skills/ai-core/chat-experience/SKILL.md +71 -0
- package/skills/ai-core/media-generation/SKILL.md +29 -1
- package/skills/ai-core/tool-calling/SKILL.md +287 -0
- package/src/activities/chat/index.ts +109 -25
- package/src/activities/chat/mcp/manager.ts +85 -0
- package/src/activities/chat/mcp/types.ts +66 -0
- package/src/activities/chat/messages.ts +1 -0
- package/src/activities/chat/stream/message-updaters.ts +22 -9
- package/src/activities/chat/stream/processor.ts +30 -3
- package/src/activities/chat/tools/tool-calls.ts +2 -0
- package/src/activities/generateVideo/index.ts +12 -0
- package/src/extend-adapter.ts +42 -24
- package/src/index.ts +10 -0
- package/src/logger/console-logger.ts +112 -17
- package/src/logger/types.ts +4 -4
- package/src/realtime/index.ts +1 -3
- package/src/types.ts +13 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"console-logger.js","sources":["../../../src/logger/console-logger.ts"],"sourcesContent":["import type { Logger } from './types'\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"console-logger.js","sources":["../../../src/logger/console-logger.ts"],"sourcesContent":["import type { Logger } from './types'\n\n/**\n * `util.inspect` options used with `console.dir` on Node so deeply nested\n * structures (e.g. provider chunk payloads with `usage`, `output`,\n * `reasoning`, `tools`) render in full instead of truncating to\n * `[Object]` / `[Array]`.\n */\nconst DIR_OPTIONS = { depth: null, colors: true } as const\n\n/**\n * How `meta` should be rendered on the current runtime:\n *\n * - `dir` — Node. `console.dir(meta, { depth: null, colors: true })` gives a\n * depth-unlimited, colored inspect dump.\n * - `json` — Cloudflare Workers / workerd. workerd never forwards\n * `console.dir` output to the terminal (with or without options), and its\n * own inspect of extra console arguments truncates nested objects, so the\n * payload is appended as circular-safe pretty-printed JSON instead.\n * - `arg` — everything else (browsers, Deno, Bun). `meta` is passed as an\n * extra console argument: devtools keep collapsible object trees and the\n * runtime's inspect handles circular references natively.\n */\ntype MetaStrategy = 'dir' | 'json' | 'arg'\n\nfunction resolveMetaStrategy(): MetaStrategy {\n // workerd must be detected before the Node check: under the `nodejs_compat`\n // flag it emulates `process.versions.node`, but still drops `console.dir`.\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- navigator is missing on Node < 21 despite the DOM lib typing it as always present\n if (globalThis.navigator?.userAgent === 'Cloudflare-Workers') return 'json'\n } catch {\n // A locked-down runtime with a throwing `userAgent` getter is not workerd;\n // fall through to the remaining checks rather than crash the log call.\n }\n if (\n typeof process !== 'undefined' &&\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- a partial process global (bundler shims) may lack versions\n typeof process.versions?.node === 'string'\n ) {\n return 'dir'\n }\n return 'arg'\n}\n\n/**\n * `JSON.stringify` hardened for debug payloads: circular references collapse\n * to `\"[Circular]\"`, `Error` instances expand to `name`/`message`/`stack`\n * (they would otherwise stringify to `{}`), and `bigint` values become\n * strings (they would otherwise throw). Never throws — falls back to\n * `String(value)` and, if even that coercion throws, a placeholder.\n */\nfunction stringifyMetaSafely(value: unknown): string {\n const seen = new WeakSet<object>()\n try {\n return JSON.stringify(\n value,\n (_key, entry: unknown) => {\n if (typeof entry === 'bigint') return entry.toString()\n if (entry instanceof Error) {\n return {\n name: entry.name,\n message: entry.message,\n stack: entry.stack,\n }\n }\n if (typeof entry === 'object' && entry !== null) {\n if (seen.has(entry)) return '[Circular]'\n seen.add(entry)\n }\n return entry\n },\n 2,\n )\n } catch {\n try {\n return String(value)\n } catch {\n return '[Unserializable meta]'\n }\n }\n}\n\n/**\n * Default `Logger` implementation that routes each level to the matching\n * `console` method:\n *\n * - `debug` → `console.debug`\n * - `info` → `console.info`\n * - `warn` → `console.warn`\n * - `error` → `console.error`\n *\n * When a `meta` object is supplied it is rendered with the strategy that\n * actually surfaces it on the current runtime (see {@link MetaStrategy}):\n * depth-unlimited `console.dir` on Node, circular-safe JSON on Cloudflare\n * Workers, and an extra console argument everywhere else.\n *\n * This is the logger used when `debug` is enabled on any activity and no\n * custom `logger` is supplied via `debug: { logger }`.\n */\nexport class ConsoleLogger implements Logger {\n /** Log a debug-level message; forwards to `console.debug`. */\n debug(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', message, meta)\n }\n\n /** Log an info-level message; forwards to `console.info`. */\n info(message: string, meta?: Record<string, unknown>): void {\n this.emit('info', message, meta)\n }\n\n /** Log a warning-level message; forwards to `console.warn`. */\n warn(message: string, meta?: Record<string, unknown>): void {\n this.emit('warn', message, meta)\n }\n\n /** Log an error-level message; forwards to `console.error`. */\n error(message: string, meta?: Record<string, unknown>): void {\n this.emit('error', message, meta)\n }\n\n private emit(\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n meta?: Record<string, unknown>,\n ): void {\n if (meta === undefined) {\n console[level](message)\n return\n }\n switch (resolveMetaStrategy()) {\n case 'dir':\n console[level](message)\n console.dir(meta, DIR_OPTIONS)\n break\n case 'json':\n console[level](`${message}\\n${stringifyMetaSafely(meta)}`)\n break\n case 'arg':\n console[level](message, meta)\n break\n }\n }\n}\n"],"names":[],"mappings":"AAQA,MAAM,cAAc,EAAE,OAAO,MAAM,QAAQ,KAAA;AAiB3C,SAAS,sBAAoC;AAG3C,MAAI;AAEF,QAAI,WAAW,WAAW,cAAc,qBAAsB,QAAO;AAAA,EACvE,QAAQ;AAAA,EAGR;AACA,MACE,OAAO,YAAY;AAAA,EAEnB,OAAO,QAAQ,UAAU,SAAS,UAClC;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AASA,SAAS,oBAAoB,OAAwB;AACnD,QAAM,2BAAW,QAAA;AACjB,MAAI;AACF,WAAO,KAAK;AAAA,MACV;AAAA,MACA,CAAC,MAAM,UAAmB;AACxB,YAAI,OAAO,UAAU,SAAU,QAAO,MAAM,SAAA;AAC5C,YAAI,iBAAiB,OAAO;AAC1B,iBAAO;AAAA,YACL,MAAM,MAAM;AAAA,YACZ,SAAS,MAAM;AAAA,YACf,OAAO,MAAM;AAAA,UAAA;AAAA,QAEjB;AACA,YAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,cAAI,KAAK,IAAI,KAAK,EAAG,QAAO;AAC5B,eAAK,IAAI,KAAK;AAAA,QAChB;AACA,eAAO;AAAA,MACT;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ,QAAQ;AACN,QAAI;AACF,aAAO,OAAO,KAAK;AAAA,IACrB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAmBO,MAAM,cAAgC;AAAA;AAAA,EAE3C,MAAM,SAAiB,MAAsC;AAC3D,SAAK,KAAK,SAAS,SAAS,IAAI;AAAA,EAClC;AAAA;AAAA,EAGA,KAAK,SAAiB,MAAsC;AAC1D,SAAK,KAAK,QAAQ,SAAS,IAAI;AAAA,EACjC;AAAA;AAAA,EAGA,KAAK,SAAiB,MAAsC;AAC1D,SAAK,KAAK,QAAQ,SAAS,IAAI;AAAA,EACjC;AAAA;AAAA,EAGA,MAAM,SAAiB,MAAsC;AAC3D,SAAK,KAAK,SAAS,SAAS,IAAI;AAAA,EAClC;AAAA,EAEQ,KACN,OACA,SACA,MACM;AACN,QAAI,SAAS,QAAW;AACtB,cAAQ,KAAK,EAAE,OAAO;AACtB;AAAA,IACF;AACA,YAAQ,uBAAoB;AAAA,MAC1B,KAAK;AACH,gBAAQ,KAAK,EAAE,OAAO;AACtB,gBAAQ,IAAI,MAAM,WAAW;AAC7B;AAAA,MACF,KAAK;AACH,gBAAQ,KAAK,EAAE,GAAG,OAAO;AAAA,EAAK,oBAAoB,IAAI,CAAC,EAAE;AACzD;AAAA,MACF,KAAK;AACH,gBAAQ,KAAK,EAAE,SAAS,IAAI;AAC5B;AAAA,IAAA;AAAA,EAEN;AACF;"}
|
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
export interface Logger {
|
|
5
5
|
/**
|
|
6
6
|
* Called for chunk-level diagnostic output (raw provider chunks, per-chunk output, agent-loop iteration markers).
|
|
7
|
-
* @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record;
|
|
7
|
+
* @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; the default `ConsoleLogger` renders it with a runtime-appropriate strategy (depth-unlimited `console.dir` on Node, JSON appended to the message on Cloudflare Workers, a second `console.<level>` argument elsewhere).
|
|
8
8
|
*/
|
|
9
9
|
debug: (message: string, meta?: Record<string, unknown>) => void;
|
|
10
10
|
/**
|
|
11
11
|
* Called for notable informational events (outgoing requests, tool invocations, middleware transitions).
|
|
12
|
-
* @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record;
|
|
12
|
+
* @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; the default `ConsoleLogger` renders it with a runtime-appropriate strategy (depth-unlimited `console.dir` on Node, JSON appended to the message on Cloudflare Workers, a second `console.<level>` argument elsewhere).
|
|
13
13
|
*/
|
|
14
14
|
info: (message: string, meta?: Record<string, unknown>) => void;
|
|
15
15
|
/**
|
|
16
16
|
* Called for notable warnings that don't halt execution (deprecations, recoverable anomalies).
|
|
17
|
-
* @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record;
|
|
17
|
+
* @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; the default `ConsoleLogger` renders it with a runtime-appropriate strategy (depth-unlimited `console.dir` on Node, JSON appended to the message on Cloudflare Workers, a second `console.<level>` argument elsewhere).
|
|
18
18
|
*/
|
|
19
19
|
warn: (message: string, meta?: Record<string, unknown>) => void;
|
|
20
20
|
/**
|
|
21
21
|
* Called for caught exceptions throughout the pipeline.
|
|
22
|
-
* @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record;
|
|
22
|
+
* @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; the default `ConsoleLogger` renders it with a runtime-appropriate strategy (depth-unlimited `console.dir` on Node, JSON appended to the message on Cloudflare Workers, a second `console.<level>` argument elsewhere).
|
|
23
23
|
*/
|
|
24
24
|
error: (message: string, meta?: Record<string, unknown>) => void;
|
|
25
25
|
}
|
|
@@ -19,9 +19,7 @@ export type * from './types.js';
|
|
|
19
19
|
* .handler(async () => {
|
|
20
20
|
* return realtimeToken({
|
|
21
21
|
* adapter: openaiRealtimeToken({
|
|
22
|
-
* model: 'gpt-
|
|
23
|
-
* voice: 'alloy',
|
|
24
|
-
* instructions: 'You are a helpful assistant...',
|
|
22
|
+
* model: 'gpt-realtime',
|
|
25
23
|
* }),
|
|
26
24
|
* })
|
|
27
25
|
* })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../src/realtime/index.ts"],"sourcesContent":["import type { RealtimeToken, RealtimeTokenOptions } from './types'\n\n// Re-export all types\nexport type * from './types'\n\n/**\n * Generate a realtime token using the provided adapter.\n *\n * This function is used on the server to generate ephemeral tokens\n * that clients can use to establish realtime connections.\n *\n * @param options - Token generation options including the adapter\n * @returns Promise resolving to a RealtimeToken\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { openaiRealtimeToken } from '@tanstack/ai-openai'\n *\n * // Server function (TanStack Start example)\n * export const getRealtimeToken = createServerFn()\n * .handler(async () => {\n * return realtimeToken({\n * adapter: openaiRealtimeToken({\n * model: 'gpt-
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../src/realtime/index.ts"],"sourcesContent":["import type { RealtimeToken, RealtimeTokenOptions } from './types'\n\n// Re-export all types\nexport type * from './types'\n\n/**\n * Generate a realtime token using the provided adapter.\n *\n * This function is used on the server to generate ephemeral tokens\n * that clients can use to establish realtime connections.\n *\n * @param options - Token generation options including the adapter\n * @returns Promise resolving to a RealtimeToken\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { openaiRealtimeToken } from '@tanstack/ai-openai'\n *\n * // Server function (TanStack Start example)\n * export const getRealtimeToken = createServerFn()\n * .handler(async () => {\n * return realtimeToken({\n * adapter: openaiRealtimeToken({\n * model: 'gpt-realtime',\n * }),\n * })\n * })\n * ```\n */\nexport async function realtimeToken(\n options: RealtimeTokenOptions,\n): Promise<RealtimeToken> {\n const { adapter } = options\n return adapter.generateToken()\n}\n"],"names":[],"mappings":"AA8BA,eAAsB,cACpB,SACwB;AACxB,QAAM,EAAE,YAAY;AACpB,SAAO,QAAQ,cAAA;AACjB;"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { BaseEvent as AGUIBaseEvent, CustomEvent as AGUICustomEvent, MessagesSna
|
|
|
6
6
|
/**
|
|
7
7
|
* Tool call states - track the lifecycle of a tool call
|
|
8
8
|
*/
|
|
9
|
-
export type ToolCallState = 'awaiting-input' | 'input-streaming' | 'input-complete' | 'approval-requested' | 'approval-responded' | 'complete';
|
|
9
|
+
export type ToolCallState = 'awaiting-input' | 'input-streaming' | 'input-complete' | 'approval-requested' | 'approval-responded' | 'complete' | 'error';
|
|
10
10
|
/**
|
|
11
11
|
* Tool result states - track the lifecycle of a tool result
|
|
12
12
|
*/
|
|
@@ -348,6 +348,12 @@ type RuntimeContextField<TContext> = IsUnknown<TContext> extends true ? {
|
|
|
348
348
|
export type ToolExecutionContext<TContext = unknown> = RuntimeContextField<TContext> & {
|
|
349
349
|
/** The ID of the tool call being executed */
|
|
350
350
|
toolCallId?: string;
|
|
351
|
+
/**
|
|
352
|
+
* Abort signal for the current chat run. Aborts when the run's
|
|
353
|
+
* `abortController` fires (or middleware aborts). Long-running tools —
|
|
354
|
+
* e.g. MCP `callTool` — should forward this to cancel in-flight work.
|
|
355
|
+
*/
|
|
356
|
+
abortSignal?: AbortSignal;
|
|
351
357
|
/**
|
|
352
358
|
* Emit a custom event during tool execution.
|
|
353
359
|
* Events are streamed to the client in real-time as AG-UI CUSTOM events.
|
|
@@ -1342,6 +1348,12 @@ export interface VideoUrlResult {
|
|
|
1342
1348
|
url: string;
|
|
1343
1349
|
/** When the URL expires, if applicable */
|
|
1344
1350
|
expiresAt?: Date;
|
|
1351
|
+
/**
|
|
1352
|
+
* Usage information for the completed generation, when the adapter can report
|
|
1353
|
+
* it. For usage-based providers (e.g. fal) this carries `unitsBilled` — the
|
|
1354
|
+
* real billed quantity — so consumers can compute exact cost.
|
|
1355
|
+
*/
|
|
1356
|
+
usage?: TokenUsage;
|
|
1345
1357
|
}
|
|
1346
1358
|
/**
|
|
1347
1359
|
* Options for text-to-speech generation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"@ag-ui/core": "^0.0.52",
|
|
69
69
|
"@standard-schema/spec": "^1.1.0",
|
|
70
70
|
"partial-json": "^0.1.7",
|
|
71
|
-
"@tanstack/ai-event-client": "0.
|
|
71
|
+
"@tanstack/ai-event-client": "0.6.0"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"@opentelemetry/api": ">=1.9.0"
|
|
@@ -27,13 +27,13 @@ import { geminiImage } from '@tanstack/ai-gemini'
|
|
|
27
27
|
| Model | Max Input | Max Output | Notes |
|
|
28
28
|
| ------------------------------- | --------- | ---------- | ---------------------------- |
|
|
29
29
|
| `gemini-3.1-pro-preview` | 1M | 65K | Latest flagship, thinking |
|
|
30
|
-
| `gemini-3-pro-preview` | 1M | 65K | Previous flagship |
|
|
31
30
|
| `gemini-3-flash-preview` | 1M | 65K | Fast, thinking, multimodal |
|
|
31
|
+
| `gemini-3.1-flash-lite` | 1M | 65K | Budget GA, thinking |
|
|
32
32
|
| `gemini-3.1-flash-lite-preview` | 1M | 65K | Budget, still capable |
|
|
33
33
|
| `gemini-2.5-pro` | 1M | 65K | Stable release, all features |
|
|
34
34
|
| `gemini-2.5-flash` | 1M | 65K | Fast stable release |
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Most Gemini text models accept `text`, `image`, `audio`, `video`, and `document` input; `gemini-2.5-flash` accepts all of these except `document`.
|
|
37
37
|
|
|
38
38
|
## Provider-Specific modelOptions
|
|
39
39
|
|
|
@@ -310,6 +310,77 @@ const { messages, sendMessage } = useChat({
|
|
|
310
310
|
The only difference is swapping `toServerSentEventsResponse` / `fetchServerSentEvents`
|
|
311
311
|
for `toHttpResponse` / `fetchHttpStream`. Everything else stays identical.
|
|
312
312
|
|
|
313
|
+
### 5. MCP Tool Discovery via `chat({ mcp })`
|
|
314
|
+
|
|
315
|
+
Pass `mcp` to let `chat()` own discovery **and** lifecycle for one or more MCP
|
|
316
|
+
clients. Useful when you want minimal boilerplate and don't need to reuse the
|
|
317
|
+
clients across calls.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// Prop shape:
|
|
321
|
+
// chat({
|
|
322
|
+
// ...,
|
|
323
|
+
// mcp: {
|
|
324
|
+
// clients: Array<MCPClient | MCPClients>,
|
|
325
|
+
// connection?: 'close' | 'keep-alive', // default: 'close'
|
|
326
|
+
// lazyTools?: boolean,
|
|
327
|
+
// onDiscoveryError?: (error: unknown, source) => void,
|
|
328
|
+
// }
|
|
329
|
+
// })
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
- **`clients`** — one or more `MCPClient` / `MCPClients` instances.
|
|
333
|
+
- **`connection`** — `'close'` (default) closes each client when the run ends
|
|
334
|
+
(after the agent loop completes and the stream is drained); with
|
|
335
|
+
`'keep-alive'`, `chat()` never closes the clients — the caller owns their
|
|
336
|
+
lifecycle (keep connections warm across requests).
|
|
337
|
+
- **`lazyTools`** — forwarded to `tools({ lazy: true })` so tool schemas are
|
|
338
|
+
sent to the LLM on demand.
|
|
339
|
+
- **`onDiscoveryError`** — throw (or re-throw) to fail the entire call fast;
|
|
340
|
+
return normally to skip that source and continue. Omit to rethrow (fail-fast).
|
|
341
|
+
|
|
342
|
+
**When to use `mcp` vs. the tools spread:**
|
|
343
|
+
|
|
344
|
+
| Approach | Use when |
|
|
345
|
+
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
346
|
+
| `chat({ mcp: { clients: [...] } })` | You want discovery + lifecycle managed for you, and don't need fully-typed input/output schemas |
|
|
347
|
+
| `tools: [...await client.tools([toolDefinition(...)])]` | You want fully-typed MCP tools with Zod input/output validation |
|
|
348
|
+
|
|
349
|
+
**Server-side example:**
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
353
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
354
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
355
|
+
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
356
|
+
|
|
357
|
+
export const Route = createFileRoute('/api/chat')({
|
|
358
|
+
server: {
|
|
359
|
+
handlers: {
|
|
360
|
+
POST: async ({ request }) => {
|
|
361
|
+
const { messages } = await request.json()
|
|
362
|
+
|
|
363
|
+
const mcpClient = await createMCPClient({
|
|
364
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const stream = chat({
|
|
368
|
+
adapter: openaiText('gpt-5.5'),
|
|
369
|
+
messages,
|
|
370
|
+
mcp: {
|
|
371
|
+
clients: [mcpClient],
|
|
372
|
+
connection: 'keep-alive', // chat() won't close it — reuse across requests
|
|
373
|
+
},
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
return toServerSentEventsResponse(stream)
|
|
377
|
+
// connection: 'keep-alive' — chat() never closes mcpClient; it stays open for reuse across runs.
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
})
|
|
382
|
+
```
|
|
383
|
+
|
|
313
384
|
## Common Mistakes
|
|
314
385
|
|
|
315
386
|
### a. CRITICAL: Using Vercel AI SDK patterns (streamText, generateText)
|
|
@@ -343,10 +343,38 @@ const { generate, result, jobId, videoStatus, isLoading } = useGenerateVideo({
|
|
|
343
343
|
console.log(`${status.status} (${status.progress}%)`),
|
|
344
344
|
})
|
|
345
345
|
|
|
346
|
-
// videoStatus: { jobId, status, progress?, url?, error? }
|
|
346
|
+
// videoStatus: { jobId, status, progress?, url?, error?, usage? }
|
|
347
347
|
// result (on completion): { url }
|
|
348
348
|
```
|
|
349
349
|
|
|
350
|
+
### 6. Cost tracking (fal billable units)
|
|
351
|
+
|
|
352
|
+
fal bills media generation by usage-based units, not tokens. Every fal media
|
|
353
|
+
adapter (`falImage`, `falAudio`, `falSpeech`, `falTranscription`, `falVideo`)
|
|
354
|
+
surfaces the real billed quantity on the result as `usage.unitsBilled`, read
|
|
355
|
+
from fal's `x-fal-billable-units` response header — no `fetch` interceptor
|
|
356
|
+
needed. It rides on the canonical `TokenUsage` shape (token fields are `0` for
|
|
357
|
+
media), mirroring how duration-billed transcription surfaces `durationSeconds`.
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
import { generateImage } from '@tanstack/ai'
|
|
361
|
+
import { falImage } from '@tanstack/ai-fal'
|
|
362
|
+
|
|
363
|
+
const result = await generateImage({
|
|
364
|
+
adapter: falImage('fal-ai/flux/dev'),
|
|
365
|
+
prompt: 'a serene mountain lake',
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// usage.unitsBilled is the priced quantity. Multiply by the endpoint unit
|
|
369
|
+
// price (GET https://api.fal.ai/v1/models/pricing?endpoint_id=…) for exact cost.
|
|
370
|
+
if (result.usage?.unitsBilled != null) {
|
|
371
|
+
const cost = result.usage.unitsBilled * unitPrice
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
For video, the units arrive with the completed result: `getVideoJobStatus()`
|
|
376
|
+
returns `usage` and emits a `video:usage` devtools event when fal reports it.
|
|
377
|
+
|
|
350
378
|
---
|
|
351
379
|
|
|
352
380
|
## Common Hook API
|
|
@@ -375,6 +375,293 @@ gets the full schema, then calls `compareProducts` directly.
|
|
|
375
375
|
Once discovered, a tool stays available for the conversation.
|
|
376
376
|
When all lazy tools are discovered, the discovery tool is removed automatically.
|
|
377
377
|
|
|
378
|
+
## MCP Tools
|
|
379
|
+
|
|
380
|
+
`@tanstack/ai-mcp` lets a server-side `chat()` call discover and invoke tools
|
|
381
|
+
hosted on any MCP server (Streamable HTTP, SSE, or stdio).
|
|
382
|
+
|
|
383
|
+
### Basic usage — auto-discovery
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// src/routes/api.chat.ts
|
|
387
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
388
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
389
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
390
|
+
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
391
|
+
|
|
392
|
+
export const Route = createFileRoute('/api/chat')({
|
|
393
|
+
server: {
|
|
394
|
+
handlers: {
|
|
395
|
+
POST: async ({ request }) => {
|
|
396
|
+
const { messages } = await request.json()
|
|
397
|
+
|
|
398
|
+
// 1. Connect to the MCP server.
|
|
399
|
+
const mcp = await createMCPClient({
|
|
400
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
// 2. Discover all tools from the server (returns ServerTool[]).
|
|
404
|
+
const mcpTools = await mcp.tools()
|
|
405
|
+
|
|
406
|
+
// 3. Spread them into chat() — they work exactly like hand-written tools.
|
|
407
|
+
// Caller owns the lifecycle — chat() never closes the client. Tools run
|
|
408
|
+
// while the response streams, so close in a middleware terminal hook
|
|
409
|
+
// (a try/finally around the return would close before tools execute).
|
|
410
|
+
const stream = chat({
|
|
411
|
+
adapter: openaiText('gpt-5.5'),
|
|
412
|
+
messages,
|
|
413
|
+
tools: [...mcpTools],
|
|
414
|
+
middleware: [
|
|
415
|
+
{
|
|
416
|
+
name: 'mcp-close',
|
|
417
|
+
onFinish: () => mcp.close(),
|
|
418
|
+
onAbort: () => mcp.close(),
|
|
419
|
+
onError: () => mcp.close(),
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
})
|
|
423
|
+
return toServerSentEventsResponse(stream)
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
})
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Typed path — pass toolDefinition instances
|
|
431
|
+
|
|
432
|
+
Pass bare `toolDefinition()` instances (no `.server()`) to `client.tools([...])`.
|
|
433
|
+
The MCP client supplies a `callTool` proxy as the execute function, while
|
|
434
|
+
input/output validation and types come from the definitions' Zod schemas.
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
438
|
+
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
439
|
+
import { z } from 'zod'
|
|
440
|
+
|
|
441
|
+
const getWeather = toolDefinition({
|
|
442
|
+
name: 'get_weather',
|
|
443
|
+
description: 'Current weather for a city',
|
|
444
|
+
inputSchema: z.object({ city: z.string() }),
|
|
445
|
+
outputSchema: z.object({ temperature: z.number(), conditions: z.string() }),
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
const mcp = await createMCPClient({
|
|
449
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
// Returns ServerTool[] typed to the definitions' input/output schemas.
|
|
453
|
+
// Throws MCPToolNotFoundError if the server does not expose a tool with that name.
|
|
454
|
+
const tools = await mcp.tools([getWeather])
|
|
455
|
+
|
|
456
|
+
const stream = chat({ adapter: openaiText('gpt-5.5'), messages, tools })
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Multiple servers with `createMCPClients`
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { createMCPClients } from '@tanstack/ai-mcp'
|
|
463
|
+
|
|
464
|
+
// Each key becomes the default prefix for that server's tools.
|
|
465
|
+
await using pool = await createMCPClients({
|
|
466
|
+
github: { transport: { type: 'http', url: 'https://mcp.github.com/mcp' } },
|
|
467
|
+
linear: { transport: { type: 'http', url: 'https://mcp.linear.app/mcp' } },
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
// Tools auto-prefixed: 'github_search_repos', 'linear_create_issue', etc.
|
|
471
|
+
const tools = await pool.tools()
|
|
472
|
+
|
|
473
|
+
const stream = chat({ adapter: openaiText('gpt-5.5'), messages, tools })
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Use `pool.clients.<name>` for typed per-server access (resources, prompts, typed
|
|
477
|
+
`tools([defs])` overload).
|
|
478
|
+
|
|
479
|
+
### `ToolExecutionContext.abortSignal` — cancelling long-running tools
|
|
480
|
+
|
|
481
|
+
Every server tool's execute function now receives `abortSignal` in its context.
|
|
482
|
+
When the chat run aborts (e.g. the client disconnects or calls the run's
|
|
483
|
+
`abortController`), the signal fires and any in-flight `callTool` call is
|
|
484
|
+
cancelled automatically.
|
|
485
|
+
|
|
486
|
+
You can also forward it from your own server tools:
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
const longRunningTool = myToolDef.server(async (args, ctx) => {
|
|
490
|
+
// Forward to fetch, a DB query, or an MCP callTool call.
|
|
491
|
+
const response = await fetch('https://slow.api/data', {
|
|
492
|
+
signal: ctx?.abortSignal,
|
|
493
|
+
})
|
|
494
|
+
return response.json()
|
|
495
|
+
})
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
MCP tools wire this automatically — `makeMcpExecute` passes `ctx?.abortSignal`
|
|
499
|
+
as the `signal` option to `client.callTool(...)`, so MCP server calls cancel
|
|
500
|
+
with the chat run without any extra code.
|
|
501
|
+
|
|
502
|
+
### stdio transport (Node-only)
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
506
|
+
import { stdioTransport } from '@tanstack/ai-mcp/stdio'
|
|
507
|
+
|
|
508
|
+
const mcp = await createMCPClient({
|
|
509
|
+
transport: stdioTransport({ command: 'npx', args: ['-y', 'my-mcp-server'] }),
|
|
510
|
+
})
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Import `stdioTransport` from the `/stdio` subpath only — it contains Node.js
|
|
514
|
+
`child_process` imports and must not be bundled for edge runtimes.
|
|
515
|
+
|
|
516
|
+
### `chat({ mcp })` — discovery + lifecycle in one prop
|
|
517
|
+
|
|
518
|
+
Instead of manually calling `client.tools()` and managing `close()`, pass an
|
|
519
|
+
`mcp` object and let `chat()` handle discovery and lifecycle.
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
// Prop shape (ChatMCPOptions):
|
|
523
|
+
// mcp: {
|
|
524
|
+
// clients: Array<MCPClient | MCPClients>,
|
|
525
|
+
// connection?: 'close' | 'keep-alive', // default: 'close'
|
|
526
|
+
// lazyTools?: boolean,
|
|
527
|
+
// onDiscoveryError?: (error: unknown, source) => void,
|
|
528
|
+
// }
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
- At run start, `chat()` calls `.tools()` on every entry in `clients` and merges
|
|
532
|
+
the results — identical to spreading `await client.tools()` into `tools: [...]`.
|
|
533
|
+
- `lazyTools: true` is forwarded to `tools({ lazy: true })`.
|
|
534
|
+
- `onDiscoveryError`: throw to fail-fast; return to skip that source.
|
|
535
|
+
- `connection: 'close'` (default) closes each client when the run ends (after
|
|
536
|
+
the agent loop completes and the stream is drained). With `'keep-alive'`,
|
|
537
|
+
`chat()` never closes the clients — the caller owns their lifecycle (keep
|
|
538
|
+
connections warm across requests).
|
|
539
|
+
|
|
540
|
+
**When to use `mcp` vs. the tools spread:**
|
|
541
|
+
|
|
542
|
+
| Approach | Use when |
|
|
543
|
+
| ------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
544
|
+
| `chat({ mcp: { clients: [...] } })` | Convenience: discovery + lifecycle in one place; untyped tool args are acceptable |
|
|
545
|
+
| `tools: [...await client.tools([toolDefinition(...)])]` | Fully-typed tool args/results via Zod schemas |
|
|
546
|
+
|
|
547
|
+
**Example:**
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
551
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
552
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
553
|
+
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
554
|
+
|
|
555
|
+
export const Route = createFileRoute('/api/chat')({
|
|
556
|
+
server: {
|
|
557
|
+
handlers: {
|
|
558
|
+
POST: async ({ request }) => {
|
|
559
|
+
const { messages } = await request.json()
|
|
560
|
+
|
|
561
|
+
const mcpClient = await createMCPClient({
|
|
562
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
const stream = chat({
|
|
566
|
+
adapter: openaiText('gpt-5.5'),
|
|
567
|
+
messages,
|
|
568
|
+
mcp: {
|
|
569
|
+
clients: [mcpClient],
|
|
570
|
+
connection: 'keep-alive',
|
|
571
|
+
onDiscoveryError: (err, source) => {
|
|
572
|
+
console.warn('MCP discovery failed, skipping source:', err)
|
|
573
|
+
// returning (not throwing) skips this source and continues
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
return toServerSentEventsResponse(stream)
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
})
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
## Provider Skills
|
|
586
|
+
|
|
587
|
+
> **Not to be confused with `@tanstack/ai-code-mode-skills`**, which are locally-generated TypeScript functions executed client-side. Provider Skills are hosted, provider-managed bundles that the model loads on demand and runs inside the provider's server-side sandbox.
|
|
588
|
+
|
|
589
|
+
Provider Skills are inert without an execution tool. The execution tool is what activates the sandbox; skills are additional capability bundles that run inside it:
|
|
590
|
+
|
|
591
|
+
- **Anthropic**: skills require the `code_execution` tool (`@tanstack/ai-anthropic/tools`).
|
|
592
|
+
- **OpenAI**: skills live inside the `shell` tool (`@tanstack/ai-openai/tools`) and are Responses API only.
|
|
593
|
+
|
|
594
|
+
### Anthropic: `codeExecutionTool` with skills
|
|
595
|
+
|
|
596
|
+
Import from `@tanstack/ai-anthropic/tools`:
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
import { codeExecutionTool } from '@tanstack/ai-anthropic/tools'
|
|
600
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
601
|
+
import { anthropicText } from '@tanstack/ai-anthropic'
|
|
602
|
+
|
|
603
|
+
export async function POST(request: Request) {
|
|
604
|
+
const { messages } = await request.json()
|
|
605
|
+
const stream = chat({
|
|
606
|
+
adapter: anthropicText('claude-sonnet-4-5'),
|
|
607
|
+
messages,
|
|
608
|
+
tools: [
|
|
609
|
+
codeExecutionTool(
|
|
610
|
+
{ type: 'code_execution_20250825', name: 'code_execution' },
|
|
611
|
+
{
|
|
612
|
+
skills: [{ type: 'anthropic', skill_id: 'pptx', version: 'latest' }],
|
|
613
|
+
},
|
|
614
|
+
),
|
|
615
|
+
],
|
|
616
|
+
})
|
|
617
|
+
return toServerSentEventsResponse(stream)
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
`AnthropicContainerSkill` shape: `{ type: 'anthropic' | 'custom'; skill_id: string; version?: string }`. Constraints: max 8 skills per request; `skill_id` must be 1–64 characters.
|
|
622
|
+
|
|
623
|
+
The adapter automatically:
|
|
624
|
+
|
|
625
|
+
- Lifts the skills into the request's top-level `container.skills` param (the shape Anthropic's API requires).
|
|
626
|
+
- Attaches the required beta headers (`code-execution-2025-08-25` plus `skills-2025-10-02` when skills are present). You do not set these manually.
|
|
627
|
+
|
|
628
|
+
**Deprecation:** Setting skills via `modelOptions.container.skills` is deprecated. Use `codeExecutionTool(config, { skills })` instead — the legacy path bypasses the beta-header wiring.
|
|
629
|
+
|
|
630
|
+
### OpenAI: `shellTool` with skills (Responses API only)
|
|
631
|
+
|
|
632
|
+
Import from `@tanstack/ai-openai/tools`:
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
import { shellTool } from '@tanstack/ai-openai/tools'
|
|
636
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
637
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
638
|
+
|
|
639
|
+
export async function POST(request: Request) {
|
|
640
|
+
const { messages } = await request.json()
|
|
641
|
+
const stream = chat({
|
|
642
|
+
adapter: openaiText('gpt-5.2'),
|
|
643
|
+
messages,
|
|
644
|
+
tools: [
|
|
645
|
+
shellTool({
|
|
646
|
+
environment: {
|
|
647
|
+
type: 'container_auto',
|
|
648
|
+
skills: [
|
|
649
|
+
{ type: 'skill_reference', skill_id: 'skill_abc', version: '2' },
|
|
650
|
+
],
|
|
651
|
+
},
|
|
652
|
+
}),
|
|
653
|
+
],
|
|
654
|
+
})
|
|
655
|
+
return toServerSentEventsResponse(stream)
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
`SkillReference` shape: `{ type: 'skill_reference'; skill_id: string; version?: string }`. `version` is a string — use a positive integer as a string (e.g. `'2'`) or `'latest'`. This is Responses API only; Chat Completions does not support the shell tool.
|
|
660
|
+
|
|
661
|
+
### Scope
|
|
662
|
+
|
|
663
|
+
Only hosted/managed-by-id skills (`type: 'anthropic'` / `type: 'custom'` for Anthropic; `type: 'skill_reference'` for OpenAI) are wired. Inline bundles, local-path, and upload-API skill creation are not handled by these factories.
|
|
664
|
+
|
|
378
665
|
## Common Mistakes
|
|
379
666
|
|
|
380
667
|
### a. HIGH: Not passing tool definitions to both server and client
|