@librechat/agents 3.1.56 → 3.1.60
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/dist/cjs/agents/AgentContext.cjs +326 -62
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +13 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +7 -27
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +303 -222
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +4 -4
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +6 -2
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +60 -0
- package/dist/cjs/llm/init.cjs.map +1 -0
- package/dist/cjs/llm/invoke.cjs +90 -0
- package/dist/cjs/llm/invoke.cjs.map +1 -0
- package/dist/cjs/llm/openai/index.cjs +2 -0
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/request.cjs +41 -0
- package/dist/cjs/llm/request.cjs.map +1 -0
- package/dist/cjs/main.cjs +40 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +76 -89
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +156 -0
- package/dist/cjs/messages/contextPruning.cjs.map +1 -0
- package/dist/cjs/messages/contextPruningSettings.cjs +53 -0
- package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -0
- package/dist/cjs/messages/core.cjs +23 -37
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +156 -11
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +1161 -49
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/reducer.cjs +87 -0
- package/dist/cjs/messages/reducer.cjs.map +1 -0
- package/dist/cjs/run.cjs +81 -42
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +54 -7
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/index.cjs +75 -0
- package/dist/cjs/summarization/index.cjs.map +1 -0
- package/dist/cjs/summarization/node.cjs +663 -0
- package/dist/cjs/summarization/node.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +16 -8
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +2 -0
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/utils/errors.cjs +115 -0
- package/dist/cjs/utils/errors.cjs.map +1 -0
- package/dist/cjs/utils/events.cjs +17 -0
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/cjs/utils/handlers.cjs +16 -0
- package/dist/cjs/utils/handlers.cjs.map +1 -1
- package/dist/cjs/utils/llm.cjs +10 -0
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +247 -14
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/truncation.cjs +107 -0
- package/dist/cjs/utils/truncation.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +325 -61
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +13 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +8 -28
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +307 -226
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +4 -4
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +6 -2
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +58 -0
- package/dist/esm/llm/init.mjs.map +1 -0
- package/dist/esm/llm/invoke.mjs +87 -0
- package/dist/esm/llm/invoke.mjs.map +1 -0
- package/dist/esm/llm/openai/index.mjs +2 -0
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/request.mjs +38 -0
- package/dist/esm/llm/request.mjs.map +1 -0
- package/dist/esm/main.mjs +13 -3
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +76 -89
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +154 -0
- package/dist/esm/messages/contextPruning.mjs.map +1 -0
- package/dist/esm/messages/contextPruningSettings.mjs +50 -0
- package/dist/esm/messages/contextPruningSettings.mjs.map +1 -0
- package/dist/esm/messages/core.mjs +23 -37
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +156 -11
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +1158 -52
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/reducer.mjs +83 -0
- package/dist/esm/messages/reducer.mjs.map +1 -0
- package/dist/esm/run.mjs +82 -43
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +54 -7
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/index.mjs +73 -0
- package/dist/esm/summarization/index.mjs.map +1 -0
- package/dist/esm/summarization/node.mjs +659 -0
- package/dist/esm/summarization/node.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +16 -8
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +2 -0
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/utils/errors.mjs +111 -0
- package/dist/esm/utils/errors.mjs.map +1 -0
- package/dist/esm/utils/events.mjs +17 -1
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/esm/utils/handlers.mjs +16 -0
- package/dist/esm/utils/handlers.mjs.map +1 -1
- package/dist/esm/utils/llm.mjs +10 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +245 -15
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/truncation.mjs +102 -0
- package/dist/esm/utils/truncation.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +124 -6
- package/dist/types/common/enum.d.ts +14 -1
- package/dist/types/graphs/Graph.d.ts +22 -27
- package/dist/types/index.d.ts +5 -0
- package/dist/types/llm/init.d.ts +18 -0
- package/dist/types/llm/invoke.d.ts +48 -0
- package/dist/types/llm/request.d.ts +14 -0
- package/dist/types/messages/contextPruning.d.ts +42 -0
- package/dist/types/messages/contextPruningSettings.d.ts +44 -0
- package/dist/types/messages/core.d.ts +1 -1
- package/dist/types/messages/format.d.ts +17 -1
- package/dist/types/messages/index.d.ts +3 -0
- package/dist/types/messages/prune.d.ts +162 -1
- package/dist/types/messages/reducer.d.ts +18 -0
- package/dist/types/run.d.ts +12 -1
- package/dist/types/summarization/index.d.ts +20 -0
- package/dist/types/summarization/node.d.ts +29 -0
- package/dist/types/tools/ToolNode.d.ts +3 -1
- package/dist/types/types/graph.d.ts +44 -6
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/run.d.ts +30 -0
- package/dist/types/types/stream.d.ts +31 -4
- package/dist/types/types/summarize.d.ts +47 -0
- package/dist/types/types/tools.d.ts +7 -0
- package/dist/types/utils/errors.d.ts +28 -0
- package/dist/types/utils/events.d.ts +13 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/utils/llm.d.ts +4 -0
- package/dist/types/utils/tokens.d.ts +14 -1
- package/dist/types/utils/truncation.d.ts +49 -0
- package/package.json +2 -2
- package/src/agents/AgentContext.ts +388 -58
- package/src/agents/__tests__/AgentContext.test.ts +265 -5
- package/src/common/enum.ts +13 -0
- package/src/events.ts +9 -39
- package/src/graphs/Graph.ts +468 -331
- package/src/index.ts +7 -0
- package/src/llm/anthropic/llm.spec.ts +3 -3
- package/src/llm/anthropic/utils/message_inputs.ts +6 -4
- package/src/llm/bedrock/llm.spec.ts +1 -1
- package/src/llm/bedrock/utils/message_inputs.ts +6 -2
- package/src/llm/init.ts +63 -0
- package/src/llm/invoke.ts +144 -0
- package/src/llm/request.ts +55 -0
- package/src/messages/__tests__/observationMasking.test.ts +221 -0
- package/src/messages/cache.ts +77 -102
- package/src/messages/contextPruning.ts +191 -0
- package/src/messages/contextPruningSettings.ts +90 -0
- package/src/messages/core.ts +32 -53
- package/src/messages/ensureThinkingBlock.test.ts +39 -39
- package/src/messages/format.ts +227 -15
- package/src/messages/formatAgentMessages.test.ts +511 -1
- package/src/messages/index.ts +3 -0
- package/src/messages/prune.ts +1548 -62
- package/src/messages/reducer.ts +22 -0
- package/src/run.ts +104 -51
- package/src/scripts/bedrock-merge-test.ts +1 -1
- package/src/scripts/test-thinking-handoff-bedrock.ts +1 -1
- package/src/scripts/test-thinking-handoff.ts +1 -1
- package/src/scripts/thinking-bedrock.ts +1 -1
- package/src/scripts/thinking.ts +1 -1
- package/src/specs/anthropic.simple.test.ts +1 -1
- package/src/specs/multi-agent-summarization.test.ts +396 -0
- package/src/specs/prune.test.ts +1196 -23
- package/src/specs/summarization-unit.test.ts +868 -0
- package/src/specs/summarization.test.ts +3810 -0
- package/src/specs/summarize-prune.test.ts +376 -0
- package/src/specs/thinking-handoff.test.ts +10 -10
- package/src/specs/thinking-prune.test.ts +7 -4
- package/src/specs/token-accounting-e2e.test.ts +1034 -0
- package/src/specs/token-accounting-pipeline.test.ts +882 -0
- package/src/specs/token-distribution-edge-case.test.ts +25 -26
- package/src/splitStream.test.ts +42 -33
- package/src/stream.ts +64 -11
- package/src/summarization/__tests__/aggregator.test.ts +153 -0
- package/src/summarization/__tests__/node.test.ts +708 -0
- package/src/summarization/__tests__/trigger.test.ts +50 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/node.ts +982 -0
- package/src/tools/ToolNode.ts +25 -3
- package/src/types/graph.ts +62 -7
- package/src/types/index.ts +1 -0
- package/src/types/run.ts +32 -0
- package/src/types/stream.ts +45 -5
- package/src/types/summarize.ts +58 -0
- package/src/types/tools.ts +7 -0
- package/src/utils/errors.ts +117 -0
- package/src/utils/events.ts +31 -0
- package/src/utils/handlers.ts +18 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/llm.ts +12 -0
- package/src/utils/tokens.ts +336 -18
- package/src/utils/truncation.ts +124 -0
- package/src/scripts/image.ts +0 -180
package/src/index.ts
CHANGED
|
@@ -8,6 +8,9 @@ export * from './messages';
|
|
|
8
8
|
/* Graphs */
|
|
9
9
|
export * from './graphs';
|
|
10
10
|
|
|
11
|
+
/* Summarization */
|
|
12
|
+
export * from './summarization';
|
|
13
|
+
|
|
11
14
|
/* Tools */
|
|
12
15
|
export * from './tools/Calculator';
|
|
13
16
|
export * from './tools/CodeExecutor';
|
|
@@ -33,3 +36,7 @@ export type {
|
|
|
33
36
|
OpenRouterReasoningEffort,
|
|
34
37
|
ChatOpenRouterCallOptions,
|
|
35
38
|
} from './llm/openrouter';
|
|
39
|
+
export { getChatModelClass } from './llm/providers';
|
|
40
|
+
export { initializeModel } from './llm/init';
|
|
41
|
+
export { attemptInvoke, tryFallbackProviders } from './llm/invoke';
|
|
42
|
+
export { isThinkingEnabled, getMaxOutputTokensKey } from './llm/request';
|
|
@@ -56,13 +56,13 @@ async function invoke(
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// use this for tests involving "extended thinking"
|
|
59
|
-
const extendedThinkingModelName = 'claude-
|
|
59
|
+
const extendedThinkingModelName = 'claude-sonnet-4-5-20250929';
|
|
60
60
|
|
|
61
61
|
// use this for tests involving citations
|
|
62
62
|
const citationsModelName = 'claude-sonnet-4-5-20250929';
|
|
63
63
|
|
|
64
64
|
// use this for tests involving PDF documents
|
|
65
|
-
const pdfModelName = 'claude-
|
|
65
|
+
const pdfModelName = 'claude-haiku-4-5';
|
|
66
66
|
|
|
67
67
|
// Use this model for all other tests
|
|
68
68
|
const modelName = 'claude-3-haiku-20240307';
|
|
@@ -1401,7 +1401,7 @@ test('Test redacted thinking blocks multiturn streaming', async () => {
|
|
|
1401
1401
|
|
|
1402
1402
|
test('Can handle google function calling blocks in content', async () => {
|
|
1403
1403
|
const chat = new ChatAnthropic({
|
|
1404
|
-
modelName: 'claude-
|
|
1404
|
+
modelName: 'claude-sonnet-4-5-20250929',
|
|
1405
1405
|
maxRetries: 0,
|
|
1406
1406
|
});
|
|
1407
1407
|
const toolCallId = 'tool_call_id';
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
AnthropicCompactionBlockParam,
|
|
35
35
|
AnthropicToolResponse,
|
|
36
36
|
} from '../types';
|
|
37
|
+
import { Constants } from '@/common';
|
|
37
38
|
|
|
38
39
|
function _formatImage(imageUrl: string) {
|
|
39
40
|
const parsed = parseBase64DataUrl({ dataUrl: imageUrl });
|
|
@@ -377,8 +378,7 @@ function _formatContent(message: BaseMessage) {
|
|
|
377
378
|
const rawPart = contentPart as Record<string, unknown>;
|
|
378
379
|
const id = rawPart.id as string;
|
|
379
380
|
|
|
380
|
-
|
|
381
|
-
if (id && id.startsWith('srvtoolu_')) {
|
|
381
|
+
if (id && id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)) {
|
|
382
382
|
let input = rawPart.input;
|
|
383
383
|
|
|
384
384
|
// Ensure input is an object
|
|
@@ -420,8 +420,10 @@ function _formatContent(message: BaseMessage) {
|
|
|
420
420
|
const toolUseId = rawPart.tool_use_id as string;
|
|
421
421
|
const content = rawPart.content;
|
|
422
422
|
|
|
423
|
-
|
|
424
|
-
|
|
423
|
+
if (
|
|
424
|
+
toolUseId &&
|
|
425
|
+
toolUseId.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)
|
|
426
|
+
) {
|
|
425
427
|
// Verify content is either an array (success) or error object
|
|
426
428
|
const isValidContent =
|
|
427
429
|
Array.isArray(content) ||
|
|
@@ -795,7 +795,7 @@ describe.skip('Integration tests', () => {
|
|
|
795
795
|
test('with thinking/reasoning enabled', async () => {
|
|
796
796
|
const model = new CustomChatBedrockConverse({
|
|
797
797
|
...integrationArgs,
|
|
798
|
-
model: 'us.anthropic.claude-
|
|
798
|
+
model: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
|
799
799
|
maxTokens: 5000,
|
|
800
800
|
additionalModelRequestFields: {
|
|
801
801
|
thinking: { type: 'enabled', budget_tokens: 2000 },
|
|
@@ -316,9 +316,13 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
|
|
|
316
316
|
const contentBlocks: BedrockContentBlock[] = [];
|
|
317
317
|
|
|
318
318
|
concatenatedBlocks.forEach((block) => {
|
|
319
|
-
if (block.type === 'text'
|
|
319
|
+
if (block.type === 'text') {
|
|
320
|
+
const text = (block as { text?: string }).text ?? '';
|
|
321
|
+
// Skip completely empty text blocks (common in AI messages with tool_use blocks)
|
|
322
|
+
if (text === '') {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
320
325
|
// Merge whitespace/newlines with previous text blocks to avoid validation errors.
|
|
321
|
-
const text = (block as { text: string }).text;
|
|
322
326
|
const cleanedText = text.replace(/\n/g, '').trim();
|
|
323
327
|
if (cleanedText === '') {
|
|
324
328
|
if (contentBlocks.length > 0) {
|
package/src/llm/init.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ChatVertexAI } from '@langchain/google-vertexai';
|
|
2
|
+
import type { Runnable } from '@langchain/core/runnables';
|
|
3
|
+
import type * as t from '@/types';
|
|
4
|
+
import { ChatOpenAI, AzureChatOpenAI } from '@/llm/openai';
|
|
5
|
+
import { getChatModelClass } from '@/llm/providers';
|
|
6
|
+
import { isOpenAILike } from '@/utils';
|
|
7
|
+
import { Providers } from '@/common';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a chat model instance for a given provider, applies provider-specific
|
|
11
|
+
* field assignments, and optionally binds tools.
|
|
12
|
+
*
|
|
13
|
+
* This is the single entry point for model creation across the codebase — used
|
|
14
|
+
* by both the agent graph (main LLM) and the summarization node (compaction LLM).
|
|
15
|
+
* An optional `override` model can be passed to skip construction entirely
|
|
16
|
+
* (useful for cached/reused model instances or test fakes).
|
|
17
|
+
*/
|
|
18
|
+
export function initializeModel({
|
|
19
|
+
provider,
|
|
20
|
+
clientOptions,
|
|
21
|
+
tools,
|
|
22
|
+
override,
|
|
23
|
+
}: {
|
|
24
|
+
provider: Providers;
|
|
25
|
+
clientOptions?: t.ClientOptions;
|
|
26
|
+
tools?: t.GraphTools;
|
|
27
|
+
override?: t.ChatModelInstance;
|
|
28
|
+
}): Runnable {
|
|
29
|
+
const model =
|
|
30
|
+
override ??
|
|
31
|
+
new (getChatModelClass(provider))(clientOptions ?? ({} as never));
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
isOpenAILike(provider) &&
|
|
35
|
+
(model instanceof ChatOpenAI || model instanceof AzureChatOpenAI)
|
|
36
|
+
) {
|
|
37
|
+
const opts = clientOptions as t.OpenAIClientOptions | undefined;
|
|
38
|
+
if (opts) {
|
|
39
|
+
model.temperature = opts.temperature as number;
|
|
40
|
+
model.topP = opts.topP as number;
|
|
41
|
+
model.frequencyPenalty = opts.frequencyPenalty as number;
|
|
42
|
+
model.presencePenalty = opts.presencePenalty as number;
|
|
43
|
+
model.n = opts.n as number;
|
|
44
|
+
}
|
|
45
|
+
} else if (provider === Providers.VERTEXAI && model instanceof ChatVertexAI) {
|
|
46
|
+
const opts = clientOptions as t.VertexAIClientOptions | undefined;
|
|
47
|
+
if (opts) {
|
|
48
|
+
model.temperature = opts.temperature as number;
|
|
49
|
+
model.topP = opts.topP as number;
|
|
50
|
+
model.topK = opts.topK as number;
|
|
51
|
+
model.topLogprobs = opts.topLogprobs as number;
|
|
52
|
+
model.frequencyPenalty = opts.frequencyPenalty as number;
|
|
53
|
+
model.presencePenalty = opts.presencePenalty as number;
|
|
54
|
+
model.maxOutputTokens = opts.maxOutputTokens as number;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!tools || tools.length === 0) {
|
|
59
|
+
return model as unknown as Runnable;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (model as t.ModelWithTools).bindTools(tools);
|
|
63
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { concat } from '@langchain/core/utils/stream';
|
|
2
|
+
import { AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
4
|
+
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
5
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
6
|
+
import type * as t from '@/types';
|
|
7
|
+
import { manualToolStreamProviders } from '@/llm/providers';
|
|
8
|
+
import { modifyDeltaProperties } from '@/messages';
|
|
9
|
+
import { ChatModelStreamHandler } from '@/stream';
|
|
10
|
+
import { GraphEvents, Providers } from '@/common';
|
|
11
|
+
import { initializeModel } from '@/llm/init';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Context passed to `attemptInvoke` for the default stream handler.
|
|
15
|
+
* Matches the subset of Graph that `ChatModelStreamHandler.handle` needs.
|
|
16
|
+
*/
|
|
17
|
+
export type InvokeContext = Parameters<ChatModelStreamHandler['handle']>[3];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Per-chunk callback for custom stream processing.
|
|
21
|
+
* When provided, replaces the default `ChatModelStreamHandler`.
|
|
22
|
+
*/
|
|
23
|
+
export type OnChunk = (chunk: AIMessageChunk) => void | Promise<void>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Invokes a chat model with the given messages, handling both streaming and
|
|
27
|
+
* non-streaming paths.
|
|
28
|
+
*
|
|
29
|
+
* By default, stream chunks are processed through a `ChatModelStreamHandler`
|
|
30
|
+
* that dispatches run steps (MESSAGE_CREATION, TOOL_CALLS) for the graph.
|
|
31
|
+
* Pass an `onChunk` callback to override this with custom chunk processing
|
|
32
|
+
* (e.g. summarization delta events).
|
|
33
|
+
*/
|
|
34
|
+
export async function attemptInvoke(
|
|
35
|
+
{
|
|
36
|
+
model,
|
|
37
|
+
messages,
|
|
38
|
+
provider,
|
|
39
|
+
context,
|
|
40
|
+
onChunk,
|
|
41
|
+
}: {
|
|
42
|
+
model: t.ChatModel;
|
|
43
|
+
messages: BaseMessage[];
|
|
44
|
+
provider: Providers;
|
|
45
|
+
context?: InvokeContext;
|
|
46
|
+
onChunk?: OnChunk;
|
|
47
|
+
},
|
|
48
|
+
config?: RunnableConfig
|
|
49
|
+
): Promise<Partial<t.BaseGraphState>> {
|
|
50
|
+
if (model.stream) {
|
|
51
|
+
const stream = await model.stream(messages, config);
|
|
52
|
+
let finalChunk: AIMessageChunk | undefined;
|
|
53
|
+
|
|
54
|
+
if (onChunk) {
|
|
55
|
+
for await (const chunk of stream) {
|
|
56
|
+
await onChunk(chunk);
|
|
57
|
+
finalChunk = finalChunk ? concat(finalChunk, chunk) : chunk;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
const metadata = config?.metadata as Record<string, unknown> | undefined;
|
|
61
|
+
const streamHandler = new ChatModelStreamHandler();
|
|
62
|
+
for await (const chunk of stream) {
|
|
63
|
+
await streamHandler.handle(
|
|
64
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
65
|
+
{ chunk },
|
|
66
|
+
metadata,
|
|
67
|
+
context
|
|
68
|
+
);
|
|
69
|
+
finalChunk = finalChunk ? concat(finalChunk, chunk) : chunk;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (manualToolStreamProviders.has(provider)) {
|
|
74
|
+
finalChunk = modifyDeltaProperties(provider, finalChunk);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if ((finalChunk?.tool_calls?.length ?? 0) > 0) {
|
|
78
|
+
finalChunk!.tool_calls = finalChunk!.tool_calls?.filter(
|
|
79
|
+
(tool_call: ToolCall) => !!tool_call.name
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { messages: [finalChunk as AIMessageChunk] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const finalMessage = await model.invoke(messages, config);
|
|
87
|
+
if ((finalMessage.tool_calls?.length ?? 0) > 0) {
|
|
88
|
+
finalMessage.tool_calls = finalMessage.tool_calls?.filter(
|
|
89
|
+
(tool_call: ToolCall) => !!tool_call.name
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return { messages: [finalMessage] };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Attempts each fallback provider in order until one succeeds.
|
|
97
|
+
* Throws the last error if all fallbacks fail.
|
|
98
|
+
*/
|
|
99
|
+
export async function tryFallbackProviders({
|
|
100
|
+
fallbacks,
|
|
101
|
+
tools,
|
|
102
|
+
messages,
|
|
103
|
+
config,
|
|
104
|
+
primaryError,
|
|
105
|
+
context,
|
|
106
|
+
onChunk,
|
|
107
|
+
}: {
|
|
108
|
+
fallbacks: Array<{ provider: Providers; clientOptions?: t.ClientOptions }>;
|
|
109
|
+
tools?: t.GraphTools;
|
|
110
|
+
messages: BaseMessage[];
|
|
111
|
+
config?: RunnableConfig;
|
|
112
|
+
primaryError: unknown;
|
|
113
|
+
context?: InvokeContext;
|
|
114
|
+
onChunk?: OnChunk;
|
|
115
|
+
}): Promise<Partial<t.BaseGraphState> | undefined> {
|
|
116
|
+
let lastError: unknown = primaryError;
|
|
117
|
+
for (const fb of fallbacks) {
|
|
118
|
+
try {
|
|
119
|
+
const fbModel = initializeModel({
|
|
120
|
+
provider: fb.provider,
|
|
121
|
+
clientOptions: fb.clientOptions,
|
|
122
|
+
tools,
|
|
123
|
+
});
|
|
124
|
+
const result = await attemptInvoke(
|
|
125
|
+
{
|
|
126
|
+
model: fbModel as t.ChatModel,
|
|
127
|
+
messages,
|
|
128
|
+
provider: fb.provider,
|
|
129
|
+
context,
|
|
130
|
+
onChunk,
|
|
131
|
+
},
|
|
132
|
+
config
|
|
133
|
+
);
|
|
134
|
+
return result;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
lastError = e;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (lastError !== undefined) {
|
|
141
|
+
throw lastError;
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type * as t from '@/types';
|
|
2
|
+
import { Providers } from '@/common';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns true when the provider + clientOptions indicate extended thinking
|
|
6
|
+
* is enabled. Works across Anthropic (direct), Bedrock (additionalModelRequestFields),
|
|
7
|
+
* and OpenAI-compat (modelKwargs.thinking).
|
|
8
|
+
*/
|
|
9
|
+
export function isThinkingEnabled(
|
|
10
|
+
provider: Providers,
|
|
11
|
+
clientOptions?: t.ClientOptions
|
|
12
|
+
): boolean {
|
|
13
|
+
if (!clientOptions) return false;
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
provider === Providers.ANTHROPIC &&
|
|
17
|
+
(clientOptions as t.AnthropicClientOptions).thinking != null
|
|
18
|
+
) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
provider === Providers.BEDROCK &&
|
|
24
|
+
(clientOptions as t.BedrockAnthropicInput).additionalModelRequestFields?.[
|
|
25
|
+
'thinking'
|
|
26
|
+
] != null
|
|
27
|
+
) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
provider === Providers.OPENAI &&
|
|
33
|
+
(
|
|
34
|
+
(clientOptions as t.OpenAIClientOptions).modelKwargs
|
|
35
|
+
?.thinking as t.AnthropicClientOptions['thinking']
|
|
36
|
+
)?.type === 'enabled'
|
|
37
|
+
) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the correct key for setting max output tokens on the model
|
|
46
|
+
* constructor options. Google/Vertex use `maxOutputTokens`, all others
|
|
47
|
+
* use `maxTokens`.
|
|
48
|
+
*/
|
|
49
|
+
export function getMaxOutputTokensKey(
|
|
50
|
+
provider: Providers | string
|
|
51
|
+
): 'maxOutputTokens' | 'maxTokens' {
|
|
52
|
+
return provider === Providers.GOOGLE || provider === Providers.VERTEXAI
|
|
53
|
+
? 'maxOutputTokens'
|
|
54
|
+
: 'maxTokens';
|
|
55
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
2
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
3
|
+
import type { TokenCounter } from '@/types/run';
|
|
4
|
+
import { maskConsumedToolResults } from '@/messages/prune';
|
|
5
|
+
|
|
6
|
+
const charCounter: TokenCounter = (msg) => {
|
|
7
|
+
const raw = msg.content;
|
|
8
|
+
if (typeof raw === 'string') return raw.length;
|
|
9
|
+
return 0;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function toolMsg(
|
|
13
|
+
content: string,
|
|
14
|
+
name = 'tool',
|
|
15
|
+
toolCallId = `tc_${Math.random().toString(36).slice(2, 8)}`
|
|
16
|
+
): ToolMessage {
|
|
17
|
+
return new ToolMessage({ content, tool_call_id: toolCallId, name });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function aiWithText(text: string): AIMessage {
|
|
21
|
+
return new AIMessage(text);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function aiToolCall(toolCallId: string, name = 'tool'): AIMessage {
|
|
25
|
+
return new AIMessage({
|
|
26
|
+
content: [{ type: 'tool_use', id: toolCallId, name, input: {} }],
|
|
27
|
+
tool_calls: [{ id: toolCallId, name, args: {}, type: 'tool_call' }],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('maskConsumedToolResults', () => {
|
|
32
|
+
it('masks consumed tool results (followed by AI with text)', () => {
|
|
33
|
+
const tcId = 'tc_1';
|
|
34
|
+
const messages: BaseMessage[] = [
|
|
35
|
+
new HumanMessage('hello'),
|
|
36
|
+
aiToolCall(tcId, 'search'),
|
|
37
|
+
toolMsg('A'.repeat(1000), 'search', tcId),
|
|
38
|
+
aiWithText('Based on the search results, here is the answer.'),
|
|
39
|
+
];
|
|
40
|
+
const map: Record<string, number | undefined> = {
|
|
41
|
+
0: 5,
|
|
42
|
+
1: 20,
|
|
43
|
+
2: 1000,
|
|
44
|
+
3: 50,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const count = maskConsumedToolResults({
|
|
48
|
+
messages,
|
|
49
|
+
indexTokenCountMap: map,
|
|
50
|
+
tokenCounter: charCounter,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(count).toBe(1);
|
|
54
|
+
const maskedContent = messages[2].content as string;
|
|
55
|
+
expect(maskedContent.length).toBeLessThan(1000);
|
|
56
|
+
expect(maskedContent.length).toBeLessThanOrEqual(300);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('does NOT mask unconsumed tool results (no subsequent AI text)', () => {
|
|
60
|
+
const tcId = 'tc_1';
|
|
61
|
+
const messages: BaseMessage[] = [
|
|
62
|
+
new HumanMessage('hello'),
|
|
63
|
+
aiToolCall(tcId, 'search'),
|
|
64
|
+
toolMsg('A'.repeat(1000), 'search', tcId),
|
|
65
|
+
];
|
|
66
|
+
const map: Record<string, number | undefined> = {
|
|
67
|
+
0: 5,
|
|
68
|
+
1: 20,
|
|
69
|
+
2: 1000,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const count = maskConsumedToolResults({
|
|
73
|
+
messages,
|
|
74
|
+
indexTokenCountMap: map,
|
|
75
|
+
tokenCounter: charCounter,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(count).toBe(0);
|
|
79
|
+
expect((messages[2].content as string).length).toBe(1000);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('does NOT mask tool results followed by AI with only tool calls (no text)', () => {
|
|
83
|
+
const tcId1 = 'tc_1';
|
|
84
|
+
const tcId2 = 'tc_2';
|
|
85
|
+
const messages: BaseMessage[] = [
|
|
86
|
+
new HumanMessage('hello'),
|
|
87
|
+
aiToolCall(tcId1, 'search'),
|
|
88
|
+
toolMsg('A'.repeat(1000), 'search', tcId1),
|
|
89
|
+
aiToolCall(tcId2, 'fetch'),
|
|
90
|
+
toolMsg('B'.repeat(500), 'fetch', tcId2),
|
|
91
|
+
];
|
|
92
|
+
const map: Record<string, number | undefined> = {
|
|
93
|
+
0: 5,
|
|
94
|
+
1: 20,
|
|
95
|
+
2: 1000,
|
|
96
|
+
3: 20,
|
|
97
|
+
4: 500,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const count = maskConsumedToolResults({
|
|
101
|
+
messages,
|
|
102
|
+
indexTokenCountMap: map,
|
|
103
|
+
tokenCounter: charCounter,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// No AI message with substantive text exists, so nothing is consumed
|
|
107
|
+
expect(count).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('masks multiple consumed results before a text AI response', () => {
|
|
111
|
+
const tcId1 = 'tc_1';
|
|
112
|
+
const tcId2 = 'tc_2';
|
|
113
|
+
const messages: BaseMessage[] = [
|
|
114
|
+
new HumanMessage('hello'),
|
|
115
|
+
aiToolCall(tcId1, 'search'),
|
|
116
|
+
toolMsg('A'.repeat(1000), 'search', tcId1),
|
|
117
|
+
aiToolCall(tcId2, 'fetch'),
|
|
118
|
+
toolMsg('B'.repeat(800), 'fetch', tcId2),
|
|
119
|
+
aiWithText('Here are the combined results.'),
|
|
120
|
+
];
|
|
121
|
+
const map: Record<string, number | undefined> = {
|
|
122
|
+
0: 5,
|
|
123
|
+
1: 20,
|
|
124
|
+
2: 1000,
|
|
125
|
+
3: 20,
|
|
126
|
+
4: 800,
|
|
127
|
+
5: 30,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const count = maskConsumedToolResults({
|
|
131
|
+
messages,
|
|
132
|
+
indexTokenCountMap: map,
|
|
133
|
+
tokenCounter: charCounter,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(count).toBe(2);
|
|
137
|
+
expect((messages[2].content as string).length).toBeLessThanOrEqual(300);
|
|
138
|
+
expect((messages[4].content as string).length).toBeLessThanOrEqual(300);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('never masks AI messages', () => {
|
|
142
|
+
const messages: BaseMessage[] = [
|
|
143
|
+
new HumanMessage('hello'),
|
|
144
|
+
aiWithText('A'.repeat(2000)),
|
|
145
|
+
];
|
|
146
|
+
const map: Record<string, number | undefined> = {
|
|
147
|
+
0: 5,
|
|
148
|
+
1: 2000,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const count = maskConsumedToolResults({
|
|
152
|
+
messages,
|
|
153
|
+
indexTokenCountMap: map,
|
|
154
|
+
tokenCounter: charCounter,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(count).toBe(0);
|
|
158
|
+
expect((messages[1].content as string).length).toBe(2000);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('skips short tool results below maxChars threshold', () => {
|
|
162
|
+
const tcId = 'tc_1';
|
|
163
|
+
const messages: BaseMessage[] = [
|
|
164
|
+
new HumanMessage('hello'),
|
|
165
|
+
aiToolCall(tcId, 'search'),
|
|
166
|
+
toolMsg('short result', 'search', tcId),
|
|
167
|
+
aiWithText('Got it.'),
|
|
168
|
+
];
|
|
169
|
+
const map: Record<string, number | undefined> = {
|
|
170
|
+
0: 5,
|
|
171
|
+
1: 20,
|
|
172
|
+
2: 12,
|
|
173
|
+
3: 7,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const count = maskConsumedToolResults({
|
|
177
|
+
messages,
|
|
178
|
+
indexTokenCountMap: map,
|
|
179
|
+
tokenCounter: charCounter,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(count).toBe(0);
|
|
183
|
+
expect(messages[2].content).toBe('short result');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('updates indexTokenCountMap for masked messages', () => {
|
|
187
|
+
const tcId = 'tc_1';
|
|
188
|
+
const messages: BaseMessage[] = [
|
|
189
|
+
new HumanMessage('hello'),
|
|
190
|
+
aiToolCall(tcId, 'search'),
|
|
191
|
+
toolMsg('A'.repeat(2000), 'search', tcId),
|
|
192
|
+
aiWithText('Summary of results.'),
|
|
193
|
+
];
|
|
194
|
+
const map: Record<string, number | undefined> = {
|
|
195
|
+
0: 5,
|
|
196
|
+
1: 20,
|
|
197
|
+
2: 2000,
|
|
198
|
+
3: 20,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
maskConsumedToolResults({
|
|
202
|
+
messages,
|
|
203
|
+
indexTokenCountMap: map,
|
|
204
|
+
tokenCounter: charCounter,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Token count should be updated to match the masked content length
|
|
208
|
+
expect(map[2]).toBeLessThan(2000);
|
|
209
|
+
expect(map[2]).toBe((messages[2].content as string).length);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('handles empty messages array', () => {
|
|
213
|
+
const map: Record<string, number | undefined> = {};
|
|
214
|
+
const count = maskConsumedToolResults({
|
|
215
|
+
messages: [],
|
|
216
|
+
indexTokenCountMap: map,
|
|
217
|
+
tokenCounter: charCounter,
|
|
218
|
+
});
|
|
219
|
+
expect(count).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
});
|