@tanstack/ai 0.53.0 → 0.55.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/README.md +14 -13
- package/dist/esm/activities/chat/index.js +22 -4
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +21 -1
- package/dist/esm/activities/chat/messages.js +50 -1
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +17 -0
- package/dist/esm/activities/chat/stream/processor.js +27 -0
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/generateLiveVideo/adapter.d.ts +69 -0
- package/dist/esm/activities/generateLiveVideo/adapter.js +23 -0
- package/dist/esm/activities/generateLiveVideo/adapter.js.map +1 -0
- package/dist/esm/activities/generateLiveVideo/index.d.ts +99 -0
- package/dist/esm/activities/generateLiveVideo/index.js +162 -0
- package/dist/esm/activities/generateLiveVideo/index.js.map +1 -0
- package/dist/esm/activities/generateVideo/index.js +3 -1
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateWorld/adapter.d.ts +69 -0
- package/dist/esm/activities/generateWorld/adapter.js +23 -0
- package/dist/esm/activities/generateWorld/adapter.js.map +1 -0
- package/dist/esm/activities/generateWorld/index.d.ts +99 -0
- package/dist/esm/activities/generateWorld/index.js +162 -0
- package/dist/esm/activities/generateWorld/index.js.map +1 -0
- package/dist/esm/activities/index.d.ts +8 -2
- package/dist/esm/activities/index.js +11 -7
- package/dist/esm/activities/middleware/types.d.ts +1 -1
- package/dist/esm/client.d.ts +4 -2
- package/dist/esm/client.js +3 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +3 -1
- package/dist/esm/middlewares/otel.js +3 -1
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +112 -0
- package/package.json +3 -3
- package/skills/ai-core/adapter-configuration/SKILL.md +91 -43
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +39 -21
- package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +5 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +14 -6
- package/skills/ai-core/adapter-configuration/references/grok-adapter.md +33 -25
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +7 -2
- package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +25 -12
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +19 -9
- package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +34 -21
- package/skills/ai-core/ag-ui-protocol/SKILL.md +16 -10
- package/skills/ai-core/chat-experience/SKILL.md +228 -108
- package/skills/ai-core/client-persistence/SKILL.md +21 -9
- package/skills/ai-core/custom-backend-integration/SKILL.md +86 -52
- package/skills/ai-core/debug-logging/SKILL.md +100 -18
- package/skills/ai-core/locks/SKILL.md +35 -7
- package/skills/ai-core/media-generation/SKILL.md +136 -61
- package/skills/ai-core/middleware/SKILL.md +174 -69
- package/skills/ai-core/structured-outputs/SKILL.md +98 -49
- package/skills/ai-core/tool-calling/SKILL.md +245 -158
- package/src/activities/chat/index.ts +29 -7
- package/src/activities/chat/messages.ts +60 -0
- package/src/activities/chat/stream/processor.ts +31 -0
- package/src/activities/generateLiveVideo/adapter.ts +99 -0
- package/src/activities/generateLiveVideo/index.ts +339 -0
- package/src/activities/generateVideo/index.ts +3 -4
- package/src/activities/generateWorld/adapter.ts +96 -0
- package/src/activities/generateWorld/index.ts +339 -0
- package/src/activities/index.ts +44 -0
- package/src/activities/middleware/types.ts +2 -0
- package/src/client.ts +8 -0
- package/src/index.ts +8 -0
- package/src/middlewares/otel.ts +2 -0
- package/src/types.ts +128 -0
|
@@ -28,7 +28,7 @@ This skill builds on ai-core. Read it first for critical rules.
|
|
|
28
28
|
|
|
29
29
|
### Server: API Route (TanStack Start)
|
|
30
30
|
|
|
31
|
-
```typescript
|
|
31
|
+
```typescript ignore
|
|
32
32
|
// src/routes/api.chat.ts
|
|
33
33
|
import { createFileRoute } from '@tanstack/react-router'
|
|
34
34
|
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
@@ -58,7 +58,7 @@ export const Route = createFileRoute('/api/chat')({
|
|
|
58
58
|
|
|
59
59
|
### Client: React Component
|
|
60
60
|
|
|
61
|
-
```
|
|
61
|
+
```tsx
|
|
62
62
|
// src/routes/index.tsx
|
|
63
63
|
import { useState } from 'react'
|
|
64
64
|
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
@@ -136,18 +136,23 @@ Server returns a streaming SSE Response; client parses it automatically.
|
|
|
136
136
|
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
137
137
|
import { anthropicText } from '@tanstack/ai-anthropic'
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
modelOptions: {
|
|
143
|
-
temperature: 0.7,
|
|
144
|
-
max_tokens: 2000, // Anthropic-native key
|
|
145
|
-
},
|
|
146
|
-
systemPrompts: ['You are a helpful assistant.'],
|
|
147
|
-
abortController,
|
|
148
|
-
})
|
|
139
|
+
export async function POST(request: Request) {
|
|
140
|
+
const { messages } = await request.json()
|
|
141
|
+
const abortController = new AbortController()
|
|
149
142
|
|
|
150
|
-
|
|
143
|
+
const stream = chat({
|
|
144
|
+
adapter: anthropicText('claude-opus-5'),
|
|
145
|
+
messages,
|
|
146
|
+
modelOptions: {
|
|
147
|
+
temperature: 0.7,
|
|
148
|
+
max_tokens: 2000, // Anthropic-native key
|
|
149
|
+
},
|
|
150
|
+
systemPrompts: ['You are a helpful assistant.'],
|
|
151
|
+
abortController,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return toServerSentEventsResponse(stream, { abortController })
|
|
155
|
+
}
|
|
151
156
|
```
|
|
152
157
|
|
|
153
158
|
To make the SSE response resumable (reconnect after a drop/refresh without
|
|
@@ -167,7 +172,7 @@ import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
|
167
172
|
|
|
168
173
|
const { messages, sendMessage, isLoading, error, stop, status } = useChat({
|
|
169
174
|
connection: fetchServerSentEvents('/api/chat'),
|
|
170
|
-
body: { provider: 'anthropic', model: 'claude-
|
|
175
|
+
body: { provider: 'anthropic', model: 'claude-opus-5' },
|
|
171
176
|
onFinish: (message) => {
|
|
172
177
|
console.log('Response complete:', message.id)
|
|
173
178
|
},
|
|
@@ -186,7 +191,7 @@ The `status` field tracks the chat lifecycle: `'ready'` | `'submitted'` | `'stre
|
|
|
186
191
|
|
|
187
192
|
Models with extended thinking (Claude, Gemini) emit `ThinkingPart` in the message parts array.
|
|
188
193
|
|
|
189
|
-
```
|
|
194
|
+
```tsx
|
|
190
195
|
import type { UIMessage } from '@tanstack/ai-react'
|
|
191
196
|
|
|
192
197
|
function MessageRenderer({ message }: { message: UIMessage }) {
|
|
@@ -199,7 +204,9 @@ function MessageRenderer({ message }: { message: UIMessage }) {
|
|
|
199
204
|
.some((p) => p.type === 'text')
|
|
200
205
|
return (
|
|
201
206
|
<details key={i} open={!isComplete}>
|
|
202
|
-
<summary>
|
|
207
|
+
<summary>
|
|
208
|
+
{isComplete ? 'Thought process' : 'Thinking...'}
|
|
209
|
+
</summary>
|
|
203
210
|
<pre>{part.content}</pre>
|
|
204
211
|
</details>
|
|
205
212
|
)
|
|
@@ -227,18 +234,25 @@ function MessageRenderer({ message }: { message: UIMessage }) {
|
|
|
227
234
|
Server-side, enable thinking via `modelOptions` on the adapter:
|
|
228
235
|
|
|
229
236
|
```typescript
|
|
237
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
230
238
|
import { geminiText } from '@tanstack/ai-gemini'
|
|
231
239
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
240
|
+
export async function POST(request: Request) {
|
|
241
|
+
const { messages } = await request.json()
|
|
242
|
+
|
|
243
|
+
const stream = chat({
|
|
244
|
+
adapter: geminiText('gemini-3.8-flash'),
|
|
245
|
+
messages,
|
|
246
|
+
modelOptions: {
|
|
247
|
+
thinkingConfig: {
|
|
248
|
+
includeThoughts: true,
|
|
249
|
+
thinkingLevel: 'HIGH', // Gemini 3.x; Gemini 2.x uses thinkingBudget
|
|
250
|
+
},
|
|
239
251
|
},
|
|
240
|
-
}
|
|
241
|
-
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
return toServerSentEventsResponse(stream)
|
|
255
|
+
}
|
|
242
256
|
```
|
|
243
257
|
|
|
244
258
|
### 3. Sending Multimodal Content (Images)
|
|
@@ -280,13 +294,16 @@ function sendImageUrl(text: string, imageUrl: string) {
|
|
|
280
294
|
|
|
281
295
|
Render image parts in received messages:
|
|
282
296
|
|
|
283
|
-
```
|
|
284
|
-
|
|
297
|
+
```tsx
|
|
298
|
+
import type { UIMessage } from '@tanstack/ai-react'
|
|
299
|
+
|
|
300
|
+
function ImagePart({ part }: { part: UIMessage['parts'][number] }) {
|
|
301
|
+
if (part.type !== 'image') return null
|
|
285
302
|
const src =
|
|
286
303
|
part.source.type === 'url'
|
|
287
304
|
? part.source.value
|
|
288
305
|
: `data:${part.source.mimeType};base64,${part.source.value}`
|
|
289
|
-
return <img
|
|
306
|
+
return <img src={src} alt="Attached image" />
|
|
290
307
|
}
|
|
291
308
|
```
|
|
292
309
|
|
|
@@ -328,13 +345,18 @@ Use `toHttpResponse` + `fetchHttpStream` for newline-delimited JSON instead of S
|
|
|
328
345
|
import { chat, toHttpResponse } from '@tanstack/ai'
|
|
329
346
|
import { openaiText } from '@tanstack/ai-openai'
|
|
330
347
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
348
|
+
export async function POST(request: Request) {
|
|
349
|
+
const { messages } = await request.json()
|
|
350
|
+
const abortController = new AbortController()
|
|
351
|
+
|
|
352
|
+
const stream = chat({
|
|
353
|
+
adapter: openaiText('gpt-5.6'),
|
|
354
|
+
messages,
|
|
355
|
+
abortController,
|
|
356
|
+
})
|
|
336
357
|
|
|
337
|
-
return toHttpResponse(stream, { abortController })
|
|
358
|
+
return toHttpResponse(stream, { abortController })
|
|
359
|
+
}
|
|
338
360
|
```
|
|
339
361
|
|
|
340
362
|
**Client:**
|
|
@@ -396,36 +418,29 @@ clients across calls.
|
|
|
396
418
|
**Server-side example:**
|
|
397
419
|
|
|
398
420
|
```typescript
|
|
399
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
400
421
|
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
401
422
|
import { openaiText } from '@tanstack/ai-openai'
|
|
402
423
|
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
403
424
|
|
|
404
|
-
export
|
|
405
|
-
|
|
406
|
-
handlers: {
|
|
407
|
-
POST: async ({ request }) => {
|
|
408
|
-
const { messages } = await request.json()
|
|
409
|
-
|
|
410
|
-
const mcpClient = await createMCPClient({
|
|
411
|
-
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
412
|
-
})
|
|
425
|
+
export async function POST(request: Request) {
|
|
426
|
+
const { messages } = await request.json()
|
|
413
427
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
mcp: {
|
|
418
|
-
clients: [mcpClient],
|
|
419
|
-
connection: 'keep-alive', // chat() won't close it — reuse across requests
|
|
420
|
-
},
|
|
421
|
-
})
|
|
428
|
+
const mcpClient = await createMCPClient({
|
|
429
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
430
|
+
})
|
|
422
431
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
432
|
+
const stream = chat({
|
|
433
|
+
adapter: openaiText('gpt-5.6'),
|
|
434
|
+
messages,
|
|
435
|
+
mcp: {
|
|
436
|
+
clients: [mcpClient],
|
|
437
|
+
connection: 'keep-alive', // chat() won't close it — reuse across requests
|
|
426
438
|
},
|
|
427
|
-
}
|
|
428
|
-
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
return toServerSentEventsResponse(stream)
|
|
442
|
+
// connection: 'keep-alive' — chat() never closes mcpClient; it stays open for reuse across runs.
|
|
443
|
+
}
|
|
429
444
|
```
|
|
430
445
|
|
|
431
446
|
### 7. Queueing Messages Sent While Streaming
|
|
@@ -470,19 +485,37 @@ generation, `stop()`, `clear()`, `unsubscribe()`, and `reload()`.
|
|
|
470
485
|
from `messages` — render pending sends distinctly and cancel with
|
|
471
486
|
`cancelQueued(id)`:
|
|
472
487
|
|
|
473
|
-
```
|
|
474
|
-
{
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
)
|
|
488
|
+
```tsx
|
|
489
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
490
|
+
|
|
491
|
+
function QueuedMessages() {
|
|
492
|
+
const { queue, cancelQueued } = useChat({
|
|
493
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<div>
|
|
498
|
+
{queue.map((q) => (
|
|
499
|
+
<div key={q.id}>
|
|
500
|
+
{typeof q.content === 'string' ? q.content : '[attachment]'}
|
|
501
|
+
<button onClick={() => cancelQueued(q.id)}>Cancel</button>
|
|
502
|
+
</div>
|
|
503
|
+
))}
|
|
504
|
+
</div>
|
|
505
|
+
)
|
|
506
|
+
}
|
|
480
507
|
```
|
|
481
508
|
|
|
482
509
|
Override the configured policy for a single send with the second argument
|
|
483
510
|
to `sendMessage`:
|
|
484
511
|
|
|
485
512
|
```typescript
|
|
513
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
514
|
+
|
|
515
|
+
const { sendMessage } = useChat({
|
|
516
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
517
|
+
})
|
|
518
|
+
|
|
486
519
|
sendMessage('Never mind, do this instead', { whenBusy: 'interrupt' })
|
|
487
520
|
```
|
|
488
521
|
|
|
@@ -561,63 +594,100 @@ option, so it works identically in `@tanstack/ai-react`, `-solid`, `-vue`,
|
|
|
561
594
|
// WRONG
|
|
562
595
|
import { streamText } from 'ai'
|
|
563
596
|
import { openai } from '@ai-sdk/openai'
|
|
564
|
-
const result = streamText({ model: openai('gpt-5.5'), messages })
|
|
565
597
|
|
|
598
|
+
const messages = [{ role: 'user' as const, content: 'Hello' }]
|
|
599
|
+
const result = streamText({ model: openai('gpt-5.6'), messages })
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
566
603
|
// CORRECT
|
|
567
604
|
import { chat } from '@tanstack/ai'
|
|
568
605
|
import { openaiText } from '@tanstack/ai-openai'
|
|
569
|
-
|
|
606
|
+
|
|
607
|
+
const messages = [{ role: 'user' as const, content: 'Hello' }]
|
|
608
|
+
const stream = chat({ adapter: openaiText('gpt-5.6'), messages })
|
|
570
609
|
```
|
|
571
610
|
|
|
572
611
|
### b. CRITICAL: Using Vercel createOpenAI() provider pattern
|
|
573
612
|
|
|
574
613
|
```typescript
|
|
575
614
|
// WRONG
|
|
615
|
+
import { streamText } from 'ai'
|
|
576
616
|
import { createOpenAI } from '@ai-sdk/openai'
|
|
577
|
-
const openai = createOpenAI({ apiKey })
|
|
578
|
-
streamText({ model: openai('gpt-5.5'), messages })
|
|
579
617
|
|
|
618
|
+
const messages = [{ role: 'user' as const, content: 'Hello' }]
|
|
619
|
+
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY })
|
|
620
|
+
streamText({ model: openai('gpt-5.6'), messages })
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
580
624
|
// CORRECT
|
|
581
625
|
import { openaiText } from '@tanstack/ai-openai'
|
|
582
626
|
import { chat } from '@tanstack/ai'
|
|
583
|
-
|
|
627
|
+
|
|
628
|
+
const messages = [{ role: 'user' as const, content: 'Hello' }]
|
|
629
|
+
chat({ adapter: openaiText('gpt-5.6'), messages })
|
|
584
630
|
```
|
|
585
631
|
|
|
586
632
|
### c. CRITICAL: Using monolithic openai() instead of openaiText()
|
|
587
633
|
|
|
588
|
-
```typescript
|
|
589
|
-
// WRONG
|
|
634
|
+
```typescript ignore
|
|
635
|
+
// WRONG — `openai()` is no longer exported from @tanstack/ai-openai
|
|
590
636
|
import { openai } from '@tanstack/ai-openai'
|
|
591
|
-
chat({ adapter: openai(), model: 'gpt-5.
|
|
637
|
+
chat({ adapter: openai(), model: 'gpt-5.6', messages })
|
|
638
|
+
```
|
|
592
639
|
|
|
640
|
+
```typescript
|
|
593
641
|
// CORRECT
|
|
642
|
+
import { chat } from '@tanstack/ai'
|
|
594
643
|
import { openaiText } from '@tanstack/ai-openai'
|
|
595
|
-
|
|
644
|
+
|
|
645
|
+
const messages = [{ role: 'user' as const, content: 'Hello' }]
|
|
646
|
+
chat({ adapter: openaiText('gpt-5.6'), messages })
|
|
596
647
|
```
|
|
597
648
|
|
|
598
|
-
The monolithic `openai()` adapter
|
|
649
|
+
The monolithic `openai()` adapter no longer exists. Use tree-shakeable adapters:
|
|
599
650
|
`openaiText()`, `openaiImage()`, `openaiSpeech()`, etc.
|
|
600
651
|
|
|
601
652
|
### d. HIGH: Using toResponseStream instead of toServerSentEventsResponse
|
|
602
653
|
|
|
603
|
-
```typescript
|
|
604
|
-
// WRONG
|
|
654
|
+
```typescript ignore
|
|
655
|
+
// WRONG — toResponseStream does not exist
|
|
605
656
|
import { toResponseStream } from '@tanstack/ai'
|
|
606
657
|
return toResponseStream(stream, { abortController })
|
|
658
|
+
```
|
|
607
659
|
|
|
660
|
+
```typescript
|
|
608
661
|
// CORRECT
|
|
609
|
-
import { toServerSentEventsResponse } from '@tanstack/ai'
|
|
610
|
-
|
|
662
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
663
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
664
|
+
|
|
665
|
+
export async function POST(request: Request) {
|
|
666
|
+
const { messages } = await request.json()
|
|
667
|
+
const abortController = new AbortController()
|
|
668
|
+
const stream = chat({
|
|
669
|
+
adapter: openaiText('gpt-5.6'),
|
|
670
|
+
messages,
|
|
671
|
+
abortController,
|
|
672
|
+
})
|
|
673
|
+
return toServerSentEventsResponse(stream, { abortController })
|
|
674
|
+
}
|
|
611
675
|
```
|
|
612
676
|
|
|
613
677
|
### e. HIGH: Passing model as separate parameter to chat()
|
|
614
678
|
|
|
615
|
-
```typescript
|
|
679
|
+
```typescript ignore
|
|
616
680
|
// WRONG
|
|
617
|
-
chat({ adapter: openaiText(), model: 'gpt-5.
|
|
681
|
+
chat({ adapter: openaiText(), model: 'gpt-5.6', messages })
|
|
682
|
+
```
|
|
618
683
|
|
|
684
|
+
```typescript
|
|
619
685
|
// CORRECT
|
|
620
|
-
|
|
686
|
+
import { chat } from '@tanstack/ai'
|
|
687
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
688
|
+
|
|
689
|
+
const messages = [{ role: 'user' as const, content: 'Hello' }]
|
|
690
|
+
chat({ adapter: openaiText('gpt-5.6'), messages })
|
|
621
691
|
```
|
|
622
692
|
|
|
623
693
|
The model is passed to the adapter factory, not to `chat()`.
|
|
@@ -628,22 +698,30 @@ Sampling options (`temperature`, token limits, `top_p`/`topP`) are **not**
|
|
|
628
698
|
top-level fields on `chat()`. They live inside `modelOptions` using the
|
|
629
699
|
provider's native key.
|
|
630
700
|
|
|
631
|
-
```typescript
|
|
701
|
+
```typescript ignore
|
|
632
702
|
// WRONG — temperature/maxTokens are not root options
|
|
633
703
|
chat({ adapter, messages, temperature: 0.7, maxTokens: 1000 })
|
|
634
704
|
|
|
635
705
|
// WRONG — there is no `options` field either
|
|
636
706
|
chat({ adapter, messages, options: { temperature: 0.7, maxTokens: 1000 } })
|
|
707
|
+
```
|
|
637
708
|
|
|
709
|
+
```typescript
|
|
638
710
|
// CORRECT — inside modelOptions, provider-native keys (OpenAI shown)
|
|
711
|
+
import { chat } from '@tanstack/ai'
|
|
712
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
713
|
+
|
|
714
|
+
const messages = [{ role: 'user' as const, content: 'Hello' }]
|
|
715
|
+
|
|
639
716
|
chat({
|
|
640
|
-
adapter,
|
|
717
|
+
adapter: openaiText('gpt-5.6'),
|
|
641
718
|
messages,
|
|
642
719
|
modelOptions: { temperature: 0.7, max_output_tokens: 1000 },
|
|
643
720
|
})
|
|
644
721
|
```
|
|
645
722
|
|
|
646
|
-
`temperature`
|
|
723
|
+
`temperature` works on most models (Claude 5 models reject sampling
|
|
724
|
+
parameters; see ai-core/adapter-configuration/SKILL.md). Token limits use provider-native
|
|
647
725
|
keys (`max_output_tokens` for OpenAI, `max_tokens` for Anthropic/Grok,
|
|
648
726
|
`maxOutputTokens` for Gemini, `max_completion_tokens` for Groq,
|
|
649
727
|
`maxCompletionTokens` for OpenRouter, and `num_predict` nested under
|
|
@@ -651,19 +729,26 @@ keys (`max_output_tokens` for OpenAI, `max_tokens` for Anthropic/Grok,
|
|
|
651
729
|
|
|
652
730
|
### g. HIGH: Using providerOptions instead of modelOptions
|
|
653
731
|
|
|
654
|
-
```typescript
|
|
732
|
+
```typescript ignore
|
|
655
733
|
// WRONG
|
|
656
734
|
chat({
|
|
657
735
|
adapter,
|
|
658
736
|
messages,
|
|
659
|
-
providerOptions: {
|
|
737
|
+
providerOptions: { text: { format: { type: 'json_object' } } },
|
|
660
738
|
})
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
// CORRECT — provider-native option under modelOptions (OpenAI Responses shown)
|
|
743
|
+
import { chat } from '@tanstack/ai'
|
|
744
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
745
|
+
|
|
746
|
+
const messages = [{ role: 'user' as const, content: 'Hello' }]
|
|
661
747
|
|
|
662
|
-
// CORRECT
|
|
663
748
|
chat({
|
|
664
|
-
adapter,
|
|
749
|
+
adapter: openaiText('gpt-5.6'),
|
|
665
750
|
messages,
|
|
666
|
-
modelOptions: {
|
|
751
|
+
modelOptions: { text: { format: { type: 'json_object' } } },
|
|
667
752
|
})
|
|
668
753
|
```
|
|
669
754
|
|
|
@@ -671,23 +756,44 @@ chat({
|
|
|
671
756
|
|
|
672
757
|
```typescript
|
|
673
758
|
// WRONG
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
759
|
+
import { chat } from '@tanstack/ai'
|
|
760
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
761
|
+
|
|
762
|
+
export async function POST(request: Request) {
|
|
763
|
+
const { messages } = await request.json()
|
|
764
|
+
const stream = chat({ adapter: openaiText('gpt-5.6'), messages })
|
|
765
|
+
|
|
766
|
+
const readable = new ReadableStream({
|
|
767
|
+
async start(controller) {
|
|
768
|
+
const encoder = new TextEncoder()
|
|
769
|
+
for await (const chunk of stream) {
|
|
770
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`))
|
|
771
|
+
}
|
|
772
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
773
|
+
controller.close()
|
|
774
|
+
},
|
|
775
|
+
})
|
|
776
|
+
return new Response(readable, {
|
|
777
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
778
|
+
})
|
|
779
|
+
}
|
|
780
|
+
```
|
|
687
781
|
|
|
782
|
+
```typescript
|
|
688
783
|
// CORRECT
|
|
689
|
-
import { toServerSentEventsResponse } from '@tanstack/ai'
|
|
690
|
-
|
|
784
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
785
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
786
|
+
|
|
787
|
+
export async function POST(request: Request) {
|
|
788
|
+
const { messages } = await request.json()
|
|
789
|
+
const abortController = new AbortController()
|
|
790
|
+
const stream = chat({
|
|
791
|
+
adapter: openaiText('gpt-5.6'),
|
|
792
|
+
messages,
|
|
793
|
+
abortController,
|
|
794
|
+
})
|
|
795
|
+
return toServerSentEventsResponse(stream, { abortController })
|
|
796
|
+
}
|
|
691
797
|
```
|
|
692
798
|
|
|
693
799
|
`toServerSentEventsResponse` handles SSE formatting, abort signals,
|
|
@@ -695,8 +801,8 @@ error events (RUN_ERROR), and correct headers automatically.
|
|
|
695
801
|
|
|
696
802
|
### i. HIGH: Implementing custom onEnd/onFinish callbacks instead of middleware
|
|
697
803
|
|
|
698
|
-
```typescript
|
|
699
|
-
// WRONG
|
|
804
|
+
```typescript ignore
|
|
805
|
+
// WRONG — chat() has no onEnd/onFinish option
|
|
700
806
|
chat({
|
|
701
807
|
adapter,
|
|
702
808
|
messages,
|
|
@@ -704,9 +810,14 @@ chat({
|
|
|
704
810
|
trackAnalytics(result)
|
|
705
811
|
},
|
|
706
812
|
})
|
|
813
|
+
```
|
|
707
814
|
|
|
815
|
+
```typescript
|
|
708
816
|
// CORRECT
|
|
817
|
+
import { chat } from '@tanstack/ai'
|
|
709
818
|
import type { ChatMiddleware } from '@tanstack/ai'
|
|
819
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
820
|
+
import { trackAnalytics, trackTokens } from './analytics'
|
|
710
821
|
|
|
711
822
|
const analytics: ChatMiddleware = {
|
|
712
823
|
name: 'analytics',
|
|
@@ -718,7 +829,8 @@ const analytics: ChatMiddleware = {
|
|
|
718
829
|
},
|
|
719
830
|
}
|
|
720
831
|
|
|
721
|
-
|
|
832
|
+
const messages = [{ role: 'user' as const, content: 'Hello' }]
|
|
833
|
+
chat({ adapter: openaiText('gpt-5.6'), messages, middleware: [analytics] })
|
|
722
834
|
```
|
|
723
835
|
|
|
724
836
|
`chat()` has no `onEnd`/`onFinish` option. Use `middleware` for lifecycle events.
|
|
@@ -730,7 +842,9 @@ See also: ai-core/middleware/SKILL.md.
|
|
|
730
842
|
// WRONG
|
|
731
843
|
import { fetchServerSentEvents } from '@tanstack/ai-client'
|
|
732
844
|
import { useChat } from '@tanstack/ai-react'
|
|
845
|
+
```
|
|
733
846
|
|
|
847
|
+
```typescript
|
|
734
848
|
// CORRECT
|
|
735
849
|
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
736
850
|
```
|
|
@@ -746,9 +860,15 @@ exceptions. The `useChat` hook surfaces these via the `error` state and
|
|
|
746
860
|
check for `RUN_ERROR` chunks:
|
|
747
861
|
|
|
748
862
|
```typescript
|
|
863
|
+
import { chat } from '@tanstack/ai'
|
|
864
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
865
|
+
|
|
866
|
+
const messages = [{ role: 'user' as const, content: 'Hello' }]
|
|
867
|
+
const stream = chat({ adapter: openaiText('gpt-5.6'), messages })
|
|
868
|
+
|
|
749
869
|
for await (const chunk of stream) {
|
|
750
870
|
if (chunk.type === 'RUN_ERROR') {
|
|
751
|
-
console.error('Stream error:', chunk.
|
|
871
|
+
console.error('Stream error:', chunk.message)
|
|
752
872
|
break
|
|
753
873
|
}
|
|
754
874
|
if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
|
|
@@ -61,6 +61,12 @@ required for normal use.
|
|
|
61
61
|
## Mode A — cache everything (client-authoritative)
|
|
62
62
|
|
|
63
63
|
```tsx
|
|
64
|
+
import {
|
|
65
|
+
useChat,
|
|
66
|
+
fetchServerSentEvents,
|
|
67
|
+
localStoragePersistence,
|
|
68
|
+
} from '@tanstack/ai-react'
|
|
69
|
+
|
|
64
70
|
function Chat() {
|
|
65
71
|
const { messages, sendMessage } = useChat({
|
|
66
72
|
threadId: 'support-chat', // stable — required
|
|
@@ -79,6 +85,8 @@ Best for: SPA, offline-first, single device, moderate conversation size.
|
|
|
79
85
|
## Mode B — server-authoritative (`persistence: true`)
|
|
80
86
|
|
|
81
87
|
```tsx
|
|
88
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
89
|
+
|
|
82
90
|
function Chat({ threadId }: { threadId: string }) {
|
|
83
91
|
const { messages, sendMessage } = useChat({
|
|
84
92
|
threadId,
|
|
@@ -135,18 +143,22 @@ The hook return is exactly `generate` / `result` / `isLoading` / `error` /
|
|
|
135
143
|
### Turning it on (`persistence: true`)
|
|
136
144
|
|
|
137
145
|
```tsx
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
import { useGenerateImage, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
147
|
+
|
|
148
|
+
function ImageGenerator({ threadId }: { threadId: string }) {
|
|
149
|
+
const image = useGenerateImage({
|
|
150
|
+
threadId, // REQUIRED — the scope the last generation is hydrated under
|
|
151
|
+
connection: fetchServerSentEvents('/api/generate/image'),
|
|
152
|
+
persistence: true,
|
|
153
|
+
})
|
|
154
|
+
// After a reload: image.status / image.result / image.error are the last
|
|
155
|
+
// generation for `threadId`, fetched from the server — nothing was cached.
|
|
156
|
+
}
|
|
145
157
|
```
|
|
146
158
|
|
|
147
159
|
The server half — the same route handles the run and the hydration `GET`:
|
|
148
160
|
|
|
149
|
-
```ts
|
|
161
|
+
```ts group=generation-persistence
|
|
150
162
|
import {
|
|
151
163
|
generateImage,
|
|
152
164
|
generationParamsFromRequest,
|
|
@@ -222,7 +234,7 @@ export function GET(request: Request) {
|
|
|
222
234
|
(`stores.artifacts` + `stores.blobs`) AND `withGenerationPersistence` is given an
|
|
223
235
|
`artifactUrl` mapper:
|
|
224
236
|
|
|
225
|
-
```ts
|
|
237
|
+
```ts group=generation-persistence
|
|
226
238
|
withGenerationPersistence(persistence, {
|
|
227
239
|
artifactUrl: (ref) => `/api/generate/image/artifact?id=${ref.artifactId}`,
|
|
228
240
|
})
|