@tanstack/ai 0.10.0 → 0.10.2
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.js +27 -3
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/package.json +6 -4
- package/skills/ai-core/SKILL.md +59 -0
- package/skills/ai-core/adapter-configuration/SKILL.md +283 -0
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +97 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +102 -0
- package/skills/ai-core/adapter-configuration/references/grok-adapter.md +77 -0
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +106 -0
- package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +82 -0
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +95 -0
- package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +99 -0
- package/skills/ai-core/ag-ui-protocol/SKILL.md +232 -0
- package/skills/ai-core/chat-experience/SKILL.md +506 -0
- package/skills/ai-core/custom-backend-integration/SKILL.md +463 -0
- package/skills/ai-core/media-generation/SKILL.md +471 -0
- package/skills/ai-core/middleware/SKILL.md +336 -0
- package/skills/ai-core/structured-outputs/SKILL.md +203 -0
- package/skills/ai-core/tool-calling/SKILL.md +411 -0
- package/src/activities/chat/index.ts +32 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-core/middleware
|
|
3
|
+
description: >
|
|
4
|
+
Chat lifecycle middleware hooks: onConfig, onStart, onChunk,
|
|
5
|
+
onBeforeToolCall, onAfterToolCall, onUsage, onFinish, onAbort, onError.
|
|
6
|
+
Use for analytics, event firing, tool caching (toolCacheMiddleware),
|
|
7
|
+
logging, and tracing. Middleware array in chat() config, left-to-right
|
|
8
|
+
execution order. NOT onEnd/onFinish callbacks on chat() — use middleware.
|
|
9
|
+
type: sub-skill
|
|
10
|
+
library: tanstack-ai
|
|
11
|
+
library_version: '0.10.0'
|
|
12
|
+
sources:
|
|
13
|
+
- 'TanStack/ai:docs/advanced/middleware.md'
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Middleware
|
|
17
|
+
|
|
18
|
+
> **Dependency note:** This skill builds on ai-core. Read it first for critical rules.
|
|
19
|
+
|
|
20
|
+
## Setup — Analytics Tracking Middleware
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
24
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
25
|
+
|
|
26
|
+
const stream = chat({
|
|
27
|
+
adapter: openaiText('gpt-5.2'),
|
|
28
|
+
messages,
|
|
29
|
+
middleware: [
|
|
30
|
+
{
|
|
31
|
+
onStart: (ctx) => {
|
|
32
|
+
console.log('Chat started:', ctx.model)
|
|
33
|
+
},
|
|
34
|
+
onFinish: (ctx) => {
|
|
35
|
+
trackAnalytics({ model: ctx.model, tokens: ctx.usage })
|
|
36
|
+
},
|
|
37
|
+
onError: (ctx) => {
|
|
38
|
+
reportError(ctx.error)
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return toServerSentEventsResponse(stream)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Hooks Reference
|
|
48
|
+
|
|
49
|
+
Every hook receives a `ChatMiddlewareContext` as its first argument, which provides
|
|
50
|
+
`requestId`, `streamId`, `phase`, `iteration`, `chunkIndex`, `model`, `provider`,
|
|
51
|
+
`signal`, `abort()`, `defer()`, and more.
|
|
52
|
+
|
|
53
|
+
| Hook | When | Second Argument |
|
|
54
|
+
| --------------------- | ------------------------------------------------------------- | ------------------------------------------------ |
|
|
55
|
+
| `onConfig` | Once at startup (`init`) + once per iteration (`beforeModel`) | `ChatMiddlewareConfig` (return partial to merge) |
|
|
56
|
+
| `onStart` | Once after initial `onConfig` | none |
|
|
57
|
+
| `onIteration` | Start of each agent loop iteration | `IterationInfo` |
|
|
58
|
+
| `onChunk` | Every streamed chunk | `StreamChunk` (return void/chunk/chunk[]/null) |
|
|
59
|
+
| `onBeforeToolCall` | Before each tool executes | `ToolCallHookContext` (return decision or void) |
|
|
60
|
+
| `onAfterToolCall` | After each tool executes | `AfterToolCallInfo` |
|
|
61
|
+
| `onToolPhaseComplete` | After all tool calls in an iteration | `ToolPhaseCompleteInfo` |
|
|
62
|
+
| `onUsage` | When `RUN_FINISHED` includes usage data | `UsageInfo` |
|
|
63
|
+
| `onFinish` | Run completed normally | `FinishInfo` |
|
|
64
|
+
| `onAbort` | Run was aborted | `AbortInfo` |
|
|
65
|
+
| `onError` | Unhandled error occurred | `ErrorInfo` |
|
|
66
|
+
|
|
67
|
+
Terminal hooks (`onFinish`, `onAbort`, `onError`) are **mutually exclusive** -- exactly
|
|
68
|
+
one fires per `chat()` invocation.
|
|
69
|
+
|
|
70
|
+
## Core Patterns
|
|
71
|
+
|
|
72
|
+
### Pattern 1: Analytics and Logging Middleware
|
|
73
|
+
|
|
74
|
+
Use `onStart`, `onFinish`, `onUsage`, and `onError` for comprehensive observability.
|
|
75
|
+
Use `ctx.defer()` for non-blocking async side effects that should not block the stream.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import {
|
|
79
|
+
chat,
|
|
80
|
+
toServerSentEventsResponse,
|
|
81
|
+
type ChatMiddleware,
|
|
82
|
+
} from '@tanstack/ai'
|
|
83
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
84
|
+
|
|
85
|
+
const analytics: ChatMiddleware = {
|
|
86
|
+
name: 'analytics',
|
|
87
|
+
onStart: (ctx) => {
|
|
88
|
+
console.log(`[${ctx.requestId}] Chat started — model: ${ctx.model}`)
|
|
89
|
+
},
|
|
90
|
+
onUsage: (ctx, usage) => {
|
|
91
|
+
console.log(`[${ctx.requestId}] Tokens: ${usage.totalTokens}`)
|
|
92
|
+
},
|
|
93
|
+
onFinish: (ctx, info) => {
|
|
94
|
+
ctx.defer(
|
|
95
|
+
fetch('/api/analytics', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
requestId: ctx.requestId,
|
|
99
|
+
model: ctx.model,
|
|
100
|
+
duration: info.duration,
|
|
101
|
+
tokens: info.usage?.totalTokens,
|
|
102
|
+
finishReason: info.finishReason,
|
|
103
|
+
}),
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
},
|
|
107
|
+
onError: (ctx, info) => {
|
|
108
|
+
ctx.defer(
|
|
109
|
+
fetch('/api/errors', {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
requestId: ctx.requestId,
|
|
113
|
+
error: String(info.error),
|
|
114
|
+
duration: info.duration,
|
|
115
|
+
}),
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const stream = chat({
|
|
122
|
+
adapter: openaiText('gpt-5.2'),
|
|
123
|
+
messages,
|
|
124
|
+
middleware: [analytics],
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return toServerSentEventsResponse(stream)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Pattern 2: Tool Interception Middleware
|
|
131
|
+
|
|
132
|
+
Use `onBeforeToolCall` to validate, gate, or transform tool arguments before execution.
|
|
133
|
+
Use `onAfterToolCall` to log results and timing. The first middleware that returns a
|
|
134
|
+
non-void decision from `onBeforeToolCall` short-circuits remaining middleware for that call.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import type { ChatMiddleware } from '@tanstack/ai'
|
|
138
|
+
|
|
139
|
+
const toolGuard: ChatMiddleware = {
|
|
140
|
+
name: 'tool-guard',
|
|
141
|
+
onBeforeToolCall: (ctx, hookCtx) => {
|
|
142
|
+
// Block dangerous tools
|
|
143
|
+
if (hookCtx.toolName === 'deleteDatabase') {
|
|
144
|
+
return { type: 'abort', reason: 'Dangerous operation blocked' }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Enforce default arguments
|
|
148
|
+
if (hookCtx.toolName === 'search' && !hookCtx.args.limit) {
|
|
149
|
+
return {
|
|
150
|
+
type: 'transformArgs',
|
|
151
|
+
args: { ...hookCtx.args, limit: 10 },
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Return void to continue normally
|
|
156
|
+
},
|
|
157
|
+
onAfterToolCall: (ctx, info) => {
|
|
158
|
+
if (info.ok) {
|
|
159
|
+
console.log(`${info.toolName} completed in ${info.duration}ms`)
|
|
160
|
+
} else {
|
|
161
|
+
console.error(`${info.toolName} failed:`, info.error)
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**`onBeforeToolCall` decision types:**
|
|
168
|
+
|
|
169
|
+
| Decision | Effect |
|
|
170
|
+
| --------------------------------- | ------------------------------------------------------------------- |
|
|
171
|
+
| `void` / `undefined` | Continue normally, next middleware decides |
|
|
172
|
+
| `{ type: 'transformArgs', args }` | Replace tool arguments before execution |
|
|
173
|
+
| `{ type: 'skip', result }` | Skip execution, use provided result (used by `toolCacheMiddleware`) |
|
|
174
|
+
| `{ type: 'abort', reason? }` | Abort the entire chat run |
|
|
175
|
+
|
|
176
|
+
### Pattern 3: Multiple Middleware Composition
|
|
177
|
+
|
|
178
|
+
Middleware executes in array order (left-to-right). Ordering matters for hooks that
|
|
179
|
+
pipe or short-circuit:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { chat, type ChatMiddleware } from '@tanstack/ai'
|
|
183
|
+
import { toolCacheMiddleware } from '@tanstack/ai/middlewares'
|
|
184
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
185
|
+
|
|
186
|
+
const logging: ChatMiddleware = {
|
|
187
|
+
name: 'logging',
|
|
188
|
+
onStart: (ctx) => console.log(`[${ctx.requestId}] started`),
|
|
189
|
+
onChunk: (ctx, chunk) => {
|
|
190
|
+
console.log(`[${ctx.requestId}] chunk: ${chunk.type}`)
|
|
191
|
+
},
|
|
192
|
+
onFinish: (ctx, info) => {
|
|
193
|
+
console.log(`[${ctx.requestId}] done in ${info.duration}ms`)
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const configTransform: ChatMiddleware = {
|
|
198
|
+
name: 'config-transform',
|
|
199
|
+
onConfig: (ctx, config) => {
|
|
200
|
+
if (ctx.phase === 'init') {
|
|
201
|
+
return {
|
|
202
|
+
systemPrompts: [...config.systemPrompts, 'Always respond in JSON.'],
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const stream = chat({
|
|
209
|
+
adapter: openaiText('gpt-5.2'),
|
|
210
|
+
messages,
|
|
211
|
+
tools: [weatherTool, stockTool],
|
|
212
|
+
middleware: [
|
|
213
|
+
logging, // Runs first
|
|
214
|
+
configTransform, // Transforms config second
|
|
215
|
+
toolCacheMiddleware({ ttl: 60_000 }), // Caches tool results third
|
|
216
|
+
],
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Composition rules by hook:**
|
|
221
|
+
|
|
222
|
+
| Hook | Composition | Effect of Order |
|
|
223
|
+
| -------------------------- | --------------------------------------------- | ------------------------------------------ |
|
|
224
|
+
| `onConfig` | **Piped** -- each receives previous output | Earlier middleware transforms first |
|
|
225
|
+
| `onStart` | Sequential | All run in order |
|
|
226
|
+
| `onChunk` | **Piped** -- chunks flow through each | If first drops a chunk, later never see it |
|
|
227
|
+
| `onBeforeToolCall` | **First-win** -- first non-void decision wins | Earlier middleware has priority |
|
|
228
|
+
| `onAfterToolCall` | Sequential | All run in order |
|
|
229
|
+
| `onUsage` | Sequential | All run in order |
|
|
230
|
+
| `onFinish/onAbort/onError` | Sequential | All run in order |
|
|
231
|
+
|
|
232
|
+
## Built-in: toolCacheMiddleware
|
|
233
|
+
|
|
234
|
+
Caches tool call results by name + arguments. Import from `@tanstack/ai/middlewares`:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { chat } from '@tanstack/ai'
|
|
238
|
+
import { toolCacheMiddleware } from '@tanstack/ai/middlewares'
|
|
239
|
+
|
|
240
|
+
const stream = chat({
|
|
241
|
+
adapter,
|
|
242
|
+
messages,
|
|
243
|
+
tools: [weatherTool],
|
|
244
|
+
middleware: [
|
|
245
|
+
toolCacheMiddleware({
|
|
246
|
+
ttl: 60_000, // Cache entries expire after 60 seconds
|
|
247
|
+
maxSize: 50, // Max 50 entries (LRU eviction)
|
|
248
|
+
toolNames: ['getWeather'], // Only cache specific tools
|
|
249
|
+
}),
|
|
250
|
+
],
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Options: `maxSize` (default 100), `ttl` (default Infinity), `toolNames` (default all),
|
|
255
|
+
`keyFn` (custom cache key), `storage` (custom backend like Redis). See
|
|
256
|
+
`docs/advanced/middleware.md` for custom storage examples.
|
|
257
|
+
|
|
258
|
+
## Common Mistakes
|
|
259
|
+
|
|
260
|
+
### a. MEDIUM: Trying to modify StreamChunks in middleware
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// WRONG -- mutating the chunk object directly
|
|
264
|
+
const broken: ChatMiddleware = {
|
|
265
|
+
name: 'broken',
|
|
266
|
+
onChunk: (ctx, chunk) => {
|
|
267
|
+
chunk.delta = 'modified' // Mutation does nothing; chunk is not modified in-place
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// CORRECT -- return a new chunk to replace the original
|
|
272
|
+
const correct: ChatMiddleware = {
|
|
273
|
+
name: 'correct',
|
|
274
|
+
onChunk: (ctx, chunk) => {
|
|
275
|
+
if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
|
|
276
|
+
return { ...chunk, delta: chunk.delta.replace(/secret/g, '[REDACTED]') }
|
|
277
|
+
}
|
|
278
|
+
// Return void to pass through unchanged
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Middleware `onChunk` hooks are functional transforms. Return a new chunk, an array
|
|
284
|
+
of chunks, null (to drop), or void (to pass through). Mutating the input object
|
|
285
|
+
has no effect on the stream output.
|
|
286
|
+
|
|
287
|
+
Source: docs/advanced/middleware.md
|
|
288
|
+
|
|
289
|
+
### b. MEDIUM: Middleware exceptions breaking the stream
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// WRONG -- unhandled error kills the entire streaming response
|
|
293
|
+
const fragile: ChatMiddleware = {
|
|
294
|
+
name: 'fragile-analytics',
|
|
295
|
+
onFinish: async (ctx, info) => {
|
|
296
|
+
// If this fetch fails, the stream breaks
|
|
297
|
+
await fetch('/api/analytics', {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
body: JSON.stringify({ duration: info.duration }),
|
|
300
|
+
})
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// CORRECT -- wrap in try-catch and/or use ctx.defer()
|
|
305
|
+
const resilient: ChatMiddleware = {
|
|
306
|
+
name: 'resilient-analytics',
|
|
307
|
+
onFinish: (ctx, info) => {
|
|
308
|
+
// Option 1: defer (non-blocking, errors are isolated)
|
|
309
|
+
ctx.defer(
|
|
310
|
+
fetch('/api/analytics', {
|
|
311
|
+
method: 'POST',
|
|
312
|
+
body: JSON.stringify({ duration: info.duration }),
|
|
313
|
+
}),
|
|
314
|
+
)
|
|
315
|
+
},
|
|
316
|
+
onChunk: (ctx, chunk) => {
|
|
317
|
+
// Option 2: try-catch for synchronous/critical hooks
|
|
318
|
+
try {
|
|
319
|
+
logChunk(chunk)
|
|
320
|
+
} catch (err) {
|
|
321
|
+
console.error('Logging failed:', err)
|
|
322
|
+
}
|
|
323
|
+
// Return void to pass through
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Wrap all middleware hooks in try-catch to prevent analytics or logging failures
|
|
329
|
+
from killing the chat stream. For async side effects, prefer `ctx.defer()` which
|
|
330
|
+
runs after the terminal hook and isolates failures.
|
|
331
|
+
|
|
332
|
+
Source: docs/advanced/middleware.md
|
|
333
|
+
|
|
334
|
+
## Cross-References
|
|
335
|
+
|
|
336
|
+
- See also: **ai-core/chat-experience/SKILL.md** -- Middleware hooks into the chat lifecycle
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-core/structured-outputs
|
|
3
|
+
description: >
|
|
4
|
+
Type-safe JSON schema responses from LLMs using outputSchema on chat().
|
|
5
|
+
Supports Zod, ArkType, and Valibot schemas. The adapter handles
|
|
6
|
+
provider-specific strategies transparently — never configure structured
|
|
7
|
+
output at the provider level. convertSchemaToJsonSchema() for manual
|
|
8
|
+
schema conversion.
|
|
9
|
+
type: sub-skill
|
|
10
|
+
library: tanstack-ai
|
|
11
|
+
library_version: '0.10.0'
|
|
12
|
+
sources:
|
|
13
|
+
- 'TanStack/ai:docs/chat/structured-outputs.md'
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Structured Outputs
|
|
17
|
+
|
|
18
|
+
> **Dependency note:** This skill builds on ai-core. Read it first for critical rules.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { chat } from '@tanstack/ai'
|
|
24
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
25
|
+
import { z } from 'zod'
|
|
26
|
+
|
|
27
|
+
const stream = chat({
|
|
28
|
+
adapter: openaiText('gpt-5.2'),
|
|
29
|
+
messages: [
|
|
30
|
+
{
|
|
31
|
+
role: 'user',
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: 'text',
|
|
35
|
+
content: 'Extract the person info from: John is 30 years old',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
outputSchema: z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
age: z.number(),
|
|
43
|
+
}),
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
When `outputSchema` is provided, `chat()` returns `Promise<InferSchemaType<TSchema>>` instead of `AsyncIterable<StreamChunk>`. The result is fully typed based on the schema.
|
|
48
|
+
|
|
49
|
+
## Core Patterns
|
|
50
|
+
|
|
51
|
+
### Pattern 1: Basic structured output with Zod
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { chat } from '@tanstack/ai'
|
|
55
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
56
|
+
import { z } from 'zod'
|
|
57
|
+
|
|
58
|
+
const PersonSchema = z.object({
|
|
59
|
+
name: z.string().meta({ description: "The person's full name" }),
|
|
60
|
+
age: z.number().meta({ description: "The person's age in years" }),
|
|
61
|
+
email: z.string().email().meta({ description: 'Email address' }),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// chat() returns Promise<{ name: string; age: number; email: string }>
|
|
65
|
+
const person = await chat({
|
|
66
|
+
adapter: openaiText('gpt-5.2'),
|
|
67
|
+
messages: [
|
|
68
|
+
{
|
|
69
|
+
role: 'user',
|
|
70
|
+
content:
|
|
71
|
+
'Extract the person info: John Doe is 30 years old, email john@example.com',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
outputSchema: PersonSchema,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
console.log(person.name) // "John Doe"
|
|
78
|
+
console.log(person.age) // 30
|
|
79
|
+
console.log(person.email) // "john@example.com"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Pattern 2: Complex nested schemas
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { chat } from '@tanstack/ai'
|
|
86
|
+
import { anthropicText } from '@tanstack/ai-anthropic'
|
|
87
|
+
import { z } from 'zod'
|
|
88
|
+
|
|
89
|
+
const CompanySchema = z.object({
|
|
90
|
+
name: z.string(),
|
|
91
|
+
founded: z.number().meta({ description: 'Year the company was founded' }),
|
|
92
|
+
headquarters: z.object({
|
|
93
|
+
city: z.string(),
|
|
94
|
+
country: z.string(),
|
|
95
|
+
address: z.string().optional(),
|
|
96
|
+
}),
|
|
97
|
+
employees: z.array(
|
|
98
|
+
z.object({
|
|
99
|
+
name: z.string(),
|
|
100
|
+
role: z.string(),
|
|
101
|
+
department: z.string(),
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
financials: z
|
|
105
|
+
.object({
|
|
106
|
+
revenue: z
|
|
107
|
+
.number()
|
|
108
|
+
.meta({ description: 'Annual revenue in millions USD' }),
|
|
109
|
+
profitable: z.boolean(),
|
|
110
|
+
})
|
|
111
|
+
.optional(),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const company = await chat({
|
|
115
|
+
adapter: anthropicText('claude-sonnet-4-5'),
|
|
116
|
+
messages: [
|
|
117
|
+
{
|
|
118
|
+
role: 'user',
|
|
119
|
+
content: 'Extract company info from this article: ...',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
outputSchema: CompanySchema,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Full type safety on nested properties
|
|
126
|
+
console.log(company.headquarters.city)
|
|
127
|
+
console.log(company.employees[0].role)
|
|
128
|
+
console.log(company.financials?.revenue)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Common Mistakes
|
|
132
|
+
|
|
133
|
+
### HIGH: Trying to implement provider-specific structured output strategies
|
|
134
|
+
|
|
135
|
+
The adapter already handles provider differences (OpenAI uses `response_format`, Anthropic uses tool-based extraction, Gemini uses `responseSchema`). Never configure this yourself.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// WRONG -- do not set provider-specific response format
|
|
139
|
+
chat({
|
|
140
|
+
adapter,
|
|
141
|
+
messages,
|
|
142
|
+
modelOptions: {
|
|
143
|
+
responseFormat: { type: 'json_schema', json_schema: mySchema },
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// CORRECT -- just pass outputSchema, the adapter handles the rest
|
|
148
|
+
chat({
|
|
149
|
+
adapter,
|
|
150
|
+
messages,
|
|
151
|
+
outputSchema: z.object({ name: z.string(), age: z.number() }),
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
There is no scenario where you need to know the provider's strategy. Just pass `outputSchema` to `chat()`.
|
|
156
|
+
|
|
157
|
+
Source: maintainer interview
|
|
158
|
+
|
|
159
|
+
### HIGH: Passing raw objects instead of using the project's schema library
|
|
160
|
+
|
|
161
|
+
Agents often generate raw JSON Schema objects or plain TypeScript types instead
|
|
162
|
+
of using the schema validation library already in the project (Zod, ArkType,
|
|
163
|
+
Valibot). Always check what the project uses and match it.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// WRONG -- raw object, no runtime validation, no type inference
|
|
167
|
+
chat({
|
|
168
|
+
adapter,
|
|
169
|
+
messages,
|
|
170
|
+
outputSchema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
name: { type: 'string' },
|
|
174
|
+
age: { type: 'number' },
|
|
175
|
+
},
|
|
176
|
+
required: ['name', 'age'],
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// CORRECT -- use the project's schema library (e.g. Zod)
|
|
182
|
+
import { z } from 'zod'
|
|
183
|
+
|
|
184
|
+
chat({
|
|
185
|
+
adapter,
|
|
186
|
+
messages,
|
|
187
|
+
outputSchema: z.object({
|
|
188
|
+
name: z.string(),
|
|
189
|
+
age: z.number(),
|
|
190
|
+
}),
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Using the project's schema library gives you runtime validation, TypeScript
|
|
195
|
+
type inference on the result, and correct JSON Schema conversion automatically.
|
|
196
|
+
Check `package.json` for `zod`, `arktype`, or `valibot` and use whichever is
|
|
197
|
+
already installed.
|
|
198
|
+
|
|
199
|
+
Source: maintainer interview
|
|
200
|
+
|
|
201
|
+
## Cross-References
|
|
202
|
+
|
|
203
|
+
- See also: ai-core/adapter-configuration/SKILL.md -- Adapter handles structured output strategy transparently
|