@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
@@ -1,9 +1,11 @@
1
+ import { prepareResolvedByokHeaders, resolveByokProviderId } from "./byok/resolve.js";
1
2
  import { createNoOpChatDevtoolsBridge } from "./devtools-noop.js";
2
- import { fetcherToConnectionAdapter, getChunkRunId, normalizeConnectionAdapter } from "./connection-adapters.js";
3
+ import { fetcherToConnectionAdapter, getChunkRunId as getChunkRunId$1, normalizeConnectionAdapter } from "./connection-adapters.js";
3
4
  import { ChatPersistor } from "./client-persistor.js";
4
5
  import { ClearedStreamTracker } from "./cleared-stream-tracker.js";
5
6
  import { InterruptManager } from "./interrupt-manager.js";
6
- import { StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, normalizeToUIMessage, parseWithStandardSchema } from "@tanstack/ai/client";
7
+ import { StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, mergeMetadata, normalizeToUIMessage, parseWithStandardSchema, restoreInboundChunk, tanstackMetadata } from "@tanstack/ai/client";
8
+ import { ByokBlockedError, ByokMissingError, ByokUnresolvedProviderError } from "@tanstack/ai/byok";
7
9
  //#region src/chat-client.ts
8
10
  function assertUniqueInterruptDefinitions(interrupts) {
9
11
  const ids = /* @__PURE__ */ new Set();
@@ -49,6 +51,7 @@ function normalizeQueueOption(option) {
49
51
  * Merge a run of queued messages into a single send for `drain: 'batch'`.
50
52
  * All-string content is joined with newlines; mixed/multimodal content is
51
53
  * flattened into a single `ContentPart` array. The last item's `body` wins.
54
+ * Object-form metadata is merged last-write-wins per key.
52
55
  */
53
56
  function mergeQueuedMessages(items) {
54
57
  const body = items.at(-1)?.body;
@@ -62,17 +65,27 @@ function mergeQueuedMessages(items) {
62
65
  ...body !== void 0 ? { body } : {}
63
66
  };
64
67
  const parts = [];
65
- for (const item of items) if (typeof item.content === "string") parts.push({
66
- type: "text",
67
- content: item.content
68
- });
69
- else if (typeof item.content.content === "string") parts.push({
70
- type: "text",
71
- content: item.content.content
72
- });
73
- else parts.push(...item.content.content);
68
+ let metadata;
69
+ for (const item of items) {
70
+ if (typeof item.content === "string") {
71
+ parts.push({
72
+ type: "text",
73
+ content: item.content
74
+ });
75
+ continue;
76
+ }
77
+ if (typeof item.content.content === "string") parts.push({
78
+ type: "text",
79
+ content: item.content.content
80
+ });
81
+ else parts.push(...item.content.content);
82
+ metadata = mergeMetadata(metadata, item.content.metadata);
83
+ }
74
84
  return {
75
- content: { content: parts },
85
+ content: {
86
+ content: parts,
87
+ ...metadata !== void 0 ? { metadata } : {}
88
+ },
76
89
  ...body !== void 0 ? { body } : {}
77
90
  };
78
91
  }
@@ -137,6 +150,8 @@ var ChatClient = class {
137
150
  activeResumeRunId = null;
138
151
  bodyOption = {};
139
152
  forwardedPropsOption = {};
153
+ byok;
154
+ byokProvider;
140
155
  context = void 0;
141
156
  pendingMessageBody = void 0;
142
157
  queueConfig;
@@ -217,6 +232,8 @@ var ChatClient = class {
217
232
  }
218
233
  this.bodyOption = options.body || {};
219
234
  this.forwardedPropsOption = options.forwardedProps || {};
235
+ this.byok = options.byok;
236
+ this.byokProvider = options.byokProvider;
220
237
  this.context = options.context;
221
238
  this.queueConfig = normalizeQueueOption(options.queue);
222
239
  this.connection = normalizeConnectionAdapter(resolveTransport(options));
@@ -511,15 +528,15 @@ var ChatClient = class {
511
528
  }
512
529
  retireIgnoredClearedTerminalChunk(chunk) {
513
530
  if (chunk.type !== "RUN_FINISHED" && chunk.type !== "RUN_ERROR") return;
514
- const runId = getChunkRunId(chunk) ?? this.clearedStreamTracker.takeRunlessRunId();
531
+ const runId = getChunkRunId$1(chunk) ?? this.clearedStreamTracker.takeRunlessRunId();
515
532
  if (!runId) return;
516
533
  this.activeRunIds.delete(runId);
517
534
  this.setSessionGenerating(this.activeRunIds.size > 0);
518
- if (!getChunkRunId(chunk)) this.resolveProcessing();
535
+ if (!getChunkRunId$1(chunk)) this.resolveProcessing();
519
536
  }
520
537
  updateRunLifecycle(chunk, options) {
521
538
  if (chunk.type === "RUN_STARTED") {
522
- const chunkRunId = getChunkRunId(chunk) ?? chunk.runId;
539
+ const chunkRunId = getChunkRunId$1(chunk) ?? chunk.runId;
523
540
  this.activeResumeThreadId = "threadId" in chunk && typeof chunk.threadId === "string" ? chunk.threadId : this.activeResumeThreadId;
524
541
  this.activeResumeRunId = chunkRunId;
525
542
  this.activeRunIds.add(chunkRunId);
@@ -532,7 +549,7 @@ var ChatClient = class {
532
549
  return;
533
550
  }
534
551
  if (chunk.type !== "RUN_FINISHED" && chunk.type !== "RUN_ERROR") return;
535
- const runId = getChunkRunId(chunk);
552
+ const runId = getChunkRunId$1(chunk);
536
553
  if (runId) {
537
554
  this.activeRunIds.delete(runId);
538
555
  this.clearedStreamTracker.onRunSettled(runId);
@@ -552,7 +569,7 @@ var ChatClient = class {
552
569
  observeInterruptState(chunk) {
553
570
  if (chunk.type !== "RUN_FINISHED" && chunk.type !== "RUN_ERROR") return;
554
571
  if (this.activeInterruptSubmission && chunk.type === "RUN_ERROR") return;
555
- const runId = getChunkRunId(chunk);
572
+ const runId = getChunkRunId$1(chunk);
556
573
  const threadId = "threadId" in chunk && typeof chunk.threadId === "string" ? chunk.threadId : this.activeResumeThreadId;
557
574
  if (chunk.type === "RUN_FINISHED" && chunk.outcome?.type === "interrupt") {
558
575
  const interruptedRunId = this.currentRunId ?? runId ?? this.activeResumeRunId ?? "";
@@ -943,11 +960,15 @@ var ChatClient = class {
943
960
  if (last && last.role === "assistant") this.processor.setMessages(messages.slice(0, -1));
944
961
  }
945
962
  async processIncomingChunk(chunk, options) {
946
- if (chunk.type === "RUN_ERROR" && this.isActiveInterruptSubmissionFailure(chunk)) this.interruptSubmissionFailure = { errors: chunk["tanstack:interruptErrors"] ?? [] };
963
+ chunk = restoreInboundChunk(chunk);
964
+ if (chunk.type === "RUN_ERROR" && this.isActiveInterruptSubmissionFailure(chunk)) {
965
+ const interruptErrors = tanstackMetadata(chunk)?.interruptErrors;
966
+ this.interruptSubmissionFailure = { errors: Array.isArray(interruptErrors) ? interruptErrors : [] };
967
+ }
947
968
  if (this.connectionStatus === "connecting") this.setConnectionStatus("connected");
948
969
  if (this.clearedStreamTracker.shouldIgnoreChunk(chunk)) {
949
970
  if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
950
- if (getChunkRunId(chunk)) this.updateRunLifecycle(chunk, { resolveProcessing: false });
971
+ if (getChunkRunId$1(chunk)) this.updateRunLifecycle(chunk, { resolveProcessing: false });
951
972
  else this.drainIgnoredRunlessChunk(chunk);
952
973
  this.retireIgnoredClearedTerminalChunk(chunk);
953
974
  this.resolveJoinedRun(chunk);
@@ -964,16 +985,19 @@ var ChatClient = class {
964
985
  }
965
986
  isActiveInterruptSubmissionFailure(chunk) {
966
987
  const submission = this.activeInterruptSubmission;
967
- const errors = chunk["tanstack:interruptErrors"];
968
- if (!submission || !errors || errors.length === 0) return false;
969
- const runId = getChunkRunId(chunk);
988
+ const errors = tanstackMetadata(chunk)?.interruptErrors;
989
+ if (!submission || !Array.isArray(errors) || errors.length === 0) return false;
990
+ const runId = getChunkRunId$1(chunk);
970
991
  if (runId !== void 0 && runId !== this.currentRunId) return false;
971
992
  if (typeof chunk.threadId === "string" && chunk.threadId !== submission.threadId) return false;
972
- return errors.every((error) => error.threadId === submission.threadId && error.interruptedRunId === submission.interruptedRunId && error.generation === submission.generation);
993
+ return errors.every((error) => {
994
+ if (error == null || typeof error !== "object" || typeof error.threadId !== "string" || typeof error.interruptedRunId !== "string" || typeof error.generation !== "number") return false;
995
+ return error.threadId === submission.threadId && error.interruptedRunId === submission.interruptedRunId && error.generation === submission.generation;
996
+ });
973
997
  }
974
998
  resolveJoinedRun(chunk) {
975
999
  if (chunk.type !== "RUN_FINISHED" && chunk.type !== "RUN_ERROR") return;
976
- const runId = getChunkRunId(chunk);
1000
+ const runId = getChunkRunId$1(chunk);
977
1001
  if (runId === void 0) return;
978
1002
  const resolve = this.joinedRunWaiters.get(runId);
979
1003
  if (resolve === void 0) return;
@@ -1009,8 +1033,12 @@ var ChatClient = class {
1009
1033
  * - A MultimodalContent object with content array and optional custom ID
1010
1034
  * @param body - Optional body parameters to merge with the client's base body for this request.
1011
1035
  * Uses shallow merge with per-message body taking priority.
1012
- * @param sendOptions - Per-call overrides, e.g. `{ whenBusy: 'interrupt' }` to
1013
- * override the configured queue policy for this one send.
1036
+ * @param sendOptions - Per-call overrides. `{ whenBusy }` overrides the
1037
+ * queue policy for this one send. `{ body }`
1038
+ * shallow-merges with `body` and with the chat-level
1039
+ * `body` / `forwardedProps`. `sendOptions.body` wins
1040
+ * on key collisions. Framework hooks forward this
1041
+ * object as their second argument.
1014
1042
  *
1015
1043
  * @example
1016
1044
  * ```ts
@@ -1020,9 +1048,13 @@ var ChatClient = class {
1020
1048
  * // Text message with custom body params
1021
1049
  * await client.sendMessage('Hello!', { temperature: 0.7 })
1022
1050
  *
1023
- * // Per-call whenBusy override (body must still be the 2nd arg on ChatClient)
1051
+ * // Per-call whenBusy override
1024
1052
  * await client.sendMessage('Urgent', undefined, { whenBusy: 'interrupt' })
1025
1053
  *
1054
+ * // Per-call body via options. Same effect as the positional arg.
1055
+ * // This is the shape the framework hooks (`useChat`, `injectChat`) forward.
1056
+ * await client.sendMessage('Hello!', undefined, { body: { temperature: 0.7 } })
1057
+ *
1026
1058
  * // Multimodal message with image
1027
1059
  * await client.sendMessage({
1028
1060
  * content: [
@@ -1048,11 +1080,15 @@ var ChatClient = class {
1048
1080
  this.mountDevtools();
1049
1081
  if (typeof content === "string" && !content.trim()) return;
1050
1082
  if (this.hasBlockingInterrupts()) throw new Error("ChatClient: cannot send normal input while pending interrupts exist. Use resumeInterrupts() instead.");
1083
+ const resolvedBody = {
1084
+ ...body,
1085
+ ...sendOptions?.body
1086
+ };
1051
1087
  if (this.isSendBusy()) {
1052
1088
  const { action, id } = this.decideWhenBusy(content, sendOptions);
1053
1089
  if (action === "drop") return;
1054
1090
  if (action === "queue") {
1055
- this.enqueueMessage(content, body, id);
1091
+ this.enqueueMessage(content, resolvedBody, id);
1056
1092
  return;
1057
1093
  }
1058
1094
  this.stopMessageQueueDrain = true;
@@ -1061,7 +1097,7 @@ var ChatClient = class {
1061
1097
  this.resetSessionGenerating();
1062
1098
  } else this.sendInFlight = true;
1063
1099
  try {
1064
- await this.deliverMessage(content, body);
1100
+ await this.deliverMessage(content, resolvedBody);
1065
1101
  } finally {
1066
1102
  this.sendInFlight = false;
1067
1103
  }
@@ -1096,7 +1132,7 @@ var ChatClient = class {
1096
1132
  try {
1097
1133
  const normalizedContent = this.normalizeMessageInput(content);
1098
1134
  this.pendingMessageBody = body;
1099
- const userMessage = this.processor.addUserMessage(normalizedContent.content, normalizedContent.id);
1135
+ const userMessage = this.processor.addUserMessage(normalizedContent.content, normalizedContent.id, normalizedContent.metadata);
1100
1136
  this.events.messageSent(userMessage.id, normalizedContent.content);
1101
1137
  return await this.streamResponse();
1102
1138
  } finally {
@@ -1149,14 +1185,15 @@ var ChatClient = class {
1149
1185
  this.emitQueueChange();
1150
1186
  }
1151
1187
  /**
1152
- * Normalize the message input to extract content and optional id.
1153
- * Trims string content automatically.
1188
+ * Normalize the message input to extract content, optional id, and
1189
+ * optional metadata. String form has no metadata. Trims string content.
1154
1190
  */
1155
1191
  normalizeMessageInput(input) {
1156
1192
  if (typeof input === "string") return { content: input.trim() };
1157
1193
  return {
1158
1194
  content: input.content,
1159
- id: input.id
1195
+ id: input.id,
1196
+ ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
1160
1197
  };
1161
1198
  }
1162
1199
  /**
@@ -1228,6 +1265,11 @@ var ChatClient = class {
1228
1265
  this.processor.prepareAssistantMessage();
1229
1266
  this.ensureSubscription();
1230
1267
  const processingComplete = this.waitForProcessing();
1268
+ let byokHeaders;
1269
+ if (this.byok) {
1270
+ const provider = resolveByokProviderId(this.byokProvider, mergedBody.provider);
1271
+ byokHeaders = await prepareResolvedByokHeaders(this.byok, provider);
1272
+ }
1231
1273
  const runContext = {
1232
1274
  threadId: resumeThreadId ?? this.threadId,
1233
1275
  runId,
@@ -1238,7 +1280,8 @@ var ChatClient = class {
1238
1280
  parameters: t.inputSchema ? convertSchemaToJsonSchema(t.inputSchema) : { type: "object" }
1239
1281
  })),
1240
1282
  forwardedProps: { ...mergedBody },
1241
- ...resumeItems ? { resume: resumeItems } : {}
1283
+ ...resumeItems ? { resume: resumeItems } : {},
1284
+ ...byokHeaders ? { headers: byokHeaders } : {}
1242
1285
  };
1243
1286
  this.devtoolsBridge.beginRun(runContext.runId, runContext.threadId);
1244
1287
  activeDevtoolsRunId = runContext.runId;
@@ -1260,22 +1303,24 @@ var ChatClient = class {
1260
1303
  this.processor.finalizeStream();
1261
1304
  streamCompletedSuccessfully = true;
1262
1305
  } catch (err) {
1263
- if (err instanceof Error) {
1264
- if (err.name === "AbortError") {
1265
- if (activeDevtoolsRunId) {
1266
- this.devtoolsBridge.emitRunLifecycle("run:cancelled", activeDevtoolsRunId, "cancelled");
1267
- runTerminalEventEmitted = true;
1268
- }
1269
- return false;
1306
+ const error = err instanceof Error ? err : new Error(String(err));
1307
+ if (error.name === "AbortError") {
1308
+ if (activeDevtoolsRunId) {
1309
+ this.devtoolsBridge.emitRunLifecycle("run:cancelled", activeDevtoolsRunId, "cancelled");
1310
+ runTerminalEventEmitted = true;
1270
1311
  }
1271
- if (generation === this.streamGeneration) {
1272
- this.reportStreamError(err);
1273
- if (activeDevtoolsRunId) {
1274
- this.devtoolsBridge.emitRunLifecycle("run:errored", activeDevtoolsRunId, "errored", { error: err.message });
1275
- runTerminalEventEmitted = true;
1276
- }
1312
+ return false;
1313
+ }
1314
+ if (error instanceof ByokMissingError) this.byok?.request(error.provider, "missing");
1315
+ if (error instanceof ByokBlockedError && error.reason === "locked") this.byok?.request(error.provider, "locked");
1316
+ if (generation === this.streamGeneration) {
1317
+ this.reportStreamError(error);
1318
+ if (activeDevtoolsRunId) {
1319
+ this.devtoolsBridge.emitRunLifecycle("run:errored", activeDevtoolsRunId, "errored", { error: error.message });
1320
+ runTerminalEventEmitted = true;
1277
1321
  }
1278
1322
  }
1323
+ if (generation === this.streamGeneration && (error instanceof ByokMissingError || error instanceof ByokBlockedError || error instanceof ByokUnresolvedProviderError)) throw error;
1279
1324
  } finally {
1280
1325
  if (generation === this.streamGeneration) {
1281
1326
  this.currentStreamId = null;
@@ -1293,18 +1338,8 @@ var ChatClient = class {
1293
1338
  }
1294
1339
  await this.drainPostStreamActions();
1295
1340
  if (streamCompletedSuccessfully) {
1296
- const lastPart = this.processor.getMessages().at(-1)?.parts.at(-1);
1297
- const { finishReason } = this.processor.getState();
1298
- if (lastPart?.type === "tool-result" && finishReason !== "stop" && this.shouldAutoSend()) try {
1299
- await this.checkForContinuation();
1300
- } catch (error) {
1301
- console.error("Failed to continue flow after tool result:", error);
1302
- await this.drainQueue();
1303
- }
1304
- else {
1305
- if (this.status !== "ready") this.setStatus("ready");
1306
- if (!this.messageQueueDraining) await this.drainQueue();
1307
- }
1341
+ if (this.status !== "ready") this.setStatus("ready");
1342
+ if (!this.messageQueueDraining) await this.drainQueue();
1308
1343
  } else this.flushQueue();
1309
1344
  }
1310
1345
  }
@@ -1661,6 +1696,8 @@ var ChatClient = class {
1661
1696
  }
1662
1697
  if (options.body !== void 0) this.bodyOption = options.body;
1663
1698
  if (options.forwardedProps !== void 0) this.forwardedPropsOption = options.forwardedProps;
1699
+ if (options.byok !== void 0) this.byok = options.byok;
1700
+ if (options.byokProvider !== void 0) this.byokProvider = options.byokProvider;
1664
1701
  if ("context" in options) this.context = options.context;
1665
1702
  if (options.tools !== void 0) {
1666
1703
  this.interruptManager.updateTools(options.tools);