@tanstack/ai-client 0.25.2 → 0.28.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/byok/client.d.ts +52 -0
- package/dist/esm/byok/client.js +300 -0
- package/dist/esm/byok/client.js.map +1 -0
- package/dist/esm/byok/passkey.d.ts +43 -0
- package/dist/esm/byok/passkey.js +251 -0
- package/dist/esm/byok/passkey.js.map +1 -0
- package/dist/esm/byok/resolve.d.ts +8 -0
- package/dist/esm/byok/resolve.js +20 -0
- package/dist/esm/byok/resolve.js.map +1 -0
- package/dist/esm/byok/storage.d.ts +15 -0
- package/dist/esm/byok/storage.js +16 -0
- package/dist/esm/byok/storage.js.map +1 -0
- package/dist/esm/byok.d.ts +8 -0
- package/dist/esm/byok.js +5 -0
- package/dist/esm/chat-client.d.ts +18 -5
- package/dist/esm/chat-client.js +96 -59
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +2 -0
- package/dist/esm/connection-adapters.js +70 -30
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/generation-client.d.ts +3 -1
- package/dist/esm/generation-client.js +27 -6
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.d.ts +14 -0
- package/dist/esm/generation-types.js +4 -2
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/interrupt-manager.d.ts +1 -0
- package/dist/esm/interrupt-manager.js +15 -6
- package/dist/esm/interrupt-manager.js.map +1 -1
- package/dist/esm/sse-parser.js +8 -1
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/types.d.ts +46 -3
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +3 -1
- package/dist/esm/video-generation-client.js +29 -8
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +7 -3
- package/src/byok/client.ts +360 -0
- package/src/byok/passkey.ts +399 -0
- package/src/byok/resolve.ts +30 -0
- package/src/byok/storage.ts +29 -0
- package/src/byok.ts +20 -0
- package/src/chat-client.ts +138 -77
- package/src/connection-adapters.ts +108 -53
- package/src/generation-client.ts +53 -9
- package/src/generation-types.ts +24 -2
- package/src/interrupt-manager.ts +20 -6
- package/src/sse-parser.ts +10 -0
- package/src/types.ts +48 -3
- package/src/video-generation-client.ts +50 -9
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
EventType,
|
|
3
|
+
getChunkRunId as getNormalizedChunkRunId,
|
|
4
|
+
restoreInboundChunk,
|
|
5
|
+
tanstackMetadata,
|
|
6
|
+
uiMessagesToWire,
|
|
7
|
+
withTanstackMetadata,
|
|
8
|
+
} from '@tanstack/ai/client'
|
|
9
|
+
import { ByokMissingError, isByokMissingBody } from '@tanstack/ai/byok'
|
|
2
10
|
import {
|
|
3
11
|
createResponseStreamTextDecoder,
|
|
4
12
|
getResponseStreamReader,
|
|
@@ -8,7 +16,6 @@ import type {
|
|
|
8
16
|
ModelMessage,
|
|
9
17
|
RunAgentResumeItem,
|
|
10
18
|
RunErrorEvent,
|
|
11
|
-
RunFinishedEvent,
|
|
12
19
|
StreamChunk,
|
|
13
20
|
UIMessage,
|
|
14
21
|
} from '@tanstack/ai/client'
|
|
@@ -34,12 +41,7 @@ export function getChunkRunId(chunk: StreamChunk): string | undefined {
|
|
|
34
41
|
// the client's run identity to win when a provider stamps its own id; for
|
|
35
42
|
// resumable reconnect/join the two ids match, so precedence is moot there.
|
|
36
43
|
const requestRunId = chunkRunIds.get(chunk)
|
|
37
|
-
return (
|
|
38
|
-
requestRunId ??
|
|
39
|
-
('runId' in chunk && typeof chunk.runId === 'string'
|
|
40
|
-
? chunk.runId
|
|
41
|
-
: undefined)
|
|
42
|
-
)
|
|
44
|
+
return requestRunId ?? getNormalizedChunkRunId(chunk)
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/**
|
|
@@ -385,6 +387,22 @@ function isNdjsonEnvelope(
|
|
|
385
387
|
)
|
|
386
388
|
}
|
|
387
389
|
|
|
390
|
+
/** Rebuild pre-wire extras after SSE/NDJSON ingest. */
|
|
391
|
+
function restoreInboundUsage(chunk: StreamChunk): StreamChunk {
|
|
392
|
+
return restoreInboundChunk(chunk)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function sseChunkModel(chunk: StreamChunk): string | undefined {
|
|
396
|
+
const tanstackModel = tanstackMetadata(chunk)?.model
|
|
397
|
+
if (typeof tanstackModel === 'string') return tanstackModel
|
|
398
|
+
const usage = 'usage' in chunk ? chunk.usage : undefined
|
|
399
|
+
if (Array.isArray(usage)) {
|
|
400
|
+
const model = (usage[0] as { model?: unknown } | undefined)?.model
|
|
401
|
+
if (typeof model === 'string') return model
|
|
402
|
+
}
|
|
403
|
+
return undefined
|
|
404
|
+
}
|
|
405
|
+
|
|
388
406
|
/**
|
|
389
407
|
* Parse SSE-format lines into stream events, pairing each chunk with the `id:`
|
|
390
408
|
* offset of the event it arrived on. Shared by the fetch- and XHR-backed SSE
|
|
@@ -431,27 +449,31 @@ async function* linesToSSEEvents(
|
|
|
431
449
|
}
|
|
432
450
|
const data = parseSseDataLine(line)
|
|
433
451
|
if (data === '[DONE]') {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
452
|
+
yield {
|
|
453
|
+
chunk: withTanstackMetadata(
|
|
454
|
+
{
|
|
455
|
+
type: EventType.RUN_FINISHED,
|
|
456
|
+
threadId: lastThreadId ?? fallbackIds?.threadId ?? '',
|
|
457
|
+
runId: lastRunId ?? fallbackIds?.runId ?? '',
|
|
458
|
+
timestamp: Date.now(),
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
finishReason: 'stop',
|
|
462
|
+
...(lastModel !== undefined ? { model: lastModel } : {}),
|
|
463
|
+
},
|
|
464
|
+
) as StreamChunk,
|
|
441
465
|
}
|
|
442
|
-
yield { chunk: synthetic }
|
|
443
466
|
return
|
|
444
467
|
}
|
|
445
|
-
const chunk = JSON.parse(data) as StreamChunk
|
|
468
|
+
const chunk = restoreInboundUsage(JSON.parse(data) as StreamChunk)
|
|
446
469
|
if ('threadId' in chunk && typeof chunk.threadId === 'string') {
|
|
447
470
|
lastThreadId = chunk.threadId
|
|
448
471
|
}
|
|
449
472
|
if ('runId' in chunk && typeof chunk.runId === 'string') {
|
|
450
473
|
lastRunId = chunk.runId
|
|
451
474
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
475
|
+
const model = sseChunkModel(chunk)
|
|
476
|
+
if (model !== undefined) lastModel = model
|
|
455
477
|
const id = pendingId
|
|
456
478
|
pendingId = undefined
|
|
457
479
|
yield { chunk, ...(id !== undefined ? { id } : {}) }
|
|
@@ -470,19 +492,42 @@ async function* linesToNdjsonEvents(
|
|
|
470
492
|
for await (const line of lines) {
|
|
471
493
|
const parsed = JSON.parse(line) as unknown
|
|
472
494
|
if (isNdjsonEnvelope(parsed)) {
|
|
473
|
-
yield { chunk: parsed.chunk, id: parsed.id }
|
|
495
|
+
yield { chunk: restoreInboundUsage(parsed.chunk), id: parsed.id }
|
|
474
496
|
} else {
|
|
475
|
-
yield { chunk: parsed as StreamChunk }
|
|
497
|
+
yield { chunk: restoreInboundUsage(parsed as StreamChunk) }
|
|
476
498
|
}
|
|
477
499
|
}
|
|
478
500
|
}
|
|
479
501
|
|
|
480
|
-
function assertResponseOk(response: Response): void {
|
|
481
|
-
if (
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
502
|
+
async function assertResponseOk(response: Response): Promise<void> {
|
|
503
|
+
if (response.ok) return
|
|
504
|
+
if (response.status === 401) {
|
|
505
|
+
const body: unknown = await response
|
|
506
|
+
.clone()
|
|
507
|
+
.json()
|
|
508
|
+
.catch(() => null)
|
|
509
|
+
if (isByokMissingBody(body)) {
|
|
510
|
+
throw new ByokMissingError(body.error.provider)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
throw new Error(
|
|
514
|
+
`HTTP error! status: ${response.status} ${response.statusText}`,
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function errorFromXhrStatus(xhr: XMLHttpRequest): Error {
|
|
519
|
+
if (xhr.status === 401) {
|
|
520
|
+
let parsed: unknown = null
|
|
521
|
+
try {
|
|
522
|
+
parsed = JSON.parse(xhr.responseText)
|
|
523
|
+
} catch {
|
|
524
|
+
parsed = null
|
|
525
|
+
}
|
|
526
|
+
if (isByokMissingBody(parsed)) {
|
|
527
|
+
return new ByokMissingError(parsed.error.provider)
|
|
528
|
+
}
|
|
485
529
|
}
|
|
530
|
+
return new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`)
|
|
486
531
|
}
|
|
487
532
|
|
|
488
533
|
/**
|
|
@@ -504,7 +549,7 @@ async function fetchThreadHydration(
|
|
|
504
549
|
headers: { Accept: 'application/json', ...headers },
|
|
505
550
|
credentials,
|
|
506
551
|
})
|
|
507
|
-
assertResponseOk(response)
|
|
552
|
+
await assertResponseOk(response)
|
|
508
553
|
const data = (await response.json()) as {
|
|
509
554
|
messages?: Array<UIMessage>
|
|
510
555
|
activeRun?: { runId?: unknown } | null
|
|
@@ -553,7 +598,7 @@ async function fetchGenerationHydration(
|
|
|
553
598
|
headers: { Accept: 'application/json', ...headers },
|
|
554
599
|
credentials,
|
|
555
600
|
})
|
|
556
|
-
assertResponseOk(response)
|
|
601
|
+
await assertResponseOk(response)
|
|
557
602
|
const raw: unknown = await response.json()
|
|
558
603
|
// A 200 carrying `null` is a legitimate hydration miss — the server has no
|
|
559
604
|
// record for this thread — and reading `.activeRun` off `null` would throw.
|
|
@@ -588,7 +633,7 @@ async function* responseToSSEEvents(
|
|
|
588
633
|
abortSignal?: AbortSignal,
|
|
589
634
|
fallbackIds?: { threadId?: string; runId?: string },
|
|
590
635
|
): AsyncGenerator<StreamEvent> {
|
|
591
|
-
assertResponseOk(response)
|
|
636
|
+
await assertResponseOk(response)
|
|
592
637
|
const reader = getResponseStreamReader(response)
|
|
593
638
|
yield* linesToSSEEvents(readStreamLines(reader, abortSignal), fallbackIds)
|
|
594
639
|
}
|
|
@@ -598,7 +643,7 @@ async function* responseToNdjsonEvents(
|
|
|
598
643
|
response: Response,
|
|
599
644
|
abortSignal?: AbortSignal,
|
|
600
645
|
): AsyncGenerator<StreamEvent> {
|
|
601
|
-
assertResponseOk(response)
|
|
646
|
+
await assertResponseOk(response)
|
|
602
647
|
const reader = getResponseStreamReader(response)
|
|
603
648
|
yield* linesToNdjsonEvents(readStreamLines(reader, abortSignal))
|
|
604
649
|
}
|
|
@@ -788,6 +833,8 @@ export interface RunAgentInputContext {
|
|
|
788
833
|
}>
|
|
789
834
|
/** Arbitrary user-controlled passthrough data. */
|
|
790
835
|
forwardedProps?: Record<string, unknown>
|
|
836
|
+
/** Extra request headers for this run (e.g. BYOK keys). POST only. */
|
|
837
|
+
headers?: Record<string, string>
|
|
791
838
|
}
|
|
792
839
|
|
|
793
840
|
export interface ConnectConnectionAdapter {
|
|
@@ -1064,21 +1111,24 @@ export function normalizeConnectionAdapter(
|
|
|
1064
1111
|
// observed, but stamp the caller's request runId so getChunkRunId()
|
|
1065
1112
|
// correlates to activeRunIds / currentRunId (same as real stream chunks).
|
|
1066
1113
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1114
|
+
push(
|
|
1115
|
+
withTanstackMetadata(
|
|
1116
|
+
{
|
|
1117
|
+
type: EventType.RUN_FINISHED,
|
|
1118
|
+
threadId: requireSyntheticId(
|
|
1119
|
+
upstreamThreadId ?? runContext?.threadId,
|
|
1120
|
+
'threadId',
|
|
1121
|
+
),
|
|
1122
|
+
runId: requireSyntheticId(
|
|
1123
|
+
upstreamRunId ?? runContext?.runId,
|
|
1124
|
+
'runId',
|
|
1125
|
+
),
|
|
1126
|
+
timestamp: Date.now(),
|
|
1127
|
+
},
|
|
1128
|
+
{ finishReason: 'stop', model: 'connect-wrapper' },
|
|
1129
|
+
) as StreamChunk,
|
|
1130
|
+
runContext?.runId,
|
|
1131
|
+
)
|
|
1082
1132
|
}
|
|
1083
1133
|
} catch (err) {
|
|
1084
1134
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
@@ -1248,6 +1298,7 @@ export function fetchServerSentEvents(
|
|
|
1248
1298
|
const requestHeaders: Record<string, string> = {
|
|
1249
1299
|
'Content-Type': 'application/json',
|
|
1250
1300
|
...mergeHeaders(resolvedOptions.headers),
|
|
1301
|
+
...mergeHeaders(runContext?.headers),
|
|
1251
1302
|
...runIdHeader(runContext?.runId),
|
|
1252
1303
|
}
|
|
1253
1304
|
|
|
@@ -1419,6 +1470,7 @@ export function fetchHttpStream(
|
|
|
1419
1470
|
const requestHeaders: Record<string, string> = {
|
|
1420
1471
|
'Content-Type': 'application/json',
|
|
1421
1472
|
...mergeHeaders(resolvedOptions.headers),
|
|
1473
|
+
...mergeHeaders(runContext?.headers),
|
|
1422
1474
|
...runIdHeader(runContext?.runId),
|
|
1423
1475
|
}
|
|
1424
1476
|
|
|
@@ -1572,7 +1624,7 @@ function readXhrLines(
|
|
|
1572
1624
|
|
|
1573
1625
|
const enqueueDelta = () => {
|
|
1574
1626
|
if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) {
|
|
1575
|
-
error =
|
|
1627
|
+
error = errorFromXhrStatus(xhr)
|
|
1576
1628
|
done = true
|
|
1577
1629
|
return
|
|
1578
1630
|
}
|
|
@@ -1601,7 +1653,7 @@ function readXhrLines(
|
|
|
1601
1653
|
// is an error, but status 0 here is not — treat the trailing buffer as a
|
|
1602
1654
|
// truncation check instead of fabricating a bogus "status: 0" error.
|
|
1603
1655
|
if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) {
|
|
1604
|
-
error =
|
|
1656
|
+
error = errorFromXhrStatus(xhr)
|
|
1605
1657
|
} else if (buffer.trim() && !aborted) {
|
|
1606
1658
|
error = new StreamTruncatedError()
|
|
1607
1659
|
}
|
|
@@ -1697,6 +1749,7 @@ function createConfiguredXhrRequest(
|
|
|
1697
1749
|
const requestHeaders: Record<string, string> = {
|
|
1698
1750
|
'Content-Type': 'application/json',
|
|
1699
1751
|
...mergeHeaders(options.headers),
|
|
1752
|
+
...mergeHeaders(method === 'POST' ? runContext?.headers : undefined),
|
|
1700
1753
|
// Client-chosen run id for durability (POST only; the GET join carries it
|
|
1701
1754
|
// in the query instead).
|
|
1702
1755
|
...(method === 'POST' ? runIdHeader(runContext?.runId) : {}),
|
|
@@ -2143,9 +2196,9 @@ export function webSocket(
|
|
|
2143
2196
|
}
|
|
2144
2197
|
if (isPingFrame(parsed)) return
|
|
2145
2198
|
const envelopeId = isNdjsonEnvelope(parsed) ? parsed.id : undefined
|
|
2146
|
-
const chunk =
|
|
2147
|
-
? parsed.chunk
|
|
2148
|
-
|
|
2199
|
+
const chunk = restoreInboundUsage(
|
|
2200
|
+
isNdjsonEnvelope(parsed) ? parsed.chunk : (parsed as StreamChunk),
|
|
2201
|
+
)
|
|
2149
2202
|
|
|
2150
2203
|
// Thread durable chunks through the active run session's tracker (if
|
|
2151
2204
|
// any) so a later reconnect knows the last offset and can skip a
|
|
@@ -2325,7 +2378,9 @@ export function webSocket(
|
|
|
2325
2378
|
}
|
|
2326
2379
|
if (isPingFrame(parsed)) return
|
|
2327
2380
|
pipe.push(
|
|
2328
|
-
|
|
2381
|
+
restoreInboundUsage(
|
|
2382
|
+
isNdjsonEnvelope(parsed) ? parsed.chunk : (parsed as StreamChunk),
|
|
2383
|
+
),
|
|
2329
2384
|
)
|
|
2330
2385
|
}
|
|
2331
2386
|
ws.onclose = (event?: CloseEvent) => {
|
|
@@ -2468,7 +2523,7 @@ export function fetcherToConnectionAdapter(
|
|
|
2468
2523
|
? { resume: runContext.resume }
|
|
2469
2524
|
: {}),
|
|
2470
2525
|
},
|
|
2471
|
-
{ signal: abortSignal },
|
|
2526
|
+
{ signal: abortSignal, headers: runContext.headers },
|
|
2472
2527
|
)
|
|
2473
2528
|
if (result instanceof Response) {
|
|
2474
2529
|
yield* responseToSSEChunks(result, abortSignal)
|
package/src/generation-client.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { ByokBlockedError, ByokMissingError } from '@tanstack/ai/byok'
|
|
2
|
+
import {
|
|
3
|
+
prepareResolvedByokHeaders,
|
|
4
|
+
resolveByokProviderId,
|
|
5
|
+
} from './byok/resolve'
|
|
1
6
|
import {
|
|
2
7
|
GENERATION_EVENTS,
|
|
3
8
|
GENERATION_STREAM_TRUNCATED_MESSAGE,
|
|
@@ -10,7 +15,9 @@ import {
|
|
|
10
15
|
} from './generation-types'
|
|
11
16
|
import { createNoOpGenerationDevtoolsBridge } from './devtools-noop'
|
|
12
17
|
import { parseSSEResponse } from './sse-parser'
|
|
18
|
+
import { restoreInboundChunk } from '@tanstack/ai/client'
|
|
13
19
|
import type { StreamChunk } from '@tanstack/ai/client'
|
|
20
|
+
import type { ByokClient } from './byok'
|
|
14
21
|
import type {
|
|
15
22
|
ConnectConnectionAdapter,
|
|
16
23
|
GenerationHydrationResult,
|
|
@@ -119,6 +126,8 @@ export class GenerationClient<
|
|
|
119
126
|
// the client hydrates the last generation for `threadId` from the server.
|
|
120
127
|
private readonly serverDriven: boolean = false
|
|
121
128
|
private body: Record<string, any>
|
|
129
|
+
private byok: ByokClient | undefined
|
|
130
|
+
private byokProvider: (() => string | undefined) | undefined
|
|
122
131
|
private result: TOutput | null = null
|
|
123
132
|
private input: TInput | null = null
|
|
124
133
|
private progress: AIDevtoolsGenerationProgress | null = null
|
|
@@ -155,6 +164,8 @@ export class GenerationClient<
|
|
|
155
164
|
this.hydrateGenerationHandler = options.hydrateGeneration
|
|
156
165
|
this.joinRunHandler = options.joinRun
|
|
157
166
|
this.body = options.body ?? {}
|
|
167
|
+
this.byok = options.byok
|
|
168
|
+
this.byokProvider = options.byokProvider
|
|
158
169
|
// `persistence` is `false`/omitted (ephemeral) or `true` (server-driven:
|
|
159
170
|
// hydrate the last generation for `threadId` from the server on mount).
|
|
160
171
|
this.serverDriven = options.persistence === true
|
|
@@ -264,9 +275,21 @@ export class GenerationClient<
|
|
|
264
275
|
const { signal } = abortController
|
|
265
276
|
|
|
266
277
|
try {
|
|
278
|
+
let headers: Record<string, string> | undefined
|
|
279
|
+
if (this.byok) {
|
|
280
|
+
const provider = resolveByokProviderId(
|
|
281
|
+
this.byokProvider,
|
|
282
|
+
this.body.provider,
|
|
283
|
+
)
|
|
284
|
+
headers = await prepareResolvedByokHeaders(this.byok, provider)
|
|
285
|
+
}
|
|
286
|
+
|
|
267
287
|
if (this.fetcher) {
|
|
268
288
|
// Direct fetch path
|
|
269
|
-
const result = await this.fetcher(
|
|
289
|
+
const result = await this.fetcher(
|
|
290
|
+
input,
|
|
291
|
+
headers === undefined ? { signal } : { signal, headers },
|
|
292
|
+
)
|
|
270
293
|
if (signal.aborted) return
|
|
271
294
|
if (result instanceof Response) {
|
|
272
295
|
// Server function returned SSE Response — parse stream
|
|
@@ -288,7 +311,7 @@ export class GenerationClient<
|
|
|
288
311
|
[],
|
|
289
312
|
mergedData,
|
|
290
313
|
signal,
|
|
291
|
-
this.createRunContext(runId),
|
|
314
|
+
this.createRunContext(runId, headers),
|
|
292
315
|
)
|
|
293
316
|
await this.processStream(stream, runId, signal)
|
|
294
317
|
} else {
|
|
@@ -311,6 +334,12 @@ export class GenerationClient<
|
|
|
311
334
|
} catch (err: unknown) {
|
|
312
335
|
if (signal.aborted) return
|
|
313
336
|
const error = err instanceof Error ? err : new Error(String(err))
|
|
337
|
+
if (error instanceof ByokMissingError) {
|
|
338
|
+
this.byok?.request(error.provider, 'missing')
|
|
339
|
+
}
|
|
340
|
+
if (error instanceof ByokBlockedError && error.reason === 'locked') {
|
|
341
|
+
this.byok?.request(error.provider, 'locked')
|
|
342
|
+
}
|
|
314
343
|
this.setError(error)
|
|
315
344
|
this.setStatus('error')
|
|
316
345
|
this.recordResumeSnapshotError(error)
|
|
@@ -349,9 +378,10 @@ export class GenerationClient<
|
|
|
349
378
|
let streamRunId: string | undefined
|
|
350
379
|
let sawTerminalChunk = false
|
|
351
380
|
|
|
352
|
-
for await (const
|
|
381
|
+
for await (const raw of source) {
|
|
353
382
|
if (signal.aborted) break
|
|
354
383
|
|
|
384
|
+
const chunk = restoreInboundChunk(raw)
|
|
355
385
|
this.callbacksRef.onChunk?.(chunk)
|
|
356
386
|
this.observeResumeSnapshot(chunk)
|
|
357
387
|
const chunkRunId =
|
|
@@ -390,11 +420,9 @@ export class GenerationClient<
|
|
|
390
420
|
this.devtoolsBridge.ensureRunStarted(
|
|
391
421
|
chunkRunId ?? streamRunId ?? fallbackRunId,
|
|
392
422
|
)
|
|
393
|
-
//
|
|
423
|
+
// Spec RUN_ERROR message. Missing message uses this fallback.
|
|
394
424
|
const msg =
|
|
395
|
-
(chunk.message as string | undefined) ||
|
|
396
|
-
chunk.error?.message ||
|
|
397
|
-
'An error occurred'
|
|
425
|
+
(chunk.message as string | undefined) || 'An error occurred'
|
|
398
426
|
throw new Error(msg)
|
|
399
427
|
}
|
|
400
428
|
default:
|
|
@@ -462,13 +490,25 @@ export class GenerationClient<
|
|
|
462
490
|
options: Partial<
|
|
463
491
|
Pick<
|
|
464
492
|
GenerationClientOptions<TInput, TResult, TOutput>,
|
|
465
|
-
|
|
493
|
+
| 'body'
|
|
494
|
+
| 'byok'
|
|
495
|
+
| 'byokProvider'
|
|
496
|
+
| 'onResult'
|
|
497
|
+
| 'onError'
|
|
498
|
+
| 'onProgress'
|
|
499
|
+
| 'onChunk'
|
|
466
500
|
>
|
|
467
501
|
>,
|
|
468
502
|
): void {
|
|
469
503
|
if (options.body !== undefined) {
|
|
470
504
|
this.body = options.body ?? {}
|
|
471
505
|
}
|
|
506
|
+
if (options.byok !== undefined) {
|
|
507
|
+
this.byok = options.byok
|
|
508
|
+
}
|
|
509
|
+
if (options.byokProvider !== undefined) {
|
|
510
|
+
this.byokProvider = options.byokProvider
|
|
511
|
+
}
|
|
472
512
|
if (options.onResult !== undefined) {
|
|
473
513
|
this.callbacksRef.onResult = options.onResult
|
|
474
514
|
}
|
|
@@ -645,10 +685,14 @@ export class GenerationClient<
|
|
|
645
685
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`
|
|
646
686
|
}
|
|
647
687
|
|
|
648
|
-
private createRunContext(
|
|
688
|
+
private createRunContext(
|
|
689
|
+
runId: string,
|
|
690
|
+
headers?: Record<string, string>,
|
|
691
|
+
): RunAgentInputContext {
|
|
649
692
|
return {
|
|
650
693
|
threadId: this.threadId,
|
|
651
694
|
runId,
|
|
695
|
+
...(headers ? { headers } : {}),
|
|
652
696
|
}
|
|
653
697
|
}
|
|
654
698
|
|
package/src/generation-types.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { tanstackMetadata } from '@tanstack/ai/client'
|
|
1
2
|
import type {
|
|
2
3
|
MediaPrompt,
|
|
3
4
|
PersistedArtifactRef,
|
|
4
5
|
StreamChunk,
|
|
5
6
|
} from '@tanstack/ai/client'
|
|
6
7
|
import type { TokenUsage, TranscriptionResponseFormat } from '@tanstack/ai'
|
|
8
|
+
import type { ByokClient } from './byok'
|
|
7
9
|
import type { ConnectConnectionAdapter } from './connection-adapters'
|
|
8
10
|
import type { AIDevtoolsClientMetadata } from './devtools'
|
|
9
11
|
import type {
|
|
@@ -270,6 +272,8 @@ export const GENERATION_EVENTS = {
|
|
|
270
272
|
export interface GenerationFetcherOptions {
|
|
271
273
|
/** AbortSignal that is triggered when the user calls `stop()` */
|
|
272
274
|
signal: AbortSignal
|
|
275
|
+
/** Extra request headers for this run (e.g. BYOK keys). */
|
|
276
|
+
headers?: Record<string, string>
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
/**
|
|
@@ -337,6 +341,19 @@ export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
|
|
|
337
341
|
/** Additional body parameters to send with connect-based adapter requests */
|
|
338
342
|
body?: Record<string, any>
|
|
339
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Optional BYOK keyring. On each generate the client prepares the resolved
|
|
346
|
+
* provider and stamps `x-byok-*` request headers. Keys never go in the body.
|
|
347
|
+
*/
|
|
348
|
+
byok?: ByokClient
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Optional provider id for this generation. If it returns a provider slug,
|
|
352
|
+
* only that key is prepared and sent. Otherwise `body.provider` is used.
|
|
353
|
+
* If no slug resolves, generate throws instead of attaching every stored key.
|
|
354
|
+
*/
|
|
355
|
+
byokProvider?: () => string | undefined
|
|
356
|
+
|
|
340
357
|
/** Metadata used to register this generation hook with TanStack AI Devtools */
|
|
341
358
|
devtools?: Partial<AIDevtoolsClientMetadata>
|
|
342
359
|
|
|
@@ -461,8 +478,13 @@ export function updateGenerationResumeSnapshot(
|
|
|
461
478
|
previous: GenerationResumeSnapshot | null | undefined,
|
|
462
479
|
chunk: StreamChunk,
|
|
463
480
|
): GenerationResumeSnapshot {
|
|
464
|
-
const
|
|
465
|
-
const
|
|
481
|
+
const tanstack = tanstackMetadata(chunk)
|
|
482
|
+
const threadId =
|
|
483
|
+
stringField(chunk, 'threadId') ??
|
|
484
|
+
(typeof tanstack?.threadId === 'string' ? tanstack.threadId : undefined)
|
|
485
|
+
const runId =
|
|
486
|
+
stringField(chunk, 'runId') ??
|
|
487
|
+
(typeof tanstack?.runId === 'string' ? tanstack.runId : undefined)
|
|
466
488
|
const carried = chunk.type === 'RUN_STARTED' ? undefined : previous
|
|
467
489
|
const previousArtifacts = carried?.pendingArtifacts ?? []
|
|
468
490
|
const next: GenerationResumeSnapshot = {
|
package/src/interrupt-manager.ts
CHANGED
|
@@ -102,6 +102,10 @@ interface TransactionToken {
|
|
|
102
102
|
active: boolean
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
interface SubmissionOperation {
|
|
106
|
+
submission: InterruptManagerSubmission
|
|
107
|
+
}
|
|
108
|
+
|
|
105
109
|
interface RuntimeInterruptCheckpoint {
|
|
106
110
|
status: InterruptItemStatus
|
|
107
111
|
resolution?: RunAgentResumeItem
|
|
@@ -511,6 +515,7 @@ export class InterruptManager<
|
|
|
511
515
|
resuming: false,
|
|
512
516
|
})
|
|
513
517
|
private activeTransaction: TransactionToken | undefined
|
|
518
|
+
private activeSubmissionOperation: SubmissionOperation | undefined
|
|
514
519
|
private retrySubmission: InterruptManagerSubmission | undefined
|
|
515
520
|
private resuming = false
|
|
516
521
|
private tools: TTools | undefined
|
|
@@ -544,6 +549,7 @@ export class InterruptManager<
|
|
|
544
549
|
hydration: InterruptManagerHydration,
|
|
545
550
|
source: InterruptManagerChangeSource = 'live',
|
|
546
551
|
): void {
|
|
552
|
+
this.activeSubmissionOperation = undefined
|
|
547
553
|
this.hydration = {
|
|
548
554
|
threadId: hydration.threadId,
|
|
549
555
|
interruptedRunId: hydration.interruptedRunId,
|
|
@@ -597,6 +603,7 @@ export class InterruptManager<
|
|
|
597
603
|
preserveRootErrors?: boolean
|
|
598
604
|
source?: InterruptManagerChangeSource
|
|
599
605
|
}): void {
|
|
606
|
+
this.activeSubmissionOperation = undefined
|
|
600
607
|
this.hydration = undefined
|
|
601
608
|
this.items = []
|
|
602
609
|
this.snapshot = Object.freeze([])
|
|
@@ -1528,25 +1535,32 @@ export class InterruptManager<
|
|
|
1528
1535
|
}
|
|
1529
1536
|
|
|
1530
1537
|
private submitBatch(submission: InterruptManagerSubmission): void {
|
|
1538
|
+
// Track ownership so a superseded submission cannot mutate current state.
|
|
1539
|
+
const operation = { submission }
|
|
1540
|
+
this.activeSubmissionOperation = operation
|
|
1531
1541
|
this.resuming = true
|
|
1532
1542
|
this.retrySubmission = undefined
|
|
1533
1543
|
for (const item of this.items) {
|
|
1534
1544
|
if (isClientOwnedInterrupt(item)) item.status = 'submitting'
|
|
1535
1545
|
}
|
|
1536
1546
|
this.publish()
|
|
1537
|
-
void this.performSubmission(
|
|
1547
|
+
void this.performSubmission(operation)
|
|
1538
1548
|
}
|
|
1539
1549
|
|
|
1540
1550
|
private async performSubmission(
|
|
1541
|
-
|
|
1551
|
+
operation: SubmissionOperation,
|
|
1542
1552
|
): Promise<void> {
|
|
1543
1553
|
try {
|
|
1544
|
-
await this.options.submit(submission)
|
|
1554
|
+
await this.options.submit(operation.submission)
|
|
1545
1555
|
} catch (error) {
|
|
1546
|
-
this.
|
|
1556
|
+
if (this.activeSubmissionOperation !== operation) return
|
|
1557
|
+
this.handleSubmissionFailure(error, operation.submission)
|
|
1547
1558
|
} finally {
|
|
1548
|
-
this.
|
|
1549
|
-
|
|
1559
|
+
if (this.activeSubmissionOperation === operation) {
|
|
1560
|
+
this.activeSubmissionOperation = undefined
|
|
1561
|
+
this.resuming = false
|
|
1562
|
+
this.publish()
|
|
1563
|
+
}
|
|
1550
1564
|
}
|
|
1551
1565
|
}
|
|
1552
1566
|
|
package/src/sse-parser.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ByokMissingError, isByokMissingBody } from '@tanstack/ai/byok'
|
|
1
2
|
import {
|
|
2
3
|
createResponseStreamTextDecoder,
|
|
3
4
|
getResponseStreamReader,
|
|
@@ -51,6 +52,15 @@ export async function* parseSSEResponse(
|
|
|
51
52
|
abortSignal?: AbortSignal,
|
|
52
53
|
): AsyncGenerator<StreamChunk> {
|
|
53
54
|
if (!response.ok) {
|
|
55
|
+
if (response.status === 401) {
|
|
56
|
+
const body: unknown = await response
|
|
57
|
+
.clone()
|
|
58
|
+
.json()
|
|
59
|
+
.catch(() => null)
|
|
60
|
+
if (isByokMissingBody(body)) {
|
|
61
|
+
throw new ByokMissingError(body.error.provider)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
54
64
|
throw new Error(
|
|
55
65
|
`HTTP error! status: ${response.status} ${response.statusText}`,
|
|
56
66
|
)
|