@tanstack/ai-client 0.25.2 → 0.26.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 +2 -1
- package/dist/esm/chat-client.d.ts +2 -2
- package/dist/esm/chat-client.js +47 -28
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.js +37 -21
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/generation-client.js +4 -2
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.js +4 -2
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/types.d.ts +17 -0
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.js +4 -2
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +3 -3
- package/src/chat-client.ts +43 -12
- package/src/connection-adapters.ts +67 -41
- package/src/generation-client.ts +5 -5
- package/src/generation-types.ts +8 -2
- package/src/types.ts +17 -0
- package/src/video-generation-client.ts +5 -5
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
EventType,
|
|
3
|
+
getChunkRunId as getNormalizedChunkRunId,
|
|
4
|
+
restoreInboundChunk,
|
|
5
|
+
tanstackMetadata,
|
|
6
|
+
uiMessagesToWire,
|
|
7
|
+
withTanstackMetadata,
|
|
8
|
+
} from '@tanstack/ai/client'
|
|
2
9
|
import {
|
|
3
10
|
createResponseStreamTextDecoder,
|
|
4
11
|
getResponseStreamReader,
|
|
@@ -8,7 +15,6 @@ import type {
|
|
|
8
15
|
ModelMessage,
|
|
9
16
|
RunAgentResumeItem,
|
|
10
17
|
RunErrorEvent,
|
|
11
|
-
RunFinishedEvent,
|
|
12
18
|
StreamChunk,
|
|
13
19
|
UIMessage,
|
|
14
20
|
} from '@tanstack/ai/client'
|
|
@@ -34,12 +40,7 @@ export function getChunkRunId(chunk: StreamChunk): string | undefined {
|
|
|
34
40
|
// the client's run identity to win when a provider stamps its own id; for
|
|
35
41
|
// resumable reconnect/join the two ids match, so precedence is moot there.
|
|
36
42
|
const requestRunId = chunkRunIds.get(chunk)
|
|
37
|
-
return (
|
|
38
|
-
requestRunId ??
|
|
39
|
-
('runId' in chunk && typeof chunk.runId === 'string'
|
|
40
|
-
? chunk.runId
|
|
41
|
-
: undefined)
|
|
42
|
-
)
|
|
43
|
+
return requestRunId ?? getNormalizedChunkRunId(chunk)
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -385,6 +386,22 @@ function isNdjsonEnvelope(
|
|
|
385
386
|
)
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
/** Rebuild pre-wire extras after SSE/NDJSON ingest. */
|
|
390
|
+
function restoreInboundUsage(chunk: StreamChunk): StreamChunk {
|
|
391
|
+
return restoreInboundChunk(chunk)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function sseChunkModel(chunk: StreamChunk): string | undefined {
|
|
395
|
+
const tanstackModel = tanstackMetadata(chunk)?.model
|
|
396
|
+
if (typeof tanstackModel === 'string') return tanstackModel
|
|
397
|
+
const usage = 'usage' in chunk ? chunk.usage : undefined
|
|
398
|
+
if (Array.isArray(usage)) {
|
|
399
|
+
const model = (usage[0] as { model?: unknown } | undefined)?.model
|
|
400
|
+
if (typeof model === 'string') return model
|
|
401
|
+
}
|
|
402
|
+
return undefined
|
|
403
|
+
}
|
|
404
|
+
|
|
388
405
|
/**
|
|
389
406
|
* Parse SSE-format lines into stream events, pairing each chunk with the `id:`
|
|
390
407
|
* offset of the event it arrived on. Shared by the fetch- and XHR-backed SSE
|
|
@@ -431,27 +448,31 @@ async function* linesToSSEEvents(
|
|
|
431
448
|
}
|
|
432
449
|
const data = parseSseDataLine(line)
|
|
433
450
|
if (data === '[DONE]') {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
451
|
+
yield {
|
|
452
|
+
chunk: withTanstackMetadata(
|
|
453
|
+
{
|
|
454
|
+
type: EventType.RUN_FINISHED,
|
|
455
|
+
threadId: lastThreadId ?? fallbackIds?.threadId ?? '',
|
|
456
|
+
runId: lastRunId ?? fallbackIds?.runId ?? '',
|
|
457
|
+
timestamp: Date.now(),
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
finishReason: 'stop',
|
|
461
|
+
...(lastModel !== undefined ? { model: lastModel } : {}),
|
|
462
|
+
},
|
|
463
|
+
) as StreamChunk,
|
|
441
464
|
}
|
|
442
|
-
yield { chunk: synthetic }
|
|
443
465
|
return
|
|
444
466
|
}
|
|
445
|
-
const chunk = JSON.parse(data) as StreamChunk
|
|
467
|
+
const chunk = restoreInboundUsage(JSON.parse(data) as StreamChunk)
|
|
446
468
|
if ('threadId' in chunk && typeof chunk.threadId === 'string') {
|
|
447
469
|
lastThreadId = chunk.threadId
|
|
448
470
|
}
|
|
449
471
|
if ('runId' in chunk && typeof chunk.runId === 'string') {
|
|
450
472
|
lastRunId = chunk.runId
|
|
451
473
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
474
|
+
const model = sseChunkModel(chunk)
|
|
475
|
+
if (model !== undefined) lastModel = model
|
|
455
476
|
const id = pendingId
|
|
456
477
|
pendingId = undefined
|
|
457
478
|
yield { chunk, ...(id !== undefined ? { id } : {}) }
|
|
@@ -470,9 +491,9 @@ async function* linesToNdjsonEvents(
|
|
|
470
491
|
for await (const line of lines) {
|
|
471
492
|
const parsed = JSON.parse(line) as unknown
|
|
472
493
|
if (isNdjsonEnvelope(parsed)) {
|
|
473
|
-
yield { chunk: parsed.chunk, id: parsed.id }
|
|
494
|
+
yield { chunk: restoreInboundUsage(parsed.chunk), id: parsed.id }
|
|
474
495
|
} else {
|
|
475
|
-
yield { chunk: parsed as StreamChunk }
|
|
496
|
+
yield { chunk: restoreInboundUsage(parsed as StreamChunk) }
|
|
476
497
|
}
|
|
477
498
|
}
|
|
478
499
|
}
|
|
@@ -1064,21 +1085,24 @@ export function normalizeConnectionAdapter(
|
|
|
1064
1085
|
// observed, but stamp the caller's request runId so getChunkRunId()
|
|
1065
1086
|
// correlates to activeRunIds / currentRunId (same as real stream chunks).
|
|
1066
1087
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1088
|
+
push(
|
|
1089
|
+
withTanstackMetadata(
|
|
1090
|
+
{
|
|
1091
|
+
type: EventType.RUN_FINISHED,
|
|
1092
|
+
threadId: requireSyntheticId(
|
|
1093
|
+
upstreamThreadId ?? runContext?.threadId,
|
|
1094
|
+
'threadId',
|
|
1095
|
+
),
|
|
1096
|
+
runId: requireSyntheticId(
|
|
1097
|
+
upstreamRunId ?? runContext?.runId,
|
|
1098
|
+
'runId',
|
|
1099
|
+
),
|
|
1100
|
+
timestamp: Date.now(),
|
|
1101
|
+
},
|
|
1102
|
+
{ finishReason: 'stop', model: 'connect-wrapper' },
|
|
1103
|
+
) as StreamChunk,
|
|
1104
|
+
runContext?.runId,
|
|
1105
|
+
)
|
|
1082
1106
|
}
|
|
1083
1107
|
} catch (err) {
|
|
1084
1108
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
@@ -2143,9 +2167,9 @@ export function webSocket(
|
|
|
2143
2167
|
}
|
|
2144
2168
|
if (isPingFrame(parsed)) return
|
|
2145
2169
|
const envelopeId = isNdjsonEnvelope(parsed) ? parsed.id : undefined
|
|
2146
|
-
const chunk =
|
|
2147
|
-
? parsed.chunk
|
|
2148
|
-
|
|
2170
|
+
const chunk = restoreInboundUsage(
|
|
2171
|
+
isNdjsonEnvelope(parsed) ? parsed.chunk : (parsed as StreamChunk),
|
|
2172
|
+
)
|
|
2149
2173
|
|
|
2150
2174
|
// Thread durable chunks through the active run session's tracker (if
|
|
2151
2175
|
// any) so a later reconnect knows the last offset and can skip a
|
|
@@ -2325,7 +2349,9 @@ export function webSocket(
|
|
|
2325
2349
|
}
|
|
2326
2350
|
if (isPingFrame(parsed)) return
|
|
2327
2351
|
pipe.push(
|
|
2328
|
-
|
|
2352
|
+
restoreInboundUsage(
|
|
2353
|
+
isNdjsonEnvelope(parsed) ? parsed.chunk : (parsed as StreamChunk),
|
|
2354
|
+
),
|
|
2329
2355
|
)
|
|
2330
2356
|
}
|
|
2331
2357
|
ws.onclose = (event?: CloseEvent) => {
|
package/src/generation-client.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from './generation-types'
|
|
11
11
|
import { createNoOpGenerationDevtoolsBridge } from './devtools-noop'
|
|
12
12
|
import { parseSSEResponse } from './sse-parser'
|
|
13
|
+
import { restoreInboundChunk } from '@tanstack/ai/client'
|
|
13
14
|
import type { StreamChunk } from '@tanstack/ai/client'
|
|
14
15
|
import type {
|
|
15
16
|
ConnectConnectionAdapter,
|
|
@@ -349,9 +350,10 @@ export class GenerationClient<
|
|
|
349
350
|
let streamRunId: string | undefined
|
|
350
351
|
let sawTerminalChunk = false
|
|
351
352
|
|
|
352
|
-
for await (const
|
|
353
|
+
for await (const raw of source) {
|
|
353
354
|
if (signal.aborted) break
|
|
354
355
|
|
|
356
|
+
const chunk = restoreInboundChunk(raw)
|
|
355
357
|
this.callbacksRef.onChunk?.(chunk)
|
|
356
358
|
this.observeResumeSnapshot(chunk)
|
|
357
359
|
const chunkRunId =
|
|
@@ -390,11 +392,9 @@ export class GenerationClient<
|
|
|
390
392
|
this.devtoolsBridge.ensureRunStarted(
|
|
391
393
|
chunkRunId ?? streamRunId ?? fallbackRunId,
|
|
392
394
|
)
|
|
393
|
-
//
|
|
395
|
+
// Spec RUN_ERROR message. Missing message uses this fallback.
|
|
394
396
|
const msg =
|
|
395
|
-
(chunk.message as string | undefined) ||
|
|
396
|
-
chunk.error?.message ||
|
|
397
|
-
'An error occurred'
|
|
397
|
+
(chunk.message as string | undefined) || 'An error occurred'
|
|
398
398
|
throw new Error(msg)
|
|
399
399
|
}
|
|
400
400
|
default:
|
package/src/generation-types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { tanstackMetadata } from '@tanstack/ai/client'
|
|
1
2
|
import type {
|
|
2
3
|
MediaPrompt,
|
|
3
4
|
PersistedArtifactRef,
|
|
@@ -461,8 +462,13 @@ export function updateGenerationResumeSnapshot(
|
|
|
461
462
|
previous: GenerationResumeSnapshot | null | undefined,
|
|
462
463
|
chunk: StreamChunk,
|
|
463
464
|
): GenerationResumeSnapshot {
|
|
464
|
-
const
|
|
465
|
-
const
|
|
465
|
+
const tanstack = tanstackMetadata(chunk)
|
|
466
|
+
const threadId =
|
|
467
|
+
stringField(chunk, 'threadId') ??
|
|
468
|
+
(typeof tanstack?.threadId === 'string' ? tanstack.threadId : undefined)
|
|
469
|
+
const runId =
|
|
470
|
+
stringField(chunk, 'runId') ??
|
|
471
|
+
(typeof tanstack?.runId === 'string' ? tanstack.runId : undefined)
|
|
466
472
|
const carried = chunk.type === 'RUN_STARTED' ? undefined : previous
|
|
467
473
|
const previousArtifacts = carried?.pendingArtifacts ?? []
|
|
468
474
|
const next: GenerationResumeSnapshot = {
|
package/src/types.ts
CHANGED
|
@@ -397,6 +397,18 @@ export interface MultimodalContent {
|
|
|
397
397
|
* If not provided, a unique ID will be generated.
|
|
398
398
|
*/
|
|
399
399
|
id?: string
|
|
400
|
+
/**
|
|
401
|
+
* Optional AG-UI metadata bag copied onto the resulting UIMessage.
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```ts
|
|
405
|
+
* await client.sendMessage({
|
|
406
|
+
* content: 'Show me failed logins',
|
|
407
|
+
* metadata: { author: { id: 'user-42', name: 'Dana' } },
|
|
408
|
+
* })
|
|
409
|
+
* ```
|
|
410
|
+
*/
|
|
411
|
+
metadata?: Record<string, any>
|
|
400
412
|
}
|
|
401
413
|
|
|
402
414
|
/**
|
|
@@ -616,6 +628,11 @@ export interface UIMessage<
|
|
|
616
628
|
role: 'system' | 'user' | 'assistant'
|
|
617
629
|
parts: Array<MessagePart<TTools, TData>>
|
|
618
630
|
createdAt?: Date
|
|
631
|
+
/**
|
|
632
|
+
* Optional AG-UI metadata bag. TanStack writes the `tanstack` key.
|
|
633
|
+
* User keys stay at the top.
|
|
634
|
+
*/
|
|
635
|
+
metadata?: Record<string, any>
|
|
619
636
|
}
|
|
620
637
|
|
|
621
638
|
/**
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from './generation-types'
|
|
11
11
|
import { createNoOpVideoDevtoolsBridge } from './devtools-noop'
|
|
12
12
|
import { parseSSEResponse } from './sse-parser'
|
|
13
|
+
import { restoreInboundChunk } from '@tanstack/ai/client'
|
|
13
14
|
import type { StreamChunk } from '@tanstack/ai/client'
|
|
14
15
|
import type {
|
|
15
16
|
ConnectConnectionAdapter,
|
|
@@ -343,9 +344,10 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
343
344
|
let streamRunId: string | undefined
|
|
344
345
|
let sawTerminalChunk = false
|
|
345
346
|
|
|
346
|
-
for await (const
|
|
347
|
+
for await (const raw of source) {
|
|
347
348
|
if (signal.aborted) break
|
|
348
349
|
|
|
350
|
+
const chunk = restoreInboundChunk(raw)
|
|
349
351
|
this.callbacksRef.onChunk?.(chunk)
|
|
350
352
|
this.observeResumeSnapshot(chunk)
|
|
351
353
|
const chunkRunId =
|
|
@@ -395,11 +397,9 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
395
397
|
this.devtoolsBridge.ensureRunStarted(
|
|
396
398
|
chunkRunId ?? streamRunId ?? fallbackRunId,
|
|
397
399
|
)
|
|
398
|
-
//
|
|
400
|
+
// Spec RUN_ERROR message. Missing message uses this fallback.
|
|
399
401
|
const msg =
|
|
400
|
-
(chunk.message as string | undefined) ||
|
|
401
|
-
chunk.error?.message ||
|
|
402
|
-
'An error occurred'
|
|
402
|
+
(chunk.message as string | undefined) || 'An error occurred'
|
|
403
403
|
throw new Error(msg)
|
|
404
404
|
}
|
|
405
405
|
default:
|