@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.
Files changed (51) hide show
  1. package/dist/esm/activities/chat/index.d.ts +7 -0
  2. package/dist/esm/activities/chat/index.js +86 -20
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/mcp/manager.d.ts +25 -0
  5. package/dist/esm/activities/chat/mcp/manager.js +71 -0
  6. package/dist/esm/activities/chat/mcp/manager.js.map +1 -0
  7. package/dist/esm/activities/chat/mcp/types.d.ts +56 -0
  8. package/dist/esm/activities/chat/messages.js +1 -1
  9. package/dist/esm/activities/chat/messages.js.map +1 -1
  10. package/dist/esm/activities/chat/stream/message-updaters.js +20 -8
  11. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  12. package/dist/esm/activities/chat/stream/processor.d.ts +6 -0
  13. package/dist/esm/activities/chat/stream/processor.js +18 -3
  14. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  15. package/dist/esm/activities/chat/tools/tool-calls.d.ts +1 -1
  16. package/dist/esm/activities/chat/tools/tool-calls.js +2 -1
  17. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  18. package/dist/esm/activities/generateVideo/index.d.ts +2 -1
  19. package/dist/esm/activities/generateVideo/index.js +12 -2
  20. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  21. package/dist/esm/extend-adapter.d.ts +22 -6
  22. package/dist/esm/extend-adapter.js.map +1 -1
  23. package/dist/esm/index.d.ts +2 -0
  24. package/dist/esm/index.js +2 -0
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/esm/logger/console-logger.d.ts +18 -0
  27. package/dist/esm/logger/console-logger.js +64 -8
  28. package/dist/esm/logger/console-logger.js.map +1 -1
  29. package/dist/esm/logger/types.d.ts +4 -4
  30. package/dist/esm/realtime/index.d.ts +1 -3
  31. package/dist/esm/realtime/index.js.map +1 -1
  32. package/dist/esm/types.d.ts +13 -1
  33. package/package.json +2 -2
  34. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -2
  35. package/skills/ai-core/chat-experience/SKILL.md +71 -0
  36. package/skills/ai-core/media-generation/SKILL.md +29 -1
  37. package/skills/ai-core/tool-calling/SKILL.md +287 -0
  38. package/src/activities/chat/index.ts +109 -25
  39. package/src/activities/chat/mcp/manager.ts +85 -0
  40. package/src/activities/chat/mcp/types.ts +66 -0
  41. package/src/activities/chat/messages.ts +1 -0
  42. package/src/activities/chat/stream/message-updaters.ts +22 -9
  43. package/src/activities/chat/stream/processor.ts +30 -3
  44. package/src/activities/chat/tools/tool-calls.ts +2 -0
  45. package/src/activities/generateVideo/index.ts +12 -0
  46. package/src/extend-adapter.ts +42 -24
  47. package/src/index.ts +10 -0
  48. package/src/logger/console-logger.ts +112 -17
  49. package/src/logger/types.ts +4 -4
  50. package/src/realtime/index.ts +1 -3
  51. 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 * 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, the message is logged first and the meta\n * object is then printed via `console.dir(meta, { depth: null, colors: true })`\n * so deeply nested structures (e.g. provider chunk payloads with `usage`,\n * `output`, `reasoning`, `tools`) render in full instead of truncating to\n * `[Object]` / `[Array]`. On Node this produces a depth-unlimited inspect\n * dump; browsers present the object as an interactive tree (extra options\n * are ignored).\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 */\nconst DIR_OPTIONS = { depth: null, colors: true } as const\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 console.debug(message)\n if (meta !== undefined) console.dir(meta, DIR_OPTIONS)\n }\n\n /** Log an info-level message; forwards to `console.info`. */\n info(message: string, meta?: Record<string, unknown>): void {\n console.info(message)\n if (meta !== undefined) console.dir(meta, DIR_OPTIONS)\n }\n\n /** Log a warning-level message; forwards to `console.warn`. */\n warn(message: string, meta?: Record<string, unknown>): void {\n console.warn(message)\n if (meta !== undefined) console.dir(meta, DIR_OPTIONS)\n }\n\n /** Log an error-level message; forwards to `console.error`. */\n error(message: string, meta?: Record<string, unknown>): void {\n console.error(message)\n if (meta !== undefined) console.dir(meta, DIR_OPTIONS)\n }\n}\n"],"names":[],"mappings":"AAsBA,MAAM,cAAc,EAAE,OAAO,MAAM,QAAQ,KAAA;AAEpC,MAAM,cAAgC;AAAA;AAAA,EAE3C,MAAM,SAAiB,MAAsC;AAC3D,YAAQ,MAAM,OAAO;AACrB,QAAI,SAAS,OAAW,SAAQ,IAAI,MAAM,WAAW;AAAA,EACvD;AAAA;AAAA,EAGA,KAAK,SAAiB,MAAsC;AAC1D,YAAQ,KAAK,OAAO;AACpB,QAAI,SAAS,OAAW,SAAQ,IAAI,MAAM,WAAW;AAAA,EACvD;AAAA;AAAA,EAGA,KAAK,SAAiB,MAAsC;AAC1D,YAAQ,KAAK,OAAO;AACpB,QAAI,SAAS,OAAW,SAAQ,IAAI,MAAM,WAAW;AAAA,EACvD;AAAA;AAAA,EAGA,MAAM,SAAiB,MAAsC;AAC3D,YAAQ,MAAM,OAAO;AACrB,QAAI,SAAS,OAAW,SAAQ,IAAI,MAAM,WAAW;AAAA,EACvD;AACF;"}
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; console-based loggers pass it as the second argument to `console.<level>`.
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; console-based loggers pass it as the second argument to `console.<level>`.
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; console-based loggers pass it as the second argument to `console.<level>`.
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; console-based loggers pass it as the second argument to `console.<level>`.
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-4o-realtime-preview',
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-4o-realtime-preview',\n * voice: 'alloy',\n * instructions: 'You are a helpful assistant...',\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":"AAgCA,eAAsB,cACpB,SACwB;AACxB,QAAM,EAAE,YAAY;AACpB,SAAO,QAAQ,cAAA;AACjB;"}
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;"}
@@ -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.27.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.5.3"
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
- All Gemini text models accept `text`, `image`, `audio`, `video`, and `document` input.
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