@librechat/agents 3.1.52 → 3.1.54

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 (36) hide show
  1. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +16 -5
  2. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  3. package/dist/cjs/llm/google/index.cjs.map +1 -1
  4. package/dist/cjs/llm/openrouter/index.cjs +59 -5
  5. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  6. package/dist/cjs/llm/vertexai/index.cjs +16 -2
  7. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  8. package/dist/cjs/main.cjs +2 -0
  9. package/dist/cjs/main.cjs.map +1 -1
  10. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +16 -5
  11. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  12. package/dist/esm/llm/google/index.mjs.map +1 -1
  13. package/dist/esm/llm/openrouter/index.mjs +59 -5
  14. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  15. package/dist/esm/llm/vertexai/index.mjs +16 -2
  16. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  17. package/dist/esm/main.mjs +1 -0
  18. package/dist/esm/main.mjs.map +1 -1
  19. package/dist/types/index.d.ts +2 -0
  20. package/dist/types/llm/bedrock/utils/message_outputs.d.ts +1 -1
  21. package/dist/types/llm/google/index.d.ts +2 -3
  22. package/dist/types/llm/openrouter/index.d.ts +21 -1
  23. package/dist/types/llm/vertexai/index.d.ts +2 -1
  24. package/dist/types/types/llm.d.ts +7 -2
  25. package/package.json +1 -1
  26. package/src/index.ts +6 -0
  27. package/src/llm/bedrock/llm.spec.ts +233 -4
  28. package/src/llm/bedrock/utils/message_outputs.ts +51 -11
  29. package/src/llm/google/index.ts +2 -3
  30. package/src/llm/openrouter/index.ts +117 -6
  31. package/src/llm/openrouter/reasoning.test.ts +207 -0
  32. package/src/llm/vertexai/index.ts +20 -3
  33. package/src/scripts/bedrock-cache-debug.ts +250 -0
  34. package/src/specs/openrouter.simple.test.ts +163 -2
  35. package/src/types/llm.ts +7 -2
  36. package/src/utils/llmConfig.ts +3 -4
@@ -7,6 +7,7 @@ import {
7
7
  UsageMetadata,
8
8
  } from '@langchain/core/messages';
9
9
  import type * as t from '@/types';
10
+ import type { ChatOpenRouterCallOptions } from '@/llm/openrouter';
10
11
  import { ToolEndHandler, ModelEndHandler } from '@/events';
11
12
  import { ContentTypes, GraphEvents, Providers, TitleMethod } from '@/common';
12
13
  import { capitalizeFirstLetter } from './spec.utils';
@@ -33,6 +34,8 @@ describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
33
34
  version: 'v2' as const,
34
35
  };
35
36
 
37
+ const baseLLMConfig = getLLMConfig(provider);
38
+
36
39
  beforeEach(async () => {
37
40
  conversationHistory = [];
38
41
  collectedUsage = [];
@@ -56,16 +59,81 @@ describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
56
59
  [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
57
60
  });
58
61
 
62
+ /**
63
+ * Helper: run a reasoning test against a specific model with the given reasoning config.
64
+ * Asserts that reasoning tokens are reported and content is produced.
65
+ */
66
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
67
+ async function runReasoningTest(opts: {
68
+ model: string;
69
+ reasoning?: ChatOpenRouterCallOptions['reasoning'];
70
+ threadId: string;
71
+ runId: string;
72
+ }) {
73
+ const { reasoning: _baseReasoning, ...baseWithoutReasoning } =
74
+ baseLLMConfig as unknown as Record<string, unknown>;
75
+ const llmConfig = {
76
+ ...baseWithoutReasoning,
77
+ model: opts.model,
78
+ ...(opts.reasoning != null ? { reasoning: opts.reasoning } : {}),
79
+ } as t.LLMConfig;
80
+ const customHandlers = setupCustomHandlers();
81
+
82
+ run = await Run.create<t.IState>({
83
+ runId: opts.runId,
84
+ graphConfig: {
85
+ type: 'standard',
86
+ llmConfig,
87
+ instructions: 'You are a helpful AI assistant. Think step by step.',
88
+ },
89
+ returnContent: true,
90
+ skipCleanup: true,
91
+ customHandlers,
92
+ });
93
+
94
+ const userMessage = 'What is 15 * 37 + 128 / 4? Show your work.';
95
+ conversationHistory.push(new HumanMessage(userMessage));
96
+
97
+ const finalContentParts = await run.processStream(
98
+ { messages: conversationHistory },
99
+ { ...configV2, configurable: { thread_id: opts.threadId } }
100
+ );
101
+
102
+ expect(finalContentParts).toBeDefined();
103
+ expect(finalContentParts?.length).toBeGreaterThan(0);
104
+
105
+ // Verify usage metadata was collected
106
+ expect(collectedUsage.length).toBeGreaterThan(0);
107
+ const usage = collectedUsage[0];
108
+ expect(usage.input_tokens).toBeGreaterThan(0);
109
+ expect(usage.output_tokens).toBeGreaterThan(0);
110
+
111
+ // Verify reasoning tokens are reported in output_token_details
112
+ const reasoningTokens =
113
+ (usage.output_token_details as Record<string, number> | undefined)
114
+ ?.reasoning ?? 0;
115
+ expect(reasoningTokens).toBeGreaterThan(0);
116
+
117
+ // Verify the final message has content
118
+ const finalMessages = run.getRunMessages();
119
+ expect(finalMessages).toBeDefined();
120
+ expect(finalMessages?.length).toBeGreaterThan(0);
121
+ const assistantMsg = finalMessages?.[0];
122
+ expect(typeof assistantMsg?.content).toBe('string');
123
+ expect((assistantMsg?.content as string).length).toBeGreaterThan(0);
124
+
125
+ return { usage, reasoningTokens, finalMessages };
126
+ }
127
+
59
128
  test(`${capitalizeFirstLetter(provider)}: simple stream + title`, async () => {
60
129
  const { userName, location } = await getArgs();
61
- const llmConfig = getLLMConfig(provider);
62
130
  const customHandlers = setupCustomHandlers();
63
131
 
64
132
  run = await Run.create<t.IState>({
65
133
  runId: 'or-run-1',
66
134
  graphConfig: {
67
135
  type: 'standard',
68
- llmConfig,
136
+ llmConfig: baseLLMConfig,
69
137
  tools: [new Calculator()],
70
138
  instructions: 'You are a friendly AI assistant.',
71
139
  additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
@@ -104,4 +172,97 @@ describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
104
172
  });
105
173
  expect(titleRes.title).toBeDefined();
106
174
  });
175
+
176
+ test(`${capitalizeFirstLetter(provider)}: Anthropic does NOT reason by default (no config)`, async () => {
177
+ const { reasoning: _baseReasoning, ...baseWithoutReasoning } =
178
+ baseLLMConfig as unknown as Record<string, unknown>;
179
+ const llmConfig = {
180
+ ...baseWithoutReasoning,
181
+ model: 'anthropic/claude-sonnet-4',
182
+ } as t.LLMConfig;
183
+ const customHandlers = setupCustomHandlers();
184
+
185
+ run = await Run.create<t.IState>({
186
+ runId: 'or-anthropic-default-1',
187
+ graphConfig: {
188
+ type: 'standard',
189
+ llmConfig,
190
+ instructions: 'You are a helpful AI assistant.',
191
+ },
192
+ returnContent: true,
193
+ skipCleanup: true,
194
+ customHandlers,
195
+ });
196
+
197
+ conversationHistory.push(
198
+ new HumanMessage('What is 15 * 37 + 128 / 4? Show your work.')
199
+ );
200
+
201
+ await run.processStream(
202
+ { messages: conversationHistory },
203
+ { ...configV2, configurable: { thread_id: 'or-anthropic-default-1' } }
204
+ );
205
+
206
+ expect(collectedUsage.length).toBeGreaterThan(0);
207
+ const usage = collectedUsage[0];
208
+ // Anthropic requires explicit reasoning config — no reasoning tokens by default
209
+ const reasoningTokens =
210
+ (usage.output_token_details as Record<string, number> | undefined)
211
+ ?.reasoning ?? 0;
212
+ expect(reasoningTokens).toBe(0);
213
+ });
214
+
215
+ test(`${capitalizeFirstLetter(provider)}: Gemini 3 reasons by default (no config)`, async () => {
216
+ await runReasoningTest({
217
+ model: 'google/gemini-3-pro-preview',
218
+ reasoning: undefined,
219
+ threadId: 'or-gemini-default-1',
220
+ runId: 'or-gemini-default-1',
221
+ });
222
+ });
223
+
224
+ test(`${capitalizeFirstLetter(provider)}: Gemini reasoning with max_tokens`, async () => {
225
+ await runReasoningTest({
226
+ model: 'google/gemini-3-pro-preview',
227
+ reasoning: { max_tokens: 4000 },
228
+ threadId: 'or-gemini-reasoning-1',
229
+ runId: 'or-gemini-reasoning-1',
230
+ });
231
+ });
232
+
233
+ test(`${capitalizeFirstLetter(provider)}: Gemini reasoning with effort`, async () => {
234
+ await runReasoningTest({
235
+ model: 'google/gemini-3-flash-preview',
236
+ reasoning: { effort: 'low' },
237
+ threadId: 'or-gemini-effort-1',
238
+ runId: 'or-gemini-effort-1',
239
+ });
240
+ });
241
+
242
+ test(`${capitalizeFirstLetter(provider)}: Anthropic reasoning with max_tokens`, async () => {
243
+ await runReasoningTest({
244
+ model: 'anthropic/claude-sonnet-4',
245
+ reasoning: { max_tokens: 4000 },
246
+ threadId: 'or-anthropic-reasoning-1',
247
+ runId: 'or-anthropic-reasoning-1',
248
+ });
249
+ });
250
+
251
+ test(`${capitalizeFirstLetter(provider)}: Anthropic sonnet-4 reasoning with effort`, async () => {
252
+ await runReasoningTest({
253
+ model: 'anthropic/claude-sonnet-4',
254
+ reasoning: { effort: 'medium' },
255
+ threadId: 'or-anthropic-effort-s4-1',
256
+ runId: 'or-anthropic-effort-s4-1',
257
+ });
258
+ });
259
+
260
+ test(`${capitalizeFirstLetter(provider)}: Anthropic sonnet-4-6 reasoning with effort`, async () => {
261
+ await runReasoningTest({
262
+ model: 'anthropic/claude-sonnet-4-6',
263
+ reasoning: { effort: 'medium' },
264
+ threadId: 'or-anthropic-effort-s46-1',
265
+ runId: 'or-anthropic-effort-s46-1',
266
+ });
267
+ });
107
268
  });
package/src/types/llm.ts CHANGED
@@ -11,7 +11,6 @@ import type {
11
11
  ClientOptions as OAIClientOptions,
12
12
  } from '@langchain/openai';
13
13
  import type { GoogleGenerativeAIChatInput } from '@langchain/google-genai';
14
- import type { GeminiGenerationConfig } from '@langchain/google-common';
15
14
  import type { ChatVertexAIInput } from '@langchain/google-vertexai';
16
15
  import type { ChatDeepSeekCallOptions } from '@langchain/deepseek';
17
16
  import type { ChatOpenRouterCallOptions } from '@/llm/openrouter';
@@ -55,6 +54,11 @@ export type AnthropicReasoning = {
55
54
  thinking?: ThinkingConfig | boolean;
56
55
  thinkingBudget?: number;
57
56
  };
57
+ export type GoogleThinkingConfig = {
58
+ thinkingBudget?: number;
59
+ includeThoughts?: boolean;
60
+ thinkingLevel?: string;
61
+ };
58
62
  export type OpenAIClientOptions = ChatOpenAIFields;
59
63
  export type AnthropicClientOptions = AnthropicInput & {
60
64
  promptCache?: boolean;
@@ -62,6 +66,7 @@ export type AnthropicClientOptions = AnthropicInput & {
62
66
  export type MistralAIClientOptions = ChatMistralAIInput;
63
67
  export type VertexAIClientOptions = ChatVertexAIInput & {
64
68
  includeThoughts?: boolean;
69
+ thinkingConfig?: GoogleThinkingConfig;
65
70
  };
66
71
  export type BedrockAnthropicInput = ChatBedrockConverseInput & {
67
72
  additionalModelRequestFields?: ChatBedrockConverseInput['additionalModelRequestFields'] &
@@ -72,7 +77,7 @@ export type BedrockConverseClientOptions = ChatBedrockConverseInput;
72
77
  export type BedrockAnthropicClientOptions = BedrockAnthropicInput;
73
78
  export type GoogleClientOptions = GoogleGenerativeAIChatInput & {
74
79
  customHeaders?: RequestOptions['customHeaders'];
75
- thinkingConfig?: GeminiGenerationConfig['thinkingConfig'];
80
+ thinkingConfig?: GoogleThinkingConfig;
76
81
  };
77
82
  export type DeepSeekClientOptions = ChatDeepSeekCallOptions;
78
83
  export type XAIClientOptions = ChatXAIInput;
@@ -67,11 +67,10 @@ export const llmConfigs: Record<string, t.LLMConfig | undefined> = {
67
67
  'X-Title': 'LibreChat',
68
68
  },
69
69
  },
70
- include_reasoning: true,
70
+ reasoning: {
71
+ max_tokens: 8000,
72
+ },
71
73
  modelKwargs: {
72
- reasoning: {
73
- max_tokens: 8000,
74
- },
75
74
  max_tokens: 10000,
76
75
  },
77
76
  } as or.ChatOpenRouterCallOptions & t.LLMConfig,