@librechat/agents 3.1.57 → 3.1.61

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 (214) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +326 -62
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +13 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +7 -27
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +303 -222
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +4 -4
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +6 -2
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/init.cjs +60 -0
  14. package/dist/cjs/llm/init.cjs.map +1 -0
  15. package/dist/cjs/llm/invoke.cjs +90 -0
  16. package/dist/cjs/llm/invoke.cjs.map +1 -0
  17. package/dist/cjs/llm/openai/index.cjs +2 -0
  18. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  19. package/dist/cjs/llm/request.cjs +41 -0
  20. package/dist/cjs/llm/request.cjs.map +1 -0
  21. package/dist/cjs/main.cjs +40 -0
  22. package/dist/cjs/main.cjs.map +1 -1
  23. package/dist/cjs/messages/cache.cjs +76 -89
  24. package/dist/cjs/messages/cache.cjs.map +1 -1
  25. package/dist/cjs/messages/contextPruning.cjs +156 -0
  26. package/dist/cjs/messages/contextPruning.cjs.map +1 -0
  27. package/dist/cjs/messages/contextPruningSettings.cjs +53 -0
  28. package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -0
  29. package/dist/cjs/messages/core.cjs +23 -37
  30. package/dist/cjs/messages/core.cjs.map +1 -1
  31. package/dist/cjs/messages/format.cjs +156 -11
  32. package/dist/cjs/messages/format.cjs.map +1 -1
  33. package/dist/cjs/messages/prune.cjs +1161 -49
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/messages/reducer.cjs +87 -0
  36. package/dist/cjs/messages/reducer.cjs.map +1 -0
  37. package/dist/cjs/run.cjs +81 -42
  38. package/dist/cjs/run.cjs.map +1 -1
  39. package/dist/cjs/stream.cjs +54 -7
  40. package/dist/cjs/stream.cjs.map +1 -1
  41. package/dist/cjs/summarization/index.cjs +75 -0
  42. package/dist/cjs/summarization/index.cjs.map +1 -0
  43. package/dist/cjs/summarization/node.cjs +663 -0
  44. package/dist/cjs/summarization/node.cjs.map +1 -0
  45. package/dist/cjs/tools/ToolNode.cjs +16 -8
  46. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  47. package/dist/cjs/tools/handlers.cjs +2 -0
  48. package/dist/cjs/tools/handlers.cjs.map +1 -1
  49. package/dist/cjs/utils/errors.cjs +115 -0
  50. package/dist/cjs/utils/errors.cjs.map +1 -0
  51. package/dist/cjs/utils/events.cjs +17 -0
  52. package/dist/cjs/utils/events.cjs.map +1 -1
  53. package/dist/cjs/utils/handlers.cjs +16 -0
  54. package/dist/cjs/utils/handlers.cjs.map +1 -1
  55. package/dist/cjs/utils/llm.cjs +10 -0
  56. package/dist/cjs/utils/llm.cjs.map +1 -1
  57. package/dist/cjs/utils/tokens.cjs +247 -14
  58. package/dist/cjs/utils/tokens.cjs.map +1 -1
  59. package/dist/cjs/utils/truncation.cjs +107 -0
  60. package/dist/cjs/utils/truncation.cjs.map +1 -0
  61. package/dist/esm/agents/AgentContext.mjs +325 -61
  62. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  63. package/dist/esm/common/enum.mjs +13 -0
  64. package/dist/esm/common/enum.mjs.map +1 -1
  65. package/dist/esm/events.mjs +8 -28
  66. package/dist/esm/events.mjs.map +1 -1
  67. package/dist/esm/graphs/Graph.mjs +307 -226
  68. package/dist/esm/graphs/Graph.mjs.map +1 -1
  69. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +4 -4
  70. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  71. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +6 -2
  72. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  73. package/dist/esm/llm/init.mjs +58 -0
  74. package/dist/esm/llm/init.mjs.map +1 -0
  75. package/dist/esm/llm/invoke.mjs +87 -0
  76. package/dist/esm/llm/invoke.mjs.map +1 -0
  77. package/dist/esm/llm/openai/index.mjs +2 -0
  78. package/dist/esm/llm/openai/index.mjs.map +1 -1
  79. package/dist/esm/llm/request.mjs +38 -0
  80. package/dist/esm/llm/request.mjs.map +1 -0
  81. package/dist/esm/main.mjs +13 -3
  82. package/dist/esm/main.mjs.map +1 -1
  83. package/dist/esm/messages/cache.mjs +76 -89
  84. package/dist/esm/messages/cache.mjs.map +1 -1
  85. package/dist/esm/messages/contextPruning.mjs +154 -0
  86. package/dist/esm/messages/contextPruning.mjs.map +1 -0
  87. package/dist/esm/messages/contextPruningSettings.mjs +50 -0
  88. package/dist/esm/messages/contextPruningSettings.mjs.map +1 -0
  89. package/dist/esm/messages/core.mjs +23 -37
  90. package/dist/esm/messages/core.mjs.map +1 -1
  91. package/dist/esm/messages/format.mjs +156 -11
  92. package/dist/esm/messages/format.mjs.map +1 -1
  93. package/dist/esm/messages/prune.mjs +1158 -52
  94. package/dist/esm/messages/prune.mjs.map +1 -1
  95. package/dist/esm/messages/reducer.mjs +83 -0
  96. package/dist/esm/messages/reducer.mjs.map +1 -0
  97. package/dist/esm/run.mjs +82 -43
  98. package/dist/esm/run.mjs.map +1 -1
  99. package/dist/esm/stream.mjs +54 -7
  100. package/dist/esm/stream.mjs.map +1 -1
  101. package/dist/esm/summarization/index.mjs +73 -0
  102. package/dist/esm/summarization/index.mjs.map +1 -0
  103. package/dist/esm/summarization/node.mjs +659 -0
  104. package/dist/esm/summarization/node.mjs.map +1 -0
  105. package/dist/esm/tools/ToolNode.mjs +16 -8
  106. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  107. package/dist/esm/tools/handlers.mjs +2 -0
  108. package/dist/esm/tools/handlers.mjs.map +1 -1
  109. package/dist/esm/utils/errors.mjs +111 -0
  110. package/dist/esm/utils/errors.mjs.map +1 -0
  111. package/dist/esm/utils/events.mjs +17 -1
  112. package/dist/esm/utils/events.mjs.map +1 -1
  113. package/dist/esm/utils/handlers.mjs +16 -0
  114. package/dist/esm/utils/handlers.mjs.map +1 -1
  115. package/dist/esm/utils/llm.mjs +10 -1
  116. package/dist/esm/utils/llm.mjs.map +1 -1
  117. package/dist/esm/utils/tokens.mjs +245 -15
  118. package/dist/esm/utils/tokens.mjs.map +1 -1
  119. package/dist/esm/utils/truncation.mjs +102 -0
  120. package/dist/esm/utils/truncation.mjs.map +1 -0
  121. package/dist/types/agents/AgentContext.d.ts +124 -6
  122. package/dist/types/common/enum.d.ts +14 -1
  123. package/dist/types/graphs/Graph.d.ts +22 -27
  124. package/dist/types/index.d.ts +5 -0
  125. package/dist/types/llm/init.d.ts +18 -0
  126. package/dist/types/llm/invoke.d.ts +48 -0
  127. package/dist/types/llm/request.d.ts +14 -0
  128. package/dist/types/messages/contextPruning.d.ts +42 -0
  129. package/dist/types/messages/contextPruningSettings.d.ts +44 -0
  130. package/dist/types/messages/core.d.ts +1 -1
  131. package/dist/types/messages/format.d.ts +17 -1
  132. package/dist/types/messages/index.d.ts +3 -0
  133. package/dist/types/messages/prune.d.ts +162 -1
  134. package/dist/types/messages/reducer.d.ts +18 -0
  135. package/dist/types/run.d.ts +12 -1
  136. package/dist/types/summarization/index.d.ts +20 -0
  137. package/dist/types/summarization/node.d.ts +29 -0
  138. package/dist/types/tools/ToolNode.d.ts +3 -1
  139. package/dist/types/types/graph.d.ts +44 -6
  140. package/dist/types/types/index.d.ts +1 -0
  141. package/dist/types/types/run.d.ts +30 -0
  142. package/dist/types/types/stream.d.ts +31 -4
  143. package/dist/types/types/summarize.d.ts +47 -0
  144. package/dist/types/types/tools.d.ts +7 -0
  145. package/dist/types/utils/errors.d.ts +28 -0
  146. package/dist/types/utils/events.d.ts +13 -0
  147. package/dist/types/utils/index.d.ts +2 -0
  148. package/dist/types/utils/llm.d.ts +4 -0
  149. package/dist/types/utils/tokens.d.ts +14 -1
  150. package/dist/types/utils/truncation.d.ts +49 -0
  151. package/package.json +3 -3
  152. package/src/agents/AgentContext.ts +388 -58
  153. package/src/agents/__tests__/AgentContext.test.ts +265 -5
  154. package/src/common/enum.ts +13 -0
  155. package/src/events.ts +9 -39
  156. package/src/graphs/Graph.ts +468 -331
  157. package/src/index.ts +7 -0
  158. package/src/llm/anthropic/llm.spec.ts +3 -3
  159. package/src/llm/anthropic/utils/message_inputs.ts +6 -4
  160. package/src/llm/bedrock/llm.spec.ts +1 -1
  161. package/src/llm/bedrock/utils/message_inputs.ts +6 -2
  162. package/src/llm/init.ts +63 -0
  163. package/src/llm/invoke.ts +144 -0
  164. package/src/llm/request.ts +55 -0
  165. package/src/messages/__tests__/observationMasking.test.ts +221 -0
  166. package/src/messages/cache.ts +77 -102
  167. package/src/messages/contextPruning.ts +191 -0
  168. package/src/messages/contextPruningSettings.ts +90 -0
  169. package/src/messages/core.ts +32 -53
  170. package/src/messages/ensureThinkingBlock.test.ts +39 -39
  171. package/src/messages/format.ts +227 -15
  172. package/src/messages/formatAgentMessages.test.ts +511 -1
  173. package/src/messages/index.ts +3 -0
  174. package/src/messages/prune.ts +1548 -62
  175. package/src/messages/reducer.ts +22 -0
  176. package/src/run.ts +104 -51
  177. package/src/scripts/bedrock-merge-test.ts +1 -1
  178. package/src/scripts/test-thinking-handoff-bedrock.ts +1 -1
  179. package/src/scripts/test-thinking-handoff.ts +1 -1
  180. package/src/scripts/thinking-bedrock.ts +1 -1
  181. package/src/scripts/thinking.ts +1 -1
  182. package/src/specs/anthropic.simple.test.ts +1 -1
  183. package/src/specs/multi-agent-summarization.test.ts +396 -0
  184. package/src/specs/prune.test.ts +1196 -23
  185. package/src/specs/summarization-unit.test.ts +868 -0
  186. package/src/specs/summarization.test.ts +3827 -0
  187. package/src/specs/summarize-prune.test.ts +376 -0
  188. package/src/specs/thinking-handoff.test.ts +10 -10
  189. package/src/specs/thinking-prune.test.ts +7 -4
  190. package/src/specs/token-accounting-e2e.test.ts +1034 -0
  191. package/src/specs/token-accounting-pipeline.test.ts +882 -0
  192. package/src/specs/token-distribution-edge-case.test.ts +25 -26
  193. package/src/splitStream.test.ts +42 -33
  194. package/src/stream.ts +64 -11
  195. package/src/summarization/__tests__/aggregator.test.ts +153 -0
  196. package/src/summarization/__tests__/node.test.ts +708 -0
  197. package/src/summarization/__tests__/trigger.test.ts +50 -0
  198. package/src/summarization/index.ts +102 -0
  199. package/src/summarization/node.ts +982 -0
  200. package/src/tools/ToolNode.ts +25 -3
  201. package/src/types/graph.ts +62 -7
  202. package/src/types/index.ts +1 -0
  203. package/src/types/run.ts +32 -0
  204. package/src/types/stream.ts +45 -5
  205. package/src/types/summarize.ts +58 -0
  206. package/src/types/tools.ts +7 -0
  207. package/src/utils/errors.ts +117 -0
  208. package/src/utils/events.ts +31 -0
  209. package/src/utils/handlers.ts +18 -0
  210. package/src/utils/index.ts +2 -0
  211. package/src/utils/llm.ts +12 -0
  212. package/src/utils/tokens.ts +336 -18
  213. package/src/utils/truncation.ts +124 -0
  214. 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-3-7-sonnet-20250219';
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-3-5-haiku-20241022';
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-3-7-sonnet-latest',
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
- // Only correct if this is definitely a server tool (ID starts with 'srvtoolu_')
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
- // Only correct if this is definitely a server tool result (tool_use_id starts with 'srvtoolu_')
424
- if (toolUseId && toolUseId.startsWith('srvtoolu_')) {
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-3-7-sonnet-20250219-v1:0',
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' && (block as { text?: string }).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) {
@@ -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
+ });