@librechat/agents 3.1.52 → 3.1.53
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/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +59 -5
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +16 -2
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +59 -5
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +16 -2
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/llm/google/index.d.ts +2 -3
- package/dist/types/llm/openrouter/index.d.ts +21 -1
- package/dist/types/llm/vertexai/index.d.ts +2 -1
- package/dist/types/types/llm.d.ts +7 -2
- package/package.json +1 -1
- package/src/index.ts +6 -0
- package/src/llm/google/index.ts +2 -3
- package/src/llm/openrouter/index.ts +117 -6
- package/src/llm/openrouter/reasoning.test.ts +207 -0
- package/src/llm/vertexai/index.ts +20 -3
- package/src/specs/openrouter.simple.test.ts +163 -2
- package/src/types/llm.ts +7 -2
- package/src/utils/llmConfig.ts +3 -4
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { ChatOpenRouter } from './index';
|
|
2
|
+
import type { OpenRouterReasoning, ChatOpenRouterCallOptions } from './index';
|
|
3
|
+
import type { OpenAIChatInput } from '@langchain/openai';
|
|
4
|
+
|
|
5
|
+
type CreateRouterOptions = Partial<
|
|
6
|
+
ChatOpenRouterCallOptions & Pick<OpenAIChatInput, 'model' | 'apiKey'>
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
function createRouter(overrides: CreateRouterOptions = {}): ChatOpenRouter {
|
|
10
|
+
return new ChatOpenRouter({
|
|
11
|
+
model: 'openrouter/test-model',
|
|
12
|
+
apiKey: 'test-key',
|
|
13
|
+
...overrides,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('ChatOpenRouter reasoning handling', () => {
|
|
18
|
+
// ---------------------------------------------------------------
|
|
19
|
+
// 1. Constructor reasoning config
|
|
20
|
+
// ---------------------------------------------------------------
|
|
21
|
+
describe('constructor reasoning config', () => {
|
|
22
|
+
it('stores reasoning when passed directly', () => {
|
|
23
|
+
const router = createRouter({ reasoning: { effort: 'high' } });
|
|
24
|
+
const params = router.invocationParams();
|
|
25
|
+
expect(params.reasoning).toEqual({ effort: 'high' });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------
|
|
30
|
+
// 2. modelKwargs reasoning extraction
|
|
31
|
+
// ---------------------------------------------------------------
|
|
32
|
+
describe('modelKwargs reasoning extraction', () => {
|
|
33
|
+
it('extracts reasoning from modelKwargs and places it into params.reasoning', () => {
|
|
34
|
+
const router = createRouter({
|
|
35
|
+
modelKwargs: { reasoning: { effort: 'medium' } },
|
|
36
|
+
});
|
|
37
|
+
const params = router.invocationParams();
|
|
38
|
+
expect(params.reasoning).toEqual({ effort: 'medium' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('does not leak reasoning into modelKwargs that reach the parent', () => {
|
|
42
|
+
const router = createRouter({
|
|
43
|
+
modelKwargs: {
|
|
44
|
+
reasoning: { effort: 'medium' },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const params = router.invocationParams();
|
|
48
|
+
// reasoning should be the structured OpenRouter object, not buried in modelKwargs
|
|
49
|
+
expect(params.reasoning).toEqual({ effort: 'medium' });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------
|
|
54
|
+
// 3. Reasoning merge precedence
|
|
55
|
+
// ---------------------------------------------------------------
|
|
56
|
+
describe('reasoning merge precedence', () => {
|
|
57
|
+
it('constructor reasoning overrides modelKwargs.reasoning', () => {
|
|
58
|
+
const router = createRouter({
|
|
59
|
+
reasoning: { effort: 'high' },
|
|
60
|
+
modelKwargs: { reasoning: { effort: 'low' } },
|
|
61
|
+
});
|
|
62
|
+
const params = router.invocationParams();
|
|
63
|
+
expect(params.reasoning).toEqual({ effort: 'high' });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('merges non-overlapping keys from modelKwargs.reasoning and constructor reasoning', () => {
|
|
67
|
+
const router = createRouter({
|
|
68
|
+
reasoning: { effort: 'high' },
|
|
69
|
+
modelKwargs: { reasoning: { max_tokens: 5000 } },
|
|
70
|
+
});
|
|
71
|
+
const params = router.invocationParams();
|
|
72
|
+
expect(params.reasoning).toEqual({ effort: 'high', max_tokens: 5000 });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------
|
|
77
|
+
// 4. invocationParams output
|
|
78
|
+
// ---------------------------------------------------------------
|
|
79
|
+
describe('invocationParams output', () => {
|
|
80
|
+
it('includes reasoning object in params', () => {
|
|
81
|
+
const router = createRouter({ reasoning: { effort: 'high' } });
|
|
82
|
+
const params = router.invocationParams();
|
|
83
|
+
expect(params.reasoning).toBeDefined();
|
|
84
|
+
expect(params.reasoning).toEqual({ effort: 'high' });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does NOT include reasoning_effort in params', () => {
|
|
88
|
+
const router = createRouter({ reasoning: { effort: 'high' } });
|
|
89
|
+
const params = router.invocationParams();
|
|
90
|
+
expect(params.reasoning_effort).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('does not include reasoning when none is configured', () => {
|
|
94
|
+
const router = createRouter();
|
|
95
|
+
const params = router.invocationParams();
|
|
96
|
+
expect(params.reasoning).toBeUndefined();
|
|
97
|
+
expect(params.reasoning_effort).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------
|
|
102
|
+
// 5. Legacy include_reasoning
|
|
103
|
+
// ---------------------------------------------------------------
|
|
104
|
+
describe('legacy include_reasoning', () => {
|
|
105
|
+
it('produces { enabled: true } when only include_reasoning is true', () => {
|
|
106
|
+
const router = createRouter({ include_reasoning: true });
|
|
107
|
+
const params = router.invocationParams();
|
|
108
|
+
expect(params.reasoning).toEqual({ enabled: true });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('does not produce reasoning when include_reasoning is false', () => {
|
|
112
|
+
const router = createRouter({ include_reasoning: false });
|
|
113
|
+
const params = router.invocationParams();
|
|
114
|
+
expect(params.reasoning).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------
|
|
119
|
+
// 6. Legacy include_reasoning ignored when reasoning is provided
|
|
120
|
+
// ---------------------------------------------------------------
|
|
121
|
+
describe('legacy include_reasoning ignored when reasoning provided', () => {
|
|
122
|
+
it('reasoning wins over include_reasoning', () => {
|
|
123
|
+
const router = createRouter({
|
|
124
|
+
reasoning: { effort: 'medium' },
|
|
125
|
+
include_reasoning: true,
|
|
126
|
+
});
|
|
127
|
+
const params = router.invocationParams();
|
|
128
|
+
// Should use the structured reasoning, NOT fall back to { enabled: true }
|
|
129
|
+
expect(params.reasoning).toEqual({ effort: 'medium' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('reasoning from modelKwargs also wins over include_reasoning', () => {
|
|
133
|
+
const router = createRouter({
|
|
134
|
+
modelKwargs: { reasoning: { effort: 'low' } },
|
|
135
|
+
include_reasoning: true,
|
|
136
|
+
});
|
|
137
|
+
const params = router.invocationParams();
|
|
138
|
+
expect(params.reasoning).toEqual({ effort: 'low' });
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------
|
|
143
|
+
// 7. Various effort levels (OpenRouter-specific)
|
|
144
|
+
// ---------------------------------------------------------------
|
|
145
|
+
describe('various effort levels', () => {
|
|
146
|
+
const efforts: Array<{
|
|
147
|
+
effort: OpenRouterReasoning['effort'];
|
|
148
|
+
}> = [
|
|
149
|
+
{ effort: 'xhigh' },
|
|
150
|
+
{ effort: 'none' },
|
|
151
|
+
{ effort: 'minimal' },
|
|
152
|
+
{ effort: 'high' },
|
|
153
|
+
{ effort: 'medium' },
|
|
154
|
+
{ effort: 'low' },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
it.each(efforts)('supports effort level "$effort"', ({ effort }) => {
|
|
158
|
+
const router = createRouter({ reasoning: { effort } });
|
|
159
|
+
const params = router.invocationParams();
|
|
160
|
+
expect(params.reasoning).toEqual({ effort });
|
|
161
|
+
expect(params.reasoning_effort).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------
|
|
166
|
+
// 8. max_tokens reasoning
|
|
167
|
+
// ---------------------------------------------------------------
|
|
168
|
+
describe('max_tokens reasoning', () => {
|
|
169
|
+
it('passes max_tokens in reasoning object', () => {
|
|
170
|
+
const router = createRouter({
|
|
171
|
+
reasoning: { max_tokens: 8000 },
|
|
172
|
+
});
|
|
173
|
+
const params = router.invocationParams();
|
|
174
|
+
expect(params.reasoning).toEqual({ max_tokens: 8000 });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('combines max_tokens with effort', () => {
|
|
178
|
+
const router = createRouter({
|
|
179
|
+
reasoning: { effort: 'high', max_tokens: 8000 },
|
|
180
|
+
});
|
|
181
|
+
const params = router.invocationParams();
|
|
182
|
+
expect(params.reasoning).toEqual({ effort: 'high', max_tokens: 8000 });
|
|
183
|
+
expect(params.reasoning_effort).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------
|
|
188
|
+
// 9. exclude reasoning
|
|
189
|
+
// ---------------------------------------------------------------
|
|
190
|
+
describe('exclude reasoning', () => {
|
|
191
|
+
it('passes exclude flag in reasoning object', () => {
|
|
192
|
+
const router = createRouter({
|
|
193
|
+
reasoning: { effort: 'high', exclude: true },
|
|
194
|
+
});
|
|
195
|
+
const params = router.invocationParams();
|
|
196
|
+
expect(params.reasoning).toEqual({ effort: 'high', exclude: true });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('supports exclude without effort', () => {
|
|
200
|
+
const router = createRouter({
|
|
201
|
+
reasoning: { exclude: true },
|
|
202
|
+
});
|
|
203
|
+
const params = router.invocationParams();
|
|
204
|
+
expect(params.reasoning).toEqual({ exclude: true });
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -6,9 +6,11 @@ import type {
|
|
|
6
6
|
GoogleAbstractedClient,
|
|
7
7
|
} from '@langchain/google-common';
|
|
8
8
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
9
|
-
import type { VertexAIClientOptions } from '@/types';
|
|
9
|
+
import type { GoogleThinkingConfig, VertexAIClientOptions } from '@/types';
|
|
10
10
|
|
|
11
11
|
class CustomChatConnection extends ChatConnection<VertexAIClientOptions> {
|
|
12
|
+
thinkingConfig?: GoogleThinkingConfig;
|
|
13
|
+
|
|
12
14
|
async formatData(
|
|
13
15
|
input: BaseMessage[],
|
|
14
16
|
parameters: GoogleAIModelRequestParams
|
|
@@ -26,6 +28,15 @@ class CustomChatConnection extends ChatConnection<VertexAIClientOptions> {
|
|
|
26
28
|
}
|
|
27
29
|
delete formattedData.generationConfig.thinkingConfig.thinkingBudget;
|
|
28
30
|
}
|
|
31
|
+
if (this.thinkingConfig?.thinkingLevel) {
|
|
32
|
+
formattedData.generationConfig ??= {};
|
|
33
|
+
(
|
|
34
|
+
formattedData.generationConfig as Record<string, unknown>
|
|
35
|
+
).thinkingConfig = {
|
|
36
|
+
...formattedData.generationConfig.thinkingConfig,
|
|
37
|
+
thinkingLevel: this.thinkingConfig.thinkingLevel,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
29
40
|
return formattedData;
|
|
30
41
|
}
|
|
31
42
|
}
|
|
@@ -315,6 +326,7 @@ class CustomChatConnection extends ChatConnection<VertexAIClientOptions> {
|
|
|
315
326
|
export class ChatVertexAI extends ChatGoogle {
|
|
316
327
|
lc_namespace = ['langchain', 'chat_models', 'vertexai'];
|
|
317
328
|
dynamicThinkingBudget = false;
|
|
329
|
+
thinkingConfig?: GoogleThinkingConfig;
|
|
318
330
|
|
|
319
331
|
static lc_name(): 'LibreChatVertexAI' {
|
|
320
332
|
return 'LibreChatVertexAI';
|
|
@@ -327,6 +339,7 @@ export class ChatVertexAI extends ChatGoogle {
|
|
|
327
339
|
platformType: 'gcp',
|
|
328
340
|
});
|
|
329
341
|
this.dynamicThinkingBudget = dynamicThinkingBudget;
|
|
342
|
+
this.thinkingConfig = fields?.thinkingConfig;
|
|
330
343
|
}
|
|
331
344
|
invocationParams(
|
|
332
345
|
options?: this['ParsedCallOptions'] | undefined
|
|
@@ -342,18 +355,22 @@ export class ChatVertexAI extends ChatGoogle {
|
|
|
342
355
|
fields: VertexAIClientOptions,
|
|
343
356
|
client: GoogleAbstractedClient
|
|
344
357
|
): void {
|
|
345
|
-
|
|
358
|
+
const connection = new CustomChatConnection(
|
|
346
359
|
{ ...fields, ...this },
|
|
347
360
|
this.caller,
|
|
348
361
|
client,
|
|
349
362
|
false
|
|
350
363
|
);
|
|
364
|
+
connection.thinkingConfig = this.thinkingConfig;
|
|
365
|
+
this.connection = connection;
|
|
351
366
|
|
|
352
|
-
|
|
367
|
+
const streamedConnection = new CustomChatConnection(
|
|
353
368
|
{ ...fields, ...this },
|
|
354
369
|
this.caller,
|
|
355
370
|
client,
|
|
356
371
|
true
|
|
357
372
|
);
|
|
373
|
+
streamedConnection.thinkingConfig = this.thinkingConfig;
|
|
374
|
+
this.streamedConnection = streamedConnection;
|
|
358
375
|
}
|
|
359
376
|
}
|
|
@@ -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?:
|
|
80
|
+
thinkingConfig?: GoogleThinkingConfig;
|
|
76
81
|
};
|
|
77
82
|
export type DeepSeekClientOptions = ChatDeepSeekCallOptions;
|
|
78
83
|
export type XAIClientOptions = ChatXAIInput;
|
package/src/utils/llmConfig.ts
CHANGED
|
@@ -67,11 +67,10 @@ export const llmConfigs: Record<string, t.LLMConfig | undefined> = {
|
|
|
67
67
|
'X-Title': 'LibreChat',
|
|
68
68
|
},
|
|
69
69
|
},
|
|
70
|
-
|
|
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,
|