@librechat/agents 3.0.33 → 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.
Files changed (55) hide show
  1. package/dist/cjs/common/enum.cjs +0 -1
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/llm/openai/index.cjs +102 -0
  4. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  5. package/dist/cjs/llm/openai/utils/index.cjs +87 -1
  6. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  7. package/dist/cjs/llm/openrouter/index.cjs +175 -1
  8. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  9. package/dist/cjs/llm/providers.cjs +0 -3
  10. package/dist/cjs/llm/providers.cjs.map +1 -1
  11. package/dist/cjs/stream.cjs +20 -0
  12. package/dist/cjs/stream.cjs.map +1 -1
  13. package/dist/cjs/utils/llm.cjs +0 -1
  14. package/dist/cjs/utils/llm.cjs.map +1 -1
  15. package/dist/esm/common/enum.mjs +0 -1
  16. package/dist/esm/common/enum.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/llm/providers.mjs +0 -3
  24. package/dist/esm/llm/providers.mjs.map +1 -1
  25. package/dist/esm/stream.mjs +20 -0
  26. package/dist/esm/stream.mjs.map +1 -1
  27. package/dist/esm/utils/llm.mjs +0 -1
  28. package/dist/esm/utils/llm.mjs.map +1 -1
  29. package/dist/types/common/enum.d.ts +0 -1
  30. package/dist/types/llm/openai/index.d.ts +1 -0
  31. package/dist/types/llm/openai/utils/index.d.ts +10 -1
  32. package/dist/types/llm/openrouter/index.d.ts +4 -1
  33. package/dist/types/types/llm.d.ts +1 -6
  34. package/package.json +2 -3
  35. package/src/common/enum.ts +0 -1
  36. package/src/llm/openai/index.ts +126 -0
  37. package/src/llm/openai/utils/index.ts +116 -1
  38. package/src/llm/openrouter/index.ts +222 -1
  39. package/src/llm/providers.ts +0 -3
  40. package/src/stream.ts +26 -0
  41. package/src/types/llm.ts +0 -6
  42. package/src/utils/llm.ts +0 -1
  43. package/src/utils/llmConfig.ts +13 -5
  44. package/dist/cjs/llm/ollama/index.cjs +0 -70
  45. package/dist/cjs/llm/ollama/index.cjs.map +0 -1
  46. package/dist/cjs/llm/ollama/utils.cjs +0 -158
  47. package/dist/cjs/llm/ollama/utils.cjs.map +0 -1
  48. package/dist/esm/llm/ollama/index.mjs +0 -68
  49. package/dist/esm/llm/ollama/index.mjs.map +0 -1
  50. package/dist/esm/llm/ollama/utils.mjs +0 -155
  51. package/dist/esm/llm/ollama/utils.mjs.map +0 -1
  52. package/dist/types/llm/ollama/index.d.ts +0 -8
  53. package/dist/types/llm/ollama/utils.d.ts +0 -7
  54. package/src/llm/ollama/index.ts +0 -92
  55. package/src/llm/ollama/utils.ts +0 -193
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.0.33",
3
+ "version": "3.0.35",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -51,7 +51,7 @@
51
51
  "caching": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/caching.ts --name 'Jo' --location 'New York, NY'",
52
52
  "thinking": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/thinking.ts --name 'Jo' --location 'New York, NY'",
53
53
  "memory": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/memory.ts --provider 'openAI' --name 'Jo' --location 'New York, NY'",
54
- "tool": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tools.ts --provider 'bedrock' --name 'Jo' --location 'New York, NY'",
54
+ "tool": "node --trace-warnings -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tools.ts --provider 'openrouter' --name 'Jo' --location 'New York, NY'",
55
55
  "search": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/search.ts --provider 'bedrock' --name 'Jo' --location 'New York, NY'",
56
56
  "ant_web_search": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/ant_web_search.ts --name 'Jo' --location 'New York, NY'",
57
57
  "ant_web_search_edge_case": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/ant_web_search_edge_case.ts --name 'Jo' --location 'New York, NY'",
@@ -106,7 +106,6 @@
106
106
  "@langchain/google-vertexai": "^0.2.18",
107
107
  "@langchain/langgraph": "^0.4.9",
108
108
  "@langchain/mistralai": "^0.2.1",
109
- "@langchain/ollama": "^0.2.3",
110
109
  "@langchain/openai": "0.5.18",
111
110
  "@langchain/textsplitters": "^0.1.0",
112
111
  "@langchain/xai": "^0.0.3",
@@ -77,7 +77,6 @@ export enum Providers {
77
77
  ANTHROPIC = 'anthropic',
78
78
  MISTRALAI = 'mistralai',
79
79
  MISTRAL = 'mistral',
80
- OLLAMA = 'ollama',
81
80
  GOOGLE = 'google',
82
81
  AZURE = 'azureOpenAI',
83
82
  DEEPSEEK = 'deepseek',
@@ -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
  }
@@ -16,13 +16,11 @@ import { CustomChatBedrockConverse } from '@/llm/bedrock';
16
16
  import { CustomAnthropic } from '@/llm/anthropic';
17
17
  import { ChatOpenRouter } from '@/llm/openrouter';
18
18
  import { ChatVertexAI } from '@/llm/vertexai';
19
- import { ChatOllama } from '@/llm/ollama';
20
19
  import { Providers } from '@/common';
21
20
 
22
21
  export const llmProviders: Partial<ChatModelConstructorMap> = {
23
22
  [Providers.XAI]: ChatXAI,
24
23
  [Providers.OPENAI]: ChatOpenAI,
25
- [Providers.OLLAMA]: ChatOllama,
26
24
  [Providers.AZURE]: AzureChatOpenAI,
27
25
  [Providers.VERTEXAI]: ChatVertexAI,
28
26
  [Providers.DEEPSEEK]: ChatDeepSeek,
@@ -38,7 +36,6 @@ export const llmProviders: Partial<ChatModelConstructorMap> = {
38
36
  export const manualToolStreamProviders = new Set<Providers | string>([
39
37
  Providers.ANTHROPIC,
40
38
  Providers.BEDROCK,
41
- Providers.OLLAMA,
42
39
  ]);
43
40
 
44
41
  export const getChatModelClass = <P extends Providers>(
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 &&
package/src/types/llm.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  // src/types/llm.ts
2
- import { ChatOllama } from '@langchain/ollama';
3
2
  import { ChatMistralAI } from '@langchain/mistralai';
4
3
  import type {
5
4
  BindToolsInput,
@@ -22,7 +21,6 @@ import type { RequestOptions } from '@google/generative-ai';
22
21
  import type { StructuredTool } from '@langchain/core/tools';
23
22
  import type { AnthropicInput } from '@langchain/anthropic';
24
23
  import type { Runnable } from '@langchain/core/runnables';
25
- import type { ChatOllamaInput } from '@langchain/ollama';
26
24
  import type { OpenAI as OpenAIClient } from 'openai';
27
25
  import type { ChatXAIInput } from '@langchain/xai';
28
26
  import {
@@ -57,7 +55,6 @@ export type AnthropicReasoning = {
57
55
  thinkingBudget?: number;
58
56
  };
59
57
  export type OpenAIClientOptions = ChatOpenAIFields;
60
- export type OllamaClientOptions = ChatOllamaInput;
61
58
  export type AnthropicClientOptions = AnthropicInput;
62
59
  export type MistralAIClientOptions = ChatMistralAIInput;
63
60
  export type VertexAIClientOptions = ChatVertexAIInput & {
@@ -80,7 +77,6 @@ export type XAIClientOptions = ChatXAIInput;
80
77
  export type ClientOptions =
81
78
  | OpenAIClientOptions
82
79
  | AzureClientOptions
83
- | OllamaClientOptions
84
80
  | AnthropicClientOptions
85
81
  | MistralAIClientOptions
86
82
  | VertexAIClientOptions
@@ -103,7 +99,6 @@ export type LLMConfig = SharedLLMConfig &
103
99
  export type ProviderOptionsMap = {
104
100
  [Providers.AZURE]: AzureClientOptions;
105
101
  [Providers.OPENAI]: OpenAIClientOptions;
106
- [Providers.OLLAMA]: OllamaClientOptions;
107
102
  [Providers.GOOGLE]: GoogleClientOptions;
108
103
  [Providers.VERTEXAI]: VertexAIClientOptions;
109
104
  [Providers.DEEPSEEK]: DeepSeekClientOptions;
@@ -118,7 +113,6 @@ export type ProviderOptionsMap = {
118
113
  export type ChatModelMap = {
119
114
  [Providers.XAI]: ChatXAI;
120
115
  [Providers.OPENAI]: ChatOpenAI;
121
- [Providers.OLLAMA]: ChatOllama;
122
116
  [Providers.AZURE]: AzureChatOpenAI;
123
117
  [Providers.DEEPSEEK]: ChatDeepSeek;
124
118
  [Providers.VERTEXAI]: ChatVertexAI;
package/src/utils/llm.ts CHANGED
@@ -12,7 +12,6 @@ export function isOpenAILike(provider?: string | Providers): boolean {
12
12
  Providers.OPENROUTER,
13
13
  Providers.XAI,
14
14
  Providers.DEEPSEEK,
15
- Providers.OLLAMA,
16
15
  ] as string[]
17
16
  ).includes(provider);
18
17
  }