@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.
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +27 -7
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +52 -0
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +1 -2
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +20 -78
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +5 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +26 -106
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +12 -31
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +27 -8
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +53 -2
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/tools/BashExecutor.mjs +20 -78
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +6 -2
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +26 -105
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +12 -31
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/types/llm/anthropic/utils/message_inputs.d.ts +15 -3
- package/dist/types/llm/vertexai/index.d.ts +29 -0
- package/dist/types/tools/CodeExecutor.d.ts +1 -7
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +5 -0
- package/package.json +10 -5
- package/src/llm/anthropic/utils/message_inputs.ts +58 -7
- package/src/llm/anthropic/utils/tool-id-normalization.test.ts +178 -0
- package/src/llm/vertexai/index.ts +69 -2
- package/src/llm/vertexai/llm.spec.ts +18 -0
- package/src/llm/vertexai/repairUsageMetadata.test.ts +54 -0
- package/src/tools/BashExecutor.ts +24 -104
- package/src/tools/BashProgrammaticToolCalling.ts +7 -2
- package/src/tools/CodeExecutor.ts +30 -133
- package/src/tools/ProgrammaticToolCalling.ts +14 -49
- 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
|
|
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.
|
|
199
|
-
"@langchain/google-gauth": "2.1.
|
|
200
|
-
"@langchain/google-genai": "2.1.
|
|
201
|
-
"@langchain/google-vertexai": "2.1.
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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:
|
|
163
|
-
|
|
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 {
|
|
10
|
-
import {
|
|
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 {
|
|
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 (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|