@librechat/agents 3.0.34 → 3.0.35

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.
@@ -1,4 +1,7 @@
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';
2
5
  import type {
3
6
  FunctionMessageChunk,
4
7
  SystemMessageChunk,
@@ -6,12 +9,25 @@ import type {
6
9
  ToolMessageChunk,
7
10
  ChatMessageChunk,
8
11
  AIMessageChunk,
12
+ BaseMessage,
9
13
  } from '@langchain/core/messages';
10
14
  import type {
11
15
  ChatOpenAICallOptions,
12
16
  OpenAIChatInput,
13
17
  OpenAIClient,
14
18
  } 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';
15
31
 
16
32
  export interface ChatOpenRouterCallOptions extends ChatOpenAICallOptions {
17
33
  include_reasoning?: boolean;
@@ -54,7 +70,212 @@ export class ChatOpenRouter extends ChatOpenAI {
54
70
  rawResponse,
55
71
  defaultRole
56
72
  );
57
- messageChunk.additional_kwargs.reasoning = delta.reasoning;
73
+ if (delta.reasoning != null) {
74
+ messageChunk.additional_kwargs.reasoning = delta.reasoning;
75
+ }
76
+ if (delta.reasoning_details != null) {
77
+ messageChunk.additional_kwargs.reasoning_details =
78
+ delta.reasoning_details;
79
+ }
58
80
  return messageChunk;
59
81
  }
82
+
83
+ async *_streamResponseChunks2(
84
+ messages: BaseMessage[],
85
+ options: this['ParsedCallOptions'],
86
+ runManager?: CallbackManagerForLLMRun
87
+ ): AsyncGenerator<ChatGenerationChunk> {
88
+ const messagesMapped: OpenAICompletionParam[] =
89
+ _convertMessagesToOpenAIParams(messages, this.model, {
90
+ includeReasoningDetails: true,
91
+ convertReasoningDetailsToContent: true,
92
+ });
93
+
94
+ const params = {
95
+ ...this.invocationParams(options, {
96
+ streaming: true,
97
+ }),
98
+ messages: messagesMapped,
99
+ stream: true as const,
100
+ };
101
+ let defaultRole: OpenAIRoleEnum | undefined;
102
+
103
+ const streamIterable = await this.completionWithRetry(params, options);
104
+ let usage: OpenAIClient.Completions.CompletionUsage | undefined;
105
+
106
+ // Store reasoning_details keyed by unique identifier to prevent incorrect merging
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ const reasoningTextByIndex: Map<number, Record<string, any>> = new Map();
109
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
110
+ const reasoningEncryptedById: Map<string, Record<string, any>> = new Map();
111
+
112
+ for await (const data of streamIterable) {
113
+ const choice = data.choices[0] as
114
+ | Partial<OpenAIClient.Chat.Completions.ChatCompletionChunk.Choice>
115
+ | undefined;
116
+ if (data.usage) {
117
+ usage = data.usage;
118
+ }
119
+ if (!choice) {
120
+ continue;
121
+ }
122
+
123
+ const { delta } = choice;
124
+ if (!delta) {
125
+ continue;
126
+ }
127
+
128
+ // Accumulate reasoning_details from each delta
129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
+ const deltaAny = delta as Record<string, any>;
131
+ if (
132
+ deltaAny.reasoning_details != null &&
133
+ Array.isArray(deltaAny.reasoning_details)
134
+ ) {
135
+ for (const detail of deltaAny.reasoning_details) {
136
+ // For encrypted reasoning (thought signatures), store by ID - MUST be separate
137
+ if (detail.type === 'reasoning.encrypted' && detail.id) {
138
+ reasoningEncryptedById.set(detail.id, {
139
+ type: detail.type,
140
+ id: detail.id,
141
+ data: detail.data,
142
+ format: detail.format,
143
+ index: detail.index,
144
+ });
145
+ } else if (detail.type === 'reasoning.text') {
146
+ // For text reasoning, accumulate text by index
147
+ const idx = detail.index ?? 0;
148
+ const existing = reasoningTextByIndex.get(idx);
149
+ if (existing) {
150
+ // Only append text, keep other fields from first entry
151
+ existing.text = (existing.text || '') + (detail.text || '');
152
+ } else {
153
+ reasoningTextByIndex.set(idx, {
154
+ type: detail.type,
155
+ text: detail.text || '',
156
+ format: detail.format,
157
+ index: idx,
158
+ });
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ const chunk = this._convertOpenAIDeltaToBaseMessageChunk(
165
+ delta,
166
+ data,
167
+ defaultRole
168
+ );
169
+
170
+ // IMPORTANT: Only set reasoning_details on the FINAL chunk to prevent
171
+ // LangChain's chunk concatenation from corrupting the array
172
+ // Check if this is the final chunk (has finish_reason)
173
+ if (choice.finish_reason != null) {
174
+ // Build properly structured reasoning_details array
175
+ // Text entries first (but we only need the encrypted ones for thought signatures)
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ const finalReasoningDetails: Record<string, any>[] = [
178
+ ...reasoningTextByIndex.values(),
179
+ ...reasoningEncryptedById.values(),
180
+ ];
181
+
182
+ if (finalReasoningDetails.length > 0) {
183
+ chunk.additional_kwargs.reasoning_details = finalReasoningDetails;
184
+ }
185
+ } else {
186
+ // Clear reasoning_details from intermediate chunks to prevent concatenation issues
187
+ delete chunk.additional_kwargs.reasoning_details;
188
+ }
189
+
190
+ defaultRole = delta.role ?? defaultRole;
191
+ const newTokenIndices = {
192
+ prompt: options.promptIndex ?? 0,
193
+ completion: choice.index ?? 0,
194
+ };
195
+ if (typeof chunk.content !== 'string') {
196
+ // eslint-disable-next-line no-console
197
+ console.log(
198
+ '[WARNING]: Received non-string content from OpenAI. This is currently not supported.'
199
+ );
200
+ continue;
201
+ }
202
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
203
+ const generationInfo: Record<string, any> = { ...newTokenIndices };
204
+ if (choice.finish_reason != null) {
205
+ generationInfo.finish_reason = choice.finish_reason;
206
+ generationInfo.system_fingerprint = data.system_fingerprint;
207
+ generationInfo.model_name = data.model;
208
+ generationInfo.service_tier = data.service_tier;
209
+ }
210
+ if (this.logprobs == true) {
211
+ generationInfo.logprobs = choice.logprobs;
212
+ }
213
+ const generationChunk = new ChatGenerationChunk({
214
+ message: chunk,
215
+ text: chunk.content,
216
+ generationInfo,
217
+ });
218
+ yield generationChunk;
219
+ if (this._lc_stream_delay != null) {
220
+ await new Promise((resolve) =>
221
+ setTimeout(resolve, this._lc_stream_delay)
222
+ );
223
+ }
224
+ await runManager?.handleLLMNewToken(
225
+ generationChunk.text || '',
226
+ newTokenIndices,
227
+ undefined,
228
+ undefined,
229
+ undefined,
230
+ { chunk: generationChunk }
231
+ );
232
+ }
233
+ if (usage) {
234
+ const inputTokenDetails = {
235
+ ...(usage.prompt_tokens_details?.audio_tokens != null && {
236
+ audio: usage.prompt_tokens_details.audio_tokens,
237
+ }),
238
+ ...(usage.prompt_tokens_details?.cached_tokens != null && {
239
+ cache_read: usage.prompt_tokens_details.cached_tokens,
240
+ }),
241
+ };
242
+ const outputTokenDetails = {
243
+ ...(usage.completion_tokens_details?.audio_tokens != null && {
244
+ audio: usage.completion_tokens_details.audio_tokens,
245
+ }),
246
+ ...(usage.completion_tokens_details?.reasoning_tokens != null && {
247
+ reasoning: usage.completion_tokens_details.reasoning_tokens,
248
+ }),
249
+ };
250
+ const generationChunk = new ChatGenerationChunk({
251
+ message: new AIMessageChunkClass({
252
+ content: '',
253
+ response_metadata: {
254
+ usage: { ...usage },
255
+ },
256
+ usage_metadata: {
257
+ input_tokens: usage.prompt_tokens,
258
+ output_tokens: usage.completion_tokens,
259
+ total_tokens: usage.total_tokens,
260
+ ...(Object.keys(inputTokenDetails).length > 0 && {
261
+ input_token_details: inputTokenDetails,
262
+ }),
263
+ ...(Object.keys(outputTokenDetails).length > 0 && {
264
+ output_token_details: outputTokenDetails,
265
+ }),
266
+ },
267
+ }),
268
+ text: '',
269
+ });
270
+ yield generationChunk;
271
+ if (this._lc_stream_delay != null) {
272
+ await new Promise((resolve) =>
273
+ setTimeout(resolve, this._lc_stream_delay)
274
+ );
275
+ }
276
+ }
277
+ if (options.signal?.aborted === true) {
278
+ throw new Error('AbortError');
279
+ }
280
+ }
60
281
  }
package/src/stream.ts CHANGED
@@ -107,6 +107,25 @@ export function getChunkContent({
107
107
  | undefined
108
108
  )?.summary?.[0]?.text;
109
109
  }
110
+ if (
111
+ provider === Providers.OPENROUTER &&
112
+ chunk?.additional_kwargs?.reasoning_details != null &&
113
+ Array.isArray(chunk.additional_kwargs.reasoning_details)
114
+ ) {
115
+ // Extract text from reasoning_details array (for Gemini, DeepSeek, etc.)
116
+ const textEntries = chunk.additional_kwargs.reasoning_details
117
+ .filter(
118
+ (detail) =>
119
+ detail.type === 'reasoning.text' &&
120
+ detail.text != null &&
121
+ detail.text !== ''
122
+ )
123
+ .map((detail) => detail.text)
124
+ .join('');
125
+ if (textEntries) {
126
+ return textEntries;
127
+ }
128
+ }
110
129
  return (
111
130
  ((chunk?.additional_kwargs?.[reasoningKey] as string | undefined) ?? '') ||
112
131
  chunk?.content
@@ -355,6 +374,13 @@ hasToolCallChunks: ${hasToolCallChunks}
355
374
  reasoning_content.summary[0].text
356
375
  ) {
357
376
  reasoning_content = 'valid';
377
+ } else if (
378
+ agentContext.provider === Providers.OPENROUTER &&
379
+ chunk.additional_kwargs?.reasoning_details != null &&
380
+ Array.isArray(chunk.additional_kwargs.reasoning_details) &&
381
+ chunk.additional_kwargs.reasoning_details.length > 0
382
+ ) {
383
+ reasoning_content = 'valid';
358
384
  }
359
385
  if (
360
386
  reasoning_content != null &&
@@ -56,8 +56,8 @@ export const llmConfigs: Record<string, t.LLMConfig | undefined> = {
56
56
  provider: Providers.OPENROUTER,
57
57
  streaming: true,
58
58
  streamUsage: true,
59
- model: 'openai/gpt-4.1',
60
- openAIApiKey: process.env.OPENROUTER_API_KEY,
59
+ model: 'anthropic/claude-sonnet-4',
60
+ apiKey: process.env.OPENROUTER_API_KEY,
61
61
  configuration: {
62
62
  baseURL: process.env.OPENROUTER_BASE_URL,
63
63
  defaultHeaders: {
@@ -66,6 +66,12 @@ export const llmConfigs: Record<string, t.LLMConfig | undefined> = {
66
66
  },
67
67
  },
68
68
  include_reasoning: true,
69
+ modelKwargs: {
70
+ reasoning: {
71
+ max_tokens: 8000,
72
+ },
73
+ max_tokens: 10000,
74
+ },
69
75
  } as or.ChatOpenRouterCallOptions & t.LLMConfig,
70
76
  [Providers.AZURE]: {
71
77
  provider: Providers.AZURE,