illuma-agents 1.0.9 → 1.0.11

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 (146) hide show
  1. package/LICENSE +1 -1
  2. package/dist/cjs/agents/AgentContext.cjs +228 -27
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/common/enum.cjs +2 -0
  5. package/dist/cjs/common/enum.cjs.map +1 -1
  6. package/dist/cjs/events.cjs +3 -0
  7. package/dist/cjs/events.cjs.map +1 -1
  8. package/dist/cjs/graphs/Graph.cjs +29 -19
  9. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  10. package/dist/cjs/instrumentation.cjs +1 -1
  11. package/dist/cjs/instrumentation.cjs.map +1 -1
  12. package/dist/cjs/llm/anthropic/index.cjs +1 -1
  13. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  14. package/dist/cjs/llm/bedrock/index.cjs +122 -7
  15. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  16. package/dist/cjs/llm/google/index.cjs +1 -1
  17. package/dist/cjs/llm/google/index.cjs.map +1 -1
  18. package/dist/cjs/llm/openai/index.cjs +108 -6
  19. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  20. package/dist/cjs/llm/openai/utils/index.cjs +87 -1
  21. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  22. package/dist/cjs/llm/openrouter/index.cjs +176 -2
  23. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  24. package/dist/cjs/main.cjs +18 -0
  25. package/dist/cjs/main.cjs.map +1 -1
  26. package/dist/cjs/messages/cache.cjs +149 -54
  27. package/dist/cjs/messages/cache.cjs.map +1 -1
  28. package/dist/cjs/messages/tools.cjs +85 -0
  29. package/dist/cjs/messages/tools.cjs.map +1 -0
  30. package/dist/cjs/stream.cjs +20 -0
  31. package/dist/cjs/stream.cjs.map +1 -1
  32. package/dist/cjs/tools/CodeExecutor.cjs +4 -0
  33. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  34. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +438 -0
  35. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -0
  36. package/dist/cjs/tools/ToolNode.cjs +54 -6
  37. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  38. package/dist/cjs/tools/ToolSearchRegex.cjs +455 -0
  39. package/dist/cjs/tools/ToolSearchRegex.cjs.map +1 -0
  40. package/dist/cjs/tools/search/tool.cjs +21 -1
  41. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  42. package/dist/cjs/utils/run.cjs +5 -1
  43. package/dist/cjs/utils/run.cjs.map +1 -1
  44. package/dist/esm/agents/AgentContext.mjs +228 -27
  45. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  46. package/dist/esm/common/enum.mjs +2 -0
  47. package/dist/esm/common/enum.mjs.map +1 -1
  48. package/dist/esm/events.mjs +4 -1
  49. package/dist/esm/events.mjs.map +1 -1
  50. package/dist/esm/graphs/Graph.mjs +29 -19
  51. package/dist/esm/graphs/Graph.mjs.map +1 -1
  52. package/dist/esm/instrumentation.mjs +1 -1
  53. package/dist/esm/instrumentation.mjs.map +1 -1
  54. package/dist/esm/llm/anthropic/index.mjs +1 -1
  55. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  56. package/dist/esm/llm/bedrock/index.mjs +122 -7
  57. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  58. package/dist/esm/llm/google/index.mjs +1 -1
  59. package/dist/esm/llm/google/index.mjs.map +1 -1
  60. package/dist/esm/llm/openai/index.mjs +109 -7
  61. package/dist/esm/llm/openai/index.mjs.map +1 -1
  62. package/dist/esm/llm/openai/utils/index.mjs +88 -2
  63. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  64. package/dist/esm/llm/openrouter/index.mjs +176 -2
  65. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  66. package/dist/esm/main.mjs +3 -0
  67. package/dist/esm/main.mjs.map +1 -1
  68. package/dist/esm/messages/cache.mjs +149 -54
  69. package/dist/esm/messages/cache.mjs.map +1 -1
  70. package/dist/esm/messages/tools.mjs +82 -0
  71. package/dist/esm/messages/tools.mjs.map +1 -0
  72. package/dist/esm/stream.mjs +20 -0
  73. package/dist/esm/stream.mjs.map +1 -1
  74. package/dist/esm/tools/CodeExecutor.mjs +4 -0
  75. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  76. package/dist/esm/tools/ProgrammaticToolCalling.mjs +430 -0
  77. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -0
  78. package/dist/esm/tools/ToolNode.mjs +54 -6
  79. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  80. package/dist/esm/tools/ToolSearchRegex.mjs +448 -0
  81. package/dist/esm/tools/ToolSearchRegex.mjs.map +1 -0
  82. package/dist/esm/tools/search/tool.mjs +21 -1
  83. package/dist/esm/tools/search/tool.mjs.map +1 -1
  84. package/dist/esm/utils/run.mjs +5 -1
  85. package/dist/esm/utils/run.mjs.map +1 -1
  86. package/dist/types/agents/AgentContext.d.ts +65 -5
  87. package/dist/types/common/enum.d.ts +2 -0
  88. package/dist/types/graphs/Graph.d.ts +3 -2
  89. package/dist/types/index.d.ts +2 -0
  90. package/dist/types/llm/anthropic/index.d.ts +1 -1
  91. package/dist/types/llm/bedrock/index.d.ts +31 -4
  92. package/dist/types/llm/google/index.d.ts +1 -1
  93. package/dist/types/llm/openai/index.d.ts +4 -3
  94. package/dist/types/llm/openai/utils/index.d.ts +10 -1
  95. package/dist/types/llm/openrouter/index.d.ts +5 -2
  96. package/dist/types/messages/cache.d.ts +23 -8
  97. package/dist/types/messages/index.d.ts +1 -0
  98. package/dist/types/messages/tools.d.ts +17 -0
  99. package/dist/types/test/mockTools.d.ts +28 -0
  100. package/dist/types/tools/ProgrammaticToolCalling.d.ts +91 -0
  101. package/dist/types/tools/ToolNode.d.ts +10 -2
  102. package/dist/types/tools/ToolSearchRegex.d.ts +80 -0
  103. package/dist/types/types/graph.d.ts +7 -1
  104. package/dist/types/types/tools.d.ts +138 -0
  105. package/package.json +8 -3
  106. package/src/agents/AgentContext.ts +267 -27
  107. package/src/agents/__tests__/AgentContext.test.ts +805 -0
  108. package/src/common/enum.ts +2 -0
  109. package/src/events.ts +5 -1
  110. package/src/graphs/Graph.ts +35 -20
  111. package/src/index.ts +2 -0
  112. package/src/instrumentation.ts +1 -1
  113. package/src/llm/anthropic/index.ts +2 -2
  114. package/src/llm/bedrock/__tests__/bedrock-caching.test.ts +473 -0
  115. package/src/llm/bedrock/index.ts +150 -13
  116. package/src/llm/google/index.ts +2 -2
  117. package/src/llm/google/llm.spec.ts +3 -1
  118. package/src/llm/openai/index.ts +135 -9
  119. package/src/llm/openai/utils/index.ts +116 -1
  120. package/src/llm/openrouter/index.ts +224 -3
  121. package/src/messages/__tests__/tools.test.ts +473 -0
  122. package/src/messages/cache.ts +163 -61
  123. package/src/messages/index.ts +1 -0
  124. package/src/messages/tools.ts +99 -0
  125. package/src/scripts/code_exec_ptc.ts +334 -0
  126. package/src/scripts/programmatic_exec.ts +396 -0
  127. package/src/scripts/programmatic_exec_agent.ts +231 -0
  128. package/src/scripts/tool_search_regex.ts +162 -0
  129. package/src/specs/thinking-prune.test.ts +52 -118
  130. package/src/stream.ts +26 -0
  131. package/src/test/mockTools.ts +366 -0
  132. package/src/tools/CodeExecutor.ts +4 -0
  133. package/src/tools/ProgrammaticToolCalling.ts +558 -0
  134. package/src/tools/ToolNode.ts +60 -7
  135. package/src/tools/ToolSearchRegex.ts +535 -0
  136. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +318 -0
  137. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +853 -0
  138. package/src/tools/__tests__/ToolSearchRegex.integration.test.ts +161 -0
  139. package/src/tools/__tests__/ToolSearchRegex.test.ts +232 -0
  140. package/src/tools/search/jina-reranker.test.ts +16 -16
  141. package/src/tools/search/tool.ts +23 -1
  142. package/src/types/graph.ts +7 -1
  143. package/src/types/tools.ts +166 -0
  144. package/src/utils/llmConfig.ts +8 -2
  145. package/src/utils/run.ts +5 -1
  146. package/src/tools/search/direct-url.test.ts +0 -530
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Optimized ChatBedrockConverse wrapper that fixes contentBlockIndex conflicts
3
+ * and adds prompt caching support for Bedrock Converse API.
3
4
  *
4
5
  * Bedrock sends the same contentBlockIndex for both text and tool_use content blocks,
5
6
  * causing LangChain's merge logic to fail with "field[contentBlockIndex] already exists"
@@ -9,28 +10,128 @@
9
10
  * The contentBlockIndex field is only used internally by Bedrock's streaming protocol
10
11
  * and isn't needed by application logic - the index field on tool_call_chunks serves
11
12
  * the purpose of tracking tool call ordering.
13
+ *
14
+ * PROMPT CACHING:
15
+ * When promptCache: true is set, this wrapper adds cachePoint markers to the tools array
16
+ * to enable Bedrock prompt caching for tool definitions. This allows tool schemas to be
17
+ * cached and reused across requests, reducing latency and costs.
18
+ *
19
+ * CACHE TOKEN EXTRACTION:
20
+ * LangChain AWS doesn't extract cacheReadInputTokens/cacheWriteInputTokens from Bedrock's
21
+ * response. This wrapper adds input_token_details to usage_metadata with cache information.
12
22
  */
13
23
 
14
24
  import { ChatBedrockConverse } from '@langchain/aws';
15
25
  import type { ChatBedrockConverseInput } from '@langchain/aws';
16
26
  import { AIMessageChunk } from '@langchain/core/messages';
17
- import type { BaseMessage } from '@langchain/core/messages';
27
+ import type { BaseMessage, UsageMetadata } from '@langchain/core/messages';
18
28
  import { ChatGenerationChunk } from '@langchain/core/outputs';
19
29
  import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
20
30
 
31
+ /** Extended input type with promptCache option */
32
+ export interface CustomChatBedrockConverseInput extends ChatBedrockConverseInput {
33
+ promptCache?: boolean;
34
+ }
35
+
21
36
  export class CustomChatBedrockConverse extends ChatBedrockConverse {
22
- constructor(fields?: ChatBedrockConverseInput) {
37
+ promptCache: boolean;
38
+
39
+ constructor(fields?: CustomChatBedrockConverseInput) {
23
40
  super(fields);
41
+ this.promptCache = fields?.promptCache ?? false;
24
42
  }
25
43
 
26
44
  static lc_name(): string {
27
- return 'IllumaBedrockConverse';
45
+ return 'LibreChatBedrockConverse';
28
46
  }
29
47
 
30
48
  /**
31
- * Override _streamResponseChunks to strip contentBlockIndex from response_metadata
32
- * This prevents LangChain's merge conflicts when the same index is used for
33
- * different content types (text vs tool calls)
49
+ * Override invocationParams to add cachePoint to tools when promptCache is enabled.
50
+ * This enables Bedrock prompt caching for tool definitions.
51
+ *
52
+ * STRATEGY: Separate cachePoints for core tools and MCP tools
53
+ * - Core tools (web_search, execute_code, etc.) are stable → cache first
54
+ * - MCP tools (have '_mcp_' in name) are dynamic → cache separately after
55
+ * - This allows core tools to stay cached when MCP selection changes
56
+ *
57
+ * NOTE: Only Claude models support cachePoint - Nova and other models will reject it.
58
+ */
59
+ invocationParams(
60
+ options?: this['ParsedCallOptions']
61
+ ): ReturnType<ChatBedrockConverse['invocationParams']> {
62
+ const params = super.invocationParams(options);
63
+
64
+ // Add cachePoint to tools array if promptCache is enabled and tools exist
65
+ // Only Claude models support cachePoint - check model name
66
+ const modelId = this.model?.toLowerCase() ?? '';
67
+ const isClaudeModel = modelId.includes('claude') || modelId.includes('anthropic');
68
+
69
+ if (
70
+ this.promptCache &&
71
+ isClaudeModel &&
72
+ params.toolConfig?.tools &&
73
+ Array.isArray(params.toolConfig.tools) &&
74
+ params.toolConfig.tools.length > 0
75
+ ) {
76
+ // Separate core tools from MCP tools
77
+ // MCP tools have '_mcp_' in their name (e.g., 'search_emails_mcp_Google-Workspace')
78
+ const coreTools: typeof params.toolConfig.tools = [];
79
+ const mcpTools: typeof params.toolConfig.tools = [];
80
+ const coreToolNames: string[] = [];
81
+ const mcpToolNames: string[] = [];
82
+
83
+ for (const tool of params.toolConfig.tools) {
84
+ // Check if tool has a name property with '_mcp_' pattern
85
+ const toolName = (tool as { toolSpec?: { name?: string } })?.toolSpec?.name ?? '';
86
+ if (toolName.includes('_mcp_')) {
87
+ mcpTools.push(tool);
88
+ mcpToolNames.push(toolName);
89
+ } else {
90
+ coreTools.push(tool);
91
+ coreToolNames.push(toolName);
92
+ }
93
+ }
94
+
95
+ // Always log cache structure (INFO level for tracking)
96
+ console.log(`[Cache] 🔧 Tools | Core: [${coreToolNames.join(', ')}] (${coreTools.length}) | MCP: [${mcpToolNames.join(', ')}] (${mcpTools.length})`);
97
+
98
+ // Build tools array with strategic cachePoints:
99
+ // [CoreTool1, CoreTool2, cachePoint] + [MCPTool1, MCPTool2, cachePoint]
100
+ const toolsWithCache: typeof params.toolConfig.tools = [];
101
+ let cachePointCount = 0;
102
+
103
+ // Add core tools with cachePoint (if any)
104
+ if (coreTools.length > 0) {
105
+ toolsWithCache.push(...coreTools);
106
+ toolsWithCache.push({ cachePoint: { type: 'default' } });
107
+ cachePointCount++;
108
+ }
109
+
110
+ // Add MCP tools with their own cachePoint (if any)
111
+ if (mcpTools.length > 0) {
112
+ toolsWithCache.push(...mcpTools);
113
+ toolsWithCache.push({ cachePoint: { type: 'default' } });
114
+ cachePointCount++;
115
+ }
116
+
117
+ // If no tools at all (shouldn't happen but safety check)
118
+ if (toolsWithCache.length === 0) {
119
+ toolsWithCache.push({ cachePoint: { type: 'default' } });
120
+ cachePointCount++;
121
+ }
122
+
123
+ console.log(`[Cache] 📍 Tool cachePoints: ${cachePointCount} | Order: [${coreToolNames.length > 0 ? 'CoreTools→CP' : ''}${mcpToolNames.length > 0 ? '→MCPTools→CP' : ''}]`);
124
+
125
+ params.toolConfig.tools = toolsWithCache;
126
+ }
127
+
128
+ return params;
129
+ }
130
+
131
+ /**
132
+ * Override _streamResponseChunks to:
133
+ * 1. Strip contentBlockIndex from response_metadata to prevent merge conflicts
134
+ * 2. Extract cacheReadInputTokens/cacheWriteInputTokens and add to usage_metadata
34
135
  */
35
136
  async *_streamResponseChunks(
36
137
  messages: BaseMessage[],
@@ -50,21 +151,57 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
50
151
  (chunk.message as Partial<AIMessageChunk>).response_metadata &&
51
152
  typeof chunk.message.response_metadata === 'object'
52
153
  ) {
53
- // Check if contentBlockIndex exists anywhere in response_metadata (top level or nested)
54
- const hasContentBlockIndex = this.hasContentBlockIndex(
55
- chunk.message.response_metadata
56
- );
154
+ const responseMetadata = chunk.message.response_metadata as Record<string, unknown>;
155
+ let needsModification = false;
156
+ let cleanedMetadata = responseMetadata;
57
157
 
158
+ // Check if contentBlockIndex exists anywhere in response_metadata
159
+ const hasContentBlockIndex = this.hasContentBlockIndex(responseMetadata);
58
160
  if (hasContentBlockIndex) {
59
- const cleanedMetadata = this.removeContentBlockIndex(
60
- chunk.message.response_metadata
61
- ) as Record<string, unknown>;
161
+ cleanedMetadata = this.removeContentBlockIndex(responseMetadata) as Record<string, unknown>;
162
+ needsModification = true;
163
+ }
164
+
165
+ // Extract cache tokens from metadata.usage (Bedrock streaming format)
166
+ // The metadata chunk contains usage with cacheReadInputTokens/cacheWriteInputTokens
167
+ const metadata = responseMetadata.metadata as Record<string, unknown> | undefined;
168
+ const usage = (metadata?.usage ?? responseMetadata.usage) as Record<string, unknown> | undefined;
169
+
170
+ let enhancedUsageMetadata: UsageMetadata | undefined = chunk.message.usage_metadata;
171
+
172
+ if (usage) {
173
+ const cacheRead = (usage.cacheReadInputTokens as number) ?? 0;
174
+ const cacheWrite = (usage.cacheWriteInputTokens as number) ?? 0;
175
+ const inputTokens = (usage.inputTokens as number) ?? 0;
176
+ const outputTokens = (usage.outputTokens as number) ?? 0;
177
+
178
+ if (cacheRead > 0 || cacheWrite > 0) {
179
+ // Always log cache results for tracking
180
+ const cacheStatus = cacheRead > 0 && cacheWrite === 0 ? '✅ HIT' :
181
+ cacheWrite > 0 && cacheRead === 0 ? '📝 WRITE' :
182
+ cacheRead > 0 && cacheWrite > 0 ? '🔄 PARTIAL' : '❌ MISS';
183
+ console.log(`[Cache] ${cacheStatus} | read=${cacheRead} | write=${cacheWrite} | input=${inputTokens} | output=${outputTokens}`);
184
+
185
+ needsModification = true;
186
+ enhancedUsageMetadata = {
187
+ input_tokens: chunk.message.usage_metadata?.input_tokens ?? inputTokens,
188
+ output_tokens: chunk.message.usage_metadata?.output_tokens ?? outputTokens,
189
+ total_tokens: chunk.message.usage_metadata?.total_tokens ?? (usage.totalTokens as number) ?? 0,
190
+ input_token_details: {
191
+ cache_read: cacheRead,
192
+ cache_creation: cacheWrite,
193
+ },
194
+ };
195
+ }
196
+ }
62
197
 
198
+ if (needsModification) {
63
199
  yield new ChatGenerationChunk({
64
200
  text: chunk.text,
65
201
  message: new AIMessageChunk({
66
202
  ...chunk.message,
67
203
  response_metadata: cleanedMetadata,
204
+ usage_metadata: enhancedUsageMetadata,
68
205
  }),
69
206
  generationInfo: chunk.generationInfo,
70
207
  });
@@ -122,8 +122,8 @@ export class CustomChatGoogleGenerativeAI extends ChatGoogleGenerativeAI {
122
122
  this.streamUsage = fields.streamUsage ?? this.streamUsage;
123
123
  }
124
124
 
125
- static lc_name(): 'IllumaGoogleGenerativeAI' {
126
- return 'IllumaGoogleGenerativeAI';
125
+ static lc_name(): 'LibreChatGoogleGenerativeAI' {
126
+ return 'LibreChatGoogleGenerativeAI';
127
127
  }
128
128
 
129
129
  /**
@@ -1,6 +1,8 @@
1
1
  import { config } from 'dotenv';
2
2
  config();
3
- import { test } from '@jest/globals';
3
+ import { test, jest } from '@jest/globals';
4
+
5
+ jest.setTimeout(90000);
4
6
  import * as fs from 'node:fs/promises';
5
7
  import * as path from 'node:path';
6
8
  import {
@@ -211,7 +211,7 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
211
211
  return this.client;
212
212
  }
213
213
  static lc_name(): string {
214
- return 'IllumaOpenAI';
214
+ return 'LibreChatOpenAI';
215
215
  }
216
216
  protected _getClientOptions(
217
217
  options?: OpenAICoreRequestOptions
@@ -466,8 +466,8 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
466
466
  public get exposedClient(): CustomOpenAIClient {
467
467
  return this.client;
468
468
  }
469
- static lc_name(): 'IllumaAzureOpenAI' {
470
- return 'IllumaAzureOpenAI';
469
+ static lc_name(): 'LibreChatAzureOpenAI' {
470
+ return 'LibreChatAzureOpenAI';
471
471
  }
472
472
  /**
473
473
  * Returns backwards compatible reasoning parameters from constructor params and call options
@@ -539,8 +539,8 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
539
539
  ...params.defaultHeaders,
540
540
  'User-Agent':
541
541
  defaultHeaders['User-Agent'] != null
542
- ? `${defaultHeaders['User-Agent']}: illuma-azure-openai-v2`
543
- : 'illuma-azure-openai-v2',
542
+ ? `${defaultHeaders['User-Agent']}: librechat-azure-openai-v2`
543
+ : 'librechat-azure-openai-v2',
544
544
  };
545
545
 
546
546
  this.client = new CustomAzureOpenAIClient({
@@ -613,8 +613,8 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
613
613
  public get exposedClient(): CustomOpenAIClient {
614
614
  return this.client;
615
615
  }
616
- static lc_name(): 'IllumaDeepSeek' {
617
- return 'IllumaDeepSeek';
616
+ static lc_name(): 'LibreChatDeepSeek' {
617
+ return 'LibreChatDeepSeek';
618
618
  }
619
619
  protected _getClientOptions(
620
620
  options?: OpenAICoreRequestOptions
@@ -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 */
@@ -688,8 +814,8 @@ export class ChatXAI extends OriginalChatXAI {
688
814
  }
689
815
  }
690
816
 
691
- static lc_name(): 'IllumaXAI' {
692
- return 'IllumaXAI';
817
+ static lc_name(): 'LibreChatXAI' {
818
+ return 'LibreChatXAI';
693
819
  }
694
820
 
695
821
  public get exposedClient(): CustomOpenAIClient {
@@ -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;