@librechat/agents 3.1.74 → 3.1.75-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +66 -0
  2. package/dist/cjs/agents/AgentContext.cjs +84 -37
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +13 -3
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/llm/anthropic/index.cjs +145 -52
  7. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  8. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +25 -15
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +84 -70
  12. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  13. package/dist/cjs/llm/bedrock/index.cjs +1 -1
  14. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  15. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +213 -3
  16. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  17. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +2 -1
  18. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  19. package/dist/cjs/llm/google/utils/common.cjs +5 -4
  20. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  21. package/dist/cjs/llm/openai/index.cjs +468 -647
  22. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  23. package/dist/cjs/llm/openai/utils/index.cjs +1 -448
  24. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  25. package/dist/cjs/llm/openrouter/index.cjs +57 -175
  26. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  27. package/dist/cjs/llm/vertexai/index.cjs +5 -3
  28. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  29. package/dist/cjs/messages/cache.cjs +39 -4
  30. package/dist/cjs/messages/cache.cjs.map +1 -1
  31. package/dist/cjs/messages/core.cjs +7 -6
  32. package/dist/cjs/messages/core.cjs.map +1 -1
  33. package/dist/cjs/messages/format.cjs +7 -6
  34. package/dist/cjs/messages/format.cjs.map +1 -1
  35. package/dist/cjs/messages/langchain.cjs +26 -0
  36. package/dist/cjs/messages/langchain.cjs.map +1 -0
  37. package/dist/cjs/messages/prune.cjs +7 -6
  38. package/dist/cjs/messages/prune.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +5 -1
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/esm/agents/AgentContext.mjs +85 -38
  42. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  43. package/dist/esm/graphs/Graph.mjs +13 -3
  44. package/dist/esm/graphs/Graph.mjs.map +1 -1
  45. package/dist/esm/llm/anthropic/index.mjs +146 -54
  46. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  47. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  48. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +25 -15
  49. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  50. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +84 -71
  51. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  52. package/dist/esm/llm/bedrock/index.mjs +1 -1
  53. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  54. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +214 -4
  55. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  56. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +2 -1
  57. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  58. package/dist/esm/llm/google/utils/common.mjs +5 -4
  59. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  60. package/dist/esm/llm/openai/index.mjs +469 -648
  61. package/dist/esm/llm/openai/index.mjs.map +1 -1
  62. package/dist/esm/llm/openai/utils/index.mjs +4 -449
  63. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  64. package/dist/esm/llm/openrouter/index.mjs +57 -175
  65. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  66. package/dist/esm/llm/vertexai/index.mjs +5 -3
  67. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  68. package/dist/esm/messages/cache.mjs +39 -4
  69. package/dist/esm/messages/cache.mjs.map +1 -1
  70. package/dist/esm/messages/core.mjs +7 -6
  71. package/dist/esm/messages/core.mjs.map +1 -1
  72. package/dist/esm/messages/format.mjs +7 -6
  73. package/dist/esm/messages/format.mjs.map +1 -1
  74. package/dist/esm/messages/langchain.mjs +23 -0
  75. package/dist/esm/messages/langchain.mjs.map +1 -0
  76. package/dist/esm/messages/prune.mjs +7 -6
  77. package/dist/esm/messages/prune.mjs.map +1 -1
  78. package/dist/esm/tools/ToolNode.mjs +5 -1
  79. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  80. package/dist/types/agents/AgentContext.d.ts +14 -4
  81. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +46 -0
  82. package/dist/types/llm/anthropic/index.d.ts +22 -9
  83. package/dist/types/llm/anthropic/types.d.ts +5 -1
  84. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +13 -6
  85. package/dist/types/llm/anthropic/utils/output_parsers.d.ts +1 -1
  86. package/dist/types/llm/openai/index.d.ts +21 -24
  87. package/dist/types/llm/openrouter/index.d.ts +11 -9
  88. package/dist/types/llm/vertexai/index.d.ts +1 -0
  89. package/dist/types/messages/cache.d.ts +4 -1
  90. package/dist/types/messages/langchain.d.ts +27 -0
  91. package/dist/types/types/graph.d.ts +26 -38
  92. package/dist/types/types/llm.d.ts +3 -3
  93. package/dist/types/types/run.d.ts +2 -0
  94. package/dist/types/types/stream.d.ts +1 -1
  95. package/package.json +17 -16
  96. package/src/agents/AgentContext.ts +123 -44
  97. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +116 -0
  98. package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +149 -0
  99. package/src/agents/__tests__/AgentContext.test.ts +155 -2
  100. package/src/agents/__tests__/promptCacheLiveHelpers.ts +165 -0
  101. package/src/graphs/Graph.ts +24 -4
  102. package/src/graphs/__tests__/composition.smoke.test.ts +188 -0
  103. package/src/llm/anthropic/index.ts +252 -84
  104. package/src/llm/anthropic/llm.spec.ts +751 -102
  105. package/src/llm/anthropic/types.ts +9 -1
  106. package/src/llm/anthropic/utils/message_inputs.ts +43 -20
  107. package/src/llm/anthropic/utils/message_outputs.ts +119 -101
  108. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +77 -0
  109. package/src/llm/bedrock/index.ts +2 -2
  110. package/src/llm/bedrock/llm.spec.ts +341 -0
  111. package/src/llm/bedrock/utils/message_inputs.ts +303 -4
  112. package/src/llm/bedrock/utils/message_outputs.ts +2 -1
  113. package/src/llm/custom-chat-models.smoke.test.ts +662 -0
  114. package/src/llm/google/llm.spec.ts +339 -57
  115. package/src/llm/google/utils/common.ts +53 -48
  116. package/src/llm/openai/contentBlocks.test.ts +346 -0
  117. package/src/llm/openai/index.ts +736 -837
  118. package/src/llm/openai/utils/index.ts +84 -64
  119. package/src/llm/openrouter/index.ts +124 -247
  120. package/src/llm/openrouter/reasoning.test.ts +8 -1
  121. package/src/llm/vertexai/index.ts +11 -5
  122. package/src/llm/vertexai/llm.spec.ts +28 -1
  123. package/src/messages/cache.test.ts +106 -4
  124. package/src/messages/cache.ts +57 -5
  125. package/src/messages/core.ts +16 -9
  126. package/src/messages/format.ts +9 -6
  127. package/src/messages/langchain.ts +39 -0
  128. package/src/messages/prune.ts +12 -8
  129. package/src/scripts/caching.ts +2 -3
  130. package/src/specs/anthropic.simple.test.ts +61 -0
  131. package/src/specs/summarization.test.ts +58 -61
  132. package/src/tools/ToolNode.ts +5 -1
  133. package/src/types/graph.ts +35 -88
  134. package/src/types/llm.ts +3 -3
  135. package/src/types/run.ts +2 -0
  136. package/src/types/stream.ts +1 -1
  137. package/src/utils/llmConfig.ts +1 -6
@@ -0,0 +1,662 @@
1
+ import { AIMessage, AIMessageChunk } from '@langchain/core/messages';
2
+ import type { OpenAIChatInput, OpenAIClient } from '@langchain/openai';
3
+ import type { ChatOpenRouterCallOptions } from '@/llm/openrouter';
4
+ import type { CustomAnthropicInput } from '@/llm/anthropic';
5
+ import type {
6
+ ChatAnthropicToolType,
7
+ AnthropicMCPServerURLDefinition,
8
+ } from '@/llm/anthropic/types';
9
+ import {
10
+ ChatXAI,
11
+ ChatOpenAI,
12
+ ChatDeepSeek,
13
+ ChatMoonshot,
14
+ AzureChatOpenAI,
15
+ CustomOpenAIClient,
16
+ CustomAzureOpenAIClient,
17
+ } from '@/llm/openai';
18
+ import { CustomChatGoogleGenerativeAI } from '@/llm/google';
19
+ import { CustomChatBedrockConverse } from '@/llm/bedrock';
20
+ import { ChatOpenRouter } from '@/llm/openrouter';
21
+ import { CustomAnthropic } from '@/llm/anthropic';
22
+ import { ChatVertexAI } from '@/llm/vertexai';
23
+
24
+ type OpenAIRequestOptions = Parameters<ChatOpenAI['_getClientOptions']>[0];
25
+ type OpenAIRequestOptionsWithBaseURL = ReturnType<
26
+ ChatXAI['_getClientOptions']
27
+ > & {
28
+ baseURL?: string;
29
+ };
30
+ type OpenAIResponsesDelegate = {
31
+ client?: unknown;
32
+ _getClientOptions: (options?: OpenAIRequestOptions) => OpenAIRequestOptions;
33
+ };
34
+ type AnthropicCallOptions = Parameters<
35
+ CustomAnthropic['invocationParams']
36
+ >[0] & {
37
+ outputConfig?: CustomAnthropicInput['outputConfig'] & {
38
+ task_budget?: { type: 'token_budget'; value: number };
39
+ };
40
+ inferenceGeo?: CustomAnthropicInput['inferenceGeo'];
41
+ betas?: CustomAnthropicInput['betas'];
42
+ container?: string;
43
+ mcp_servers?: AnthropicMCPServerURLDefinition[];
44
+ tools?: ChatAnthropicToolType[];
45
+ };
46
+ type AzureReasoningModel = AzureChatOpenAI & {
47
+ reasoning?: { effort: 'low' | 'high' };
48
+ };
49
+ type OpenRouterFields = Partial<
50
+ ChatOpenRouterCallOptions & Pick<OpenAIChatInput, 'model' | 'apiKey'>
51
+ >;
52
+ type CompletionDelegate = {
53
+ completionWithRetry: (request: { messages?: unknown }) => Promise<unknown>;
54
+ };
55
+ type CompletionBackedModel = {
56
+ completions: CompletionDelegate;
57
+ };
58
+ type StreamingCompletionRequest = {
59
+ messages?: unknown;
60
+ stream?: boolean;
61
+ };
62
+ type StreamingCompletionDelegate = {
63
+ completionWithRetry: (
64
+ request: StreamingCompletionRequest
65
+ ) => Promise<
66
+ AsyncIterable<OpenAIClient.Chat.Completions.ChatCompletionChunk>
67
+ >;
68
+ };
69
+ type StreamingCompletionBackedModel = {
70
+ completions: StreamingCompletionDelegate;
71
+ };
72
+ type OpenRouterReasoningStreamDelta =
73
+ OpenAIClient.Chat.Completions.ChatCompletionChunk.Choice.Delta & {
74
+ reasoning_details?: Array<
75
+ | {
76
+ type: 'reasoning.text';
77
+ text?: string;
78
+ format?: string;
79
+ index?: number;
80
+ }
81
+ | {
82
+ type: 'reasoning.encrypted';
83
+ id?: string;
84
+ data?: string;
85
+ format?: string;
86
+ index?: number;
87
+ }
88
+ >;
89
+ };
90
+ type OpenRouterReasoningStreamChoice = Omit<
91
+ OpenAIClient.Chat.Completions.ChatCompletionChunk.Choice,
92
+ 'delta'
93
+ > & {
94
+ delta: OpenRouterReasoningStreamDelta;
95
+ };
96
+
97
+ const baseAzureFields = {
98
+ azureOpenAIApiKey: 'test-azure-key',
99
+ azureOpenAIApiVersion: '2024-10-21',
100
+ azureOpenAIApiInstanceName: 'test-instance',
101
+ azureOpenAIApiDeploymentName: 'test-deployment',
102
+ };
103
+
104
+ const baseBedrockFields = {
105
+ region: 'us-east-1',
106
+ credentials: {
107
+ accessKeyId: 'test-access-key',
108
+ secretAccessKey: 'test-secret-key',
109
+ },
110
+ };
111
+
112
+ describe('custom chat model class smoke tests', () => {
113
+ it('keeps the custom OpenAI client, stream delay, and reasoning precedence', () => {
114
+ const model = new ChatOpenAI({
115
+ model: 'gpt-5',
116
+ apiKey: 'test-key',
117
+ reasoning: { effort: 'low' },
118
+ _lc_stream_delay: 3,
119
+ });
120
+
121
+ const requestOptions = model._getClientOptions({
122
+ headers: { 'x-smoke': 'openai' },
123
+ } as OpenAIRequestOptions);
124
+
125
+ expect(ChatOpenAI.lc_name()).toBe('LibreChatOpenAI');
126
+ expect(model._lc_stream_delay).toBe(3);
127
+ expect(model.exposedClient).toBeInstanceOf(CustomOpenAIClient);
128
+ expect(requestOptions.headers).toEqual(
129
+ expect.objectContaining({ 'x-smoke': 'openai' })
130
+ );
131
+ expect(model.getReasoningParams({ reasoning: { effort: 'high' } })).toEqual(
132
+ { effort: 'high' }
133
+ );
134
+ const params = model.invocationParams({ reasoningEffort: 'medium' }) as {
135
+ reasoning?: unknown;
136
+ reasoning_effort?: unknown;
137
+ };
138
+ expect(params.reasoning ?? { effort: params.reasoning_effort }).toEqual({
139
+ effort: 'low',
140
+ });
141
+ });
142
+
143
+ it('keeps the custom OpenAI client on the Responses API path', () => {
144
+ const model = new ChatOpenAI({
145
+ model: 'gpt-5',
146
+ apiKey: 'test-key',
147
+ useResponsesApi: true,
148
+ maxTokens: 32,
149
+ reasoning: { effort: 'low' },
150
+ });
151
+
152
+ const params = model.invocationParams({
153
+ reasoningEffort: 'high',
154
+ }) as {
155
+ max_output_tokens?: number;
156
+ max_completion_tokens?: number;
157
+ reasoning?: unknown;
158
+ };
159
+ const responses = (
160
+ model as unknown as { responses: OpenAIResponsesDelegate }
161
+ ).responses;
162
+ const requestOptions = responses._getClientOptions({
163
+ headers: { 'x-smoke': 'responses' },
164
+ } as OpenAIRequestOptions);
165
+
166
+ expect(params.max_output_tokens).toBe(32);
167
+ expect(params.max_completion_tokens).toBeUndefined();
168
+ expect(params.reasoning).toEqual({ effort: 'low' });
169
+ expect(responses.client).toBeInstanceOf(CustomOpenAIClient);
170
+ const responsesClient = responses.client as CustomOpenAIClient;
171
+ responsesClient.abortHandler = (): void => undefined;
172
+ expect(model.exposedClient).toBe(responsesClient);
173
+ expect(requestOptions?.headers).toEqual(
174
+ expect.objectContaining({ 'x-smoke': 'responses' })
175
+ );
176
+ });
177
+
178
+ it('keeps Azure client customization and gates reasoning to reasoning models', () => {
179
+ const model = new AzureChatOpenAI({
180
+ ...baseAzureFields,
181
+ _lc_stream_delay: 4,
182
+ }) as AzureReasoningModel;
183
+ model.model = 'gpt-5';
184
+ model.reasoning = { effort: 'low' };
185
+
186
+ const requestOptions = model._getClientOptions({
187
+ headers: { 'x-smoke': 'azure' },
188
+ });
189
+
190
+ expect(AzureChatOpenAI.lc_name()).toBe('LibreChatAzureOpenAI');
191
+ expect(model._lc_stream_delay).toBe(4);
192
+ expect(model.exposedClient).toBeInstanceOf(CustomAzureOpenAIClient);
193
+ const azureResponses = (
194
+ model as unknown as { responses: OpenAIResponsesDelegate }
195
+ ).responses;
196
+ azureResponses._getClientOptions(undefined);
197
+ expect(azureResponses.client).toBeInstanceOf(CustomAzureOpenAIClient);
198
+ const azureResponsesClient =
199
+ azureResponses.client as CustomAzureOpenAIClient;
200
+ azureResponsesClient.abortHandler = (): void => undefined;
201
+ expect(model.exposedClient).toBe(azureResponsesClient);
202
+ expect(requestOptions.headers).toEqual(
203
+ expect.objectContaining({
204
+ 'api-key': 'test-azure-key',
205
+ 'x-smoke': 'azure',
206
+ })
207
+ );
208
+ expect(requestOptions.query).toEqual(
209
+ expect.objectContaining({ 'api-version': '2024-10-21' })
210
+ );
211
+ expect(model.getReasoningParams()).toEqual({ effort: 'low' });
212
+
213
+ const nonReasoningModel = new AzureChatOpenAI({
214
+ ...baseAzureFields,
215
+ }) as AzureReasoningModel;
216
+ nonReasoningModel.model = 'gpt-4o';
217
+ nonReasoningModel.reasoning = { effort: 'low' };
218
+ expect(nonReasoningModel.getReasoningParams()).toBeUndefined();
219
+ });
220
+
221
+ it('keeps DeepSeek, Moonshot, and xAI on LibreChat wrapper semantics', () => {
222
+ const deepSeek = new ChatDeepSeek({
223
+ model: 'deepseek-chat',
224
+ apiKey: 'test-key',
225
+ _lc_stream_delay: 5,
226
+ });
227
+ deepSeek._getClientOptions();
228
+
229
+ const moonshot = new ChatMoonshot({
230
+ model: 'moonshot-v1-8k',
231
+ apiKey: 'test-key',
232
+ _lc_stream_delay: 6,
233
+ });
234
+
235
+ const xai = new ChatXAI({
236
+ model: 'grok-3-fast',
237
+ apiKey: 'test-key',
238
+ configuration: { baseURL: 'https://xai.test/v1' },
239
+ _lc_stream_delay: 7,
240
+ });
241
+ const xaiRequestOptions =
242
+ xai._getClientOptions() as OpenAIRequestOptionsWithBaseURL;
243
+
244
+ expect(ChatDeepSeek.lc_name()).toBe('LibreChatDeepSeek');
245
+ expect(deepSeek._lc_stream_delay).toBe(5);
246
+ expect(deepSeek.exposedClient).toBeInstanceOf(CustomOpenAIClient);
247
+ expect(ChatMoonshot.lc_name()).toBe('LibreChatMoonshot');
248
+ expect(moonshot._lc_stream_delay).toBe(6);
249
+ expect(ChatXAI.lc_name()).toBe('LibreChatXAI');
250
+ expect(xai._lc_stream_delay).toBe(7);
251
+ expect(xai.exposedClient).toBeInstanceOf(CustomOpenAIClient);
252
+ expect(xaiRequestOptions.baseURL).toBe('https://xai.test/v1');
253
+ });
254
+
255
+ it('keeps Moonshot reasoning content in completion requests', async () => {
256
+ const moonshot = new ChatMoonshot({
257
+ model: 'moonshot-v1-8k',
258
+ apiKey: 'test-key',
259
+ streaming: false,
260
+ });
261
+ const completions = (moonshot as unknown as CompletionBackedModel)
262
+ .completions;
263
+ let requestMessages: unknown;
264
+
265
+ completions.completionWithRetry = async (request): Promise<unknown> => {
266
+ requestMessages = request.messages;
267
+ return {
268
+ id: 'chatcmpl-test',
269
+ object: 'chat.completion',
270
+ created: 0,
271
+ model: 'moonshot-v1-8k',
272
+ choices: [
273
+ {
274
+ index: 0,
275
+ finish_reason: 'stop',
276
+ message: {
277
+ role: 'assistant',
278
+ content: 'ok',
279
+ },
280
+ },
281
+ ],
282
+ };
283
+ };
284
+
285
+ await moonshot.invoke([
286
+ new AIMessage({
287
+ content: '',
288
+ additional_kwargs: { reasoning_content: 'kept-thinking' },
289
+ tool_calls: [
290
+ {
291
+ id: 'call_1',
292
+ name: 'lookup',
293
+ args: { q: 'test' },
294
+ type: 'tool_call',
295
+ },
296
+ ],
297
+ }),
298
+ ]);
299
+
300
+ expect(requestMessages).toEqual([
301
+ expect.objectContaining({
302
+ role: 'assistant',
303
+ content: '',
304
+ reasoning_content: 'kept-thinking',
305
+ tool_calls: expect.any(Array),
306
+ }),
307
+ ]);
308
+ });
309
+
310
+ it('keeps OpenRouter reasoning isolated from OpenAI reasoning_effort', () => {
311
+ const fields: OpenRouterFields = {
312
+ model: 'openrouter/test-model',
313
+ apiKey: 'test-key',
314
+ reasoning: { effort: 'xhigh', max_tokens: 2048 },
315
+ };
316
+ const model = new ChatOpenRouter(fields);
317
+
318
+ const params = model.invocationParams();
319
+
320
+ expect(ChatOpenRouter.lc_name()).toBe('LibreChatOpenRouter');
321
+ expect(params.reasoning).toEqual({ effort: 'xhigh', max_tokens: 2048 });
322
+ expect(params.reasoning_effort).toBeUndefined();
323
+
324
+ const callParams = model.invocationParams({
325
+ reasoning: { effort: 'low', exclude: true },
326
+ } as ChatOpenRouterCallOptions);
327
+ expect(callParams.reasoning).toEqual({
328
+ effort: 'low',
329
+ max_tokens: 2048,
330
+ exclude: true,
331
+ });
332
+
333
+ const legacyModel = new ChatOpenRouter({
334
+ model: 'openrouter/test-model',
335
+ apiKey: 'test-key',
336
+ include_reasoning: true,
337
+ });
338
+ expect(legacyModel.invocationParams().reasoning).toEqual({
339
+ enabled: true,
340
+ });
341
+ });
342
+
343
+ it('keeps OpenRouter streaming reasoning details stable', async () => {
344
+ const model = new ChatOpenRouter({
345
+ model: 'anthropic/claude-sonnet-test',
346
+ apiKey: 'test-key',
347
+ });
348
+ const completions = (model as unknown as StreamingCompletionBackedModel)
349
+ .completions;
350
+ let requestMessages: unknown;
351
+ const createChunk = (
352
+ choice: OpenRouterReasoningStreamChoice
353
+ ): OpenAIClient.Chat.Completions.ChatCompletionChunk => ({
354
+ id: 'chatcmpl-openrouter-test',
355
+ object: 'chat.completion.chunk',
356
+ created: 0,
357
+ model: 'anthropic/claude-sonnet-test',
358
+ choices: [choice],
359
+ });
360
+
361
+ async function* streamChunks(): AsyncGenerator<OpenAIClient.Chat.Completions.ChatCompletionChunk> {
362
+ yield createChunk({
363
+ index: 0,
364
+ delta: {
365
+ role: 'assistant',
366
+ content: '',
367
+ reasoning_details: [
368
+ {
369
+ type: 'reasoning.text',
370
+ text: 'Think ',
371
+ format: 'text',
372
+ index: 0,
373
+ },
374
+ ],
375
+ },
376
+ finish_reason: null,
377
+ });
378
+ yield createChunk({
379
+ index: 0,
380
+ delta: {
381
+ content: 'answer',
382
+ reasoning_details: [
383
+ { type: 'reasoning.text', text: 'hard', index: 0 },
384
+ {
385
+ type: 'reasoning.encrypted',
386
+ id: 'sig_1',
387
+ data: 'encrypted',
388
+ format: 'anthropic',
389
+ index: 1,
390
+ },
391
+ ],
392
+ },
393
+ finish_reason: null,
394
+ });
395
+ yield createChunk({
396
+ index: 0,
397
+ delta: { content: '' },
398
+ finish_reason: 'stop',
399
+ });
400
+ }
401
+
402
+ completions.completionWithRetry = async (
403
+ request
404
+ ): Promise<
405
+ AsyncIterable<OpenAIClient.Chat.Completions.ChatCompletionChunk>
406
+ > => {
407
+ requestMessages = request.messages;
408
+ return streamChunks();
409
+ };
410
+
411
+ const chunks: AIMessageChunk[] = [];
412
+ const stream = await model.stream([
413
+ new AIMessage({
414
+ content: '',
415
+ additional_kwargs: {
416
+ reasoning_details: [
417
+ {
418
+ type: 'reasoning.text',
419
+ text: 'previous thought',
420
+ index: 0,
421
+ },
422
+ {
423
+ type: 'reasoning.encrypted',
424
+ id: 'prev_sig',
425
+ data: 'previous encrypted',
426
+ index: 1,
427
+ },
428
+ ],
429
+ },
430
+ tool_calls: [
431
+ {
432
+ id: 'call_1',
433
+ name: 'lookup',
434
+ args: { q: 'test' },
435
+ type: 'tool_call',
436
+ },
437
+ ],
438
+ }),
439
+ ]);
440
+ for await (const chunk of stream) {
441
+ chunks.push(chunk);
442
+ }
443
+
444
+ expect(requestMessages).toEqual([
445
+ expect.objectContaining({
446
+ role: 'assistant',
447
+ tool_calls: expect.any(Array),
448
+ content: [
449
+ expect.objectContaining({
450
+ type: 'thinking',
451
+ thinking: 'previous thought',
452
+ }),
453
+ expect.objectContaining({
454
+ type: 'redacted_thinking',
455
+ data: 'previous encrypted',
456
+ id: 'prev_sig',
457
+ }),
458
+ ],
459
+ }),
460
+ ]);
461
+ expect(chunks).toHaveLength(3);
462
+ expect(chunks[0].additional_kwargs.reasoning).toBe('Think ');
463
+ expect(chunks[0].additional_kwargs.reasoning_details).toBeUndefined();
464
+ expect(chunks[1].additional_kwargs.reasoning).toBe('hard');
465
+ expect(chunks[1].additional_kwargs.reasoning_details).toBeUndefined();
466
+ expect(chunks[2].additional_kwargs.reasoning_details).toEqual([
467
+ {
468
+ type: 'reasoning.text',
469
+ text: 'Think hard',
470
+ format: 'text',
471
+ index: 0,
472
+ },
473
+ {
474
+ type: 'reasoning.encrypted',
475
+ id: 'sig_1',
476
+ data: 'encrypted',
477
+ format: 'anthropic',
478
+ index: 1,
479
+ },
480
+ ]);
481
+ });
482
+
483
+ it('keeps Anthropic output, residency, compaction, and stream-delay options', () => {
484
+ const contextManagement = {
485
+ edits: [
486
+ {
487
+ type: 'compact_20260112' as const,
488
+ trigger: { type: 'input_tokens' as const, value: 50000 },
489
+ },
490
+ ],
491
+ };
492
+ const model = new CustomAnthropic({
493
+ model: 'claude-sonnet-4-5-20250929',
494
+ apiKey: 'test-key',
495
+ maxTokens: 4096,
496
+ outputConfig: { effort: 'medium' },
497
+ inferenceGeo: 'us',
498
+ contextManagement,
499
+ _lc_stream_delay: 8,
500
+ });
501
+
502
+ const params = model.invocationParams({
503
+ outputConfig: { effort: 'low' },
504
+ inferenceGeo: 'eu',
505
+ } as AnthropicCallOptions);
506
+
507
+ expect(CustomAnthropic.lc_name()).toBe('LibreChatAnthropic');
508
+ expect(model._lc_stream_delay).toBe(8);
509
+ expect(params.output_config).toEqual({ effort: 'low' });
510
+ expect(params.inference_geo).toBe('eu');
511
+ expect(params.context_management).toEqual(contextManagement);
512
+ });
513
+
514
+ it('keeps Anthropic beta, MCP, and container request wiring current', () => {
515
+ const contextManagement = {
516
+ edits: [
517
+ {
518
+ type: 'compact_20260112' as const,
519
+ trigger: { type: 'input_tokens' as const, value: 50000 },
520
+ },
521
+ ],
522
+ };
523
+ const mcpServers: AnthropicMCPServerURLDefinition[] = [
524
+ {
525
+ type: 'url',
526
+ url: 'https://example.com/mcp',
527
+ name: 'docs',
528
+ },
529
+ ];
530
+ const model = new CustomAnthropic({
531
+ model: 'claude-opus-4-7-test',
532
+ apiKey: 'test-key',
533
+ maxTokens: 4096,
534
+ contextManagement,
535
+ betas: ['model-beta'],
536
+ });
537
+
538
+ const params = model.invocationParams({
539
+ outputConfig: {
540
+ effort: 'low',
541
+ task_budget: { type: 'token_budget', value: 1024 },
542
+ },
543
+ betas: ['request-beta', 'model-beta'],
544
+ container: 'container_123',
545
+ mcp_servers: mcpServers,
546
+ tools: [
547
+ {
548
+ type: 'tool_search_tool_bm25_20251119',
549
+ name: 'search',
550
+ } as ChatAnthropicToolType,
551
+ ],
552
+ } as AnthropicCallOptions);
553
+
554
+ expect(params.betas).toEqual([
555
+ 'model-beta',
556
+ 'request-beta',
557
+ 'advanced-tool-use-2025-11-20',
558
+ 'compact-2026-01-12',
559
+ 'task-budgets-2026-03-13',
560
+ ]);
561
+ expect(params.container).toBe('container_123');
562
+ expect(params.mcp_servers).toBe(mcpServers);
563
+ expect(params.temperature).toBeUndefined();
564
+ expect(params.top_k).toBeUndefined();
565
+ expect(params.top_p).toBeUndefined();
566
+ });
567
+
568
+ it('matches Anthropic Opus 4.7 sampling compatibility checks', () => {
569
+ const thinkingModel = new CustomAnthropic({
570
+ model: 'claude-opus-4-7-test',
571
+ apiKey: 'test-key',
572
+ maxTokens: 4096,
573
+ thinking: { type: 'enabled', budget_tokens: 1024 },
574
+ });
575
+ const topKModel = new CustomAnthropic({
576
+ model: 'claude-opus-4-7-test',
577
+ apiKey: 'test-key',
578
+ maxTokens: 4096,
579
+ topK: 5,
580
+ });
581
+
582
+ expect(() => thinkingModel.invocationParams()).toThrow(
583
+ 'thinking.type="enabled" is not supported for claude-opus-4-7'
584
+ );
585
+ expect(() => topKModel.invocationParams()).toThrow(
586
+ 'topK is not supported for claude-opus-4-7'
587
+ );
588
+ });
589
+
590
+ it('keeps Bedrock Converse application profiles and service tier passthroughs', () => {
591
+ const applicationInferenceProfile =
592
+ 'arn:aws:bedrock:eu-west-1:123456789012:application-inference-profile/test-profile';
593
+ const model = new CustomChatBedrockConverse({
594
+ ...baseBedrockFields,
595
+ model: 'anthropic.claude-3-haiku-20240307-v1:0',
596
+ applicationInferenceProfile,
597
+ serviceTier: 'priority',
598
+ });
599
+
600
+ expect(CustomChatBedrockConverse.lc_name()).toBe(
601
+ 'LibreChatBedrockConverse'
602
+ );
603
+ expect(model.applicationInferenceProfile).toBe(applicationInferenceProfile);
604
+ expect(model.invocationParams({}).serviceTier).toEqual({
605
+ type: 'priority',
606
+ });
607
+ expect(model.invocationParams({ serviceTier: 'flex' }).serviceTier).toEqual(
608
+ { type: 'flex' }
609
+ );
610
+ });
611
+
612
+ it('keeps Google and Vertex thinking configuration wiring offline', () => {
613
+ const thinkingConfig = {
614
+ thinkingLevel: 'HIGH' as const,
615
+ includeThoughts: true,
616
+ };
617
+ const google = new CustomChatGoogleGenerativeAI({
618
+ model: 'models/gemini-3-pro-preview',
619
+ apiKey: 'test-key',
620
+ thinkingConfig,
621
+ });
622
+ const vertex = new ChatVertexAI({
623
+ model: 'gemini-3-pro-preview',
624
+ location: 'global',
625
+ thinkingBudget: -1,
626
+ thinkingConfig,
627
+ });
628
+
629
+ expect(CustomChatGoogleGenerativeAI.lc_name()).toBe(
630
+ 'LibreChatGoogleGenerativeAI'
631
+ );
632
+ expect(google.model).toBe('gemini-3-pro-preview');
633
+ expect(google._isMultimodalModel).toBe(true);
634
+ expect(google.thinkingConfig).toEqual(thinkingConfig);
635
+ expect(ChatVertexAI.lc_name()).toBe('LibreChatVertexAI');
636
+ expect(vertex.dynamicThinkingBudget).toBe(true);
637
+ expect(vertex.thinkingConfig).toEqual(thinkingConfig);
638
+ expect(vertex.invocationParams({}).maxReasoningTokens).toBe(-1);
639
+ });
640
+
641
+ it('uppercases custom OpenAI fetch methods before dispatch', async () => {
642
+ let method: string | undefined;
643
+ const client = new CustomOpenAIClient({
644
+ apiKey: 'test-key',
645
+ fetch: async (_url, init): Promise<Response> => {
646
+ method = init?.method;
647
+ return new Response('{}', { status: 200 });
648
+ },
649
+ });
650
+
651
+ const response = await client.fetchWithTimeout(
652
+ 'https://example.test/v1/chat/completions',
653
+ { method: 'patch' },
654
+ 1000,
655
+ new AbortController()
656
+ );
657
+
658
+ expect(response.status).toBe(200);
659
+ expect(method).toBe('PATCH');
660
+ expect(client.abortHandler).toBeDefined();
661
+ });
662
+ });