@tanstack/ai-client 0.25.1 → 0.26.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 CHANGED
@@ -210,8 +210,9 @@ Official adapters include:
210
210
  | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
211
211
  | [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking |
212
212
  | [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools |
213
- | [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, and structured outputs |
213
+ | [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude |
214
214
  | [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation |
215
+ | [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials |
215
216
  | [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models |
216
217
  | [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
217
218
  | [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
@@ -378,8 +378,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
378
378
  private decideWhenBusy;
379
379
  private enqueueMessage;
380
380
  /**
381
- * Normalize the message input to extract content and optional id.
382
- * Trims string content automatically.
381
+ * Normalize the message input to extract content, optional id, and
382
+ * optional metadata. String form has no metadata. Trims string content.
383
383
  */
384
384
  private normalizeMessageInput;
385
385
  /**
@@ -1,9 +1,9 @@
1
1
  import { createNoOpChatDevtoolsBridge } from "./devtools-noop.js";
2
- import { fetcherToConnectionAdapter, getChunkRunId, normalizeConnectionAdapter } from "./connection-adapters.js";
2
+ import { fetcherToConnectionAdapter, getChunkRunId as getChunkRunId$1, normalizeConnectionAdapter } from "./connection-adapters.js";
3
3
  import { ChatPersistor } from "./client-persistor.js";
4
4
  import { ClearedStreamTracker } from "./cleared-stream-tracker.js";
5
5
  import { InterruptManager } from "./interrupt-manager.js";
6
- import { StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, normalizeToUIMessage, parseWithStandardSchema } from "@tanstack/ai/client";
6
+ import { StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, mergeMetadata, normalizeToUIMessage, parseWithStandardSchema, restoreInboundChunk, tanstackMetadata } from "@tanstack/ai/client";
7
7
  //#region src/chat-client.ts
8
8
  function assertUniqueInterruptDefinitions(interrupts) {
9
9
  const ids = /* @__PURE__ */ new Set();
@@ -49,6 +49,7 @@ function normalizeQueueOption(option) {
49
49
  * Merge a run of queued messages into a single send for `drain: 'batch'`.
50
50
  * All-string content is joined with newlines; mixed/multimodal content is
51
51
  * flattened into a single `ContentPart` array. The last item's `body` wins.
52
+ * Object-form metadata is merged last-write-wins per key.
52
53
  */
53
54
  function mergeQueuedMessages(items) {
54
55
  const body = items.at(-1)?.body;
@@ -62,17 +63,27 @@ function mergeQueuedMessages(items) {
62
63
  ...body !== void 0 ? { body } : {}
63
64
  };
64
65
  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);
66
+ let metadata;
67
+ for (const item of items) {
68
+ if (typeof item.content === "string") {
69
+ parts.push({
70
+ type: "text",
71
+ content: item.content
72
+ });
73
+ continue;
74
+ }
75
+ if (typeof item.content.content === "string") parts.push({
76
+ type: "text",
77
+ content: item.content.content
78
+ });
79
+ else parts.push(...item.content.content);
80
+ metadata = mergeMetadata(metadata, item.content.metadata);
81
+ }
74
82
  return {
75
- content: { content: parts },
83
+ content: {
84
+ content: parts,
85
+ ...metadata !== void 0 ? { metadata } : {}
86
+ },
76
87
  ...body !== void 0 ? { body } : {}
77
88
  };
78
89
  }
@@ -511,15 +522,15 @@ var ChatClient = class {
511
522
  }
512
523
  retireIgnoredClearedTerminalChunk(chunk) {
513
524
  if (chunk.type !== "RUN_FINISHED" && chunk.type !== "RUN_ERROR") return;
514
- const runId = getChunkRunId(chunk) ?? this.clearedStreamTracker.takeRunlessRunId();
525
+ const runId = getChunkRunId$1(chunk) ?? this.clearedStreamTracker.takeRunlessRunId();
515
526
  if (!runId) return;
516
527
  this.activeRunIds.delete(runId);
517
528
  this.setSessionGenerating(this.activeRunIds.size > 0);
518
- if (!getChunkRunId(chunk)) this.resolveProcessing();
529
+ if (!getChunkRunId$1(chunk)) this.resolveProcessing();
519
530
  }
520
531
  updateRunLifecycle(chunk, options) {
521
532
  if (chunk.type === "RUN_STARTED") {
522
- const chunkRunId = getChunkRunId(chunk) ?? chunk.runId;
533
+ const chunkRunId = getChunkRunId$1(chunk) ?? chunk.runId;
523
534
  this.activeResumeThreadId = "threadId" in chunk && typeof chunk.threadId === "string" ? chunk.threadId : this.activeResumeThreadId;
524
535
  this.activeResumeRunId = chunkRunId;
525
536
  this.activeRunIds.add(chunkRunId);
@@ -532,7 +543,7 @@ var ChatClient = class {
532
543
  return;
533
544
  }
534
545
  if (chunk.type !== "RUN_FINISHED" && chunk.type !== "RUN_ERROR") return;
535
- const runId = getChunkRunId(chunk);
546
+ const runId = getChunkRunId$1(chunk);
536
547
  if (runId) {
537
548
  this.activeRunIds.delete(runId);
538
549
  this.clearedStreamTracker.onRunSettled(runId);
@@ -552,7 +563,7 @@ var ChatClient = class {
552
563
  observeInterruptState(chunk) {
553
564
  if (chunk.type !== "RUN_FINISHED" && chunk.type !== "RUN_ERROR") return;
554
565
  if (this.activeInterruptSubmission && chunk.type === "RUN_ERROR") return;
555
- const runId = getChunkRunId(chunk);
566
+ const runId = getChunkRunId$1(chunk);
556
567
  const threadId = "threadId" in chunk && typeof chunk.threadId === "string" ? chunk.threadId : this.activeResumeThreadId;
557
568
  if (chunk.type === "RUN_FINISHED" && chunk.outcome?.type === "interrupt") {
558
569
  const interruptedRunId = this.currentRunId ?? runId ?? this.activeResumeRunId ?? "";
@@ -943,11 +954,15 @@ var ChatClient = class {
943
954
  if (last && last.role === "assistant") this.processor.setMessages(messages.slice(0, -1));
944
955
  }
945
956
  async processIncomingChunk(chunk, options) {
946
- if (chunk.type === "RUN_ERROR" && this.isActiveInterruptSubmissionFailure(chunk)) this.interruptSubmissionFailure = { errors: chunk["tanstack:interruptErrors"] ?? [] };
957
+ chunk = restoreInboundChunk(chunk);
958
+ if (chunk.type === "RUN_ERROR" && this.isActiveInterruptSubmissionFailure(chunk)) {
959
+ const interruptErrors = tanstackMetadata(chunk)?.interruptErrors;
960
+ this.interruptSubmissionFailure = { errors: Array.isArray(interruptErrors) ? interruptErrors : [] };
961
+ }
947
962
  if (this.connectionStatus === "connecting") this.setConnectionStatus("connected");
948
963
  if (this.clearedStreamTracker.shouldIgnoreChunk(chunk)) {
949
964
  if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
950
- if (getChunkRunId(chunk)) this.updateRunLifecycle(chunk, { resolveProcessing: false });
965
+ if (getChunkRunId$1(chunk)) this.updateRunLifecycle(chunk, { resolveProcessing: false });
951
966
  else this.drainIgnoredRunlessChunk(chunk);
952
967
  this.retireIgnoredClearedTerminalChunk(chunk);
953
968
  this.resolveJoinedRun(chunk);
@@ -959,21 +974,24 @@ var ChatClient = class {
959
974
  this.processor.processChunk(chunk);
960
975
  this.updateRunLifecycle(chunk);
961
976
  this.observeInterruptState(chunk);
962
- if (options?.defer !== false) await new Promise((resolve) => setTimeout(resolve, 0));
977
+ if (options?.defer !== false && (typeof document === "undefined" || !document.hidden)) await new Promise((resolve) => setTimeout(resolve, 0));
963
978
  this.resolveJoinedRun(chunk);
964
979
  }
965
980
  isActiveInterruptSubmissionFailure(chunk) {
966
981
  const submission = this.activeInterruptSubmission;
967
- const errors = chunk["tanstack:interruptErrors"];
968
- if (!submission || !errors || errors.length === 0) return false;
969
- const runId = getChunkRunId(chunk);
982
+ const errors = tanstackMetadata(chunk)?.interruptErrors;
983
+ if (!submission || !Array.isArray(errors) || errors.length === 0) return false;
984
+ const runId = getChunkRunId$1(chunk);
970
985
  if (runId !== void 0 && runId !== this.currentRunId) return false;
971
986
  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);
987
+ return errors.every((error) => {
988
+ if (error == null || typeof error !== "object" || typeof error.threadId !== "string" || typeof error.interruptedRunId !== "string" || typeof error.generation !== "number") return false;
989
+ return error.threadId === submission.threadId && error.interruptedRunId === submission.interruptedRunId && error.generation === submission.generation;
990
+ });
973
991
  }
974
992
  resolveJoinedRun(chunk) {
975
993
  if (chunk.type !== "RUN_FINISHED" && chunk.type !== "RUN_ERROR") return;
976
- const runId = getChunkRunId(chunk);
994
+ const runId = getChunkRunId$1(chunk);
977
995
  if (runId === void 0) return;
978
996
  const resolve = this.joinedRunWaiters.get(runId);
979
997
  if (resolve === void 0) return;
@@ -1096,7 +1114,7 @@ var ChatClient = class {
1096
1114
  try {
1097
1115
  const normalizedContent = this.normalizeMessageInput(content);
1098
1116
  this.pendingMessageBody = body;
1099
- const userMessage = this.processor.addUserMessage(normalizedContent.content, normalizedContent.id);
1117
+ const userMessage = this.processor.addUserMessage(normalizedContent.content, normalizedContent.id, normalizedContent.metadata);
1100
1118
  this.events.messageSent(userMessage.id, normalizedContent.content);
1101
1119
  return await this.streamResponse();
1102
1120
  } finally {
@@ -1149,14 +1167,15 @@ var ChatClient = class {
1149
1167
  this.emitQueueChange();
1150
1168
  }
1151
1169
  /**
1152
- * Normalize the message input to extract content and optional id.
1153
- * Trims string content automatically.
1170
+ * Normalize the message input to extract content, optional id, and
1171
+ * optional metadata. String form has no metadata. Trims string content.
1154
1172
  */
1155
1173
  normalizeMessageInput(input) {
1156
1174
  if (typeof input === "string") return { content: input.trim() };
1157
1175
  return {
1158
1176
  content: input.content,
1159
- id: input.id
1177
+ id: input.id,
1178
+ ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
1160
1179
  };
1161
1180
  }
1162
1181
  /**