@tanstack/ai 0.22.0 → 0.23.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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@tanstack/ai",
3
- "version": "0.22.0",
3
+ "version": "0.23.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",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/TanStack/ai.git",
10
- "directory": "packages/typescript/ai"
10
+ "directory": "packages/ai"
11
11
  },
12
12
  "type": "module",
13
13
  "module": "./dist/esm/index.js",
@@ -17,6 +17,10 @@
17
17
  "types": "./dist/esm/index.d.ts",
18
18
  "import": "./dist/esm/index.js"
19
19
  },
20
+ "./client": {
21
+ "types": "./dist/esm/client.d.ts",
22
+ "import": "./dist/esm/client.js"
23
+ },
20
24
  "./adapters": {
21
25
  "types": "./dist/esm/activities/index.d.ts",
22
26
  "import": "./dist/esm/activities/index.js"
@@ -64,7 +68,7 @@
64
68
  "@ag-ui/core": "^0.0.52",
65
69
  "@standard-schema/spec": "^1.1.0",
66
70
  "partial-json": "^0.1.7",
67
- "@tanstack/ai-event-client": "0.3.11"
71
+ "@tanstack/ai-event-client": "0.4.1"
68
72
  },
69
73
  "peerDependencies": {
70
74
  "@opentelemetry/api": ">=1.9.0"
@@ -26,7 +26,7 @@ sources:
26
26
 
27
27
  > **Before implementing:** Ask the user which provider and model they want.
28
28
  > Then fetch the latest available models from the provider's source code
29
- > (check the adapter's model metadata file, e.g. `packages/typescript/ai-openai/src/model-meta.ts`)
29
+ > (check the adapter's model metadata file, e.g. `packages/ai-openai/src/model-meta.ts`)
30
30
  > or from the provider's API/docs to recommend the most current model.
31
31
  > The model lists in this skill and its reference files may be outdated.
32
32
  > Always verify against the source before recommending a specific model.
@@ -253,7 +253,7 @@ const safe: Logger = {
253
253
  }
254
254
  ```
255
255
 
256
- Source: packages/typescript/ai/src/logger/internal-logger.ts
256
+ Source: packages/ai/src/logger/internal-logger.ts
257
257
 
258
258
  ## Cross-References
259
259
 
@@ -481,6 +481,7 @@ class TextEngine<
481
481
  this.middlewareCtx = {
482
482
  requestId: this.requestId,
483
483
  streamId: this.streamId,
484
+ runId: this.runIdOverride ?? this.requestId,
484
485
  threadId: this.threadId,
485
486
  // Legacy alias kept on the ctx so middleware that reads
486
487
  // `ctx.conversationId` keeps working. Always equals `threadId`.
@@ -4,6 +4,7 @@ import type {
4
4
  StreamChunk,
5
5
  Tool,
6
6
  ToolCall,
7
+ UsageTotals,
7
8
  } from '../../../types'
8
9
  import type { SystemPrompt } from '../../../system-prompts'
9
10
 
@@ -38,6 +39,8 @@ export interface ChatMiddlewareContext {
38
39
  requestId: string
39
40
  /** Unique identifier for this stream */
40
41
  streamId: string
42
+ /** AG-UI run identifier for correlating client and server events */
43
+ runId: string
41
44
  /**
42
45
  * AG-UI thread identifier — a stable per-conversation ID used to
43
46
  * correlate client and server devtools events. Resolves to the
@@ -262,12 +265,12 @@ export interface ToolPhaseCompleteInfo {
262
265
  /**
263
266
  * Token usage statistics passed to the onUsage hook.
264
267
  * Extracted from the RUN_FINISHED chunk when usage data is present.
268
+ *
269
+ * Includes optional provider-reported `cost`/`costDetails` (see {@link UsageTotals}).
270
+ * Kept as an interface extending `UsageTotals` to preserve declaration merging for
271
+ * this publicly exported type.
265
272
  */
266
- export interface UsageInfo {
267
- promptTokens: number
268
- completionTokens: number
269
- totalTokens: number
270
- }
273
+ export interface UsageInfo extends UsageTotals {}
271
274
 
272
275
  // ===========================
273
276
  // Terminal Hook Info
@@ -283,14 +286,8 @@ export interface FinishInfo {
283
286
  duration: number
284
287
  /** Final accumulated text content */
285
288
  content: string
286
- /** Final usage totals, if available */
287
- usage?:
288
- | {
289
- promptTokens: number
290
- completionTokens: number
291
- totalTokens: number
292
- }
293
- | undefined
289
+ /** Final usage totals, if available (optionally including provider-reported cost) */
290
+ usage?: UsageTotals | undefined
294
291
  }
295
292
 
296
293
  /**
@@ -98,6 +98,17 @@ export interface StreamProcessorEvents {
98
98
  stepId: string,
99
99
  content: string,
100
100
  ) => void
101
+ onStructuredOutputChange?: (args: {
102
+ phase: 'start' | 'update' | 'complete' | 'error'
103
+ messageId: string
104
+ status: 'streaming' | 'complete' | 'error'
105
+ raw: string
106
+ partial?: unknown
107
+ data?: unknown
108
+ reasoning?: string
109
+ errorMessage?: string
110
+ delta?: string
111
+ }) => void
101
112
  }
102
113
 
103
114
  /**
@@ -116,6 +127,8 @@ export interface StreamProcessorOptions {
116
127
  initialMessages?: Array<UIMessage>
117
128
  }
118
129
 
130
+ const STRUCTURED_OUTPUT_UPDATE_BATCH_SIZE = 12
131
+
119
132
  /**
120
133
  * StreamProcessor - State machine for processing AI response streams
121
134
  *
@@ -149,6 +162,13 @@ export class StreamProcessor {
149
162
  private pendingThinkingStepId: string | null = null
150
163
 
151
164
  private readonly structuredMessageIds: Set<string> = new Set()
165
+ private readonly structuredOutputUpdateBatches = new Map<
166
+ string,
167
+ {
168
+ delta: string
169
+ chunkCount: number
170
+ }
171
+ >()
152
172
 
153
173
  // Run tracking (for concurrent run safety)
154
174
  private readonly activeRuns = new Set<string>()
@@ -401,6 +421,9 @@ export class StreamProcessor {
401
421
  for (const id of this.structuredMessageIds) {
402
422
  if (!keptIds.has(id)) this.structuredMessageIds.delete(id)
403
423
  }
424
+ for (const id of this.structuredOutputUpdateBatches.keys()) {
425
+ if (!keptIds.has(id)) this.structuredOutputUpdateBatches.delete(id)
426
+ }
404
427
  for (const id of this.messageStates.keys()) {
405
428
  if (!keptIds.has(id)) this.messageStates.delete(id)
406
429
  }
@@ -423,6 +446,7 @@ export class StreamProcessor {
423
446
  this.activeMessageIds.clear()
424
447
  this.toolCallToMessage.clear()
425
448
  this.structuredMessageIds.clear()
449
+ this.structuredOutputUpdateBatches.clear()
426
450
  this.pendingManualMessageId = null
427
451
  this.emitMessagesChange()
428
452
  }
@@ -899,6 +923,7 @@ export class StreamProcessor {
899
923
  delta,
900
924
  )
901
925
  state.totalTextContent += delta
926
+ this.queueStructuredOutputUpdate(messageId, delta)
902
927
  this.emitMessagesChange()
903
928
  }
904
929
  return
@@ -1269,12 +1294,14 @@ export class StreamProcessor {
1269
1294
  }
1270
1295
 
1271
1296
  if (this.structuredMessageIds.has(messageId)) {
1297
+ this.flushStructuredOutputUpdate(messageId)
1272
1298
  this.messages = errorStructuredOutputPart(
1273
1299
  this.messages,
1274
1300
  messageId,
1275
1301
  errorMessage,
1276
1302
  )
1277
1303
  this.structuredMessageIds.delete(messageId)
1304
+ this.emitStructuredOutputChange(messageId, 'error')
1278
1305
  this.emitMessagesChange()
1279
1306
  }
1280
1307
 
@@ -1463,6 +1490,13 @@ export class StreamProcessor {
1463
1490
  if (targetId) {
1464
1491
  this.ensureAssistantMessage(targetId)
1465
1492
  this.structuredMessageIds.add(targetId)
1493
+ this.structuredOutputUpdateBatches.delete(targetId)
1494
+ this.events.onStructuredOutputChange?.({
1495
+ phase: 'start',
1496
+ messageId: targetId,
1497
+ status: 'streaming',
1498
+ raw: '',
1499
+ })
1466
1500
  }
1467
1501
  return
1468
1502
  }
@@ -1476,6 +1510,7 @@ export class StreamProcessor {
1476
1510
  }
1477
1511
  const targetId = v.messageId ?? messageId
1478
1512
  if (targetId) {
1513
+ this.flushStructuredOutputUpdate(targetId)
1479
1514
  this.messages = completeStructuredOutputPart(
1480
1515
  this.messages,
1481
1516
  targetId,
@@ -1484,6 +1519,7 @@ export class StreamProcessor {
1484
1519
  v.reasoning,
1485
1520
  )
1486
1521
  this.structuredMessageIds.delete(targetId)
1522
+ this.emitStructuredOutputChange(targetId, 'complete')
1487
1523
  this.emitMessagesChange()
1488
1524
  }
1489
1525
  // Fall through so user `onCustomEvent` callbacks still observe the event.
@@ -1665,6 +1701,58 @@ export class StreamProcessor {
1665
1701
  this.events.onTextUpdate?.(messageId, state.currentSegmentText)
1666
1702
  }
1667
1703
 
1704
+ private queueStructuredOutputUpdate(messageId: string, delta: string): void {
1705
+ const existing = this.structuredOutputUpdateBatches.get(messageId)
1706
+ const next = {
1707
+ delta: `${existing?.delta ?? ''}${delta}`,
1708
+ chunkCount: (existing?.chunkCount ?? 0) + 1,
1709
+ }
1710
+
1711
+ this.structuredOutputUpdateBatches.set(messageId, next)
1712
+
1713
+ if (next.chunkCount >= STRUCTURED_OUTPUT_UPDATE_BATCH_SIZE) {
1714
+ this.flushStructuredOutputUpdate(messageId)
1715
+ }
1716
+ }
1717
+
1718
+ private flushStructuredOutputUpdate(messageId: string): void {
1719
+ const batch = this.structuredOutputUpdateBatches.get(messageId)
1720
+ if (!batch || batch.chunkCount === 0) return
1721
+
1722
+ this.structuredOutputUpdateBatches.delete(messageId)
1723
+ this.emitStructuredOutputChange(messageId, 'update', batch.delta)
1724
+ }
1725
+
1726
+ private emitStructuredOutputChange(
1727
+ messageId: string,
1728
+ phase: 'update' | 'complete' | 'error',
1729
+ delta?: string,
1730
+ ): void {
1731
+ const part = this.messages
1732
+ .find((message) => message.id === messageId)
1733
+ ?.parts.find(
1734
+ (
1735
+ messagePart,
1736
+ ): messagePart is Extract<MessagePart, { type: 'structured-output' }> =>
1737
+ messagePart.type === 'structured-output',
1738
+ )
1739
+ if (!part) return
1740
+
1741
+ this.events.onStructuredOutputChange?.({
1742
+ phase,
1743
+ messageId,
1744
+ status: part.status,
1745
+ raw: part.raw,
1746
+ ...(part.partial !== undefined ? { partial: part.partial } : {}),
1747
+ ...(part.data !== undefined ? { data: part.data } : {}),
1748
+ ...(part.reasoning !== undefined ? { reasoning: part.reasoning } : {}),
1749
+ ...(part.errorMessage !== undefined
1750
+ ? { errorMessage: part.errorMessage }
1751
+ : {}),
1752
+ ...(delta !== undefined ? { delta } : {}),
1753
+ })
1754
+ }
1755
+
1668
1756
  /**
1669
1757
  * Emit messages change event
1670
1758
  */
@@ -1718,13 +1806,16 @@ export class StreamProcessor {
1718
1806
  // definition a non-errored, never-completed run (the multi-run case:
1719
1807
  // run-A errors, run-B is still streaming when finalize fires).
1720
1808
  for (const messageId of this.structuredMessageIds) {
1809
+ this.flushStructuredOutputUpdate(messageId)
1721
1810
  this.messages = errorStructuredOutputPart(
1722
1811
  this.messages,
1723
1812
  messageId,
1724
1813
  'Stream ended without structured-output.complete',
1725
1814
  )
1815
+ this.emitStructuredOutputChange(messageId, 'error')
1726
1816
  }
1727
1817
  this.structuredMessageIds.clear()
1818
+ this.structuredOutputUpdateBatches.clear()
1728
1819
 
1729
1820
  this.activeMessageIds.clear()
1730
1821
 
@@ -1855,6 +1946,7 @@ export class StreamProcessor {
1855
1946
  this.activeRuns.clear()
1856
1947
  this.toolCallToMessage.clear()
1857
1948
  this.structuredMessageIds.clear()
1949
+ this.structuredOutputUpdateBatches.clear()
1858
1950
  this.pendingManualMessageId = null
1859
1951
  this.pendingThinkingStepId = null
1860
1952
  this.finishReason = null
package/src/client.ts ADDED
@@ -0,0 +1,132 @@
1
+ export enum EventType {
2
+ TEXT_MESSAGE_START = 'TEXT_MESSAGE_START',
3
+ TEXT_MESSAGE_CONTENT = 'TEXT_MESSAGE_CONTENT',
4
+ TEXT_MESSAGE_END = 'TEXT_MESSAGE_END',
5
+ TEXT_MESSAGE_CHUNK = 'TEXT_MESSAGE_CHUNK',
6
+ TOOL_CALL_START = 'TOOL_CALL_START',
7
+ TOOL_CALL_ARGS = 'TOOL_CALL_ARGS',
8
+ TOOL_CALL_END = 'TOOL_CALL_END',
9
+ TOOL_CALL_CHUNK = 'TOOL_CALL_CHUNK',
10
+ TOOL_CALL_RESULT = 'TOOL_CALL_RESULT',
11
+ THINKING_START = 'THINKING_START',
12
+ THINKING_END = 'THINKING_END',
13
+ THINKING_TEXT_MESSAGE_START = 'THINKING_TEXT_MESSAGE_START',
14
+ THINKING_TEXT_MESSAGE_CONTENT = 'THINKING_TEXT_MESSAGE_CONTENT',
15
+ THINKING_TEXT_MESSAGE_END = 'THINKING_TEXT_MESSAGE_END',
16
+ STATE_SNAPSHOT = 'STATE_SNAPSHOT',
17
+ STATE_DELTA = 'STATE_DELTA',
18
+ MESSAGES_SNAPSHOT = 'MESSAGES_SNAPSHOT',
19
+ ACTIVITY_SNAPSHOT = 'ACTIVITY_SNAPSHOT',
20
+ ACTIVITY_DELTA = 'ACTIVITY_DELTA',
21
+ RAW = 'RAW',
22
+ CUSTOM = 'CUSTOM',
23
+ RUN_STARTED = 'RUN_STARTED',
24
+ RUN_FINISHED = 'RUN_FINISHED',
25
+ RUN_ERROR = 'RUN_ERROR',
26
+ STEP_STARTED = 'STEP_STARTED',
27
+ STEP_FINISHED = 'STEP_FINISHED',
28
+ REASONING_START = 'REASONING_START',
29
+ REASONING_MESSAGE_START = 'REASONING_MESSAGE_START',
30
+ REASONING_MESSAGE_CONTENT = 'REASONING_MESSAGE_CONTENT',
31
+ REASONING_MESSAGE_END = 'REASONING_MESSAGE_END',
32
+ REASONING_MESSAGE_CHUNK = 'REASONING_MESSAGE_CHUNK',
33
+ REASONING_END = 'REASONING_END',
34
+ REASONING_ENCRYPTED_VALUE = 'REASONING_ENCRYPTED_VALUE',
35
+ }
36
+
37
+ export {
38
+ toolDefinition,
39
+ type AnyClientTool,
40
+ type ClientTool,
41
+ type InferToolInput,
42
+ type InferToolName,
43
+ type InferToolOutput,
44
+ type ToolDefinition,
45
+ type ToolDefinitionConfig,
46
+ type ToolDefinitionInstance,
47
+ } from './activities/chat/tools/tool-definition'
48
+
49
+ export { convertSchemaToJsonSchema } from './activities/chat/tools/schema-converter'
50
+
51
+ export {
52
+ convertMessagesToModelMessages,
53
+ generateMessageId,
54
+ modelMessageToUIMessage,
55
+ modelMessagesToUIMessages,
56
+ normalizeToUIMessage,
57
+ uiMessageToModelMessages,
58
+ } from './activities/chat/messages'
59
+
60
+ export {
61
+ BatchStrategy,
62
+ CompositeStrategy,
63
+ defaultJSONParser,
64
+ ImmediateStrategy,
65
+ parsePartialJSON,
66
+ PartialJSONParser,
67
+ PunctuationStrategy,
68
+ StreamProcessor,
69
+ WordBoundaryStrategy,
70
+ } from './activities/chat/stream/index'
71
+ export type {
72
+ ChunkRecording,
73
+ ChunkStrategy,
74
+ InternalToolCallState,
75
+ JSONParser,
76
+ ProcessorResult,
77
+ ProcessorState,
78
+ StreamProcessorEvents,
79
+ StreamProcessorOptions,
80
+ ToolCallState,
81
+ ToolResultState,
82
+ } from './activities/chat/stream/index'
83
+
84
+ export { uiMessagesToWire } from './utilities/ag-ui-wire'
85
+ export type { WireMessage } from './utilities/ag-ui-wire'
86
+
87
+ export type {
88
+ AudioPart,
89
+ ContentPart,
90
+ ContentPartDataSource,
91
+ ContentPartSource,
92
+ ContentPartUrlSource,
93
+ CustomEvent,
94
+ DocumentPart,
95
+ ImagePart,
96
+ MessagePart,
97
+ ModelMessage,
98
+ RunErrorEvent,
99
+ RunFinishedEvent,
100
+ SchemaInput,
101
+ StreamChunk,
102
+ StructuredOutputPart,
103
+ TextPart,
104
+ ThinkingPart,
105
+ ToolCall,
106
+ ToolCallPart,
107
+ ToolResultPart,
108
+ UIMessage,
109
+ VideoPart,
110
+ InferSchemaType,
111
+ } from './types'
112
+
113
+ export type {
114
+ AudioVisualization,
115
+ RealtimeError,
116
+ RealtimeErrorCode,
117
+ RealtimeEvent,
118
+ RealtimeEventHandler,
119
+ RealtimeEventPayloads,
120
+ RealtimeMessage,
121
+ RealtimeMessagePart,
122
+ RealtimeMode,
123
+ RealtimeSessionConfig,
124
+ RealtimeStatus,
125
+ RealtimeToken,
126
+ RealtimeAudioPart,
127
+ RealtimeImagePart,
128
+ RealtimeTextPart,
129
+ RealtimeToolCallPart,
130
+ RealtimeToolResultPart,
131
+ VADConfig,
132
+ } from './realtime/types'
package/src/types.ts CHANGED
@@ -925,6 +925,39 @@ export interface RunStartedEvent extends AGUIRunStartedEvent {
925
925
  model?: string
926
926
  }
927
927
 
928
+ /**
929
+ * Provider-reported cost breakdown for a single request, normalized onto a
930
+ * canonical shape so consumer code is portable across gateways. Each adapter's
931
+ * extractor maps its provider-specific wire keys (e.g. OpenRouter's
932
+ * `upstream_inference_prompt_cost`, `upstream_inference_input_cost`) onto these
933
+ * fields at runtime.
934
+ */
935
+ export interface UsageCostBreakdown {
936
+ /** Total cost the gateway paid the upstream provider. */
937
+ upstreamCost?: number
938
+ /** Upstream cost for input (prompt) tokens. */
939
+ upstreamInputCost?: number
940
+ /** Upstream cost for output (completion) tokens. */
941
+ upstreamOutputCost?: number
942
+ }
943
+
944
+ /**
945
+ * Token usage totals for a run, optionally including provider-reported cost.
946
+ *
947
+ * `cost` and `costDetails` are populated only by adapters whose provider returns
948
+ * authoritative per-request cost (e.g. OpenRouter). They are absent for adapters
949
+ * that do not report cost, so consumers must treat them as optional.
950
+ */
951
+ export interface UsageTotals {
952
+ promptTokens: number
953
+ completionTokens: number
954
+ totalTokens: number
955
+ /** Provider-reported cost for the request, when available. */
956
+ cost?: number
957
+ /** Provider-reported cost breakdown, when available. */
958
+ costDetails?: UsageCostBreakdown
959
+ }
960
+
928
961
  /**
929
962
  * Emitted when a run completes successfully.
930
963
  *
@@ -936,12 +969,8 @@ export interface RunFinishedEvent extends AGUIRunFinishedEvent {
936
969
  model?: string
937
970
  /** Why the generation stopped */
938
971
  finishReason?: 'stop' | 'length' | 'content_filter' | 'tool_calls' | null
939
- /** Token usage statistics */
940
- usage?: {
941
- promptTokens: number
942
- completionTokens: number
943
- totalTokens: number
944
- }
972
+ /** Token usage statistics, optionally including provider-reported cost. */
973
+ usage?: UsageTotals
945
974
  }
946
975
 
947
976
  /**