@tanstack/ai 0.53.0 → 0.54.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 (60) hide show
  1. package/README.md +14 -13
  2. package/dist/esm/activities/chat/index.js +5 -3
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/generateLiveVideo/adapter.d.ts +69 -0
  5. package/dist/esm/activities/generateLiveVideo/adapter.js +23 -0
  6. package/dist/esm/activities/generateLiveVideo/adapter.js.map +1 -0
  7. package/dist/esm/activities/generateLiveVideo/index.d.ts +99 -0
  8. package/dist/esm/activities/generateLiveVideo/index.js +162 -0
  9. package/dist/esm/activities/generateLiveVideo/index.js.map +1 -0
  10. package/dist/esm/activities/generateVideo/index.js +3 -1
  11. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  12. package/dist/esm/activities/generateWorld/adapter.d.ts +69 -0
  13. package/dist/esm/activities/generateWorld/adapter.js +23 -0
  14. package/dist/esm/activities/generateWorld/adapter.js.map +1 -0
  15. package/dist/esm/activities/generateWorld/index.d.ts +99 -0
  16. package/dist/esm/activities/generateWorld/index.js +162 -0
  17. package/dist/esm/activities/generateWorld/index.js.map +1 -0
  18. package/dist/esm/activities/index.d.ts +8 -2
  19. package/dist/esm/activities/index.js +11 -7
  20. package/dist/esm/activities/middleware/types.d.ts +1 -1
  21. package/dist/esm/client.d.ts +4 -2
  22. package/dist/esm/client.js +3 -1
  23. package/dist/esm/client.js.map +1 -1
  24. package/dist/esm/index.d.ts +4 -2
  25. package/dist/esm/index.js +3 -1
  26. package/dist/esm/middlewares/otel.js +3 -1
  27. package/dist/esm/middlewares/otel.js.map +1 -1
  28. package/dist/esm/types.d.ts +112 -0
  29. package/package.json +2 -2
  30. package/skills/ai-core/adapter-configuration/SKILL.md +90 -42
  31. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +39 -21
  32. package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +5 -0
  33. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +14 -6
  34. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +33 -25
  35. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +7 -2
  36. package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +25 -12
  37. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +19 -9
  38. package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +34 -21
  39. package/skills/ai-core/ag-ui-protocol/SKILL.md +16 -10
  40. package/skills/ai-core/chat-experience/SKILL.md +228 -108
  41. package/skills/ai-core/client-persistence/SKILL.md +21 -9
  42. package/skills/ai-core/custom-backend-integration/SKILL.md +86 -52
  43. package/skills/ai-core/debug-logging/SKILL.md +100 -18
  44. package/skills/ai-core/locks/SKILL.md +35 -7
  45. package/skills/ai-core/media-generation/SKILL.md +114 -49
  46. package/skills/ai-core/middleware/SKILL.md +174 -69
  47. package/skills/ai-core/structured-outputs/SKILL.md +98 -49
  48. package/skills/ai-core/tool-calling/SKILL.md +245 -158
  49. package/src/activities/chat/index.ts +6 -7
  50. package/src/activities/generateLiveVideo/adapter.ts +99 -0
  51. package/src/activities/generateLiveVideo/index.ts +339 -0
  52. package/src/activities/generateVideo/index.ts +3 -4
  53. package/src/activities/generateWorld/adapter.ts +96 -0
  54. package/src/activities/generateWorld/index.ts +339 -0
  55. package/src/activities/index.ts +44 -0
  56. package/src/activities/middleware/types.ts +2 -0
  57. package/src/client.ts +8 -0
  58. package/src/index.ts +8 -0
  59. package/src/middlewares/otel.ts +2 -0
  60. 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
- ```typescript
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
- const stream = chat({
140
- adapter: anthropicText('claude-sonnet-4-5'),
141
- messages,
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
- return toServerSentEventsResponse(stream, { abortController })
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-sonnet-4-5' },
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
- ```typescript
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>{isComplete ? 'Thought process' : 'Thinking...'}</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
- const stream = chat({
233
- adapter: geminiText('gemini-2.5-flash'),
234
- messages,
235
- modelOptions: {
236
- thinkingConfig: {
237
- includeThoughts: true,
238
- thinkingBudget: 100,
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
- ```typescript
284
- if (part.type === 'image') {
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 key={i} src={src} alt="Attached image" />
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
- const stream = chat({
332
- adapter: openaiText('gpt-5.5'),
333
- messages,
334
- abortController,
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 const Route = createFileRoute('/api/chat')({
405
- server: {
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
- const stream = chat({
415
- adapter: openaiText('gpt-5.5'),
416
- messages,
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
- return toServerSentEventsResponse(stream)
424
- // connection: 'keep-alive' — chat() never closes mcpClient; it stays open for reuse across runs.
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
- ```typescript
474
- {queue.map((q) => (
475
- <div key={q.id}>
476
- {typeof q.content === 'string' ? q.content : '[attachment]'}
477
- <button onClick={() => cancelQueued(q.id)}>Cancel</button>
478
- </div>
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
- const stream = chat({ adapter: openaiText('gpt-5.5'), messages })
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
- chat({ adapter: openaiText('gpt-5.5'), messages })
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.5', messages })
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
- chat({ adapter: openaiText('gpt-5.5'), messages })
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 is deprecated. Use tree-shakeable adapters:
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
- return toServerSentEventsResponse(stream, { abortController })
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.5', messages })
681
+ chat({ adapter: openaiText(), model: 'gpt-5.6', messages })
682
+ ```
618
683
 
684
+ ```typescript
619
685
  // CORRECT
620
- chat({ adapter: openaiText('gpt-5.5'), messages })
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` is universal across providers; token limits use provider-native
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: { responseFormat: { type: 'json_object' } },
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: { responseFormat: { type: 'json_object' } },
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
- const readable = new ReadableStream({
675
- async start(controller) {
676
- const encoder = new TextEncoder()
677
- for await (const chunk of stream) {
678
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`))
679
- }
680
- controller.enqueue(encoder.encode('data: [DONE]\n\n'))
681
- controller.close()
682
- },
683
- })
684
- return new Response(readable, {
685
- headers: { 'Content-Type': 'text/event-stream' },
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
- return toServerSentEventsResponse(stream, { abortController })
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
- chat({ adapter, messages, middleware: [analytics] })
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.error.message)
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
- const image = useGenerateImage({
139
- threadId, // REQUIRED — the scope the last generation is hydrated under
140
- connection: fetchServerSentEvents('/api/generate/image'),
141
- persistence: true,
142
- })
143
- // After a reload: image.status / image.result / image.error are the last
144
- // generation for `threadId`, fetched from the server — nothing was cached.
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
  })