@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.
Files changed (56) hide show
  1. package/dist/esm/byok/client.d.ts +52 -0
  2. package/dist/esm/byok/client.js +300 -0
  3. package/dist/esm/byok/client.js.map +1 -0
  4. package/dist/esm/byok/passkey.d.ts +43 -0
  5. package/dist/esm/byok/passkey.js +251 -0
  6. package/dist/esm/byok/passkey.js.map +1 -0
  7. package/dist/esm/byok/resolve.d.ts +8 -0
  8. package/dist/esm/byok/resolve.js +20 -0
  9. package/dist/esm/byok/resolve.js.map +1 -0
  10. package/dist/esm/byok/storage.d.ts +15 -0
  11. package/dist/esm/byok/storage.js +16 -0
  12. package/dist/esm/byok/storage.js.map +1 -0
  13. package/dist/esm/byok.d.ts +8 -0
  14. package/dist/esm/byok.js +5 -0
  15. package/dist/esm/chat-client.d.ts +19 -3
  16. package/dist/esm/chat-client.js +82 -42
  17. package/dist/esm/chat-client.js.map +1 -1
  18. package/dist/esm/connection-adapters.d.ts +2 -0
  19. package/dist/esm/connection-adapters.js +35 -10
  20. package/dist/esm/connection-adapters.js.map +1 -1
  21. package/dist/esm/generation-client.d.ts +3 -1
  22. package/dist/esm/generation-client.js +23 -4
  23. package/dist/esm/generation-client.js.map +1 -1
  24. package/dist/esm/generation-types.d.ts +14 -0
  25. package/dist/esm/generation-types.js.map +1 -1
  26. package/dist/esm/interrupt-manager.d.ts +1 -0
  27. package/dist/esm/interrupt-manager.js +15 -6
  28. package/dist/esm/interrupt-manager.js.map +1 -1
  29. package/dist/esm/message-date-normalizer.d.ts +3 -0
  30. package/dist/esm/message-date-normalizer.js +35 -0
  31. package/dist/esm/message-date-normalizer.js.map +1 -0
  32. package/dist/esm/sse-parser.js +8 -1
  33. package/dist/esm/sse-parser.js.map +1 -1
  34. package/dist/esm/storage-adapters.js +13 -1
  35. package/dist/esm/storage-adapters.js.map +1 -1
  36. package/dist/esm/types.d.ts +34 -3
  37. package/dist/esm/types.js.map +1 -1
  38. package/dist/esm/video-generation-client.d.ts +3 -1
  39. package/dist/esm/video-generation-client.js +25 -6
  40. package/dist/esm/video-generation-client.js.map +1 -1
  41. package/package.json +7 -3
  42. package/src/byok/client.ts +360 -0
  43. package/src/byok/passkey.ts +399 -0
  44. package/src/byok/resolve.ts +30 -0
  45. package/src/byok/storage.ts +29 -0
  46. package/src/byok.ts +20 -0
  47. package/src/chat-client.ts +165 -72
  48. package/src/connection-adapters.ts +46 -14
  49. package/src/generation-client.ts +48 -4
  50. package/src/generation-types.ts +16 -0
  51. package/src/interrupt-manager.ts +20 -6
  52. package/src/message-date-normalizer.ts +29 -0
  53. package/src/sse-parser.ts +10 -0
  54. package/src/storage-adapters.ts +13 -2
  55. package/src/types.ts +36 -3
  56. package/src/video-generation-client.ts +45 -4
@@ -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(input, { signal })
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
- 'body' | 'onResult' | 'onError' | 'onProgress' | 'onChunk'
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(runId: string): RunAgentInputContext {
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
 
@@ -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
 
@@ -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(submission)
1547
+ void this.performSubmission(operation)
1538
1548
  }
1539
1549
 
1540
1550
  private async performSubmission(
1541
- submission: InterruptManagerSubmission,
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.handleSubmissionFailure(error, submission)
1556
+ if (this.activeSubmissionOperation !== operation) return
1557
+ this.handleSubmissionFailure(error, operation.submission)
1547
1558
  } finally {
1548
- this.resuming = false
1549
- this.publish()
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
  )
@@ -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}: `'queue' | 'drop' | 'interrupt'`. Concurrent
478
- * streams are not supported. `pending.id` is the id that will be stored if the
479
- * action is `'queue'` (safe to pass to `cancelQueued`).
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(input, { signal })
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(runId: string): RunAgentInputContext {
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