@librechat/agents 3.1.80-dev.2 → 3.1.80

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 (42) hide show
  1. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +27 -7
  2. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  3. package/dist/cjs/llm/vertexai/index.cjs +52 -0
  4. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  5. package/dist/cjs/main.cjs +1 -2
  6. package/dist/cjs/main.cjs.map +1 -1
  7. package/dist/cjs/tools/BashExecutor.cjs +20 -78
  8. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  9. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +5 -1
  10. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  11. package/dist/cjs/tools/CodeExecutor.cjs +26 -106
  12. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  13. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +12 -31
  14. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  15. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +27 -8
  16. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  17. package/dist/esm/llm/vertexai/index.mjs +53 -2
  18. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  19. package/dist/esm/main.mjs +1 -1
  20. package/dist/esm/tools/BashExecutor.mjs +20 -78
  21. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  22. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +6 -2
  23. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  24. package/dist/esm/tools/CodeExecutor.mjs +26 -105
  25. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  26. package/dist/esm/tools/ProgrammaticToolCalling.mjs +12 -31
  27. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  28. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +15 -3
  29. package/dist/types/llm/vertexai/index.d.ts +29 -0
  30. package/dist/types/tools/CodeExecutor.d.ts +1 -7
  31. package/dist/types/tools/ProgrammaticToolCalling.d.ts +5 -0
  32. package/package.json +10 -5
  33. package/src/llm/anthropic/utils/message_inputs.ts +58 -7
  34. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +178 -0
  35. package/src/llm/vertexai/index.ts +69 -2
  36. package/src/llm/vertexai/llm.spec.ts +18 -0
  37. package/src/llm/vertexai/repairUsageMetadata.test.ts +54 -0
  38. package/src/tools/BashExecutor.ts +24 -104
  39. package/src/tools/BashProgrammaticToolCalling.ts +7 -2
  40. package/src/tools/CodeExecutor.ts +30 -133
  41. package/src/tools/ProgrammaticToolCalling.ts +14 -49
  42. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +32 -131
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.1.80-dev.2",
3
+ "version": "3.1.80",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -184,6 +184,11 @@
184
184
  "@browserbasehq/stagehand": {
185
185
  "openai": "$openai"
186
186
  },
187
+ "@langchain/google-common": "$@langchain/google-common",
188
+ "@langchain/google-gauth": "$@langchain/google-gauth",
189
+ "@langchain/google-genai": "$@langchain/google-genai",
190
+ "@langchain/google-vertexai": "$@langchain/google-vertexai",
191
+ "uuid": "$uuid",
187
192
  "fast-xml-parser": "5.7.2",
188
193
  "ajv": "6.14.0",
189
194
  "minimatch": "3.1.4"
@@ -195,10 +200,10 @@
195
200
  "@langchain/aws": "^1.3.5",
196
201
  "@langchain/core": "1.1.44",
197
202
  "@langchain/deepseek": "^1.0.25",
198
- "@langchain/google-common": "2.1.30",
199
- "@langchain/google-gauth": "2.1.30",
200
- "@langchain/google-genai": "2.1.30",
201
- "@langchain/google-vertexai": "2.1.30",
203
+ "@langchain/google-common": "2.1.28",
204
+ "@langchain/google-gauth": "2.1.28",
205
+ "@langchain/google-genai": "2.1.28",
206
+ "@langchain/google-vertexai": "2.1.28",
202
207
  "@langchain/langgraph": "^1.2.9",
203
208
  "@langchain/mistralai": "^1.0.8",
204
209
  "@langchain/openai": "1.4.5",
@@ -3,6 +3,7 @@
3
3
  /**
4
4
  * This util file contains functions for converting LangChain messages to Anthropic messages.
5
5
  */
6
+ import { createHash } from 'node:crypto';
6
7
  import {
7
8
  type BaseMessage,
8
9
  type SystemMessage,
@@ -92,6 +93,49 @@ function _formatImage(imageUrl: string) {
92
93
  );
93
94
  }
94
95
 
96
+ const ANTHROPIC_TOOL_USE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
97
+ const ANTHROPIC_TOOL_USE_ID_MAX_LENGTH = 64;
98
+ const ANTHROPIC_TOOL_USE_ID_HASH_LENGTH = 10;
99
+
100
+ /**
101
+ * Normalize a tool-call ID to satisfy Anthropic's `^[a-zA-Z0-9_-]+$` and 64-char
102
+ * constraints. Pure and deterministic — same input always yields the same output,
103
+ * so paired `tool_use.id` and `tool_result.tool_use_id` stay matched without
104
+ * needing a session map. IDs that already comply pass through unchanged.
105
+ *
106
+ * For non-compliant inputs we sanitize then append a short SHA-256 prefix of
107
+ * the original ID to preserve uniqueness when truncation would otherwise
108
+ * collapse distinct IDs to the same value (e.g. two long Responses-style IDs
109
+ * sharing a 64-char prefix). The hash is computed against the raw input so
110
+ * inputs that differ only after the truncation cutoff still produce distinct
111
+ * outputs.
112
+ */
113
+ export function normalizeAnthropicToolCallId(id: string): string;
114
+ export function normalizeAnthropicToolCallId(
115
+ id: string | undefined
116
+ ): string | undefined;
117
+ export function normalizeAnthropicToolCallId(
118
+ id: string | undefined
119
+ ): string | undefined {
120
+ if (id == null) {
121
+ return id;
122
+ }
123
+ if (
124
+ id.length <= ANTHROPIC_TOOL_USE_ID_MAX_LENGTH &&
125
+ ANTHROPIC_TOOL_USE_ID_PATTERN.test(id)
126
+ ) {
127
+ return id;
128
+ }
129
+ const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, '_');
130
+ const hash = createHash('sha256')
131
+ .update(id)
132
+ .digest('hex')
133
+ .slice(0, ANTHROPIC_TOOL_USE_ID_HASH_LENGTH);
134
+ const prefixMaxLength =
135
+ ANTHROPIC_TOOL_USE_ID_MAX_LENGTH - ANTHROPIC_TOOL_USE_ID_HASH_LENGTH - 1;
136
+ return `${sanitized.slice(0, prefixMaxLength)}_${hash}`;
137
+ }
138
+
95
139
  function _ensureMessageContents(
96
140
  messages: BaseMessage[]
97
141
  ): (SystemMessage | HumanMessage | AIMessage)[] {
@@ -111,7 +155,9 @@ function _ensureMessageContents(
111
155
  (previousMessage.content as MessageContentComplex[]).push({
112
156
  type: 'tool_result',
113
157
  content: message.content,
114
- tool_use_id: (message as ToolMessage).tool_call_id,
158
+ tool_use_id: normalizeAnthropicToolCallId(
159
+ (message as ToolMessage).tool_call_id
160
+ ),
115
161
  });
116
162
  } else {
117
163
  // If not, we create a new human message with the tool result.
@@ -121,7 +167,9 @@ function _ensureMessageContents(
121
167
  {
122
168
  type: 'tool_result',
123
169
  content: message.content,
124
- tool_use_id: (message as ToolMessage).tool_call_id,
170
+ tool_use_id: normalizeAnthropicToolCallId(
171
+ (message as ToolMessage).tool_call_id
172
+ ),
125
173
  },
126
174
  ],
127
175
  })
@@ -139,7 +187,9 @@ function _ensureMessageContents(
139
187
  ...(toolMessageContent != null
140
188
  ? { content: _formatContent(message) }
141
189
  : {}),
142
- tool_use_id: (message as ToolMessage).tool_call_id,
190
+ tool_use_id: normalizeAnthropicToolCallId(
191
+ (message as ToolMessage).tool_call_id
192
+ ),
143
193
  },
144
194
  ],
145
195
  })
@@ -158,11 +208,12 @@ export function _convertLangChainToolCallToAnthropic(
158
208
  if (toolCall.id === undefined) {
159
209
  throw new Error('Anthropic requires all tool calls to have an "id".');
160
210
  }
211
+ const isServerTool = toolCall.id.startsWith(
212
+ Constants.ANTHROPIC_SERVER_TOOL_PREFIX
213
+ );
161
214
  return {
162
- type: toolCall.id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)
163
- ? 'server_tool_use'
164
- : 'tool_use',
165
- id: toolCall.id,
215
+ type: isServerTool ? 'server_tool_use' : 'tool_use',
216
+ id: isServerTool ? toolCall.id : normalizeAnthropicToolCallId(toolCall.id),
166
217
  name: toolCall.name,
167
218
  input: toolCall.args,
168
219
  };
@@ -0,0 +1,178 @@
1
+ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
2
+ import {
3
+ _convertLangChainToolCallToAnthropic,
4
+ _convertMessagesToAnthropicPayload,
5
+ normalizeAnthropicToolCallId,
6
+ } from './message_inputs';
7
+
8
+ describe('normalizeAnthropicToolCallId', () => {
9
+ it('returns valid IDs unchanged', () => {
10
+ expect(normalizeAnthropicToolCallId('toolu_01ABcdEFgh')).toBe(
11
+ 'toolu_01ABcdEFgh'
12
+ );
13
+ expect(normalizeAnthropicToolCallId('call_abc123XYZ')).toBe(
14
+ 'call_abc123XYZ'
15
+ );
16
+ expect(normalizeAnthropicToolCallId('a-b_c-d')).toBe('a-b_c-d');
17
+ });
18
+
19
+ it('sanitizes invalid characters and appends a hash suffix', () => {
20
+ const out = normalizeAnthropicToolCallId(
21
+ 'fc_67abc1234def567|call_abc123def456ghi789jkl0mnopqrs'
22
+ );
23
+ expect(/^[a-zA-Z0-9_-]+$/.test(out)).toBe(true);
24
+ expect(out.length).toBeLessThanOrEqual(64);
25
+ expect(
26
+ out.startsWith('fc_67abc1234def567_call_abc123def456ghi789jkl0mn')
27
+ ).toBe(true);
28
+ // Suffix is `_<10-hex-char hash>`
29
+ expect(out).toMatch(/_[0-9a-f]{10}$/);
30
+ });
31
+
32
+ it('produces compliant output for IDs of any length', () => {
33
+ const long = 'fc_' + 'a'.repeat(80);
34
+ const out = normalizeAnthropicToolCallId(long);
35
+ expect(out).toHaveLength(64);
36
+ expect(/^[a-zA-Z0-9_-]+$/.test(out)).toBe(true);
37
+ });
38
+
39
+ it('produces uniquely distinguishable outputs for IDs that share a 64-char prefix', () => {
40
+ const sharedPrefix = 'fc_' + 'a'.repeat(80);
41
+ const idA = sharedPrefix + '|call_unique_A';
42
+ const idB = sharedPrefix + '|call_unique_B';
43
+
44
+ const outA = normalizeAnthropicToolCallId(idA);
45
+ const outB = normalizeAnthropicToolCallId(idB);
46
+
47
+ expect(outA).not.toBe(outB);
48
+ expect(outA).toHaveLength(64);
49
+ expect(outB).toHaveLength(64);
50
+ expect(/^[a-zA-Z0-9_-]+$/.test(outA)).toBe(true);
51
+ expect(/^[a-zA-Z0-9_-]+$/.test(outB)).toBe(true);
52
+ });
53
+
54
+ it('disambiguates short IDs that sanitize to the same value', () => {
55
+ expect(normalizeAnthropicToolCallId('a|b')).not.toBe(
56
+ normalizeAnthropicToolCallId('a.b')
57
+ );
58
+ });
59
+
60
+ it('handles combined length and character violations', () => {
61
+ const id = 'fc_' + 'x|'.repeat(100);
62
+ const out = normalizeAnthropicToolCallId(id);
63
+ expect(out).toHaveLength(64);
64
+ expect(/^[a-zA-Z0-9_-]+$/.test(out)).toBe(true);
65
+ });
66
+
67
+ it('is deterministic — same input always yields same output', () => {
68
+ const id = 'fc_a|b|c';
69
+ expect(normalizeAnthropicToolCallId(id)).toBe(
70
+ normalizeAnthropicToolCallId(id)
71
+ );
72
+ });
73
+
74
+ it('passes through undefined for the optional overload', () => {
75
+ expect(normalizeAnthropicToolCallId(undefined)).toBeUndefined();
76
+ });
77
+
78
+ it('handles empty string by producing a deterministic compliant output', () => {
79
+ const out = normalizeAnthropicToolCallId('');
80
+ expect(/^[a-zA-Z0-9_-]+$/.test(out)).toBe(true);
81
+ expect(out.length).toBeLessThanOrEqual(64);
82
+ expect(out).toBe(normalizeAnthropicToolCallId(''));
83
+ });
84
+ });
85
+
86
+ describe('_convertMessagesToAnthropicPayload — cross-provider ID normalization', () => {
87
+ it('normalizes Responses-style IDs on tool_use AND matching tool_result', () => {
88
+ const responsesId = 'fc_67abc1234def567|call_abc123def456ghi789jkl0mnopqrs';
89
+
90
+ const payload = _convertMessagesToAnthropicPayload([
91
+ new HumanMessage('weather?'),
92
+ new AIMessage({
93
+ content: '',
94
+ tool_calls: [
95
+ {
96
+ id: responsesId,
97
+ name: 'get_weather',
98
+ args: { location: 'Tokyo' },
99
+ type: 'tool_call',
100
+ },
101
+ ],
102
+ }),
103
+ new ToolMessage({
104
+ tool_call_id: responsesId,
105
+ content: '{"temp": 21}',
106
+ }),
107
+ ]);
108
+
109
+ const assistantMsg = payload.messages.find((m) => m.role === 'assistant')!;
110
+ const userToolResultMsg = payload.messages.find(
111
+ (m) =>
112
+ m.role === 'user' &&
113
+ Array.isArray(m.content) &&
114
+ (m.content as Array<{ type: string }>)[0]?.type === 'tool_result'
115
+ )!;
116
+
117
+ const toolUseBlock = (
118
+ assistantMsg.content as Array<{ type: string; id?: string }>
119
+ ).find((b) => b.type === 'tool_use')!;
120
+ const toolResultBlock = (
121
+ userToolResultMsg.content as Array<{
122
+ type: string;
123
+ tool_use_id?: string;
124
+ }>
125
+ ).find((b) => b.type === 'tool_result')!;
126
+
127
+ const expected = normalizeAnthropicToolCallId(responsesId);
128
+ expect(toolUseBlock.id).toBe(expected);
129
+ expect(toolResultBlock.tool_use_id).toBe(expected);
130
+ expect(toolUseBlock.id).toBe(toolResultBlock.tool_use_id);
131
+ expect(/^[a-zA-Z0-9_-]+$/.test(toolUseBlock.id!)).toBe(true);
132
+ expect(toolUseBlock.id!.length).toBeLessThanOrEqual(64);
133
+ });
134
+
135
+ it('passes through Anthropic-native IDs unchanged', () => {
136
+ const nativeId = 'toolu_01ABcdEFgh23ijKL';
137
+
138
+ const payload = _convertMessagesToAnthropicPayload([
139
+ new HumanMessage('hi'),
140
+ new AIMessage({
141
+ content: '',
142
+ tool_calls: [
143
+ {
144
+ id: nativeId,
145
+ name: 'noop',
146
+ args: {},
147
+ type: 'tool_call',
148
+ },
149
+ ],
150
+ }),
151
+ new ToolMessage({
152
+ tool_call_id: nativeId,
153
+ content: 'ok',
154
+ }),
155
+ ]);
156
+
157
+ const assistantMsg = payload.messages.find((m) => m.role === 'assistant')!;
158
+ const toolUseBlock = (
159
+ assistantMsg.content as Array<{ type: string; id?: string }>
160
+ ).find((b) => b.type === 'tool_use')!;
161
+
162
+ expect(toolUseBlock.id).toBe(nativeId);
163
+ });
164
+
165
+ it('does not normalize server tool IDs (srvtoolu_ prefix)', () => {
166
+ const serverId = 'srvtoolu_01abcXYZ';
167
+
168
+ const block = _convertLangChainToolCallToAnthropic({
169
+ id: serverId,
170
+ name: 'web_search',
171
+ args: { query: 'x' },
172
+ type: 'tool_call',
173
+ });
174
+
175
+ expect(block.type).toBe('server_tool_use');
176
+ expect(block.id).toBe(serverId);
177
+ });
178
+ });
@@ -6,10 +6,48 @@ import type {
6
6
  GoogleAIModelRequestParams,
7
7
  GoogleAbstractedClient,
8
8
  } from '@langchain/google-common';
9
- import type { BaseMessage } from '@langchain/core/messages';
10
- import { isAIMessage } from '@langchain/core/messages';
9
+ import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
10
+ import type { BaseMessage, UsageMetadata } from '@langchain/core/messages';
11
+ import { AIMessageChunk, isAIMessage } from '@langchain/core/messages';
12
+ import type { ChatGenerationChunk } from '@langchain/core/outputs';
11
13
  import type { GoogleThinkingConfig, VertexAIClientOptions } from '@/types';
12
14
 
15
+ /**
16
+ * `@langchain/google-common`'s `_streamResponseChunks` emits usage on TWO
17
+ * different paths within the same stream:
18
+ *
19
+ * - Streaming chunks set `chunk.generationInfo.usage_metadata` via
20
+ * `responseToUsageMetadata`, which correctly sums
21
+ * `candidatesTokenCount + thoughtsTokenCount` and includes
22
+ * `output_token_details.reasoning`.
23
+ * - The trailing fallback chunk (emitted after the API stream exhausts)
24
+ * attaches its own `chunk.message.usage_metadata` built inline as
25
+ * `output_tokens = candidatesTokenCount` only — dropping
26
+ * `thoughtsTokenCount` and `output_token_details` entirely.
27
+ *
28
+ * After `AIMessageChunk.concat`, only `message.usage_metadata` survives —
29
+ * which is the buggy fallback value. This breaks the documented
30
+ * `total_tokens === input_tokens + output_tokens` invariant and silently
31
+ * undercharges thinking models for reasoning tokens.
32
+ *
33
+ * The repair: track the last `generationInfo.usage_metadata` we see, and
34
+ * when the fallback chunk arrives with its buggy `message.usage_metadata`,
35
+ * replace it with the tracked good value. `CustomChatGoogleGenerativeAI`
36
+ * solves the same problem for the Google API path differently — by
37
+ * overriding `_convertToUsageMetadata`.
38
+ */
39
+ export function repairStreamUsageMetadata(
40
+ current: UsageMetadata | undefined,
41
+ generationInfoUsage: UsageMetadata | undefined
42
+ ): UsageMetadata | undefined {
43
+ if (!current) return current;
44
+ if (!generationInfoUsage) return current;
45
+ if (generationInfoUsage.total_tokens !== current.total_tokens) return current;
46
+ if (generationInfoUsage.output_tokens <= current.output_tokens)
47
+ return current;
48
+ return generationInfoUsage;
49
+ }
50
+
13
51
  type AdditionalKwargs =
14
52
  | undefined
15
53
  | (BaseMessage['additional_kwargs'] & {
@@ -446,6 +484,35 @@ export class ChatVertexAI extends ChatGoogle {
446
484
  }
447
485
  return params;
448
486
  }
487
+ async *_streamResponseChunks(
488
+ messages: BaseMessage[],
489
+ options: this['ParsedCallOptions'],
490
+ runManager?: CallbackManagerForLLMRun
491
+ ): AsyncGenerator<ChatGenerationChunk> {
492
+ let lastGoodUsage: UsageMetadata | undefined;
493
+ for await (const chunk of super._streamResponseChunks(
494
+ messages,
495
+ options,
496
+ runManager
497
+ )) {
498
+ const genUsage = (
499
+ chunk.generationInfo as { usage_metadata?: UsageMetadata } | undefined
500
+ )?.usage_metadata;
501
+ if (genUsage) {
502
+ lastGoodUsage = genUsage;
503
+ }
504
+ if (chunk.message instanceof AIMessageChunk) {
505
+ const repaired = repairStreamUsageMetadata(
506
+ chunk.message.usage_metadata,
507
+ lastGoodUsage
508
+ );
509
+ if (repaired !== chunk.message.usage_metadata) {
510
+ chunk.message.usage_metadata = repaired;
511
+ }
512
+ }
513
+ yield chunk;
514
+ }
515
+ }
449
516
  buildConnection(
450
517
  fields: VertexAIClientOptions | undefined,
451
518
  client: GoogleAbstractedClient
@@ -76,6 +76,24 @@ describe.each(gemini3Models)(
76
76
  (reasoningTokens as Record<string, number>)?.reasoning
77
77
  ).toBeGreaterThan(0);
78
78
  });
79
+
80
+ test('stream: usage_metadata includes reasoning in output_tokens (issue LibreChat#13006)', async () => {
81
+ let finalChunk: AIMessageChunk | undefined;
82
+ for await (const chunk of await model.stream(
83
+ 'What is 2+2? Think step by step.'
84
+ )) {
85
+ finalChunk = finalChunk ? finalChunk.concat(chunk) : chunk;
86
+ }
87
+ const usage = finalChunk?.usage_metadata;
88
+ expect(usage).toBeDefined();
89
+ const reasoning = (
90
+ usage as { output_token_details?: { reasoning?: number } }
91
+ )?.output_token_details?.reasoning;
92
+ expect(reasoning).toBeGreaterThan(0);
93
+ expect(usage!.total_tokens).toBe(
94
+ usage!.input_tokens + usage!.output_tokens
95
+ );
96
+ });
79
97
  }
80
98
  );
81
99
 
@@ -0,0 +1,54 @@
1
+ import { expect, test, describe } from '@jest/globals';
2
+ import type { UsageMetadata } from '@langchain/core/messages';
3
+ import { repairStreamUsageMetadata } from './index';
4
+
5
+ const goodUsage: UsageMetadata = {
6
+ input_tokens: 80657,
7
+ output_tokens: 2608,
8
+ total_tokens: 83265,
9
+ output_token_details: { reasoning: 1842 },
10
+ };
11
+
12
+ const buggyFallbackUsage: UsageMetadata = {
13
+ input_tokens: 80657,
14
+ output_tokens: 766,
15
+ total_tokens: 83265,
16
+ };
17
+
18
+ describe('repairStreamUsageMetadata', () => {
19
+ test('replaces buggy fallback usage with tracked good usage from generationInfo', () => {
20
+ const result = repairStreamUsageMetadata(buggyFallbackUsage, goodUsage);
21
+ expect(result).toBe(goodUsage);
22
+ });
23
+
24
+ test('returns current unchanged when no generationInfo usage was tracked', () => {
25
+ const result = repairStreamUsageMetadata(buggyFallbackUsage, undefined);
26
+ expect(result).toBe(buggyFallbackUsage);
27
+ });
28
+
29
+ test('returns undefined unchanged', () => {
30
+ const result = repairStreamUsageMetadata(undefined, goodUsage);
31
+ expect(result).toBeUndefined();
32
+ });
33
+
34
+ test('does not replace when total_tokens differ (different request)', () => {
35
+ const stale: UsageMetadata = { ...goodUsage, total_tokens: 100 };
36
+ const result = repairStreamUsageMetadata(buggyFallbackUsage, stale);
37
+ expect(result).toBe(buggyFallbackUsage);
38
+ });
39
+
40
+ test('does not replace when generationInfo output_tokens is not larger (already correct)', () => {
41
+ const equivalent: UsageMetadata = {
42
+ ...buggyFallbackUsage,
43
+ output_tokens: buggyFallbackUsage.output_tokens,
44
+ };
45
+ const result = repairStreamUsageMetadata(buggyFallbackUsage, equivalent);
46
+ expect(result).toBe(buggyFallbackUsage);
47
+ });
48
+
49
+ test('does not replace when generationInfo output_tokens is smaller', () => {
50
+ const smaller: UsageMetadata = { ...goodUsage, output_tokens: 100 };
51
+ const result = repairStreamUsageMetadata(buggyFallbackUsage, smaller);
52
+ expect(result).toBe(buggyFallbackUsage);
53
+ });
54
+ });
@@ -3,24 +3,11 @@ import fetch, { RequestInit } from 'node-fetch';
3
3
  import { HttpsProxyAgent } from 'https-proxy-agent';
4
4
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
5
5
  import type * as t from '@/types';
6
- import { getCodeBaseURL, renderFileSection } from './CodeExecutor';
6
+ import { emptyOutputMessage, getCodeBaseURL } from './CodeExecutor';
7
7
  import { Constants } from '@/common';
8
8
 
9
9
  config();
10
10
 
11
- const otherMessage = 'File is already downloaded by the user';
12
- const inheritedFileMessage =
13
- 'Available as an input — already known to the user';
14
- const accessMessage =
15
- 'Note: Files from previous executions are automatically available and can be modified.';
16
- const emptyOutputMessage =
17
- 'stdout: Empty. Ensure you\'re writing output explicitly.\n';
18
- const inheritedFilesHeader =
19
- 'Available files (inputs, not generated by this execution):';
20
- const generatedFilesHeader = 'Generated files:';
21
- const inheritedNote =
22
- 'Note: Files in "Available files" are inputs the user (or a skill) already provided to the sandbox. They were not produced by this execution and you should not present them as new outputs in your response.';
23
-
24
11
  const baseEndpoint = getCodeBaseURL();
25
12
  const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
26
13
 
@@ -133,54 +120,20 @@ function createBashExecutionTool(
133
120
  ...params,
134
121
  };
135
122
 
123
+ /* See `CodeExecutor.ts` for the rationale — `/files/<session_id>`
124
+ * HTTP fallback was removed because codeapi's sessionAuth requires
125
+ * kind/id query params unavailable at this point. */
136
126
  if (_injected_files && _injected_files.length > 0) {
137
127
  postData.files = _injected_files;
138
- } else if (session_id != null && session_id.length > 0) {
139
- try {
140
- const filesEndpoint = `${baseEndpoint}/files/${session_id}?detail=full`;
141
- const fetchOptions: RequestInit = {
142
- method: 'GET',
143
- headers: {
144
- 'User-Agent': 'LibreChat/1.0',
145
- },
146
- };
147
-
148
- if (process.env.PROXY != null && process.env.PROXY !== '') {
149
- fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);
150
- }
151
-
152
- const response = await fetch(filesEndpoint, fetchOptions);
153
- if (!response.ok) {
154
- throw new Error(
155
- `Failed to fetch files for session: ${response.status}`
156
- );
157
- }
158
-
159
- const files = await response.json();
160
- if (Array.isArray(files) && files.length > 0) {
161
- const fileReferences: t.CodeEnvFile[] = files.map((file) => {
162
- const nameParts = file.name.split('/');
163
- const id = nameParts.length > 1 ? nameParts[1].split('.')[0] : '';
164
-
165
- return {
166
- storage_session_id: session_id,
167
- /* `/files` fallback returns code-output files belonging
168
- * to the user; tag them user-private. */
169
- kind: 'user' as const,
170
- id,
171
- /* `resource_id` informational for `kind: 'user'` —
172
- * codeapi derives sessionKey from auth context. */
173
- resource_id: id,
174
- name: file.metadata['original-filename'],
175
- };
176
- });
177
-
178
- postData.files = fileReferences;
179
- }
180
- } catch {
181
- // eslint-disable-next-line no-console
182
- console.warn(`Failed to fetch files for session: ${session_id}`);
183
- }
128
+ } else if (
129
+ session_id != null &&
130
+ session_id.length > 0 &&
131
+ !Array.isArray(postData.files)
132
+ ) {
133
+ // eslint-disable-next-line no-console
134
+ console.debug(
135
+ `[BashExecutor] No injected files for session_id=${session_id} — exec will run without input files`
136
+ );
184
137
  }
185
138
 
186
139
  try {
@@ -202,6 +155,11 @@ function createBashExecutionTool(
202
155
  }
203
156
 
204
157
  const result: t.ExecuteResult = await response.json();
158
+ /* See `CodeExecutor.ts` — file listings were removed from the
159
+ * LLM-facing tool result. Bash especially benefits: models
160
+ * naturally `ls /mnt/data/` to discover what's available
161
+ * rather than relying on a prescriptive summary that
162
+ * misleads as often as it helps. */
205
163
  let formattedOutput = '';
206
164
  if (result.stdout) {
207
165
  formattedOutput += `stdout:\n${result.stdout}\n`;
@@ -209,53 +167,15 @@ function createBashExecutionTool(
209
167
  formattedOutput += emptyOutputMessage;
210
168
  }
211
169
  if (result.stderr) formattedOutput += `stderr:\n${result.stderr}\n`;
212
- if (result.files && result.files.length > 0) {
213
- /* Split inherited (read-only / unchanged-input passthroughs from
214
- * codeapi) from genuine generated outputs. The LLM was previously
215
- * shown skill files under "Generated files:" with the message
216
- * "File is already downloaded by the user", which led it to
217
- * (a) believe it had just produced files it merely referenced
218
- * and (b) sometimes invent paths like /mnt/user-data/uploads/
219
- * trying to find the "originals". Labeling them as inputs makes
220
- * the mental model accurate. */
221
- const inheritedFiles = result.files.filter(
222
- (f) => f.inherited === true
223
- );
224
- const generatedFiles = result.files.filter(
225
- (f) => f.inherited !== true
226
- );
227
-
228
- formattedOutput += renderFileSection(
229
- generatedFilesHeader,
230
- generatedFiles,
231
- otherMessage
232
- );
233
- formattedOutput += renderFileSection(
234
- inheritedFilesHeader,
235
- inheritedFiles,
236
- inheritedFileMessage
237
- );
238
-
239
- if (generatedFiles.length > 0) {
240
- formattedOutput += `\n\n${accessMessage}`;
241
- }
242
- if (inheritedFiles.length > 0) {
243
- formattedOutput += `\n\n${inheritedNote}`;
244
- }
245
- return [
246
- formattedOutput.trim(),
247
- {
248
- session_id: result.session_id,
249
- files: result.files,
250
- } satisfies t.CodeExecutionArtifact,
251
- ];
252
- }
253
170
 
171
+ const hasFiles = result.files != null && result.files.length > 0;
254
172
  return [
255
173
  formattedOutput.trim(),
256
- {
257
- session_id: result.session_id,
258
- } satisfies t.CodeExecutionArtifact,
174
+ (hasFiles
175
+ ? { session_id: result.session_id, files: result.files }
176
+ : {
177
+ session_id: result.session_id,
178
+ }) satisfies t.CodeExecutionArtifact,
259
179
  ];
260
180
  } catch (error) {
261
181
  throw new Error(
@@ -5,7 +5,6 @@ import type * as t from '@/types';
5
5
  import {
6
6
  makeRequest,
7
7
  executeTools,
8
- fetchSessionFiles,
9
8
  formatCompletedResponse,
10
9
  } from './ProgrammaticToolCalling';
11
10
  import { getCodeBaseURL } from './CodeExecutor';
@@ -290,11 +289,17 @@ export function createBashProgrammaticToolCallingTool(
290
289
  );
291
290
  }
292
291
 
292
+ /* `/files/<session_id>` HTTP fallback removed — codeapi's
293
+ * sessionAuth requires kind/id query params unavailable at
294
+ * this point. See `CodeExecutor.ts` for full rationale. */
293
295
  let files: t.CodeEnvFile[] | undefined;
294
296
  if (_injected_files && _injected_files.length > 0) {
295
297
  files = _injected_files;
296
298
  } else if (session_id != null && session_id.length > 0) {
297
- files = await fetchSessionFiles(baseUrl, session_id, proxy);
299
+ // eslint-disable-next-line no-console
300
+ console.debug(
301
+ `[BashProgrammaticToolCalling] No injected files for session_id=${session_id} — exec will run without input files`
302
+ );
298
303
  }
299
304
 
300
305
  let response = await makeRequest(