@librechat/agents 3.1.74 → 3.1.75-dev.1

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 (203) hide show
  1. package/README.md +66 -0
  2. package/dist/cjs/agents/AgentContext.cjs +84 -37
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +13 -3
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/langchain/google-common.cjs +3 -0
  7. package/dist/cjs/langchain/google-common.cjs.map +1 -0
  8. package/dist/cjs/langchain/index.cjs +86 -0
  9. package/dist/cjs/langchain/index.cjs.map +1 -0
  10. package/dist/cjs/langchain/language_models/chat_models.cjs +3 -0
  11. package/dist/cjs/langchain/language_models/chat_models.cjs.map +1 -0
  12. package/dist/cjs/langchain/messages/tool.cjs +3 -0
  13. package/dist/cjs/langchain/messages/tool.cjs.map +1 -0
  14. package/dist/cjs/langchain/messages.cjs +51 -0
  15. package/dist/cjs/langchain/messages.cjs.map +1 -0
  16. package/dist/cjs/langchain/openai.cjs +3 -0
  17. package/dist/cjs/langchain/openai.cjs.map +1 -0
  18. package/dist/cjs/langchain/prompts.cjs +11 -0
  19. package/dist/cjs/langchain/prompts.cjs.map +1 -0
  20. package/dist/cjs/langchain/runnables.cjs +19 -0
  21. package/dist/cjs/langchain/runnables.cjs.map +1 -0
  22. package/dist/cjs/langchain/tools.cjs +23 -0
  23. package/dist/cjs/langchain/tools.cjs.map +1 -0
  24. package/dist/cjs/langchain/utils/env.cjs +11 -0
  25. package/dist/cjs/langchain/utils/env.cjs.map +1 -0
  26. package/dist/cjs/llm/anthropic/index.cjs +145 -52
  27. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  28. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  29. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +25 -15
  30. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  31. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +84 -70
  32. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  33. package/dist/cjs/llm/bedrock/index.cjs +1 -1
  34. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  35. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +213 -3
  36. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  37. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +2 -1
  38. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  39. package/dist/cjs/llm/google/utils/common.cjs +5 -4
  40. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  41. package/dist/cjs/llm/openai/index.cjs +468 -647
  42. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  43. package/dist/cjs/llm/openai/utils/index.cjs +1 -448
  44. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  45. package/dist/cjs/llm/openrouter/index.cjs +57 -175
  46. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  47. package/dist/cjs/llm/vertexai/index.cjs +5 -3
  48. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  49. package/dist/cjs/main.cjs +83 -3
  50. package/dist/cjs/main.cjs.map +1 -1
  51. package/dist/cjs/messages/cache.cjs +39 -4
  52. package/dist/cjs/messages/cache.cjs.map +1 -1
  53. package/dist/cjs/messages/core.cjs +7 -6
  54. package/dist/cjs/messages/core.cjs.map +1 -1
  55. package/dist/cjs/messages/format.cjs +7 -6
  56. package/dist/cjs/messages/format.cjs.map +1 -1
  57. package/dist/cjs/messages/langchain.cjs +26 -0
  58. package/dist/cjs/messages/langchain.cjs.map +1 -0
  59. package/dist/cjs/messages/prune.cjs +7 -6
  60. package/dist/cjs/messages/prune.cjs.map +1 -1
  61. package/dist/cjs/tools/ToolNode.cjs +5 -1
  62. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  63. package/dist/esm/agents/AgentContext.mjs +85 -38
  64. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  65. package/dist/esm/graphs/Graph.mjs +13 -3
  66. package/dist/esm/graphs/Graph.mjs.map +1 -1
  67. package/dist/esm/langchain/google-common.mjs +2 -0
  68. package/dist/esm/langchain/google-common.mjs.map +1 -0
  69. package/dist/esm/langchain/index.mjs +5 -0
  70. package/dist/esm/langchain/index.mjs.map +1 -0
  71. package/dist/esm/langchain/language_models/chat_models.mjs +2 -0
  72. package/dist/esm/langchain/language_models/chat_models.mjs.map +1 -0
  73. package/dist/esm/langchain/messages/tool.mjs +2 -0
  74. package/dist/esm/langchain/messages/tool.mjs.map +1 -0
  75. package/dist/esm/langchain/messages.mjs +2 -0
  76. package/dist/esm/langchain/messages.mjs.map +1 -0
  77. package/dist/esm/langchain/openai.mjs +2 -0
  78. package/dist/esm/langchain/openai.mjs.map +1 -0
  79. package/dist/esm/langchain/prompts.mjs +2 -0
  80. package/dist/esm/langchain/prompts.mjs.map +1 -0
  81. package/dist/esm/langchain/runnables.mjs +2 -0
  82. package/dist/esm/langchain/runnables.mjs.map +1 -0
  83. package/dist/esm/langchain/tools.mjs +2 -0
  84. package/dist/esm/langchain/tools.mjs.map +1 -0
  85. package/dist/esm/langchain/utils/env.mjs +2 -0
  86. package/dist/esm/langchain/utils/env.mjs.map +1 -0
  87. package/dist/esm/llm/anthropic/index.mjs +146 -54
  88. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  89. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  90. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +25 -15
  91. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  92. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +84 -71
  93. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  94. package/dist/esm/llm/bedrock/index.mjs +1 -1
  95. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  96. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +214 -4
  97. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  98. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +2 -1
  99. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  100. package/dist/esm/llm/google/utils/common.mjs +5 -4
  101. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  102. package/dist/esm/llm/openai/index.mjs +469 -648
  103. package/dist/esm/llm/openai/index.mjs.map +1 -1
  104. package/dist/esm/llm/openai/utils/index.mjs +4 -449
  105. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  106. package/dist/esm/llm/openrouter/index.mjs +57 -175
  107. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  108. package/dist/esm/llm/vertexai/index.mjs +5 -3
  109. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  110. package/dist/esm/main.mjs +4 -0
  111. package/dist/esm/main.mjs.map +1 -1
  112. package/dist/esm/messages/cache.mjs +39 -4
  113. package/dist/esm/messages/cache.mjs.map +1 -1
  114. package/dist/esm/messages/core.mjs +7 -6
  115. package/dist/esm/messages/core.mjs.map +1 -1
  116. package/dist/esm/messages/format.mjs +7 -6
  117. package/dist/esm/messages/format.mjs.map +1 -1
  118. package/dist/esm/messages/langchain.mjs +23 -0
  119. package/dist/esm/messages/langchain.mjs.map +1 -0
  120. package/dist/esm/messages/prune.mjs +7 -6
  121. package/dist/esm/messages/prune.mjs.map +1 -1
  122. package/dist/esm/tools/ToolNode.mjs +5 -1
  123. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  124. package/dist/types/agents/AgentContext.d.ts +14 -4
  125. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +46 -0
  126. package/dist/types/index.d.ts +1 -0
  127. package/dist/types/langchain/google-common.d.ts +1 -0
  128. package/dist/types/langchain/index.d.ts +8 -0
  129. package/dist/types/langchain/language_models/chat_models.d.ts +1 -0
  130. package/dist/types/langchain/messages/tool.d.ts +1 -0
  131. package/dist/types/langchain/messages.d.ts +2 -0
  132. package/dist/types/langchain/openai.d.ts +1 -0
  133. package/dist/types/langchain/prompts.d.ts +1 -0
  134. package/dist/types/langchain/runnables.d.ts +2 -0
  135. package/dist/types/langchain/tools.d.ts +2 -0
  136. package/dist/types/langchain/utils/env.d.ts +1 -0
  137. package/dist/types/llm/anthropic/index.d.ts +22 -9
  138. package/dist/types/llm/anthropic/types.d.ts +5 -1
  139. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +13 -6
  140. package/dist/types/llm/anthropic/utils/output_parsers.d.ts +1 -1
  141. package/dist/types/llm/openai/index.d.ts +21 -24
  142. package/dist/types/llm/openrouter/index.d.ts +11 -9
  143. package/dist/types/llm/vertexai/index.d.ts +1 -0
  144. package/dist/types/messages/cache.d.ts +4 -1
  145. package/dist/types/messages/langchain.d.ts +27 -0
  146. package/dist/types/types/graph.d.ts +26 -38
  147. package/dist/types/types/llm.d.ts +3 -3
  148. package/dist/types/types/run.d.ts +2 -0
  149. package/dist/types/types/stream.d.ts +1 -1
  150. package/package.json +80 -17
  151. package/src/agents/AgentContext.ts +123 -44
  152. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +116 -0
  153. package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +149 -0
  154. package/src/agents/__tests__/AgentContext.test.ts +155 -2
  155. package/src/agents/__tests__/promptCacheLiveHelpers.ts +165 -0
  156. package/src/graphs/Graph.ts +24 -4
  157. package/src/graphs/__tests__/composition.smoke.test.ts +188 -0
  158. package/src/index.ts +3 -0
  159. package/src/langchain/google-common.ts +1 -0
  160. package/src/langchain/index.ts +8 -0
  161. package/src/langchain/language_models/chat_models.ts +1 -0
  162. package/src/langchain/messages/tool.ts +5 -0
  163. package/src/langchain/messages.ts +21 -0
  164. package/src/langchain/openai.ts +1 -0
  165. package/src/langchain/prompts.ts +1 -0
  166. package/src/langchain/runnables.ts +7 -0
  167. package/src/langchain/tools.ts +8 -0
  168. package/src/langchain/utils/env.ts +1 -0
  169. package/src/llm/anthropic/index.ts +252 -84
  170. package/src/llm/anthropic/llm.spec.ts +751 -102
  171. package/src/llm/anthropic/types.ts +9 -1
  172. package/src/llm/anthropic/utils/message_inputs.ts +43 -20
  173. package/src/llm/anthropic/utils/message_outputs.ts +119 -101
  174. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +77 -0
  175. package/src/llm/bedrock/index.ts +2 -2
  176. package/src/llm/bedrock/llm.spec.ts +341 -0
  177. package/src/llm/bedrock/utils/message_inputs.ts +303 -4
  178. package/src/llm/bedrock/utils/message_outputs.ts +2 -1
  179. package/src/llm/custom-chat-models.smoke.test.ts +662 -0
  180. package/src/llm/google/llm.spec.ts +339 -57
  181. package/src/llm/google/utils/common.ts +53 -48
  182. package/src/llm/openai/contentBlocks.test.ts +346 -0
  183. package/src/llm/openai/index.ts +736 -837
  184. package/src/llm/openai/utils/index.ts +84 -64
  185. package/src/llm/openrouter/index.ts +124 -247
  186. package/src/llm/openrouter/reasoning.test.ts +8 -1
  187. package/src/llm/vertexai/index.ts +11 -5
  188. package/src/llm/vertexai/llm.spec.ts +28 -1
  189. package/src/messages/cache.test.ts +106 -4
  190. package/src/messages/cache.ts +57 -5
  191. package/src/messages/core.ts +16 -9
  192. package/src/messages/format.ts +9 -6
  193. package/src/messages/langchain.ts +39 -0
  194. package/src/messages/prune.ts +12 -8
  195. package/src/scripts/caching.ts +2 -3
  196. package/src/specs/anthropic.simple.test.ts +61 -0
  197. package/src/specs/summarization.test.ts +58 -61
  198. package/src/tools/ToolNode.ts +5 -1
  199. package/src/types/graph.ts +35 -88
  200. package/src/types/llm.ts +3 -3
  201. package/src/types/run.ts +2 -0
  202. package/src/types/stream.ts +1 -1
  203. package/src/utils/llmConfig.ts +1 -6
@@ -9,11 +9,16 @@ import {
9
9
  import type { AnthropicMessage } from '@/types/messages';
10
10
  import type Anthropic from '@anthropic-ai/sdk';
11
11
  import { ContentTypes } from '@/common/enum';
12
+ import { toLangChainContent } from './langchain';
12
13
 
13
14
  type MessageWithContent = {
14
15
  content?: string | MessageContentComplex[];
15
16
  };
16
17
 
18
+ type MessageContentWithCacheControl = MessageContentComplex & {
19
+ cache_control?: unknown;
20
+ };
21
+
17
22
  /**
18
23
  * Deep clones a message's content to prevent mutation of the original.
19
24
  */
@@ -41,7 +46,7 @@ function cloneMessage<T extends MessageWithContent>(
41
46
  ): T {
42
47
  if (message instanceof BaseMessage) {
43
48
  const baseParams = {
44
- content,
49
+ content: toLangChainContent(content),
45
50
  additional_kwargs: { ...message.additional_kwargs },
46
51
  response_metadata: { ...message.response_metadata },
47
52
  id: message.id,
@@ -101,6 +106,40 @@ function cloneMessage<T extends MessageWithContent>(
101
106
  return cloned;
102
107
  }
103
108
 
109
+ function stripAnthropicCacheControlFromBlocks(
110
+ content: MessageContentComplex[]
111
+ ): { content: MessageContentComplex[]; modified: boolean } {
112
+ let modified = false;
113
+ const strippedContent = content.map((block) => {
114
+ if (!('cache_control' in block)) {
115
+ return block;
116
+ }
117
+
118
+ const cloned: MessageContentWithCacheControl = { ...block };
119
+ delete cloned.cache_control;
120
+ modified = true;
121
+ return cloned;
122
+ });
123
+
124
+ return { content: strippedContent, modified };
125
+ }
126
+
127
+ function sanitizeBedrockSystemMessage<T extends MessageWithContent>(
128
+ message: T
129
+ ): T {
130
+ const content = message.content;
131
+ if (!Array.isArray(content)) {
132
+ return message;
133
+ }
134
+
135
+ const stripped = stripAnthropicCacheControlFromBlocks(content);
136
+ if (!stripped.modified) {
137
+ return message;
138
+ }
139
+
140
+ return cloneMessage(message, stripped.content);
141
+ }
142
+
104
143
  /**
105
144
  * Anthropic API: Adds cache control to the appropriate user messages in the payload.
106
145
  * Strips ALL existing cache control (both Anthropic and Bedrock formats) from all messages,
@@ -299,7 +338,7 @@ export function stripBedrockCacheControl<T extends MessageWithContent>(
299
338
  * @returns - A new array of message objects with cache points added.
300
339
  */
301
340
  export function addBedrockCacheControl<
302
- T extends Partial<BaseMessage> & MessageWithContent,
341
+ T extends MessageWithContent & { getType?: () => string; role?: string },
303
342
  >(messages: T[]): T[] {
304
343
  if (!Array.isArray(messages) || messages.length < 2) {
305
344
  return messages;
@@ -310,11 +349,24 @@ export function addBedrockCacheControl<
310
349
 
311
350
  for (let i = updatedMessages.length - 1; i >= 0; i--) {
312
351
  const originalMessage = updatedMessages[i];
313
- const isToolMessage =
352
+ const messageType =
314
353
  'getType' in originalMessage &&
315
- typeof originalMessage.getType === 'function' &&
316
- originalMessage.getType() === 'tool';
354
+ typeof originalMessage.getType === 'function'
355
+ ? originalMessage.getType()
356
+ : undefined;
357
+ const messageRole =
358
+ 'role' in originalMessage && typeof originalMessage.role === 'string'
359
+ ? originalMessage.role
360
+ : undefined;
361
+
362
+ const isSystemMessage =
363
+ messageType === 'system' || messageRole === 'system';
364
+ if (isSystemMessage) {
365
+ updatedMessages[i] = sanitizeBedrockSystemMessage(originalMessage);
366
+ continue;
367
+ }
317
368
 
369
+ const isToolMessage = messageType === 'tool' || messageRole === 'tool';
318
370
  const content = originalMessage.content;
319
371
  const hasArrayContent = Array.isArray(content);
320
372
  const isEmptyString = typeof content === 'string' && content === '';
@@ -9,6 +9,7 @@ import {
9
9
  import type { ToolCall } from '@langchain/core/messages/tool';
10
10
  import type * as t from '@/types';
11
11
  import { Providers } from '@/common';
12
+ import { toLangChainContent } from './langchain';
12
13
 
13
14
  export function getConverseOverrideMessage({
14
15
  userMessage,
@@ -153,14 +154,18 @@ export function modifyDeltaProperties(
153
154
  : '';
154
155
 
155
156
  if (provider === Providers.BEDROCK && Array.isArray(obj.content)) {
156
- obj.content = reduceBlocks(obj.content as ContentBlock[]);
157
+ obj.content = toLangChainContent(
158
+ reduceBlocks(obj.content as ContentBlock[])
159
+ );
157
160
  }
158
161
  if (Array.isArray(obj.content)) {
159
- obj.content = modifyContent({
160
- provider,
161
- messageType,
162
- content: obj.content,
163
- }) as t.MessageContentComplex[];
162
+ obj.content = toLangChainContent(
163
+ modifyContent({
164
+ provider,
165
+ messageType,
166
+ content: obj.content as t.ExtendedMessageContent[],
167
+ }) as t.MessageContentComplex[]
168
+ );
164
169
  }
165
170
  if (
166
171
  (obj as Partial<AIMessageChunk>).lc_kwargs &&
@@ -182,7 +187,7 @@ export function modifyDeltaProperties(
182
187
 
183
188
  export function formatAnthropicMessage(message: AIMessageChunk): AIMessage {
184
189
  if (!message.tool_calls || message.tool_calls.length === 0) {
185
- return new AIMessage({ content: message.content });
190
+ return new AIMessage({ content: toLangChainContent(message.content) });
186
191
  }
187
192
 
188
193
  const toolCallMap = new Map(message.tool_calls.map((tc) => [tc.id, tc]));
@@ -269,7 +274,7 @@ export function formatAnthropicMessage(message: AIMessageChunk): AIMessage {
269
274
  );
270
275
 
271
276
  return new AIMessage({
272
- content: formattedContent,
277
+ content: toLangChainContent(formattedContent),
273
278
  tool_calls: formattedToolCalls as ToolCall[],
274
279
  additional_kwargs: {
275
280
  ...message.additional_kwargs,
@@ -437,7 +442,9 @@ export function formatArtifactPayload(messages: BaseMessage[]): void {
437
442
  }
438
443
 
439
444
  if (aggregatedContent.length > 0) {
440
- messages.push(new HumanMessage({ content: aggregatedContent }));
445
+ messages.push(
446
+ new HumanMessage({ content: toLangChainContent(aggregatedContent) })
447
+ );
441
448
  }
442
449
  }
443
450
 
@@ -22,6 +22,7 @@ import type {
22
22
  import type { RunnableConfig } from '@langchain/core/runnables';
23
23
  import { emitAgentLog } from '@/utils/events';
24
24
  import { Providers, ContentTypes, Constants } from '@/common';
25
+ import { toLangChainContent, toLangChainMessageFields } from './langchain';
25
26
 
26
27
  interface MediaMessageParams {
27
28
  message: {
@@ -210,7 +211,7 @@ export const formatMessage = ({
210
211
  return mediaMessage;
211
212
  }
212
213
 
213
- return new HumanMessage(mediaMessage);
214
+ return new HumanMessage(toLangChainMessageFields(mediaMessage));
214
215
  }
215
216
 
216
217
  if (!langChain) {
@@ -218,11 +219,11 @@ export const formatMessage = ({
218
219
  }
219
220
 
220
221
  if (role === 'user') {
221
- return new HumanMessage(formattedMessage);
222
+ return new HumanMessage(toLangChainMessageFields(formattedMessage));
222
223
  } else if (role === 'assistant') {
223
- return new AIMessage(formattedMessage);
224
+ return new AIMessage(toLangChainMessageFields(formattedMessage));
224
225
  } else {
225
- return new SystemMessage(formattedMessage);
226
+ return new SystemMessage(toLangChainMessageFields(formattedMessage));
226
227
  }
227
228
  };
228
229
 
@@ -413,7 +414,9 @@ function formatAssistantMessage(
413
414
  formattedMessages.push(new AIMessage({ content }));
414
415
  }
415
416
  } else if (currentContent.length > 0) {
416
- formattedMessages.push(new AIMessage({ content: currentContent }));
417
+ formattedMessages.push(
418
+ new AIMessage({ content: toLangChainContent(currentContent) })
419
+ );
417
420
  }
418
421
 
419
422
  return formattedMessages;
@@ -1542,7 +1545,7 @@ export function ensureThinkingBlockInMessages(
1542
1545
  'ensureThinkingBlockInMessages: injecting [Previous agent context] HumanMessage' +
1543
1546
  ` (${parts.length} msgs at index ${i}, no thinking block in chain)`
1544
1547
  );
1545
- result.push(new HumanMessage({ content: parts }));
1548
+ result.push(new HumanMessage({ content: toLangChainContent(parts) }));
1546
1549
  i = j;
1547
1550
  } else {
1548
1551
  // Keep the message as is
@@ -0,0 +1,39 @@
1
+ import type { MessageContent } from '@langchain/core/messages';
2
+ import type * as t from '@/types';
3
+
4
+ type LibreChatMessageContent =
5
+ | MessageContent
6
+ | string
7
+ | t.MessageContentComplex[]
8
+ | t.ExtendedMessageContent[];
9
+
10
+ type WithLangChainContent<T extends { content: LibreChatMessageContent }> =
11
+ Omit<T, 'content'> & {
12
+ content: MessageContent;
13
+ };
14
+
15
+ /**
16
+ * Bridges LibreChat's extended content blocks to LangChain 1.x MessageContent.
17
+ *
18
+ * LangChain 1.x narrowed message constructor types around ContentBlock, while
19
+ * LibreChat still carries provider-specific blocks through the same content
20
+ * field. This helper keeps the runtime shape unchanged during the dependency
21
+ * upgrade; tracking issue: https://github.com/danny-avila/agents/issues/130.
22
+ */
23
+ export function toLangChainContent(
24
+ content: LibreChatMessageContent
25
+ ): MessageContent {
26
+ return content as MessageContent;
27
+ }
28
+
29
+ /**
30
+ * Applies the same LangChain 1.x content bridge to message constructor fields.
31
+ *
32
+ * Keep this cast-only helper local to constructor boundaries so follow-up work
33
+ * can replace it with aligned content types or explicit conversion logic.
34
+ */
35
+ export function toLangChainMessageFields<
36
+ T extends { content: LibreChatMessageContent },
37
+ >(message: T): WithLangChainContent<T> {
38
+ return message as WithLangChainContent<T>;
39
+ }
@@ -19,6 +19,7 @@ import {
19
19
  import { resolveContextPruningSettings } from './contextPruningSettings';
20
20
  import { ContentTypes, Providers, Constants } from '@/common';
21
21
  import { applyContextPruning } from './contextPruning';
22
+ import { toLangChainContent } from './langchain';
22
23
 
23
24
  function sumTokenCounts(
24
25
  tokenMap: Record<string, number | undefined>,
@@ -343,7 +344,7 @@ function stripOrphanToolUseBlocks(
343
344
 
344
345
  return new AIMessage({
345
346
  ...message,
346
- content: keptContent,
347
+ content: toLangChainContent(keptContent),
347
348
  tool_calls: keptToolCalls.length > 0 ? keptToolCalls : undefined,
348
349
  });
349
350
  }
@@ -542,7 +543,7 @@ function addThinkingBlock(
542
543
  content.unshift(thinkingBlock);
543
544
  return new AIMessage({
544
545
  ...message,
545
- content,
546
+ content: toLangChainContent(content),
546
547
  });
547
548
  }
548
549
 
@@ -817,7 +818,7 @@ export function getMessagesWithinTokenLimit({
817
818
 
818
819
  thinkingStartIndex = originalLength - 1 - assistantIndex;
819
820
  const thinkingTokenCount = tokenCounter(
820
- new AIMessage({ content: [thinkingBlock] })
821
+ new AIMessage({ content: toLangChainContent([thinkingBlock]) })
821
822
  );
822
823
  const newRemainingCount = remainingContextTokens - thinkingTokenCount;
823
824
  const newMessage = addThinkingBlock(
@@ -856,7 +857,7 @@ export function getMessagesWithinTokenLimit({
856
857
  }
857
858
  }
858
859
 
859
- const firstMessage: AIMessage = newContext[newContext.length - 1];
860
+ const firstMessage = newContext[newContext.length - 1];
860
861
  const firstMessageType = newContext[newContext.length - 1].getType();
861
862
  if (firstMessageType === 'tool') {
862
863
  startType = ['ai', 'human'];
@@ -887,7 +888,10 @@ export function getMessagesWithinTokenLimit({
887
888
  }
888
889
 
889
890
  if (firstMessageType === 'ai') {
890
- const newMessage = addThinkingBlock(firstMessage, thinkingBlock);
891
+ const newMessage = addThinkingBlock(
892
+ firstMessage as AIMessage,
893
+ thinkingBlock
894
+ );
891
895
  newContext[newContext.length - 1] = newMessage;
892
896
  } else {
893
897
  newContext.push(thinkingMessage);
@@ -1178,7 +1182,7 @@ export function preFlightTruncateToolCallInputs(params: {
1178
1182
 
1179
1183
  messages[i] = new AIMessage({
1180
1184
  ...aiMsg,
1181
- content: newContent,
1185
+ content: toLangChainContent(newContent),
1182
1186
  tool_calls: newToolCalls.length > 0 ? newToolCalls : undefined,
1183
1187
  });
1184
1188
  indexTokenCountMap[i] = tokenCounter(messages[i]);
@@ -1290,7 +1294,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
1290
1294
 
1291
1295
  params.messages[i] = new AIMessage({
1292
1296
  ...message,
1293
- content: [thinkingBlock],
1297
+ content: toLangChainContent([thinkingBlock]),
1294
1298
  additional_kwargs: {
1295
1299
  ...message.additional_kwargs,
1296
1300
  reasoning_content: undefined,
@@ -1966,7 +1970,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
1966
1970
  });
1967
1971
  emergencyMessages[i] = new AIMessage({
1968
1972
  ...aiMsg,
1969
- content: newContent,
1973
+ content: toLangChainContent(newContent),
1970
1974
  tool_calls: newToolCalls.length > 0 ? newToolCalls : undefined,
1971
1975
  });
1972
1976
  indexTokenCountMap[i] = factoryParams.tokenCounter(
@@ -50,9 +50,8 @@ ${CACHED_TEXT}`;
50
50
  },
51
51
  };
52
52
 
53
- const baseLlmConfig: t.LLMConfig & t.AnthropicClientOptions = getLLMConfig(
54
- Providers.ANTHROPIC
55
- );
53
+ const baseLlmConfig = getLLMConfig(Providers.ANTHROPIC) as t.LLMConfig &
54
+ t.AnthropicClientOptions;
56
55
 
57
56
  if (baseLlmConfig.provider !== 'anthropic') {
58
57
  console.error(
@@ -376,6 +376,67 @@ describe(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
376
376
  );
377
377
  });
378
378
 
379
+ test(`${capitalizeFirstLetter(provider)}: follow-up after assistant message with only whitespace text content`, async () => {
380
+ /**
381
+ * Regression for LibreChat discussion #12806.
382
+ *
383
+ * The Anthropic API has two distinct rejection rules (verified against
384
+ * the live API):
385
+ * 1. Strict empty `text: ''` → rejected anywhere
386
+ * "messages: text content blocks must be non-empty"
387
+ * 2. Whitespace-only `text: ' '` / '\n' / '\t' → rejected when the
388
+ * assistant message has no other accepted blocks (no tool blocks,
389
+ * no non-whitespace text)
390
+ * "messages: text content blocks must contain non-whitespace text"
391
+ *
392
+ * Anthropic responses for some prompts include a whitespace-only text
393
+ * block as the sole text content. Re-sending that history on a
394
+ * follow-up turn triggers rule 2.
395
+ *
396
+ * The wire-send filter in `_formatContent` must drop any text block
397
+ * whose trimmed content is empty. The previous filter used strict
398
+ * `text === ''` only, which caught rule 1 but not rule 2.
399
+ */
400
+ const llmConfig = getLLMConfig(provider);
401
+ const customHandlers1 = setupCustomHandlers();
402
+
403
+ const followUpRun = await Run.create<t.IState>({
404
+ runId: 'repro-12806-followup',
405
+ graphConfig: {
406
+ type: 'standard',
407
+ llmConfig,
408
+ instructions: 'You are a friendly AI assistant.',
409
+ },
410
+ returnContent: true,
411
+ skipCleanup: true,
412
+ customHandlers: customHandlers1,
413
+ });
414
+
415
+ // Build history with an assistant message whose entire content array
416
+ // is a single whitespace-only text block. This is the precise shape
417
+ // the API rejects under rule 2 above.
418
+ conversationHistory = [
419
+ new HumanMessage('hi'),
420
+ new (require('@langchain/core/messages').AIMessage)({
421
+ content: [{ type: 'text', text: ' ' }],
422
+ }),
423
+ new HumanMessage('please respond with a short greeting'),
424
+ ];
425
+
426
+ // With the fix: `_formatContent` drops the whitespace text block,
427
+ // the assistant content becomes an empty array, and the API accepts.
428
+ // Without the fix: the whitespace block is forwarded and the API
429
+ // rejects with "messages: text content blocks must contain non-whitespace text".
430
+ const finalContentParts = await followUpRun.processStream(
431
+ { messages: conversationHistory },
432
+ config
433
+ );
434
+ expect(finalContentParts).toBeDefined();
435
+ const finalMessages = followUpRun.getRunMessages();
436
+ expect(finalMessages).toBeDefined();
437
+ expect(finalMessages?.length).toBeGreaterThan(0);
438
+ });
439
+
379
440
  test('should handle errors appropriately', async () => {
380
441
  // Test error scenarios
381
442
  await expect(async () => {
@@ -22,6 +22,8 @@ import { formatAgentMessages } from '@/messages/format';
22
22
  import { FakeListChatModel } from '@langchain/core/utils/testing';
23
23
  import * as providers from '@/llm/providers';
24
24
 
25
+ const SUMMARY_WRAPPER_OVERHEAD_TOKENS = 33;
26
+
25
27
  /** Extract plain text from a SummaryContentBlock's content array (test helper). */
26
28
  function getSummaryText(summary: t.SummaryContentBlock | undefined): string {
27
29
  if (!summary) return '';
@@ -136,6 +138,7 @@ async function createSummarizationRun(opts: {
136
138
  tools?: t.GraphTools;
137
139
  indexTokenCountMap?: Record<string, number>;
138
140
  llmConfigOverride?: Record<string, unknown>;
141
+ maxSummaryTokens?: number;
139
142
  }): Promise<Run<t.IState>> {
140
143
  const llmConfig = {
141
144
  ...getLLMConfig(opts.agentProvider),
@@ -155,6 +158,7 @@ async function createSummarizationRun(opts: {
155
158
  summarizationConfig: {
156
159
  provider: opts.summarizationProvider,
157
160
  model: opts.summarizationModel,
161
+ maxSummaryTokens: opts.maxSummaryTokens,
158
162
  },
159
163
  },
160
164
  returnContent: true,
@@ -242,6 +246,33 @@ function buildIndexTokenCountMap(
242
246
  return map;
243
247
  }
244
248
 
249
+ function sumTokenCountMap(map: Record<string, number | undefined>): number {
250
+ let total = 0;
251
+ for (const key in map) {
252
+ total += map[key] ?? 0;
253
+ }
254
+ return total;
255
+ }
256
+
257
+ function createSeededTokenAuditHistory(): BaseMessage[] {
258
+ const details =
259
+ 'Token audit context preserves index token counts, summary replacement, calibration data, and post-summary continuity. ' +
260
+ 'Important retained values: alpha=1024, beta=2048, gamma=4096, checksum TOKEN-AUDIT-7F3. ' +
261
+ 'The repeated detail intentionally exceeds a compact context budget. ';
262
+ const padding = details.repeat(8);
263
+
264
+ return [
265
+ new HumanMessage(
266
+ `Audit turn 1: establish the accounting scenario. ${padding}`
267
+ ),
268
+ new AIMessage(`Recorded turn 1 accounting notes. ${padding}`),
269
+ new HumanMessage(`Audit turn 2: add more retained details. ${padding}`),
270
+ new AIMessage(`Recorded turn 2 accounting notes. ${padding}`),
271
+ new HumanMessage(`Audit turn 3: preserve final identifiers. ${padding}`),
272
+ new AIMessage(`Recorded turn 3 accounting notes. ${padding}`),
273
+ ];
274
+ }
275
+
245
276
  function logTurn(
246
277
  label: string,
247
278
  conversationHistory: BaseMessage[],
@@ -1443,7 +1474,8 @@ describe('Cross-run summary lifecycle (no API keys)', () => {
1443
1474
  expect(completePayload.summary!.tokenCount ?? 0).toBeGreaterThan(0);
1444
1475
 
1445
1476
  const expectedTokenCount =
1446
- tokenCounter(new SystemMessage(KNOWN_SUMMARY)) + 33;
1477
+ tokenCounter(new SystemMessage(KNOWN_SUMMARY)) +
1478
+ SUMMARY_WRAPPER_OVERHEAD_TOKENS;
1447
1479
  expect(completePayload.summary!.tokenCount).toBe(expectedTokenCount);
1448
1480
 
1449
1481
  const summaryBlock = completePayload.summary!;
@@ -2414,10 +2446,10 @@ const hasAnyApiKey =
2414
2446
  test('token count map is accurate after summarization cycle', async () => {
2415
2447
  const spies = createSpies();
2416
2448
  let collectedUsage: UsageMetadata[] = [];
2417
- const conversationHistory: BaseMessage[] = [];
2449
+ const conversationHistory = createSeededTokenAuditHistory();
2418
2450
  const tokenCounter = await createTokenCounter();
2419
2451
 
2420
- const createRun = async (maxTokens = 4000): Promise<Run<t.IState>> => {
2452
+ const createRun = async (maxTokens = 1200): Promise<Run<t.IState>> => {
2421
2453
  collectedUsage = [];
2422
2454
  const { aggregateContent } = createContentAggregator();
2423
2455
  const indexTokenCountMap = buildIndexTokenCountMap(
@@ -2429,80 +2461,44 @@ const hasAnyApiKey =
2429
2461
  summarizationProvider,
2430
2462
  summarizationModel,
2431
2463
  maxContextTokens: maxTokens,
2432
- instructions: INSTRUCTIONS,
2464
+ instructions:
2465
+ 'You are a concise assistant. Preserve checkpoint context and answer in one short sentence.',
2433
2466
  collectedUsage,
2434
2467
  aggregateContent,
2435
2468
  spies,
2436
2469
  tokenCounter,
2437
2470
  indexTokenCountMap,
2471
+ maxSummaryTokens: 300,
2472
+ tools: [],
2473
+ llmConfigOverride: {
2474
+ maxTokens: 128,
2475
+ },
2438
2476
  });
2439
2477
  };
2440
2478
 
2441
- // Accumulate messages over 6 turns at generous budget
2442
- let run = await createRun();
2443
- await runTurn(
2444
- { run, conversationHistory },
2445
- 'What is 42 * 58? Calculator.',
2446
- streamConfig
2447
- );
2448
-
2449
- run = await createRun();
2450
- await runTurn(
2451
- { run, conversationHistory },
2452
- 'Now compute 2436 + 1000. Calculator.',
2453
- streamConfig
2454
- );
2455
-
2456
- run = await createRun();
2457
- await runTurn(
2458
- { run, conversationHistory },
2459
- 'What is 3436 / 4? Calculator.',
2460
- streamConfig
2461
- );
2462
-
2463
- run = await createRun();
2464
- await runTurn(
2465
- { run, conversationHistory },
2466
- 'Compute 999 * 2. Calculator.',
2467
- streamConfig
2468
- );
2469
-
2470
- run = await createRun();
2471
- await runTurn(
2472
- { run, conversationHistory },
2473
- 'What is 2^10? Calculator. Also list everything.',
2474
- streamConfig
2479
+ const originalMap = buildIndexTokenCountMap(
2480
+ conversationHistory,
2481
+ tokenCounter
2475
2482
  );
2483
+ const originalTokenTotal = sumTokenCountMap(originalMap);
2484
+ expect(originalTokenTotal).toBeGreaterThan(1200);
2476
2485
 
2477
- run = await createRun();
2486
+ const run = await createRun();
2478
2487
  await runTurn(
2479
2488
  { run, conversationHistory },
2480
- 'Calculate 355 / 113. Calculator.',
2489
+ 'Acknowledge the preserved token audit context in one short sentence.',
2481
2490
  streamConfig
2482
2491
  );
2483
2492
 
2484
- // Squeeze progressively to force summarization
2485
- for (const squeeze of [3500, 3200, 3100, 3000, 2800, 2500, 2000]) {
2486
- if (spies.onSummarizeStartSpy.mock.calls.length > 0) {
2487
- break;
2488
- }
2489
- run = await createRun(squeeze);
2490
- await runTurn(
2491
- { run, conversationHistory },
2492
- `What is ${squeeze} - 1000? Calculator.`,
2493
- streamConfig
2494
- );
2495
- }
2496
-
2497
- // Verify summarization fired
2498
2493
  expect(spies.onSummarizeCompleteSpy).toHaveBeenCalled();
2499
2494
 
2500
2495
  const completePayload = spies.onSummarizeCompleteSpy.mock
2501
2496
  .calls[0][0] as t.SummarizeCompleteEvent;
2502
- expect(completePayload.summary!.tokenCount).toBeGreaterThan(10);
2503
- expect(completePayload.summary!.tokenCount).toBeLessThan(1500);
2497
+ const summaryTokenCount = completePayload.summary!.tokenCount ?? 0;
2498
+ expect(summaryTokenCount).toBeGreaterThan(10);
2499
+ expect(summaryTokenCount).toBeLessThan(1500);
2500
+ expect(summaryTokenCount).toBeLessThan(originalTokenTotal);
2504
2501
 
2505
- // Token accounting: collectedUsage should have valid entries
2506
2502
  const validUsage = collectedUsage.filter(
2507
2503
  (u: Partial<UsageMetadata>) =>
2508
2504
  u.input_tokens != null && u.input_tokens > 0
@@ -2510,8 +2506,8 @@ const hasAnyApiKey =
2510
2506
  expect(validUsage.length).toBeGreaterThan(0);
2511
2507
 
2512
2508
  console.log(
2513
- ` Token audit: summary=${completePayload.summary!.tokenCount} tokens, ` +
2514
- `usageEntries=${validUsage.length}`
2509
+ ` Token audit: summary=${summaryTokenCount} tokens, ` +
2510
+ `preTotal=${originalTokenTotal}, usageEntries=${validUsage.length}`
2515
2511
  );
2516
2512
  }, 180_000);
2517
2513
 
@@ -2605,8 +2601,9 @@ const hasAnyApiKey =
2605
2601
  const summaryText = getSummaryText(completePayload.summary);
2606
2602
  const reportedTokenCount = completePayload.summary!.tokenCount ?? 0;
2607
2603
 
2608
- // Count tokens locally using the same tokenizer
2609
- const localTokenCount = tokenCounter(new SystemMessage(summaryText));
2604
+ const localTokenCount =
2605
+ tokenCounter(new SystemMessage(summaryText)) +
2606
+ SUMMARY_WRAPPER_OVERHEAD_TOKENS;
2610
2607
 
2611
2608
  console.log(
2612
2609
  ` Token match: reported=${reportedTokenCount}, local=${localTokenCount}`
@@ -33,6 +33,7 @@ import {
33
33
  } from '@/utils/truncation';
34
34
  import { safeDispatchCustomEvent } from '@/utils/events';
35
35
  import { executeHooks } from '@/hooks';
36
+ import { toLangChainContent } from '@/messages/langchain';
36
37
  import { Constants, GraphEvents, CODE_EXECUTION_TOOLS } from '@/common';
37
38
  import {
38
39
  buildReferenceKey,
@@ -1282,7 +1283,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1282
1283
  if (msg.skillName != null) additional_kwargs.skillName = msg.skillName;
1283
1284
 
1284
1285
  converted.push(
1285
- new HumanMessage({ content: msg.content, additional_kwargs })
1286
+ new HumanMessage({
1287
+ content: toLangChainContent(msg.content),
1288
+ additional_kwargs,
1289
+ })
1286
1290
  );
1287
1291
  }
1288
1292
  return converted;