@tanstack/ai-client 0.26.0 → 0.29.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/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 +19 -3
- package/dist/esm/chat-client.js +82 -42
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +2 -0
- package/dist/esm/connection-adapters.js +35 -10
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/generation-client.d.ts +3 -1
- package/dist/esm/generation-client.js +23 -4
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.d.ts +14 -0
- 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/message-date-normalizer.d.ts +3 -0
- package/dist/esm/message-date-normalizer.js +35 -0
- package/dist/esm/message-date-normalizer.js.map +1 -0
- package/dist/esm/sse-parser.js +8 -1
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/storage-adapters.js +13 -1
- package/dist/esm/storage-adapters.js.map +1 -1
- package/dist/esm/types.d.ts +34 -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 +25 -6
- 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 +165 -72
- package/src/connection-adapters.ts +46 -14
- package/src/generation-client.ts +48 -4
- package/src/generation-types.ts +16 -0
- package/src/interrupt-manager.ts +20 -6
- package/src/message-date-normalizer.ts +29 -0
- package/src/sse-parser.ts +10 -0
- package/src/storage-adapters.ts +13 -2
- package/src/types.ts +36 -3
- package/src/video-generation-client.ts +45 -4
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,
|
|
@@ -12,6 +17,7 @@ import { createNoOpGenerationDevtoolsBridge } from './devtools-noop'
|
|
|
12
17
|
import { parseSSEResponse } from './sse-parser'
|
|
13
18
|
import { restoreInboundChunk } from '@tanstack/ai/client'
|
|
14
19
|
import type { StreamChunk } from '@tanstack/ai/client'
|
|
20
|
+
import type { ByokClient } from './byok'
|
|
15
21
|
import type {
|
|
16
22
|
ConnectConnectionAdapter,
|
|
17
23
|
GenerationHydrationResult,
|
|
@@ -120,6 +126,8 @@ export class GenerationClient<
|
|
|
120
126
|
// the client hydrates the last generation for `threadId` from the server.
|
|
121
127
|
private readonly serverDriven: boolean = false
|
|
122
128
|
private body: Record<string, any>
|
|
129
|
+
private byok: ByokClient | undefined
|
|
130
|
+
private byokProvider: (() => string | undefined) | undefined
|
|
123
131
|
private result: TOutput | null = null
|
|
124
132
|
private input: TInput | null = null
|
|
125
133
|
private progress: AIDevtoolsGenerationProgress | null = null
|
|
@@ -156,6 +164,8 @@ export class GenerationClient<
|
|
|
156
164
|
this.hydrateGenerationHandler = options.hydrateGeneration
|
|
157
165
|
this.joinRunHandler = options.joinRun
|
|
158
166
|
this.body = options.body ?? {}
|
|
167
|
+
this.byok = options.byok
|
|
168
|
+
this.byokProvider = options.byokProvider
|
|
159
169
|
// `persistence` is `false`/omitted (ephemeral) or `true` (server-driven:
|
|
160
170
|
// hydrate the last generation for `threadId` from the server on mount).
|
|
161
171
|
this.serverDriven = options.persistence === true
|
|
@@ -265,9 +275,21 @@ export class GenerationClient<
|
|
|
265
275
|
const { signal } = abortController
|
|
266
276
|
|
|
267
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
|
+
|
|
268
287
|
if (this.fetcher) {
|
|
269
288
|
// Direct fetch path
|
|
270
|
-
const result = await this.fetcher(
|
|
289
|
+
const result = await this.fetcher(
|
|
290
|
+
input,
|
|
291
|
+
headers === undefined ? { signal } : { signal, headers },
|
|
292
|
+
)
|
|
271
293
|
if (signal.aborted) return
|
|
272
294
|
if (result instanceof Response) {
|
|
273
295
|
// Server function returned SSE Response — parse stream
|
|
@@ -289,7 +311,7 @@ export class GenerationClient<
|
|
|
289
311
|
[],
|
|
290
312
|
mergedData,
|
|
291
313
|
signal,
|
|
292
|
-
this.createRunContext(runId),
|
|
314
|
+
this.createRunContext(runId, headers),
|
|
293
315
|
)
|
|
294
316
|
await this.processStream(stream, runId, signal)
|
|
295
317
|
} else {
|
|
@@ -312,6 +334,12 @@ export class GenerationClient<
|
|
|
312
334
|
} catch (err: unknown) {
|
|
313
335
|
if (signal.aborted) return
|
|
314
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
|
+
}
|
|
315
343
|
this.setError(error)
|
|
316
344
|
this.setStatus('error')
|
|
317
345
|
this.recordResumeSnapshotError(error)
|
|
@@ -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
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
StreamChunk,
|
|
6
6
|
} from '@tanstack/ai/client'
|
|
7
7
|
import type { TokenUsage, TranscriptionResponseFormat } from '@tanstack/ai'
|
|
8
|
+
import type { ByokClient } from './byok'
|
|
8
9
|
import type { ConnectConnectionAdapter } from './connection-adapters'
|
|
9
10
|
import type { AIDevtoolsClientMetadata } from './devtools'
|
|
10
11
|
import type {
|
|
@@ -271,6 +272,8 @@ export const GENERATION_EVENTS = {
|
|
|
271
272
|
export interface GenerationFetcherOptions {
|
|
272
273
|
/** AbortSignal that is triggered when the user calls `stop()` */
|
|
273
274
|
signal: AbortSignal
|
|
275
|
+
/** Extra request headers for this run (e.g. BYOK keys). */
|
|
276
|
+
headers?: Record<string, string>
|
|
274
277
|
}
|
|
275
278
|
|
|
276
279
|
/**
|
|
@@ -338,6 +341,19 @@ export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
|
|
|
338
341
|
/** Additional body parameters to send with connect-based adapter requests */
|
|
339
342
|
body?: Record<string, any>
|
|
340
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
|
+
|
|
341
357
|
/** Metadata used to register this generation hook with TanStack AI Devtools */
|
|
342
358
|
devtools?: Partial<AIDevtoolsClientMetadata>
|
|
343
359
|
|
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
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { UIMessage } from './types'
|
|
2
|
+
|
|
3
|
+
function validDate(value: unknown): Date | undefined {
|
|
4
|
+
if (value instanceof Date)
|
|
5
|
+
return Number.isNaN(value.getTime()) ? undefined : value
|
|
6
|
+
if (typeof value !== 'string') return undefined
|
|
7
|
+
const date = new Date(value)
|
|
8
|
+
return Number.isNaN(date.getTime()) ? undefined : date
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeMessageDates(message: UIMessage): UIMessage {
|
|
12
|
+
const messageDate = validDate(message.createdAt)
|
|
13
|
+
const parts = message.parts.map((part) => {
|
|
14
|
+
if (part.type !== 'tool-result') return part
|
|
15
|
+
const date = validDate(part.createdAt)
|
|
16
|
+
const { createdAt: _ignored, ...rest } = part
|
|
17
|
+
return date ? { ...rest, createdAt: date } : rest
|
|
18
|
+
})
|
|
19
|
+
const { createdAt: _ignored, ...rest } = message
|
|
20
|
+
return messageDate
|
|
21
|
+
? { ...rest, parts, createdAt: messageDate }
|
|
22
|
+
: { ...rest, parts }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeMessagesDates(
|
|
26
|
+
messages: Array<UIMessage>,
|
|
27
|
+
): Array<UIMessage> {
|
|
28
|
+
return messages.map(normalizeMessageDates)
|
|
29
|
+
}
|
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
|
)
|
package/src/storage-adapters.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ChatPersistedState, ChatStorageAdapter } from './types'
|
|
1
|
+
import type { ChatPersistedState, ChatStorageAdapter, UIMessage } from './types'
|
|
2
|
+
import { normalizeMessagesDates } from './message-date-normalizer'
|
|
2
3
|
|
|
3
4
|
export interface WebStoragePersistenceOptions {
|
|
4
5
|
keyPrefix?: string
|
|
@@ -45,6 +46,16 @@ function stringifyJson(value: ChatPersistedState): string {
|
|
|
45
46
|
return serialized
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function reviveMessageCreatedAt(message: UIMessage): UIMessage {
|
|
50
|
+
return normalizeMessagesDates([message]).at(0) ?? message
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function revivePersistedState(state: ChatPersistedState): ChatPersistedState {
|
|
54
|
+
if (state == null || typeof state !== 'object') return state
|
|
55
|
+
if (!Array.isArray(state.messages)) return state
|
|
56
|
+
return { ...state, messages: state.messages.map(reviveMessageCreatedAt) }
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
function createWebStoragePersistence(
|
|
49
60
|
storageName: 'localStorage' | 'sessionStorage',
|
|
50
61
|
options: WebStoragePersistenceOptions,
|
|
@@ -69,7 +80,7 @@ function createWebStoragePersistence(
|
|
|
69
80
|
return {
|
|
70
81
|
getItem(id) {
|
|
71
82
|
const item = getStorage().getItem(key(id))
|
|
72
|
-
return item === null ? null : deserialize(item)
|
|
83
|
+
return item === null ? null : revivePersistedState(deserialize(item))
|
|
73
84
|
},
|
|
74
85
|
setItem(id, value) {
|
|
75
86
|
getStorage().setItem(key(id), serialize(value))
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
UIResourcePart,
|
|
26
26
|
VideoPart,
|
|
27
27
|
} from '@tanstack/ai/client'
|
|
28
|
+
import type { ByokClient } from './byok'
|
|
28
29
|
import type { ConnectionAdapter } from './connection-adapters'
|
|
29
30
|
import type { AIDevtoolsClientMetadata } from './devtools'
|
|
30
31
|
import type { ChatDevtoolsBridgeFactory } from './devtools-noop'
|
|
@@ -291,6 +292,8 @@ export interface ChatFetcherInput {
|
|
|
291
292
|
export interface ChatFetcherOptions {
|
|
292
293
|
/** Fires when `stop()` is called or the request is superseded. */
|
|
293
294
|
signal: AbortSignal
|
|
295
|
+
/** Extra request headers for this run (e.g. BYOK keys). */
|
|
296
|
+
headers?: Record<string, string>
|
|
294
297
|
}
|
|
295
298
|
|
|
296
299
|
/**
|
|
@@ -474,9 +477,9 @@ export interface QueueConfig {
|
|
|
474
477
|
* function form (no `batch` via function). Per-call `sendOptions.whenBusy`
|
|
475
478
|
* overrides the strategy for that send.
|
|
476
479
|
*
|
|
477
|
-
* Actions match {@link WhenBusy}
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
+
* Actions match {@link WhenBusy}. Concurrent streams are not supported.
|
|
481
|
+
* `pending.id` is the id that will be stored if the action is `'queue'`
|
|
482
|
+
* (safe to pass to `cancelQueued`).
|
|
480
483
|
*/
|
|
481
484
|
export type QueueStrategy = (ctx: {
|
|
482
485
|
pending: QueuedMessage
|
|
@@ -491,6 +494,16 @@ export type QueueOption = WhenBusy | QueueConfig | QueueStrategy
|
|
|
491
494
|
export interface SendMessageOptions {
|
|
492
495
|
/** Overrides the configured `whenBusy` for this one send. */
|
|
493
496
|
whenBusy?: WhenBusy
|
|
497
|
+
/**
|
|
498
|
+
* Extra JSON merged into this request's wire `forwardedProps`.
|
|
499
|
+
* Shallow merge: `{ ...chatBody, ...positionalBody, ...body }`.
|
|
500
|
+
* This field wins on key collisions.
|
|
501
|
+
*
|
|
502
|
+
* Framework hooks (`useChat`, `injectChat`, `createChat`) expose
|
|
503
|
+
* `sendMessage(content, options)` with no positional body, so this field
|
|
504
|
+
* is the per-call body channel on those surfaces.
|
|
505
|
+
*/
|
|
506
|
+
body?: Record<string, any>
|
|
494
507
|
}
|
|
495
508
|
|
|
496
509
|
/**
|
|
@@ -582,10 +595,14 @@ export type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> =
|
|
|
582
595
|
|
|
583
596
|
export interface ToolResultPart {
|
|
584
597
|
type: 'tool-result'
|
|
598
|
+
id?: string
|
|
599
|
+
name?: string
|
|
585
600
|
toolCallId: string
|
|
586
601
|
content: string | Array<ContentPart>
|
|
587
602
|
state: ToolResultState
|
|
588
603
|
error?: string // Error message if state is "error"
|
|
604
|
+
metadata?: Record<string, unknown>
|
|
605
|
+
createdAt?: Date
|
|
589
606
|
}
|
|
590
607
|
|
|
591
608
|
export interface ThinkingPart {
|
|
@@ -626,6 +643,7 @@ export interface UIMessage<
|
|
|
626
643
|
> {
|
|
627
644
|
id: string
|
|
628
645
|
role: 'system' | 'user' | 'assistant'
|
|
646
|
+
name?: string
|
|
629
647
|
parts: Array<MessagePart<TTools, TData>>
|
|
630
648
|
createdAt?: Date
|
|
631
649
|
/**
|
|
@@ -862,6 +880,21 @@ export interface ChatClientBaseOptions<
|
|
|
862
880
|
*/
|
|
863
881
|
body?: Record<string, any>
|
|
864
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Optional BYOK keyring. On each send the client prepares the resolved
|
|
885
|
+
* provider and stamps `x-byok-*` request headers. Keys never go in the body.
|
|
886
|
+
*/
|
|
887
|
+
byok?: ByokClient
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Optional provider id for this chat. If it returns a provider slug,
|
|
891
|
+
* only that key is prepared and sent. Otherwise the merged `provider`
|
|
892
|
+
* from `forwardedProps`, `body`, and per-call `sendMessage` `body` is
|
|
893
|
+
* used. Later sources win. If no slug resolves, the send throws
|
|
894
|
+
* instead of attaching every stored key.
|
|
895
|
+
*/
|
|
896
|
+
byokProvider?: () => string | undefined
|
|
897
|
+
|
|
865
898
|
/**
|
|
866
899
|
* Client-local runtime context passed to client tool implementations.
|
|
867
900
|
*
|
|
@@ -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,
|
|
@@ -12,6 +17,7 @@ import { createNoOpVideoDevtoolsBridge } from './devtools-noop'
|
|
|
12
17
|
import { parseSSEResponse } from './sse-parser'
|
|
13
18
|
import { restoreInboundChunk } from '@tanstack/ai/client'
|
|
14
19
|
import type { StreamChunk } from '@tanstack/ai/client'
|
|
20
|
+
import type { ByokClient } from './byok'
|
|
15
21
|
import type {
|
|
16
22
|
ConnectConnectionAdapter,
|
|
17
23
|
GenerationHydrationResult,
|
|
@@ -122,6 +128,8 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
122
128
|
// the client hydrates the last generation for `threadId` from the server.
|
|
123
129
|
private readonly serverDriven: boolean = false
|
|
124
130
|
private body: Record<string, any>
|
|
131
|
+
private byok: ByokClient | undefined
|
|
132
|
+
private byokProvider: (() => string | undefined) | undefined
|
|
125
133
|
|
|
126
134
|
private result: TOutput | null = null
|
|
127
135
|
private input: VideoGenerateInput | null = null
|
|
@@ -156,6 +164,8 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
156
164
|
this.hydrateGenerationHandler = options.hydrateGeneration
|
|
157
165
|
this.joinRunHandler = options.joinRun
|
|
158
166
|
this.body = options.body ?? {}
|
|
167
|
+
this.byok = options.byok
|
|
168
|
+
this.byokProvider = options.byokProvider
|
|
159
169
|
// `persistence` is `false`/omitted (ephemeral) or `true` (server-driven:
|
|
160
170
|
// hydrate the last generation for `threadId` from the server on mount).
|
|
161
171
|
this.serverDriven = options.persistence === true
|
|
@@ -258,15 +268,24 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
258
268
|
const { signal } = abortController
|
|
259
269
|
|
|
260
270
|
try {
|
|
271
|
+
let headers: Record<string, string> | undefined
|
|
272
|
+
if (this.byok) {
|
|
273
|
+
const provider = resolveByokProviderId(
|
|
274
|
+
this.byokProvider,
|
|
275
|
+
this.body.provider,
|
|
276
|
+
)
|
|
277
|
+
headers = await prepareResolvedByokHeaders(this.byok, provider)
|
|
278
|
+
}
|
|
279
|
+
|
|
261
280
|
if (this.fetcher) {
|
|
262
|
-
await this.generateWithFetcher(input, signal, runId)
|
|
281
|
+
await this.generateWithFetcher(input, signal, runId, headers)
|
|
263
282
|
} else if (this.connection) {
|
|
264
283
|
const mergedData = { ...this.body, ...input }
|
|
265
284
|
const stream = this.connection.connect(
|
|
266
285
|
[],
|
|
267
286
|
mergedData,
|
|
268
287
|
signal,
|
|
269
|
-
this.createRunContext(runId),
|
|
288
|
+
this.createRunContext(runId, headers),
|
|
270
289
|
)
|
|
271
290
|
await this.processStream(stream, runId, signal)
|
|
272
291
|
} else {
|
|
@@ -284,6 +303,12 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
284
303
|
} catch (err: unknown) {
|
|
285
304
|
if (signal.aborted) return
|
|
286
305
|
const error = err instanceof Error ? err : new Error(String(err))
|
|
306
|
+
if (error instanceof ByokMissingError) {
|
|
307
|
+
this.byok?.request(error.provider, 'missing')
|
|
308
|
+
}
|
|
309
|
+
if (error instanceof ByokBlockedError && error.reason === 'locked') {
|
|
310
|
+
this.byok?.request(error.provider, 'locked')
|
|
311
|
+
}
|
|
287
312
|
this.setError(error)
|
|
288
313
|
this.setStatus('error')
|
|
289
314
|
this.recordResumeSnapshotError(error)
|
|
@@ -309,11 +334,15 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
309
334
|
input: VideoGenerateInput,
|
|
310
335
|
signal: AbortSignal,
|
|
311
336
|
runId: string,
|
|
337
|
+
headers?: Record<string, string>,
|
|
312
338
|
): Promise<void> {
|
|
313
339
|
if (!this.fetcher) return
|
|
314
340
|
|
|
315
341
|
// Fetcher returns a completed result directly, or a Response with SSE body
|
|
316
|
-
const result = await this.fetcher(
|
|
342
|
+
const result = await this.fetcher(
|
|
343
|
+
input,
|
|
344
|
+
headers === undefined ? { signal } : { signal, headers },
|
|
345
|
+
)
|
|
317
346
|
if (signal.aborted) return
|
|
318
347
|
|
|
319
348
|
if (result instanceof Response) {
|
|
@@ -470,6 +499,8 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
470
499
|
Pick<
|
|
471
500
|
VideoGenerationClientOptions<TOutput>,
|
|
472
501
|
| 'body'
|
|
502
|
+
| 'byok'
|
|
503
|
+
| 'byokProvider'
|
|
473
504
|
| 'onResult'
|
|
474
505
|
| 'onError'
|
|
475
506
|
| 'onProgress'
|
|
@@ -482,6 +513,12 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
482
513
|
if (options.body !== undefined) {
|
|
483
514
|
this.body = options.body ?? {}
|
|
484
515
|
}
|
|
516
|
+
if (options.byok !== undefined) {
|
|
517
|
+
this.byok = options.byok
|
|
518
|
+
}
|
|
519
|
+
if (options.byokProvider !== undefined) {
|
|
520
|
+
this.byokProvider = options.byokProvider
|
|
521
|
+
}
|
|
485
522
|
if (options.onResult !== undefined) {
|
|
486
523
|
this.callbacksRef.onResult = options.onResult
|
|
487
524
|
}
|
|
@@ -694,10 +731,14 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
694
731
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`
|
|
695
732
|
}
|
|
696
733
|
|
|
697
|
-
private createRunContext(
|
|
734
|
+
private createRunContext(
|
|
735
|
+
runId: string,
|
|
736
|
+
headers?: Record<string, string>,
|
|
737
|
+
): RunAgentInputContext {
|
|
698
738
|
return {
|
|
699
739
|
threadId: this.threadId,
|
|
700
740
|
runId,
|
|
741
|
+
...(headers ? { headers } : {}),
|
|
701
742
|
}
|
|
702
743
|
}
|
|
703
744
|
|