@librechat/agents 3.1.74 → 3.1.75-dev.1

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 (203) 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/langchain/google-common.cjs +3 -0
  7. package/dist/cjs/langchain/google-common.cjs.map +1 -0
  8. package/dist/cjs/langchain/index.cjs +86 -0
  9. package/dist/cjs/langchain/index.cjs.map +1 -0
  10. package/dist/cjs/langchain/language_models/chat_models.cjs +3 -0
  11. package/dist/cjs/langchain/language_models/chat_models.cjs.map +1 -0
  12. package/dist/cjs/langchain/messages/tool.cjs +3 -0
  13. package/dist/cjs/langchain/messages/tool.cjs.map +1 -0
  14. package/dist/cjs/langchain/messages.cjs +51 -0
  15. package/dist/cjs/langchain/messages.cjs.map +1 -0
  16. package/dist/cjs/langchain/openai.cjs +3 -0
  17. package/dist/cjs/langchain/openai.cjs.map +1 -0
  18. package/dist/cjs/langchain/prompts.cjs +11 -0
  19. package/dist/cjs/langchain/prompts.cjs.map +1 -0
  20. package/dist/cjs/langchain/runnables.cjs +19 -0
  21. package/dist/cjs/langchain/runnables.cjs.map +1 -0
  22. package/dist/cjs/langchain/tools.cjs +23 -0
  23. package/dist/cjs/langchain/tools.cjs.map +1 -0
  24. package/dist/cjs/langchain/utils/env.cjs +11 -0
  25. package/dist/cjs/langchain/utils/env.cjs.map +1 -0
  26. package/dist/cjs/llm/anthropic/index.cjs +145 -52
  27. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  28. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  29. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +25 -15
  30. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  31. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +84 -70
  32. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  33. package/dist/cjs/llm/bedrock/index.cjs +1 -1
  34. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  35. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +213 -3
  36. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  37. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +2 -1
  38. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  39. package/dist/cjs/llm/google/utils/common.cjs +5 -4
  40. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  41. package/dist/cjs/llm/openai/index.cjs +468 -647
  42. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  43. package/dist/cjs/llm/openai/utils/index.cjs +1 -448
  44. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  45. package/dist/cjs/llm/openrouter/index.cjs +57 -175
  46. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  47. package/dist/cjs/llm/vertexai/index.cjs +5 -3
  48. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  49. package/dist/cjs/main.cjs +83 -3
  50. package/dist/cjs/main.cjs.map +1 -1
  51. package/dist/cjs/messages/cache.cjs +39 -4
  52. package/dist/cjs/messages/cache.cjs.map +1 -1
  53. package/dist/cjs/messages/core.cjs +7 -6
  54. package/dist/cjs/messages/core.cjs.map +1 -1
  55. package/dist/cjs/messages/format.cjs +7 -6
  56. package/dist/cjs/messages/format.cjs.map +1 -1
  57. package/dist/cjs/messages/langchain.cjs +26 -0
  58. package/dist/cjs/messages/langchain.cjs.map +1 -0
  59. package/dist/cjs/messages/prune.cjs +7 -6
  60. package/dist/cjs/messages/prune.cjs.map +1 -1
  61. package/dist/cjs/tools/ToolNode.cjs +5 -1
  62. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  63. package/dist/esm/agents/AgentContext.mjs +85 -38
  64. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  65. package/dist/esm/graphs/Graph.mjs +13 -3
  66. package/dist/esm/graphs/Graph.mjs.map +1 -1
  67. package/dist/esm/langchain/google-common.mjs +2 -0
  68. package/dist/esm/langchain/google-common.mjs.map +1 -0
  69. package/dist/esm/langchain/index.mjs +5 -0
  70. package/dist/esm/langchain/index.mjs.map +1 -0
  71. package/dist/esm/langchain/language_models/chat_models.mjs +2 -0
  72. package/dist/esm/langchain/language_models/chat_models.mjs.map +1 -0
  73. package/dist/esm/langchain/messages/tool.mjs +2 -0
  74. package/dist/esm/langchain/messages/tool.mjs.map +1 -0
  75. package/dist/esm/langchain/messages.mjs +2 -0
  76. package/dist/esm/langchain/messages.mjs.map +1 -0
  77. package/dist/esm/langchain/openai.mjs +2 -0
  78. package/dist/esm/langchain/openai.mjs.map +1 -0
  79. package/dist/esm/langchain/prompts.mjs +2 -0
  80. package/dist/esm/langchain/prompts.mjs.map +1 -0
  81. package/dist/esm/langchain/runnables.mjs +2 -0
  82. package/dist/esm/langchain/runnables.mjs.map +1 -0
  83. package/dist/esm/langchain/tools.mjs +2 -0
  84. package/dist/esm/langchain/tools.mjs.map +1 -0
  85. package/dist/esm/langchain/utils/env.mjs +2 -0
  86. package/dist/esm/langchain/utils/env.mjs.map +1 -0
  87. package/dist/esm/llm/anthropic/index.mjs +146 -54
  88. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  89. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  90. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +25 -15
  91. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  92. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +84 -71
  93. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  94. package/dist/esm/llm/bedrock/index.mjs +1 -1
  95. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  96. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +214 -4
  97. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  98. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +2 -1
  99. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  100. package/dist/esm/llm/google/utils/common.mjs +5 -4
  101. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  102. package/dist/esm/llm/openai/index.mjs +469 -648
  103. package/dist/esm/llm/openai/index.mjs.map +1 -1
  104. package/dist/esm/llm/openai/utils/index.mjs +4 -449
  105. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  106. package/dist/esm/llm/openrouter/index.mjs +57 -175
  107. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  108. package/dist/esm/llm/vertexai/index.mjs +5 -3
  109. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  110. package/dist/esm/main.mjs +4 -0
  111. package/dist/esm/main.mjs.map +1 -1
  112. package/dist/esm/messages/cache.mjs +39 -4
  113. package/dist/esm/messages/cache.mjs.map +1 -1
  114. package/dist/esm/messages/core.mjs +7 -6
  115. package/dist/esm/messages/core.mjs.map +1 -1
  116. package/dist/esm/messages/format.mjs +7 -6
  117. package/dist/esm/messages/format.mjs.map +1 -1
  118. package/dist/esm/messages/langchain.mjs +23 -0
  119. package/dist/esm/messages/langchain.mjs.map +1 -0
  120. package/dist/esm/messages/prune.mjs +7 -6
  121. package/dist/esm/messages/prune.mjs.map +1 -1
  122. package/dist/esm/tools/ToolNode.mjs +5 -1
  123. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  124. package/dist/types/agents/AgentContext.d.ts +14 -4
  125. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +46 -0
  126. package/dist/types/index.d.ts +1 -0
  127. package/dist/types/langchain/google-common.d.ts +1 -0
  128. package/dist/types/langchain/index.d.ts +8 -0
  129. package/dist/types/langchain/language_models/chat_models.d.ts +1 -0
  130. package/dist/types/langchain/messages/tool.d.ts +1 -0
  131. package/dist/types/langchain/messages.d.ts +2 -0
  132. package/dist/types/langchain/openai.d.ts +1 -0
  133. package/dist/types/langchain/prompts.d.ts +1 -0
  134. package/dist/types/langchain/runnables.d.ts +2 -0
  135. package/dist/types/langchain/tools.d.ts +2 -0
  136. package/dist/types/langchain/utils/env.d.ts +1 -0
  137. package/dist/types/llm/anthropic/index.d.ts +22 -9
  138. package/dist/types/llm/anthropic/types.d.ts +5 -1
  139. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +13 -6
  140. package/dist/types/llm/anthropic/utils/output_parsers.d.ts +1 -1
  141. package/dist/types/llm/openai/index.d.ts +21 -24
  142. package/dist/types/llm/openrouter/index.d.ts +11 -9
  143. package/dist/types/llm/vertexai/index.d.ts +1 -0
  144. package/dist/types/messages/cache.d.ts +4 -1
  145. package/dist/types/messages/langchain.d.ts +27 -0
  146. package/dist/types/types/graph.d.ts +26 -38
  147. package/dist/types/types/llm.d.ts +3 -3
  148. package/dist/types/types/run.d.ts +2 -0
  149. package/dist/types/types/stream.d.ts +1 -1
  150. package/package.json +80 -17
  151. package/src/agents/AgentContext.ts +123 -44
  152. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +116 -0
  153. package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +149 -0
  154. package/src/agents/__tests__/AgentContext.test.ts +155 -2
  155. package/src/agents/__tests__/promptCacheLiveHelpers.ts +165 -0
  156. package/src/graphs/Graph.ts +24 -4
  157. package/src/graphs/__tests__/composition.smoke.test.ts +188 -0
  158. package/src/index.ts +3 -0
  159. package/src/langchain/google-common.ts +1 -0
  160. package/src/langchain/index.ts +8 -0
  161. package/src/langchain/language_models/chat_models.ts +1 -0
  162. package/src/langchain/messages/tool.ts +5 -0
  163. package/src/langchain/messages.ts +21 -0
  164. package/src/langchain/openai.ts +1 -0
  165. package/src/langchain/prompts.ts +1 -0
  166. package/src/langchain/runnables.ts +7 -0
  167. package/src/langchain/tools.ts +8 -0
  168. package/src/langchain/utils/env.ts +1 -0
  169. package/src/llm/anthropic/index.ts +252 -84
  170. package/src/llm/anthropic/llm.spec.ts +751 -102
  171. package/src/llm/anthropic/types.ts +9 -1
  172. package/src/llm/anthropic/utils/message_inputs.ts +43 -20
  173. package/src/llm/anthropic/utils/message_outputs.ts +119 -101
  174. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +77 -0
  175. package/src/llm/bedrock/index.ts +2 -2
  176. package/src/llm/bedrock/llm.spec.ts +341 -0
  177. package/src/llm/bedrock/utils/message_inputs.ts +303 -4
  178. package/src/llm/bedrock/utils/message_outputs.ts +2 -1
  179. package/src/llm/custom-chat-models.smoke.test.ts +662 -0
  180. package/src/llm/google/llm.spec.ts +339 -57
  181. package/src/llm/google/utils/common.ts +53 -48
  182. package/src/llm/openai/contentBlocks.test.ts +346 -0
  183. package/src/llm/openai/index.ts +736 -837
  184. package/src/llm/openai/utils/index.ts +84 -64
  185. package/src/llm/openrouter/index.ts +124 -247
  186. package/src/llm/openrouter/reasoning.test.ts +8 -1
  187. package/src/llm/vertexai/index.ts +11 -5
  188. package/src/llm/vertexai/llm.spec.ts +28 -1
  189. package/src/messages/cache.test.ts +106 -4
  190. package/src/messages/cache.ts +57 -5
  191. package/src/messages/core.ts +16 -9
  192. package/src/messages/format.ts +9 -6
  193. package/src/messages/langchain.ts +39 -0
  194. package/src/messages/prune.ts +12 -8
  195. package/src/scripts/caching.ts +2 -3
  196. package/src/specs/anthropic.simple.test.ts +61 -0
  197. package/src/specs/summarization.test.ts +58 -61
  198. package/src/tools/ToolNode.ts +5 -1
  199. package/src/types/graph.ts +35 -88
  200. package/src/types/llm.ts +3 -3
  201. package/src/types/run.ts +2 -0
  202. package/src/types/stream.ts +1 -1
  203. package/src/utils/llmConfig.ts +1 -6
@@ -1,33 +1,12 @@
1
1
  import { ChatOpenAI } from '@/llm/openai';
2
- import { ChatGenerationChunk } from '@langchain/core/outputs';
3
- import { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
4
- import { AIMessageChunk as AIMessageChunkClass } from '@langchain/core/messages';
5
- import type {
6
- FunctionMessageChunk,
7
- SystemMessageChunk,
8
- HumanMessageChunk,
9
- ToolMessageChunk,
10
- ChatMessageChunk,
11
- AIMessageChunk,
12
- BaseMessage,
13
- } from '@langchain/core/messages';
2
+ import type { BaseMessage } from '@langchain/core/messages';
3
+ import type { ChatGenerationChunk } from '@langchain/core/outputs';
4
+ import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
14
5
  import type {
15
6
  ChatOpenAICallOptions,
16
7
  OpenAIChatInput,
17
8
  OpenAIClient,
18
9
  } from '@langchain/openai';
19
- import { _convertMessagesToOpenAIParams } from '@/llm/openai/utils';
20
-
21
- type OpenAICompletionParam =
22
- OpenAIClient.Chat.Completions.ChatCompletionMessageParam;
23
-
24
- type OpenAIRoleEnum =
25
- | 'system'
26
- | 'developer'
27
- | 'assistant'
28
- | 'user'
29
- | 'function'
30
- | 'tool';
31
10
 
32
11
  export type OpenRouterReasoningEffort =
33
12
  | 'xhigh'
@@ -52,6 +31,10 @@ export interface ChatOpenRouterCallOptions
52
31
  modelKwargs?: OpenAIChatInput['modelKwargs'];
53
32
  }
54
33
 
34
+ export type ChatOpenRouterInput = Partial<
35
+ ChatOpenRouterCallOptions & OpenAIChatInput
36
+ >;
37
+
55
38
  /** invocationParams return type extended with OpenRouter reasoning */
56
39
  export type OpenRouterInvocationParams = Omit<
57
40
  OpenAIClient.Chat.ChatCompletionCreateParams,
@@ -59,12 +42,68 @@ export type OpenRouterInvocationParams = Omit<
59
42
  > & {
60
43
  reasoning?: OpenRouterReasoning;
61
44
  };
45
+
46
+ type InvocationParamsExtra = {
47
+ streaming?: boolean;
48
+ };
49
+
50
+ interface OpenRouterReasoningTextDetail {
51
+ type: 'reasoning.text';
52
+ text?: string;
53
+ format?: string;
54
+ index?: number;
55
+ }
56
+
57
+ interface OpenRouterReasoningEncryptedDetail {
58
+ type: 'reasoning.encrypted';
59
+ id?: string;
60
+ data?: string;
61
+ format?: string;
62
+ index?: number;
63
+ }
64
+
65
+ type OpenRouterReasoningDetail =
66
+ | OpenRouterReasoningTextDetail
67
+ | OpenRouterReasoningEncryptedDetail;
68
+
69
+ function isReasoningTextDetail(
70
+ value: unknown
71
+ ): value is OpenRouterReasoningTextDetail {
72
+ return (
73
+ typeof value === 'object' &&
74
+ value !== null &&
75
+ 'type' in value &&
76
+ value.type === 'reasoning.text'
77
+ );
78
+ }
79
+
80
+ function isReasoningEncryptedDetail(
81
+ value: unknown
82
+ ): value is OpenRouterReasoningEncryptedDetail {
83
+ return (
84
+ typeof value === 'object' &&
85
+ value !== null &&
86
+ 'type' in value &&
87
+ value.type === 'reasoning.encrypted'
88
+ );
89
+ }
90
+
91
+ function getReasoningDetails(value: unknown): OpenRouterReasoningDetail[] {
92
+ if (!Array.isArray(value)) {
93
+ return [];
94
+ }
95
+ return value.filter(
96
+ (detail): detail is OpenRouterReasoningDetail =>
97
+ isReasoningTextDetail(detail) || isReasoningEncryptedDetail(detail)
98
+ );
99
+ }
100
+
62
101
  export class ChatOpenRouter extends ChatOpenAI {
63
102
  private openRouterReasoning?: OpenRouterReasoning;
64
103
  /** @deprecated Use `reasoning` object instead */
65
104
  private includeReasoning?: boolean;
66
105
 
67
- constructor(_fields: Partial<ChatOpenRouterCallOptions>) {
106
+ constructor(_fields: ChatOpenRouterInput) {
68
107
  const {
69
108
  include_reasoning,
70
109
  reasoning: openRouterReasoning,
@@ -80,6 +119,8 @@ export class ChatOpenRouter extends ChatOpenAI {
80
119
  super({
81
120
  ...fields,
82
121
  modelKwargs: restModelKwargs,
122
+ includeReasoningDetails: true,
123
+ convertReasoningDetailsToContent: true,
83
124
  });
84
125
 
85
126
  // Merge reasoning config: modelKwargs.reasoning < constructor reasoning
@@ -101,21 +142,26 @@ export class ChatOpenRouter extends ChatOpenAI {
101
142
  // The parent's generic conditional return type cannot be widened in an override.
102
143
  override invocationParams(
103
144
  options?: this['ParsedCallOptions'],
104
- extra?: { streaming?: boolean }
145
+ extra?: InvocationParamsExtra
105
146
  ): OpenRouterInvocationParams {
106
147
  type MutableParams = Omit<
107
148
  OpenAIClient.Chat.ChatCompletionCreateParams,
108
149
  'messages'
109
150
  > & { reasoning_effort?: string; reasoning?: OpenRouterReasoning };
110
151
 
111
- const params = super.invocationParams(options, extra) as MutableParams;
152
+ const optionsWithDefaults = this._combineCallOptions(options);
153
+ const params = (
154
+ this._useResponsesApi(options)
155
+ ? this.responses.invocationParams(optionsWithDefaults)
156
+ : this.completions.invocationParams(optionsWithDefaults, extra)
157
+ ) as MutableParams;
112
158
 
113
159
  // Remove the OpenAI-native reasoning_effort that the parent sets;
114
160
  // OpenRouter uses a `reasoning` object instead
115
161
  delete params.reasoning_effort;
116
162
 
117
163
  // Build the OpenRouter reasoning config
118
- const reasoning = this.buildOpenRouterReasoning(options);
164
+ const reasoning = this.buildOpenRouterReasoning(optionsWithDefaults);
119
165
  if (reasoning != null) {
120
166
  params.reasoning = reasoning;
121
167
  } else {
@@ -158,245 +204,76 @@ export class ChatOpenRouter extends ChatOpenAI {
158
204
 
159
205
  return reasoning;
160
206
  }
161
- protected override _convertOpenAIDeltaToBaseMessageChunk(
162
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
- delta: Record<string, any>,
164
- rawResponse: OpenAIClient.ChatCompletionChunk,
165
- defaultRole?:
166
- | 'function'
167
- | 'user'
168
- | 'system'
169
- | 'developer'
170
- | 'assistant'
171
- | 'tool'
172
- ):
173
- | AIMessageChunk
174
- | HumanMessageChunk
175
- | SystemMessageChunk
176
- | FunctionMessageChunk
177
- | ToolMessageChunk
178
- | ChatMessageChunk {
179
- const messageChunk = super._convertOpenAIDeltaToBaseMessageChunk(
180
- delta,
181
- rawResponse,
182
- defaultRole
183
- );
184
- if (delta.reasoning != null) {
185
- messageChunk.additional_kwargs.reasoning = delta.reasoning;
186
- }
187
- if (delta.reasoning_details != null) {
188
- messageChunk.additional_kwargs.reasoning_details =
189
- delta.reasoning_details;
190
- }
191
- return messageChunk;
192
- }
193
207
 
194
- async *_streamResponseChunks2(
208
+ override async *_streamResponseChunks(
195
209
  messages: BaseMessage[],
196
210
  options: this['ParsedCallOptions'],
197
211
  runManager?: CallbackManagerForLLMRun
198
212
  ): AsyncGenerator<ChatGenerationChunk> {
199
- const messagesMapped: OpenAICompletionParam[] =
200
- _convertMessagesToOpenAIParams(messages, this.model, {
201
- includeReasoningDetails: true,
202
- convertReasoningDetailsToContent: true,
203
- });
204
-
205
- const params = {
206
- ...this.invocationParams(options, {
207
- streaming: true,
208
- }),
209
- messages: messagesMapped,
210
- stream: true as const,
211
- };
212
- let defaultRole: OpenAIRoleEnum | undefined;
213
-
214
- const streamIterable = await this.completionWithRetry(params, options);
215
- let usage: OpenAIClient.Completions.CompletionUsage | undefined;
216
-
217
- // Store reasoning_details keyed by unique identifier to prevent incorrect merging
218
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
219
- const reasoningTextByIndex: Map<number, Record<string, any>> = new Map();
220
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
221
- const reasoningEncryptedById: Map<string, Record<string, any>> = new Map();
222
-
223
- for await (const data of streamIterable) {
224
- const choice = data.choices[0] as
225
- | Partial<OpenAIClient.Chat.Completions.ChatCompletionChunk.Choice>
226
- | undefined;
227
- if (data.usage) {
228
- usage = data.usage;
229
- }
230
- if (!choice) {
231
- continue;
232
- }
233
-
234
- const { delta } = choice;
235
- if (!delta) {
236
- continue;
237
- }
213
+ const reasoningTextByIndex = new Map<
214
+ number,
215
+ OpenRouterReasoningTextDetail
216
+ >();
217
+ const reasoningEncryptedById = new Map<
218
+ string,
219
+ OpenRouterReasoningEncryptedDetail
220
+ >();
221
+
222
+ for await (const generationChunk of super._streamResponseChunks(
223
+ messages,
224
+ options,
225
+ runManager
226
+ )) {
227
+ let currentReasoningText = '';
228
+ const reasoningDetails = getReasoningDetails(
229
+ generationChunk.message.additional_kwargs.reasoning_details
230
+ );
238
231
 
239
- // Accumulate reasoning_details from each delta
240
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
241
- const deltaAny = delta as Record<string, any>;
242
- // Extract current chunk's reasoning text for streaming (before accumulation)
243
- let currentChunkReasoningText = '';
244
- if (
245
- deltaAny.reasoning_details != null &&
246
- Array.isArray(deltaAny.reasoning_details)
247
- ) {
248
- for (const detail of deltaAny.reasoning_details) {
249
- // For encrypted reasoning (thought signatures), store by ID - MUST be separate
250
- if (detail.type === 'reasoning.encrypted' && detail.id) {
251
- reasoningEncryptedById.set(detail.id, {
252
- type: detail.type,
253
- id: detail.id,
254
- data: detail.data,
255
- format: detail.format,
256
- index: detail.index,
257
- });
258
- } else if (detail.type === 'reasoning.text') {
259
- // Extract current chunk's text for streaming
260
- currentChunkReasoningText += detail.text || '';
261
- // For text reasoning, accumulate text by index for final message
262
- const idx = detail.index ?? 0;
263
- const existing = reasoningTextByIndex.get(idx);
264
- if (existing) {
265
- // Only append text, keep other fields from first entry
266
- existing.text = (existing.text || '') + (detail.text || '');
267
- } else {
268
- reasoningTextByIndex.set(idx, {
269
- type: detail.type,
270
- text: detail.text || '',
271
- format: detail.format,
272
- index: idx,
273
- });
274
- }
232
+ for (const detail of reasoningDetails) {
233
+ if (detail.type === 'reasoning.text') {
234
+ currentReasoningText += detail.text ?? '';
235
+ const index = detail.index ?? 0;
236
+ const existing = reasoningTextByIndex.get(index);
237
+ if (existing != null) {
238
+ existing.text = `${existing.text ?? ''}${detail.text ?? ''}`;
239
+ continue;
275
240
  }
241
+ reasoningTextByIndex.set(index, {
242
+ ...detail,
243
+ text: detail.text ?? '',
244
+ });
245
+ continue;
246
+ }
247
+ if (detail.id != null) {
248
+ reasoningEncryptedById.set(detail.id, { ...detail });
276
249
  }
277
250
  }
278
251
 
279
- const chunk = this._convertOpenAIDeltaToBaseMessageChunk(
280
- delta,
281
- data,
282
- defaultRole
283
- );
284
-
285
- // For models that send reasoning_details (Gemini style) instead of reasoning (DeepSeek style),
286
- // set the current chunk's reasoning text to additional_kwargs.reasoning for streaming
287
- if (currentChunkReasoningText && !chunk.additional_kwargs.reasoning) {
288
- chunk.additional_kwargs.reasoning = currentChunkReasoningText;
252
+ if (
253
+ currentReasoningText.length > 0 &&
254
+ generationChunk.message.additional_kwargs.reasoning == null
255
+ ) {
256
+ generationChunk.message.additional_kwargs.reasoning =
257
+ currentReasoningText;
289
258
  }
290
259
 
291
- // IMPORTANT: Only set reasoning_details on the FINAL chunk to prevent
292
- // LangChain's chunk concatenation from corrupting the array
293
- // Check if this is the final chunk (has finish_reason)
294
- if (choice.finish_reason != null) {
295
- // Build properly structured reasoning_details array
296
- // Text entries first (but we only need the encrypted ones for thought signatures)
297
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
298
- const finalReasoningDetails: Record<string, any>[] = [
260
+ if (generationChunk.generationInfo?.finish_reason != null) {
261
+ const finalReasoningDetails = [
299
262
  ...reasoningTextByIndex.values(),
300
263
  ...reasoningEncryptedById.values(),
301
264
  ];
302
-
303
265
  if (finalReasoningDetails.length > 0) {
304
- chunk.additional_kwargs.reasoning_details = finalReasoningDetails;
266
+ generationChunk.message.additional_kwargs.reasoning_details =
267
+ finalReasoningDetails;
268
+ } else {
269
+ delete generationChunk.message.additional_kwargs.reasoning_details;
305
270
  }
306
- } else {
307
- // Clear reasoning_details from intermediate chunks to prevent concatenation issues
308
- delete chunk.additional_kwargs.reasoning_details;
309
- }
310
-
311
- defaultRole = delta.role ?? defaultRole;
312
- const newTokenIndices = {
313
- prompt: options.promptIndex ?? 0,
314
- completion: choice.index ?? 0,
315
- };
316
- if (typeof chunk.content !== 'string') {
317
- // eslint-disable-next-line no-console
318
- console.log(
319
- '[WARNING]: Received non-string content from OpenAI. This is currently not supported.'
320
- );
271
+ yield generationChunk;
321
272
  continue;
322
273
  }
323
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
324
- const generationInfo: Record<string, any> = { ...newTokenIndices };
325
- if (choice.finish_reason != null) {
326
- generationInfo.finish_reason = choice.finish_reason;
327
- generationInfo.system_fingerprint = data.system_fingerprint;
328
- generationInfo.model_name = data.model;
329
- generationInfo.service_tier = data.service_tier;
330
- }
331
- if (this.logprobs == true) {
332
- generationInfo.logprobs = choice.logprobs;
333
- }
334
- const generationChunk = new ChatGenerationChunk({
335
- message: chunk,
336
- text: chunk.content,
337
- generationInfo,
338
- });
339
- yield generationChunk;
340
- if (this._lc_stream_delay != null) {
341
- await new Promise((resolve) =>
342
- setTimeout(resolve, this._lc_stream_delay)
343
- );
344
- }
345
- await runManager?.handleLLMNewToken(
346
- generationChunk.text || '',
347
- newTokenIndices,
348
- undefined,
349
- undefined,
350
- undefined,
351
- { chunk: generationChunk }
352
- );
353
- }
354
- if (usage) {
355
- const inputTokenDetails = {
356
- ...(usage.prompt_tokens_details?.audio_tokens != null && {
357
- audio: usage.prompt_tokens_details.audio_tokens,
358
- }),
359
- ...(usage.prompt_tokens_details?.cached_tokens != null && {
360
- cache_read: usage.prompt_tokens_details.cached_tokens,
361
- }),
362
- };
363
- const outputTokenDetails = {
364
- ...(usage.completion_tokens_details?.audio_tokens != null && {
365
- audio: usage.completion_tokens_details.audio_tokens,
366
- }),
367
- ...(usage.completion_tokens_details?.reasoning_tokens != null && {
368
- reasoning: usage.completion_tokens_details.reasoning_tokens,
369
- }),
370
- };
371
- const generationChunk = new ChatGenerationChunk({
372
- message: new AIMessageChunkClass({
373
- content: '',
374
- response_metadata: {
375
- usage: { ...usage },
376
- },
377
- usage_metadata: {
378
- input_tokens: usage.prompt_tokens,
379
- output_tokens: usage.completion_tokens,
380
- total_tokens: usage.total_tokens,
381
- ...(Object.keys(inputTokenDetails).length > 0 && {
382
- input_token_details: inputTokenDetails,
383
- }),
384
- ...(Object.keys(outputTokenDetails).length > 0 && {
385
- output_token_details: outputTokenDetails,
386
- }),
387
- },
388
- }),
389
- text: '',
390
- });
274
+
275
+ delete generationChunk.message.additional_kwargs.reasoning_details;
391
276
  yield generationChunk;
392
- if (this._lc_stream_delay != null) {
393
- await new Promise((resolve) =>
394
- setTimeout(resolve, this._lc_stream_delay)
395
- );
396
- }
397
- }
398
- if (options.signal?.aborted === true) {
399
- throw new Error('AbortError');
400
277
  }
401
278
  }
402
279
  }
@@ -3,7 +3,8 @@ import type { OpenRouterReasoning, ChatOpenRouterCallOptions } from './index';
3
3
  import type { OpenAIChatInput } from '@langchain/openai';
4
4
 
5
5
  type CreateRouterOptions = Partial<
6
- ChatOpenRouterCallOptions & Pick<OpenAIChatInput, 'model' | 'apiKey'>
6
+ ChatOpenRouterCallOptions &
7
+ Pick<OpenAIChatInput, 'model' | 'apiKey' | 'streamUsage'>
7
8
  >;
8
9
 
9
10
  function createRouter(overrides: CreateRouterOptions = {}): ChatOpenRouter {
@@ -96,6 +97,12 @@ describe('ChatOpenRouter reasoning handling', () => {
96
97
  expect(params.reasoning).toBeUndefined();
97
98
  expect(params.reasoning_effort).toBeUndefined();
98
99
  });
100
+
101
+ it('preserves streaming extras from parent invocation params', () => {
102
+ const router = createRouter({ streamUsage: true });
103
+ const params = router.invocationParams(undefined, { streaming: true });
104
+ expect(params.stream_options).toEqual({ include_usage: true });
105
+ });
99
106
  });
100
107
 
101
108
  // ---------------------------------------------------------------
@@ -97,10 +97,7 @@ class CustomChatConnection extends ChatConnection<VertexAIClientOptions> {
97
97
  }
98
98
  delete formattedData.generationConfig.thinkingConfig.thinkingBudget;
99
99
  }
100
- if (
101
- this.thinkingConfig?.thinkingLevel != null &&
102
- this.thinkingConfig.thinkingLevel !== ''
103
- ) {
100
+ if (this.thinkingConfig?.thinkingLevel != null) {
104
101
  formattedData.generationConfig ??= {};
105
102
  // thinkingLevel and thinkingBudget cannot coexist — the API rejects the request.
106
103
  // Remove thinkingBudget when thinkingLevel is set.
@@ -422,7 +419,16 @@ export class ChatVertexAI extends ChatGoogle {
422
419
  return 'LibreChatVertexAI';
423
420
  }
424
421
 
425
- constructor(fields?: VertexAIClientOptions) {
422
+ constructor(model: string, fields?: Omit<VertexAIClientOptions, 'model'>);
423
+ constructor(fields?: VertexAIClientOptions);
424
+ constructor(
425
+ modelOrFields?: string | VertexAIClientOptions,
426
+ params?: Omit<VertexAIClientOptions, 'model'>
427
+ ) {
428
+ const fields =
429
+ typeof modelOrFields === 'string'
430
+ ? { ...(params ?? {}), model: modelOrFields }
431
+ : modelOrFields;
426
432
  const dynamicThinkingBudget = fields?.thinkingBudget === -1;
427
433
  super({
428
434
  ...fields,
@@ -1,6 +1,6 @@
1
1
  import { config } from 'dotenv';
2
2
  config();
3
- import { test, describe, jest } from '@jest/globals';
3
+ import { expect, test, describe, jest } from '@jest/globals';
4
4
 
5
5
  jest.setTimeout(90000);
6
6
  import {
@@ -26,6 +26,33 @@ const weatherTool = tool(async () => 'The weather is 80 degrees and sunny', {
26
26
  }),
27
27
  });
28
28
 
29
+ describe('ChatVertexAI upstream compatibility', () => {
30
+ test('serialization uses the LibreChat constructor name on the Vertex namespace', () => {
31
+ const model = new ChatVertexAI();
32
+ expect(JSON.stringify(model)).toEqual(
33
+ '{"lc":1,"type":"constructor","id":["langchain","chat_models","vertexai","LibreChatVertexAI"],"kwargs":{"platform_type":"gcp"}}'
34
+ );
35
+ });
36
+
37
+ test('labels parameter support', () => {
38
+ expect(() => {
39
+ const model = new ChatVertexAI({
40
+ labels: {
41
+ team: 'test',
42
+ environment: 'development',
43
+ },
44
+ });
45
+ expect(model.platform).toEqual('gcp');
46
+ }).not.toThrow();
47
+ });
48
+
49
+ test('constructor overload supports model string', () => {
50
+ const model = new ChatVertexAI('gemini-1.5-pro');
51
+ expect(model.model).toEqual('gemini-1.5-pro');
52
+ expect(model.platform).toEqual('gcp');
53
+ });
54
+ });
55
+
29
56
  describe.each(gemini3Models)(
30
57
  'Vertex AI reasoning with thinkingLevel (%s)',
31
58
  (modelName) => {
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  AIMessage,
3
3
  BaseMessage,
4
- ToolMessage,
5
4
  HumanMessage,
6
- MessageContentComplex,
5
+ SystemMessage,
6
+ ToolMessage,
7
7
  } from '@langchain/core/messages';
8
+ import type { MessageContentComplex } from '@langchain/core/messages';
8
9
  import type Anthropic from '@anthropic-ai/sdk';
9
10
  import type { AnthropicMessages } from '@/types/messages';
10
11
  import {
@@ -13,6 +14,7 @@ import {
13
14
  addBedrockCacheControl,
14
15
  addCacheControl,
15
16
  } from './cache';
17
+ import { toLangChainContent } from './langchain';
16
18
  import { ContentTypes } from '@/common/enum';
17
19
 
18
20
  describe('addCacheControl', () => {
@@ -404,7 +406,107 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
404
406
  expect(first[1]).toEqual({ cachePoint: { type: 'default' } });
405
407
  });
406
408
 
407
- it('works with the example from the langchain pr (with multi-turn behavior)', () => {
409
+ it('preserves LangChain system message content unchanged', () => {
410
+ const systemContent = [
411
+ { type: ContentTypes.TEXT, text: 'Stable system text' },
412
+ { cachePoint: { type: 'default' } },
413
+ { type: ContentTypes.TEXT, text: 'Dynamic system text' },
414
+ ] as MessageContentComplex[];
415
+ const messages: BaseMessage[] = [
416
+ new SystemMessage({ content: toLangChainContent(systemContent) }),
417
+ new HumanMessage('Hello'),
418
+ new AIMessage('Hi'),
419
+ ];
420
+
421
+ const result = addBedrockCacheControl(messages);
422
+
423
+ expect(result[0]).toBe(messages[0]);
424
+ expect(result[0].content).toEqual(systemContent);
425
+ });
426
+
427
+ it('preserves serialized system message content unchanged', () => {
428
+ const systemContent = [
429
+ { type: ContentTypes.TEXT, text: 'Stable system text' },
430
+ { cachePoint: { type: 'default' } },
431
+ { type: ContentTypes.TEXT, text: 'Dynamic system text' },
432
+ ] as MessageContentComplex[];
433
+ const messages: TestMsg[] = [
434
+ { role: 'system', content: systemContent },
435
+ { role: 'user', content: 'Hello' },
436
+ { role: 'assistant', content: 'Hi' },
437
+ ];
438
+
439
+ const result = addBedrockCacheControl(messages);
440
+
441
+ expect(result[0]).toBe(messages[0]);
442
+ expect(result[0].content).toEqual(systemContent);
443
+ });
444
+
445
+ it('strips Anthropic cache_control from LangChain system messages without moving cache points', () => {
446
+ const systemContent = [
447
+ {
448
+ type: ContentTypes.TEXT,
449
+ text: 'Stable system text',
450
+ cache_control: { type: 'ephemeral' },
451
+ } as MessageContentComplex,
452
+ { cachePoint: { type: 'default' } },
453
+ {
454
+ type: ContentTypes.TEXT,
455
+ text: 'Dynamic system text',
456
+ cache_control: { type: 'ephemeral' },
457
+ } as MessageContentComplex,
458
+ ] as MessageContentComplex[];
459
+ const messages: BaseMessage[] = [
460
+ new SystemMessage({ content: toLangChainContent(systemContent) }),
461
+ new HumanMessage('Hello'),
462
+ new AIMessage('Hi'),
463
+ ];
464
+
465
+ const result = addBedrockCacheControl(messages);
466
+
467
+ expect(result[0]).not.toBe(messages[0]);
468
+ expect(result[0].content).toEqual([
469
+ { type: ContentTypes.TEXT, text: 'Stable system text' },
470
+ { cachePoint: { type: 'default' } },
471
+ { type: ContentTypes.TEXT, text: 'Dynamic system text' },
472
+ ]);
473
+ expect(systemContent[0]).toHaveProperty('cache_control');
474
+ expect(systemContent[2]).toHaveProperty('cache_control');
475
+ });
476
+
477
+ it('strips Anthropic cache_control from serialized system messages without moving cache points', () => {
478
+ const systemContent = [
479
+ {
480
+ type: ContentTypes.TEXT,
481
+ text: 'Stable system text',
482
+ cache_control: { type: 'ephemeral' },
483
+ } as MessageContentComplex,
484
+ { cachePoint: { type: 'default' } },
485
+ {
486
+ type: ContentTypes.TEXT,
487
+ text: 'Dynamic system text',
488
+ cache_control: { type: 'ephemeral' },
489
+ } as MessageContentComplex,
490
+ ] as MessageContentComplex[];
491
+ const messages: TestMsg[] = [
492
+ { role: 'system', content: systemContent },
493
+ { role: 'user', content: 'Hello' },
494
+ { role: 'assistant', content: 'Hi' },
495
+ ];
496
+
497
+ const result = addBedrockCacheControl(messages);
498
+
499
+ expect(result[0]).not.toBe(messages[0]);
500
+ expect(result[0].content).toEqual([
501
+ { type: ContentTypes.TEXT, text: 'Stable system text' },
502
+ { cachePoint: { type: 'default' } },
503
+ { type: ContentTypes.TEXT, text: 'Dynamic system text' },
504
+ ]);
505
+ expect(systemContent[0]).toHaveProperty('cache_control');
506
+ expect(systemContent[2]).toHaveProperty('cache_control');
507
+ });
508
+
509
+ it('skips serialized system messages while adding cache points to non-system turns', () => {
408
510
  const messages: TestMsg[] = [
409
511
  {
410
512
  role: 'system',
@@ -429,7 +531,7 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
429
531
  type: ContentTypes.TEXT,
430
532
  text: 'You\'re an advanced AI assistant.',
431
533
  });
432
- expect(system[1]).toEqual({ cachePoint: { type: 'default' } });
534
+ expect(system).toHaveLength(1);
433
535
  expect(user[0]).toEqual({
434
536
  type: ContentTypes.TEXT,
435
537
  text: 'What is the capital of France?',