@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.
Files changed (51) hide show
  1. package/README.md +2 -1
  2. package/dist/esm/byok/client.d.ts +52 -0
  3. package/dist/esm/byok/client.js +300 -0
  4. package/dist/esm/byok/client.js.map +1 -0
  5. package/dist/esm/byok/passkey.d.ts +43 -0
  6. package/dist/esm/byok/passkey.js +251 -0
  7. package/dist/esm/byok/passkey.js.map +1 -0
  8. package/dist/esm/byok/resolve.d.ts +8 -0
  9. package/dist/esm/byok/resolve.js +20 -0
  10. package/dist/esm/byok/resolve.js.map +1 -0
  11. package/dist/esm/byok/storage.d.ts +15 -0
  12. package/dist/esm/byok/storage.js +16 -0
  13. package/dist/esm/byok/storage.js.map +1 -0
  14. package/dist/esm/byok.d.ts +8 -0
  15. package/dist/esm/byok.js +5 -0
  16. package/dist/esm/chat-client.d.ts +18 -5
  17. package/dist/esm/chat-client.js +96 -59
  18. package/dist/esm/chat-client.js.map +1 -1
  19. package/dist/esm/connection-adapters.d.ts +2 -0
  20. package/dist/esm/connection-adapters.js +70 -30
  21. package/dist/esm/connection-adapters.js.map +1 -1
  22. package/dist/esm/generation-client.d.ts +3 -1
  23. package/dist/esm/generation-client.js +27 -6
  24. package/dist/esm/generation-client.js.map +1 -1
  25. package/dist/esm/generation-types.d.ts +14 -0
  26. package/dist/esm/generation-types.js +4 -2
  27. package/dist/esm/generation-types.js.map +1 -1
  28. package/dist/esm/interrupt-manager.d.ts +1 -0
  29. package/dist/esm/interrupt-manager.js +15 -6
  30. package/dist/esm/interrupt-manager.js.map +1 -1
  31. package/dist/esm/sse-parser.js +8 -1
  32. package/dist/esm/sse-parser.js.map +1 -1
  33. package/dist/esm/types.d.ts +46 -3
  34. package/dist/esm/types.js.map +1 -1
  35. package/dist/esm/video-generation-client.d.ts +3 -1
  36. package/dist/esm/video-generation-client.js +29 -8
  37. package/dist/esm/video-generation-client.js.map +1 -1
  38. package/package.json +7 -3
  39. package/src/byok/client.ts +360 -0
  40. package/src/byok/passkey.ts +399 -0
  41. package/src/byok/resolve.ts +30 -0
  42. package/src/byok/storage.ts +29 -0
  43. package/src/byok.ts +20 -0
  44. package/src/chat-client.ts +138 -77
  45. package/src/connection-adapters.ts +108 -53
  46. package/src/generation-client.ts +53 -9
  47. package/src/generation-types.ts +24 -2
  48. package/src/interrupt-manager.ts +20 -6
  49. package/src/sse-parser.ts +10 -0
  50. package/src/types.ts +48 -3
  51. package/src/video-generation-client.ts +50 -9
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
  /**
@@ -397,6 +400,18 @@ export interface MultimodalContent {
397
400
  * If not provided, a unique ID will be generated.
398
401
  */
399
402
  id?: string
403
+ /**
404
+ * Optional AG-UI metadata bag copied onto the resulting UIMessage.
405
+ *
406
+ * @example
407
+ * ```ts
408
+ * await client.sendMessage({
409
+ * content: 'Show me failed logins',
410
+ * metadata: { author: { id: 'user-42', name: 'Dana' } },
411
+ * })
412
+ * ```
413
+ */
414
+ metadata?: Record<string, any>
400
415
  }
401
416
 
402
417
  /**
@@ -462,9 +477,9 @@ export interface QueueConfig {
462
477
  * function form (no `batch` via function). Per-call `sendOptions.whenBusy`
463
478
  * overrides the strategy for that send.
464
479
  *
465
- * Actions match {@link WhenBusy}: `'queue' | 'drop' | 'interrupt'`. Concurrent
466
- * streams are not supported. `pending.id` is the id that will be stored if the
467
- * 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`).
468
483
  */
469
484
  export type QueueStrategy = (ctx: {
470
485
  pending: QueuedMessage
@@ -479,6 +494,16 @@ export type QueueOption = WhenBusy | QueueConfig | QueueStrategy
479
494
  export interface SendMessageOptions {
480
495
  /** Overrides the configured `whenBusy` for this one send. */
481
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>
482
507
  }
483
508
 
484
509
  /**
@@ -616,6 +641,11 @@ export interface UIMessage<
616
641
  role: 'system' | 'user' | 'assistant'
617
642
  parts: Array<MessagePart<TTools, TData>>
618
643
  createdAt?: Date
644
+ /**
645
+ * Optional AG-UI metadata bag. TanStack writes the `tanstack` key.
646
+ * User keys stay at the top.
647
+ */
648
+ metadata?: Record<string, any>
619
649
  }
620
650
 
621
651
  /**
@@ -845,6 +875,21 @@ export interface ChatClientBaseOptions<
845
875
  */
846
876
  body?: Record<string, any>
847
877
 
878
+ /**
879
+ * Optional BYOK keyring. On each send the client prepares the resolved
880
+ * provider and stamps `x-byok-*` request headers. Keys never go in the body.
881
+ */
882
+ byok?: ByokClient
883
+
884
+ /**
885
+ * Optional provider id for this chat. If it returns a provider slug,
886
+ * only that key is prepared and sent. Otherwise the merged `provider`
887
+ * from `forwardedProps`, `body`, and per-call `sendMessage` `body` is
888
+ * used. Later sources win. If no slug resolves, the send throws
889
+ * instead of attaching every stored key.
890
+ */
891
+ byokProvider?: () => string | undefined
892
+
848
893
  /**
849
894
  * Client-local runtime context passed to client tool implementations.
850
895
  *
@@ -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 { createNoOpVideoDevtoolsBridge } 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,
@@ -121,6 +128,8 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
121
128
  // the client hydrates the last generation for `threadId` from the server.
122
129
  private readonly serverDriven: boolean = false
123
130
  private body: Record<string, any>
131
+ private byok: ByokClient | undefined
132
+ private byokProvider: (() => string | undefined) | undefined
124
133
 
125
134
  private result: TOutput | null = null
126
135
  private input: VideoGenerateInput | null = null
@@ -155,6 +164,8 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
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
@@ -257,15 +268,24 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
257
268
  const { signal } = abortController
258
269
 
259
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
+
260
280
  if (this.fetcher) {
261
- await this.generateWithFetcher(input, signal, runId)
281
+ await this.generateWithFetcher(input, signal, runId, headers)
262
282
  } else if (this.connection) {
263
283
  const mergedData = { ...this.body, ...input }
264
284
  const stream = this.connection.connect(
265
285
  [],
266
286
  mergedData,
267
287
  signal,
268
- this.createRunContext(runId),
288
+ this.createRunContext(runId, headers),
269
289
  )
270
290
  await this.processStream(stream, runId, signal)
271
291
  } else {
@@ -283,6 +303,12 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
283
303
  } catch (err: unknown) {
284
304
  if (signal.aborted) return
285
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
+ }
286
312
  this.setError(error)
287
313
  this.setStatus('error')
288
314
  this.recordResumeSnapshotError(error)
@@ -308,11 +334,15 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
308
334
  input: VideoGenerateInput,
309
335
  signal: AbortSignal,
310
336
  runId: string,
337
+ headers?: Record<string, string>,
311
338
  ): Promise<void> {
312
339
  if (!this.fetcher) return
313
340
 
314
341
  // Fetcher returns a completed result directly, or a Response with SSE body
315
- const result = await this.fetcher(input, { signal })
342
+ const result = await this.fetcher(
343
+ input,
344
+ headers === undefined ? { signal } : { signal, headers },
345
+ )
316
346
  if (signal.aborted) return
317
347
 
318
348
  if (result instanceof Response) {
@@ -343,9 +373,10 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
343
373
  let streamRunId: string | undefined
344
374
  let sawTerminalChunk = false
345
375
 
346
- for await (const chunk of source) {
376
+ for await (const raw of source) {
347
377
  if (signal.aborted) break
348
378
 
379
+ const chunk = restoreInboundChunk(raw)
349
380
  this.callbacksRef.onChunk?.(chunk)
350
381
  this.observeResumeSnapshot(chunk)
351
382
  const chunkRunId =
@@ -395,11 +426,9 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
395
426
  this.devtoolsBridge.ensureRunStarted(
396
427
  chunkRunId ?? streamRunId ?? fallbackRunId,
397
428
  )
398
- // Prefer spec `message`; fall back to deprecated `error.message`
429
+ // Spec RUN_ERROR message. Missing message uses this fallback.
399
430
  const msg =
400
- (chunk.message as string | undefined) ||
401
- chunk.error?.message ||
402
- 'An error occurred'
431
+ (chunk.message as string | undefined) || 'An error occurred'
403
432
  throw new Error(msg)
404
433
  }
405
434
  default:
@@ -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