@librechat/agents 3.0.34 → 3.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cjs/graphs/Graph.cjs +2 -1
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/instrumentation.cjs +1 -1
  4. package/dist/cjs/instrumentation.cjs.map +1 -1
  5. package/dist/cjs/llm/openai/index.cjs +102 -0
  6. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  7. package/dist/cjs/llm/openai/utils/index.cjs +87 -1
  8. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  9. package/dist/cjs/llm/openrouter/index.cjs +175 -1
  10. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  11. package/dist/cjs/stream.cjs +20 -0
  12. package/dist/cjs/stream.cjs.map +1 -1
  13. package/dist/esm/graphs/Graph.mjs +2 -1
  14. package/dist/esm/graphs/Graph.mjs.map +1 -1
  15. package/dist/esm/instrumentation.mjs +1 -1
  16. package/dist/esm/instrumentation.mjs.map +1 -1
  17. package/dist/esm/llm/openai/index.mjs +103 -1
  18. package/dist/esm/llm/openai/index.mjs.map +1 -1
  19. package/dist/esm/llm/openai/utils/index.mjs +88 -2
  20. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  21. package/dist/esm/llm/openrouter/index.mjs +175 -1
  22. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  23. package/dist/esm/stream.mjs +20 -0
  24. package/dist/esm/stream.mjs.map +1 -1
  25. package/dist/types/llm/openai/index.d.ts +1 -0
  26. package/dist/types/llm/openai/utils/index.d.ts +10 -1
  27. package/dist/types/llm/openrouter/index.d.ts +4 -1
  28. package/package.json +2 -2
  29. package/src/graphs/Graph.ts +2 -1
  30. package/src/instrumentation.ts +1 -1
  31. package/src/llm/google/llm.spec.ts +3 -1
  32. package/src/llm/openai/index.ts +126 -0
  33. package/src/llm/openai/utils/index.ts +116 -1
  34. package/src/llm/openrouter/index.ts +222 -1
  35. package/src/stream.ts +26 -0
  36. package/src/utils/llmConfig.ts +8 -2
@@ -643,6 +643,132 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
643
643
  } as OpenAICoreRequestOptions;
644
644
  return requestOptions;
645
645
  }
646
+
647
+ async *_streamResponseChunks(
648
+ messages: BaseMessage[],
649
+ options: this['ParsedCallOptions'],
650
+ runManager?: CallbackManagerForLLMRun
651
+ ): AsyncGenerator<ChatGenerationChunk> {
652
+ const messagesMapped: OpenAICompletionParam[] =
653
+ _convertMessagesToOpenAIParams(messages, this.model, {
654
+ includeReasoningContent: true,
655
+ });
656
+
657
+ const params = {
658
+ ...this.invocationParams(options, {
659
+ streaming: true,
660
+ }),
661
+ messages: messagesMapped,
662
+ stream: true as const,
663
+ };
664
+ let defaultRole: OpenAIRoleEnum | undefined;
665
+
666
+ const streamIterable = await this.completionWithRetry(params, options);
667
+ let usage: OpenAIClient.Completions.CompletionUsage | undefined;
668
+ for await (const data of streamIterable) {
669
+ const choice = data.choices[0] as
670
+ | Partial<OpenAIClient.Chat.Completions.ChatCompletionChunk.Choice>
671
+ | undefined;
672
+ if (data.usage) {
673
+ usage = data.usage;
674
+ }
675
+ if (!choice) {
676
+ continue;
677
+ }
678
+
679
+ const { delta } = choice;
680
+ if (!delta) {
681
+ continue;
682
+ }
683
+ const chunk = this._convertOpenAIDeltaToBaseMessageChunk(
684
+ delta,
685
+ data,
686
+ defaultRole
687
+ );
688
+ if ('reasoning_content' in delta) {
689
+ chunk.additional_kwargs.reasoning_content = delta.reasoning_content;
690
+ }
691
+ defaultRole = delta.role ?? defaultRole;
692
+ const newTokenIndices = {
693
+ prompt: (options as OpenAIChatCallOptions).promptIndex ?? 0,
694
+ completion: choice.index ?? 0,
695
+ };
696
+ if (typeof chunk.content !== 'string') {
697
+ // eslint-disable-next-line no-console
698
+ console.log(
699
+ '[WARNING]: Received non-string content from OpenAI. This is currently not supported.'
700
+ );
701
+ continue;
702
+ }
703
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
704
+ const generationInfo: Record<string, any> = { ...newTokenIndices };
705
+ if (choice.finish_reason != null) {
706
+ generationInfo.finish_reason = choice.finish_reason;
707
+ generationInfo.system_fingerprint = data.system_fingerprint;
708
+ generationInfo.model_name = data.model;
709
+ generationInfo.service_tier = data.service_tier;
710
+ }
711
+ if (this.logprobs == true) {
712
+ generationInfo.logprobs = choice.logprobs;
713
+ }
714
+ const generationChunk = new ChatGenerationChunk({
715
+ message: chunk,
716
+ text: chunk.content,
717
+ generationInfo,
718
+ });
719
+ yield generationChunk;
720
+ await runManager?.handleLLMNewToken(
721
+ generationChunk.text || '',
722
+ newTokenIndices,
723
+ undefined,
724
+ undefined,
725
+ undefined,
726
+ { chunk: generationChunk }
727
+ );
728
+ }
729
+ if (usage) {
730
+ const inputTokenDetails = {
731
+ ...(usage.prompt_tokens_details?.audio_tokens != null && {
732
+ audio: usage.prompt_tokens_details.audio_tokens,
733
+ }),
734
+ ...(usage.prompt_tokens_details?.cached_tokens != null && {
735
+ cache_read: usage.prompt_tokens_details.cached_tokens,
736
+ }),
737
+ };
738
+ const outputTokenDetails = {
739
+ ...(usage.completion_tokens_details?.audio_tokens != null && {
740
+ audio: usage.completion_tokens_details.audio_tokens,
741
+ }),
742
+ ...(usage.completion_tokens_details?.reasoning_tokens != null && {
743
+ reasoning: usage.completion_tokens_details.reasoning_tokens,
744
+ }),
745
+ };
746
+ const generationChunk = new ChatGenerationChunk({
747
+ message: new AIMessageChunk({
748
+ content: '',
749
+ response_metadata: {
750
+ usage: { ...usage },
751
+ },
752
+ usage_metadata: {
753
+ input_tokens: usage.prompt_tokens,
754
+ output_tokens: usage.completion_tokens,
755
+ total_tokens: usage.total_tokens,
756
+ ...(Object.keys(inputTokenDetails).length > 0 && {
757
+ input_token_details: inputTokenDetails,
758
+ }),
759
+ ...(Object.keys(outputTokenDetails).length > 0 && {
760
+ output_token_details: outputTokenDetails,
761
+ }),
762
+ },
763
+ }),
764
+ text: '',
765
+ });
766
+ yield generationChunk;
767
+ }
768
+ if (options.signal?.aborted === true) {
769
+ throw new Error('AbortError');
770
+ }
771
+ }
646
772
  }
647
773
 
648
774
  /** xAI-specific usage metadata type */
@@ -286,10 +286,21 @@ const completionsApiContentBlockConverter: StandardContentBlockConverter<{
286
286
  },
287
287
  };
288
288
 
289
+ /** Options for converting messages to OpenAI params */
290
+ export interface ConvertMessagesOptions {
291
+ /** Include reasoning_content field for DeepSeek thinking mode with tool calls */
292
+ includeReasoningContent?: boolean;
293
+ /** Include reasoning_details field for OpenRouter/Gemini thinking mode with tool calls */
294
+ includeReasoningDetails?: boolean;
295
+ /** Convert reasoning_details to content blocks for Claude (requires content array format) */
296
+ convertReasoningDetailsToContent?: boolean;
297
+ }
298
+
289
299
  // Used in LangSmith, export is important here
290
300
  export function _convertMessagesToOpenAIParams(
291
301
  messages: BaseMessage[],
292
- model?: string
302
+ model?: string,
303
+ options?: ConvertMessagesOptions
293
304
  ): OpenAICompletionParam[] {
294
305
  // TODO: Function messages do not support array content, fix cast
295
306
  return messages.flatMap((message) => {
@@ -333,9 +344,113 @@ export function _convertMessagesToOpenAIParams(
333
344
  convertLangChainToolCallToOpenAI
334
345
  );
335
346
  completionParam.content = hasAnthropicThinkingBlock ? content : '';
347
+ if (
348
+ options?.includeReasoningContent === true &&
349
+ message.additional_kwargs.reasoning_content != null
350
+ ) {
351
+ completionParam.reasoning_content =
352
+ message.additional_kwargs.reasoning_content;
353
+ }
354
+ if (
355
+ options?.includeReasoningDetails === true &&
356
+ message.additional_kwargs.reasoning_details != null
357
+ ) {
358
+ // For Claude via OpenRouter, convert reasoning_details to content blocks
359
+ const isClaudeModel =
360
+ model?.includes('claude') === true ||
361
+ model?.includes('anthropic') === true;
362
+ if (
363
+ options.convertReasoningDetailsToContent === true &&
364
+ isClaudeModel
365
+ ) {
366
+ const reasoningDetails = message.additional_kwargs
367
+ .reasoning_details as Record<string, unknown>[];
368
+ const contentBlocks = [];
369
+
370
+ // Add thinking blocks from reasoning_details
371
+ for (const detail of reasoningDetails) {
372
+ if (detail.type === 'reasoning.text' && detail.text != null) {
373
+ contentBlocks.push({
374
+ type: 'thinking',
375
+ thinking: detail.text,
376
+ });
377
+ } else if (
378
+ detail.type === 'reasoning.encrypted' &&
379
+ detail.data != null
380
+ ) {
381
+ contentBlocks.push({
382
+ type: 'redacted_thinking',
383
+ data: detail.data,
384
+ id: detail.id,
385
+ });
386
+ }
387
+ }
388
+
389
+ // Set content to array with thinking blocks
390
+ if (contentBlocks.length > 0) {
391
+ completionParam.content = contentBlocks;
392
+ }
393
+ } else {
394
+ // For non-Claude models, pass as separate field
395
+ completionParam.reasoning_details =
396
+ message.additional_kwargs.reasoning_details;
397
+ }
398
+ }
336
399
  } else {
337
400
  if (message.additional_kwargs.tool_calls != null) {
338
401
  completionParam.tool_calls = message.additional_kwargs.tool_calls;
402
+ if (
403
+ options?.includeReasoningContent === true &&
404
+ message.additional_kwargs.reasoning_content != null
405
+ ) {
406
+ completionParam.reasoning_content =
407
+ message.additional_kwargs.reasoning_content;
408
+ }
409
+ if (
410
+ options?.includeReasoningDetails === true &&
411
+ message.additional_kwargs.reasoning_details != null
412
+ ) {
413
+ // For Claude via OpenRouter, convert reasoning_details to content blocks
414
+ const isClaudeModel =
415
+ model?.includes('claude') === true ||
416
+ model?.includes('anthropic') === true;
417
+ if (
418
+ options.convertReasoningDetailsToContent === true &&
419
+ isClaudeModel
420
+ ) {
421
+ const reasoningDetails = message.additional_kwargs
422
+ .reasoning_details as Record<string, unknown>[];
423
+ const contentBlocks = [];
424
+
425
+ // Add thinking blocks from reasoning_details
426
+ for (const detail of reasoningDetails) {
427
+ if (detail.type === 'reasoning.text' && detail.text != null) {
428
+ contentBlocks.push({
429
+ type: 'thinking',
430
+ thinking: detail.text,
431
+ });
432
+ } else if (
433
+ detail.type === 'reasoning.encrypted' &&
434
+ detail.data != null
435
+ ) {
436
+ contentBlocks.push({
437
+ type: 'redacted_thinking',
438
+ data: detail.data,
439
+ id: detail.id,
440
+ });
441
+ }
442
+ }
443
+
444
+ // Set content to array with thinking blocks
445
+ if (contentBlocks.length > 0) {
446
+ completionParam.content = contentBlocks;
447
+ }
448
+ } else {
449
+ // For non-Claude models, pass as separate field
450
+ completionParam.reasoning_details =
451
+ message.additional_kwargs.reasoning_details;
452
+ }
453
+ }
339
454
  }
340
455
  if ((message as ToolMessage).tool_call_id != null) {
341
456
  completionParam.tool_call_id = (message as ToolMessage).tool_call_id;
@@ -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,