@librechat/agents 3.1.73 → 3.1.75-dev.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 +66 -0
- package/dist/cjs/agents/AgentContext.cjs +146 -57
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +13 -3
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +145 -52
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +25 -15
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +84 -70
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +1 -1
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +213 -3
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +2 -1
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +5 -4
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +468 -647
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +1 -448
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +57 -175
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +5 -3
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +39 -4
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +7 -6
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +7 -6
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/langchain.cjs +26 -0
- package/dist/cjs/messages/langchain.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +7 -6
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +21 -11
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +37 -10
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +16 -11
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +5 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +147 -58
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +13 -3
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +146 -54
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +25 -15
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +84 -71
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +1 -1
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +214 -4
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs +2 -1
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +5 -4
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +469 -648
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +4 -449
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +57 -175
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +5 -3
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/messages/cache.mjs +39 -4
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +7 -6
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +7 -6
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/langchain.mjs +23 -0
- package/dist/esm/messages/langchain.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +7 -6
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +22 -12
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +37 -11
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +17 -12
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +5 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +29 -4
- package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +46 -0
- package/dist/types/llm/anthropic/index.d.ts +22 -9
- package/dist/types/llm/anthropic/types.d.ts +5 -1
- package/dist/types/llm/anthropic/utils/message_outputs.d.ts +13 -6
- package/dist/types/llm/anthropic/utils/output_parsers.d.ts +1 -1
- package/dist/types/llm/openai/index.d.ts +21 -24
- package/dist/types/llm/openrouter/index.d.ts +11 -9
- package/dist/types/llm/vertexai/index.d.ts +1 -0
- package/dist/types/messages/cache.d.ts +4 -1
- package/dist/types/messages/langchain.d.ts +27 -0
- package/dist/types/tools/CodeExecutor.d.ts +6 -0
- package/dist/types/types/graph.d.ts +26 -38
- package/dist/types/types/llm.d.ts +3 -3
- package/dist/types/types/run.d.ts +2 -0
- package/dist/types/types/stream.d.ts +1 -1
- package/dist/types/types/tools.d.ts +9 -0
- package/package.json +17 -16
- package/src/agents/AgentContext.ts +189 -71
- package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +116 -0
- package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +149 -0
- package/src/agents/__tests__/AgentContext.test.ts +333 -2
- package/src/agents/__tests__/promptCacheLiveHelpers.ts +165 -0
- package/src/graphs/Graph.ts +24 -4
- package/src/graphs/__tests__/composition.smoke.test.ts +188 -0
- package/src/llm/anthropic/index.ts +252 -84
- package/src/llm/anthropic/llm.spec.ts +751 -102
- package/src/llm/anthropic/types.ts +9 -1
- package/src/llm/anthropic/utils/message_inputs.ts +43 -20
- package/src/llm/anthropic/utils/message_outputs.ts +119 -101
- package/src/llm/anthropic/utils/server-tool-inputs.test.ts +77 -0
- package/src/llm/bedrock/index.ts +2 -2
- package/src/llm/bedrock/llm.spec.ts +341 -0
- package/src/llm/bedrock/utils/message_inputs.ts +303 -4
- package/src/llm/bedrock/utils/message_outputs.ts +2 -1
- package/src/llm/custom-chat-models.smoke.test.ts +662 -0
- package/src/llm/google/llm.spec.ts +339 -57
- package/src/llm/google/utils/common.ts +53 -48
- package/src/llm/openai/contentBlocks.test.ts +346 -0
- package/src/llm/openai/index.ts +736 -837
- package/src/llm/openai/utils/index.ts +84 -64
- package/src/llm/openrouter/index.ts +124 -247
- package/src/llm/openrouter/reasoning.test.ts +8 -1
- package/src/llm/vertexai/index.ts +11 -5
- package/src/llm/vertexai/llm.spec.ts +28 -1
- package/src/messages/cache.test.ts +106 -4
- package/src/messages/cache.ts +57 -5
- package/src/messages/core.ts +16 -9
- package/src/messages/format.ts +9 -6
- package/src/messages/langchain.ts +39 -0
- package/src/messages/prune.ts +12 -8
- package/src/scripts/caching.ts +2 -3
- package/src/specs/anthropic.simple.test.ts +61 -0
- package/src/specs/summarization.test.ts +58 -61
- package/src/tools/BashExecutor.ts +37 -13
- package/src/tools/CodeExecutor.ts +55 -11
- package/src/tools/ProgrammaticToolCalling.ts +29 -14
- package/src/tools/ToolNode.ts +5 -1
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +60 -0
- package/src/types/graph.ts +35 -88
- package/src/types/llm.ts +3 -3
- package/src/types/run.ts +2 -0
- package/src/types/stream.ts +1 -1
- package/src/types/tools.ts +9 -0
- package/src/utils/llmConfig.ts +1 -6
package/src/messages/cache.ts
CHANGED
|
@@ -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
|
|
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
|
|
352
|
+
const messageType =
|
|
314
353
|
'getType' in originalMessage &&
|
|
315
|
-
typeof originalMessage.getType === 'function'
|
|
316
|
-
|
|
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 === '';
|
package/src/messages/core.ts
CHANGED
|
@@ -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 =
|
|
157
|
+
obj.content = toLangChainContent(
|
|
158
|
+
reduceBlocks(obj.content as ContentBlock[])
|
|
159
|
+
);
|
|
157
160
|
}
|
|
158
161
|
if (Array.isArray(obj.content)) {
|
|
159
|
-
obj.content =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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(
|
|
445
|
+
messages.push(
|
|
446
|
+
new HumanMessage({ content: toLangChainContent(aggregatedContent) })
|
|
447
|
+
);
|
|
441
448
|
}
|
|
442
449
|
}
|
|
443
450
|
|
package/src/messages/format.ts
CHANGED
|
@@ -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(
|
|
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
|
+
}
|
package/src/messages/prune.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
package/src/scripts/caching.ts
CHANGED
|
@@ -50,9 +50,8 @@ ${CACHED_TEXT}`;
|
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const baseLlmConfig
|
|
54
|
-
|
|
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)) +
|
|
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
|
|
2449
|
+
const conversationHistory = createSeededTokenAuditHistory();
|
|
2418
2450
|
const tokenCounter = await createTokenCounter();
|
|
2419
2451
|
|
|
2420
|
-
const createRun = async (maxTokens =
|
|
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:
|
|
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
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
2503
|
-
expect(
|
|
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=${
|
|
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
|
-
|
|
2609
|
-
|
|
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}`
|