@librechat/agents 3.2.45 → 3.2.51
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/index.cjs +4 -3
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/tools.cjs +2 -1
- package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/cachePoints.cjs +28 -0
- package/dist/cjs/llm/bedrock/cachePoints.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/index.cjs +10 -1
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +1 -1
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +4 -3
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/tools.mjs +2 -1
- package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
- package/dist/esm/llm/bedrock/cachePoints.mjs +28 -0
- package/dist/esm/llm/bedrock/cachePoints.mjs.map +1 -0
- package/dist/esm/llm/bedrock/index.mjs +10 -1
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +1 -1
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/types/llm/anthropic/utils/tools.d.ts +1 -1
- package/dist/types/llm/bedrock/cachePoints.d.ts +9 -0
- package/dist/types/messages/format.d.ts +4 -0
- package/package.json +16 -21
- package/src/llm/anthropic/index.ts +13 -2
- package/src/llm/anthropic/inherited-content-utils.spec.ts +244 -0
- package/src/llm/anthropic/inherited-stream-events.spec.ts +752 -0
- package/src/llm/anthropic/inherited-strict.spec.ts +302 -0
- package/src/llm/anthropic/llm.spec.ts +65 -0
- package/src/llm/anthropic/utils/tools.ts +7 -1
- package/src/llm/bedrock/cachePoints.ts +86 -0
- package/src/llm/bedrock/index.ts +9 -0
- package/src/llm/bedrock/inherited-cache.spec.ts +144 -0
- package/src/llm/bedrock/inherited.spec.ts +724 -0
- package/src/llm/google/inherited-stream-events.spec.ts +350 -0
- package/src/llm/openai/inherited-deepseek.spec.ts +347 -0
- package/src/llm/openai/inherited-xai.spec.ts +416 -0
- package/src/llm/openai/llm.spec.ts +1568 -0
- package/src/llm/openrouter/index.ts +1 -3
- package/src/llm/vertexai/inherited-stream-events.spec.ts +271 -0
- package/src/messages/format.ts +6 -1
- package/src/messages/formatAgentMessages.test.ts +76 -0
|
@@ -161,9 +161,7 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|
|
161
161
|
return 'LibreChatOpenRouter';
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
//
|
|
165
|
-
// effort levels ('xhigh' | 'none' | 'minimal') not in ReasoningEffort.
|
|
166
|
-
// The parent's generic conditional return type cannot be widened in an override.
|
|
164
|
+
// OpenRouter widens OpenAI reasoning with extra effort levels ('xhigh' | 'none' | 'minimal').
|
|
167
165
|
override invocationParams(
|
|
168
166
|
options?: this['ParsedCallOptions'],
|
|
169
167
|
extra?: InvocationParamsExtra
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Inherited stream-events specs for the LibreChat Vertex fork, ported from two
|
|
2
|
+
// upstream @langchain/google-common@2.2.0 suites:
|
|
3
|
+
//
|
|
4
|
+
// 1. utils/tests/stream_events.test.ts (the original prompt references it as
|
|
5
|
+
// google-common_stream_events.test.ts) — unit tests for the pure converter
|
|
6
|
+
// `convertGoogleGeminiStream`, which turns Gemini-style stream responses
|
|
7
|
+
// into LangChain `ChatModelStreamEvent`s. Our fork's `ChatVertexAI` does
|
|
8
|
+
// not re-implement this converter; it is re-exported from
|
|
9
|
+
// `@langchain/google-common` and exercised verbatim here as a pure unit.
|
|
10
|
+
//
|
|
11
|
+
// 2. chat_models/tests/chat_models_stream_events.test.ts (the prompt references
|
|
12
|
+
// it as google-common_chat_models_stream_events.test.ts) — tests
|
|
13
|
+
// `ChatGoogle.streamEvents()`. Upstream drove a `TestChatGoogle` helper whose
|
|
14
|
+
// `authOptions.resultFile` replayed recorded mock JSON and asserted via
|
|
15
|
+
// vitest custom matchers (`toHaveStreamText` / `toHaveStreamReasoning` /
|
|
16
|
+
// `toHaveStreamToolCalls`). Those fixture files and matchers are not
|
|
17
|
+
// available in this repo, so each case is routed through OUR fork:
|
|
18
|
+
// `new ChatVertexAI({...})` (which extends ChatGoogle and inherits the
|
|
19
|
+
// native `_streamChatModelEvents` path) with the google transport mocked at
|
|
20
|
+
// the `streamedConnection.request` boundary — it returns `{ data: <json
|
|
21
|
+
// stream> }`, the exact shape `_streamChatModelEvents` consumes. The matchers
|
|
22
|
+
// are re-expressed against the public `ChatModelStream` sub-streams
|
|
23
|
+
// (`.text` / `.reasoning` / `.toolCalls`) that those matchers wrap.
|
|
24
|
+
//
|
|
25
|
+
// All cases are deterministic and run against a mocked transport — no live
|
|
26
|
+
// Vertex access. vitest -> jest, `zod/v3` -> n/a (no zod here).
|
|
27
|
+
|
|
28
|
+
import { describe, it, test, expect } from '@jest/globals';
|
|
29
|
+
import { convertGoogleGeminiStream } from '@langchain/google-common';
|
|
30
|
+
import { ChatModelStream } from '@langchain/core/language_models/stream';
|
|
31
|
+
import type { BaseChatModelCallOptions } from '@langchain/core/language_models/chat_models';
|
|
32
|
+
import type { ChatModelStreamEvent } from '@langchain/core/language_models/event';
|
|
33
|
+
import { ChatVertexAI } from '@/llm/vertexai';
|
|
34
|
+
|
|
35
|
+
type GeminiChunk = Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
// --- Converter unit-test harness (upstream source 1) -----------------------
|
|
38
|
+
|
|
39
|
+
async function* asAsyncIterable(
|
|
40
|
+
chunks: GeminiChunk[]
|
|
41
|
+
): AsyncGenerator<GeminiChunk> {
|
|
42
|
+
for (const chunk of chunks) {
|
|
43
|
+
yield chunk;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function collectEvents(
|
|
48
|
+
chunks: GeminiChunk[]
|
|
49
|
+
): Promise<ChatModelStreamEvent[]> {
|
|
50
|
+
const out: ChatModelStreamEvent[] = [];
|
|
51
|
+
for await (const event of convertGoogleGeminiStream(
|
|
52
|
+
asAsyncIterable(chunks)
|
|
53
|
+
)) {
|
|
54
|
+
out.push(event);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Class streamEvents harness (upstream source 2, routed through fork) ----
|
|
60
|
+
|
|
61
|
+
interface MockJsonStream {
|
|
62
|
+
readonly streamDone: boolean;
|
|
63
|
+
nextChunk(): Promise<GeminiChunk | null>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeJsonStream(chunks: GeminiChunk[]): MockJsonStream {
|
|
67
|
+
let i = 0;
|
|
68
|
+
return {
|
|
69
|
+
get streamDone(): boolean {
|
|
70
|
+
return i >= chunks.length;
|
|
71
|
+
},
|
|
72
|
+
async nextChunk(): Promise<GeminiChunk | null> {
|
|
73
|
+
if (i >= chunks.length) return null;
|
|
74
|
+
const chunk = chunks[i];
|
|
75
|
+
i += 1;
|
|
76
|
+
return chunk;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface StreamedConnectionModel {
|
|
82
|
+
streamedConnection: {
|
|
83
|
+
request: () => Promise<{ data: MockJsonStream }>;
|
|
84
|
+
};
|
|
85
|
+
_streamChatModelEvents: (
|
|
86
|
+
messages: unknown[],
|
|
87
|
+
options: BaseChatModelCallOptions
|
|
88
|
+
) => AsyncGenerator<ChatModelStreamEvent>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createStreamModel(chunks: GeminiChunk[]): ChatVertexAI {
|
|
92
|
+
const model = new ChatVertexAI({
|
|
93
|
+
model: 'gemini-2.0-flash',
|
|
94
|
+
authOptions: {
|
|
95
|
+
credentials: { client_email: 'test@example.com', private_key: 'test' },
|
|
96
|
+
projectId: 'test-project',
|
|
97
|
+
},
|
|
98
|
+
} as ConstructorParameters<typeof ChatVertexAI>[0]);
|
|
99
|
+
(model as unknown as StreamedConnectionModel).streamedConnection.request =
|
|
100
|
+
async () => ({ data: makeJsonStream(chunks) });
|
|
101
|
+
return model;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function streamEvents(
|
|
105
|
+
model: ChatVertexAI,
|
|
106
|
+
options: BaseChatModelCallOptions = {} as BaseChatModelCallOptions
|
|
107
|
+
): AsyncGenerator<ChatModelStreamEvent> {
|
|
108
|
+
return (model as unknown as StreamedConnectionModel)._streamChatModelEvents(
|
|
109
|
+
[],
|
|
110
|
+
options
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Source 1: convertGoogleGeminiStream (pure converter, re-exported from
|
|
116
|
+
// @langchain/google-common and inherited unchanged by the fork)
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
describe('convertGoogleGeminiStream (inherited from @langchain/google-common)', () => {
|
|
120
|
+
test('text-only streaming', async () => {
|
|
121
|
+
const events = await collectEvents([
|
|
122
|
+
{ candidates: [{ content: { parts: [{ text: 'Hello' }] } }] },
|
|
123
|
+
{ candidates: [{ content: { parts: [{ text: ' world' }] } }] },
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const textDeltas = events.filter(
|
|
127
|
+
(e) =>
|
|
128
|
+
e.event === 'content-block-delta' &&
|
|
129
|
+
(e as { delta: { type: string } }).delta.type === 'text-delta'
|
|
130
|
+
);
|
|
131
|
+
expect(textDeltas).toHaveLength(2);
|
|
132
|
+
|
|
133
|
+
expect(
|
|
134
|
+
events.find((e) => e.event === 'content-block-finish')
|
|
135
|
+
).toMatchObject({
|
|
136
|
+
content: { text: 'Hello world' },
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('maps Gemini finish reasons', async () => {
|
|
141
|
+
const lengthEvents = await collectEvents([
|
|
142
|
+
{
|
|
143
|
+
candidates: [
|
|
144
|
+
{
|
|
145
|
+
content: { parts: [{ text: 'Hello' }] },
|
|
146
|
+
finishReason: 'MAX_TOKENS',
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
]);
|
|
151
|
+
const lengthFinish = lengthEvents.find((e) => e.event === 'message-finish');
|
|
152
|
+
expect(lengthFinish).toMatchObject({ reason: 'length' });
|
|
153
|
+
|
|
154
|
+
const filterEvents = await collectEvents([
|
|
155
|
+
{
|
|
156
|
+
candidates: [
|
|
157
|
+
{
|
|
158
|
+
content: { parts: [{ text: 'Hello' }] },
|
|
159
|
+
finishReason: 'SAFETY',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
]);
|
|
164
|
+
const filterFinish = filterEvents.find((e) => e.event === 'message-finish');
|
|
165
|
+
expect(filterFinish).toMatchObject({ reason: 'content_filter' });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('thinking parts map to reasoning', async () => {
|
|
169
|
+
const events = await collectEvents([
|
|
170
|
+
{
|
|
171
|
+
candidates: [
|
|
172
|
+
{
|
|
173
|
+
content: { parts: [{ text: 'Let me think', thought: true }] },
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
events.find(
|
|
181
|
+
(e) =>
|
|
182
|
+
e.event === 'content-block-finish' &&
|
|
183
|
+
(e as { content: { type: string } }).content.type === 'reasoning'
|
|
184
|
+
)
|
|
185
|
+
).toMatchObject({
|
|
186
|
+
content: { reasoning: 'Let me think' },
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('usage snapshots', async () => {
|
|
191
|
+
const events = await collectEvents([
|
|
192
|
+
{
|
|
193
|
+
usageMetadata: {
|
|
194
|
+
promptTokenCount: 10,
|
|
195
|
+
candidatesTokenCount: 4,
|
|
196
|
+
totalTokenCount: 14,
|
|
197
|
+
},
|
|
198
|
+
candidates: [{ content: { parts: [{ text: 'Hi' }] } }],
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
expect(events.filter((e) => e.event === 'usage').length).toBe(1);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Source 2: ChatGoogle.streamEvents — inherited native streamEvents path on
|
|
208
|
+
// the fork (ChatVertexAI does not override _streamChatModelEvents). Re-expressed
|
|
209
|
+
// against the public ChatModelStream sub-streams that the upstream vitest
|
|
210
|
+
// matchers wrapped.
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
describe('ChatVertexAI.streamEvents (inherited native path)', () => {
|
|
214
|
+
test('streams text', async () => {
|
|
215
|
+
const model = createStreamModel([
|
|
216
|
+
{ candidates: [{ content: { parts: [{ text: 'Hello' }] } }] },
|
|
217
|
+
{
|
|
218
|
+
candidates: [
|
|
219
|
+
{ content: { parts: [{ text: ' world' }] }, finishReason: 'STOP' },
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
const stream = new ChatModelStream(streamEvents(model));
|
|
224
|
+
expect(await stream.text).toBe('Hello world');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('streams reasoning', async () => {
|
|
228
|
+
const model = createStreamModel([
|
|
229
|
+
{
|
|
230
|
+
candidates: [
|
|
231
|
+
{
|
|
232
|
+
content: { parts: [{ text: 'Let me reason...', thought: true }] },
|
|
233
|
+
finishReason: 'STOP',
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
]);
|
|
238
|
+
const stream = new ChatModelStream(streamEvents(model));
|
|
239
|
+
expect(await stream.reasoning).toBe('Let me reason...');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('streams tool calls', async () => {
|
|
243
|
+
const model = createStreamModel([
|
|
244
|
+
{
|
|
245
|
+
candidates: [
|
|
246
|
+
{
|
|
247
|
+
content: {
|
|
248
|
+
parts: [
|
|
249
|
+
{
|
|
250
|
+
functionCall: {
|
|
251
|
+
name: 'web_search',
|
|
252
|
+
args: { query: 'weather' },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
finishReason: 'STOP',
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
]);
|
|
262
|
+
const stream = new ChatModelStream(streamEvents(model));
|
|
263
|
+
const calls = await stream.toolCalls;
|
|
264
|
+
expect(calls).toEqual([
|
|
265
|
+
expect.objectContaining({
|
|
266
|
+
name: 'web_search',
|
|
267
|
+
args: { query: 'weather' },
|
|
268
|
+
}),
|
|
269
|
+
]);
|
|
270
|
+
});
|
|
271
|
+
});
|
package/src/messages/format.ts
CHANGED
|
@@ -334,6 +334,10 @@ interface FormatAssistantMessageOptions {
|
|
|
334
334
|
|
|
335
335
|
interface FormatAgentMessagesOptions {
|
|
336
336
|
provider?: Providers;
|
|
337
|
+
/** Reconstruct hidden `reasoning_content` from `THINK` parts onto prior
|
|
338
|
+
* tool-call messages. Explicit opt-in for OpenAI-compatible endpoints that
|
|
339
|
+
* replay reasoning across turns; defaults to on for DeepSeek thinking-mode. */
|
|
340
|
+
preserveReasoningContent?: boolean;
|
|
337
341
|
/** Skill names already primed fresh this turn (manual/always-apply). Their
|
|
338
342
|
* historical `skill` tool_calls are not reconstructed into a HumanMessage,
|
|
339
343
|
* so the same SKILL.md body is not injected twice in one request. */
|
|
@@ -1435,7 +1439,8 @@ export const formatAgentMessages = (
|
|
|
1435
1439
|
|
|
1436
1440
|
const formattedMessages = formatAssistantMessage(processedMessage, {
|
|
1437
1441
|
preserveUnpairedServerToolUses: i === payload.length - 1,
|
|
1438
|
-
preserveReasoningContent:
|
|
1442
|
+
preserveReasoningContent:
|
|
1443
|
+
options?.preserveReasoningContent ?? options?.provider === Providers.DEEPSEEK,
|
|
1439
1444
|
provider: options?.provider,
|
|
1440
1445
|
});
|
|
1441
1446
|
if (sourceMessageId != null && sourceMessageId !== '') {
|
|
@@ -2189,6 +2189,82 @@ describe('formatAgentMessages', () => {
|
|
|
2189
2189
|
);
|
|
2190
2190
|
});
|
|
2191
2191
|
|
|
2192
|
+
it('should preserve hidden reasoning_content via explicit preserveReasoningContent without a provider', () => {
|
|
2193
|
+
const payload: TPayload = [
|
|
2194
|
+
{
|
|
2195
|
+
role: 'assistant',
|
|
2196
|
+
content: [
|
|
2197
|
+
{
|
|
2198
|
+
type: ContentTypes.THINK,
|
|
2199
|
+
[ContentTypes.THINK]: 'Need calculator.',
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
type: ContentTypes.TEXT,
|
|
2203
|
+
[ContentTypes.TEXT]: 'Using calculator.',
|
|
2204
|
+
tool_call_ids: ['call_1'],
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
type: ContentTypes.TOOL_CALL,
|
|
2208
|
+
tool_call: {
|
|
2209
|
+
id: 'call_1',
|
|
2210
|
+
name: 'calculator',
|
|
2211
|
+
args: '{"input":"127 * 453"}',
|
|
2212
|
+
output: '57531',
|
|
2213
|
+
},
|
|
2214
|
+
},
|
|
2215
|
+
],
|
|
2216
|
+
},
|
|
2217
|
+
];
|
|
2218
|
+
|
|
2219
|
+
const result = formatAgentMessages(payload, undefined, undefined, undefined, {
|
|
2220
|
+
preserveReasoningContent: true,
|
|
2221
|
+
});
|
|
2222
|
+
|
|
2223
|
+
const toolCallMessage = result.messages[0] as AIMessage;
|
|
2224
|
+
expect(toolCallMessage.tool_calls).toHaveLength(1);
|
|
2225
|
+
expect(toolCallMessage.additional_kwargs.reasoning_content).toBe(
|
|
2226
|
+
'Need calculator.'
|
|
2227
|
+
);
|
|
2228
|
+
});
|
|
2229
|
+
|
|
2230
|
+
it('should not reconstruct reasoning_content when preserveReasoningContent is explicitly false for DeepSeek', () => {
|
|
2231
|
+
const payload: TPayload = [
|
|
2232
|
+
{
|
|
2233
|
+
role: 'assistant',
|
|
2234
|
+
content: [
|
|
2235
|
+
{
|
|
2236
|
+
type: ContentTypes.THINK,
|
|
2237
|
+
[ContentTypes.THINK]: 'Need calculator.',
|
|
2238
|
+
},
|
|
2239
|
+
{
|
|
2240
|
+
type: ContentTypes.TEXT,
|
|
2241
|
+
[ContentTypes.TEXT]: 'Using calculator.',
|
|
2242
|
+
tool_call_ids: ['call_1'],
|
|
2243
|
+
},
|
|
2244
|
+
{
|
|
2245
|
+
type: ContentTypes.TOOL_CALL,
|
|
2246
|
+
tool_call: {
|
|
2247
|
+
id: 'call_1',
|
|
2248
|
+
name: 'calculator',
|
|
2249
|
+
args: '{"input":"127 * 453"}',
|
|
2250
|
+
output: '57531',
|
|
2251
|
+
},
|
|
2252
|
+
},
|
|
2253
|
+
],
|
|
2254
|
+
},
|
|
2255
|
+
];
|
|
2256
|
+
|
|
2257
|
+
const result = formatAgentMessages(payload, undefined, undefined, undefined, {
|
|
2258
|
+
provider: Providers.DEEPSEEK,
|
|
2259
|
+
preserveReasoningContent: false,
|
|
2260
|
+
});
|
|
2261
|
+
|
|
2262
|
+
const toolCallMessage = result.messages[0] as AIMessage;
|
|
2263
|
+
expect(
|
|
2264
|
+
toolCallMessage.additional_kwargs.reasoning_content
|
|
2265
|
+
).toBeUndefined();
|
|
2266
|
+
});
|
|
2267
|
+
|
|
2192
2268
|
it('should preserve DeepSeek reasoning from supported hidden content blocks', () => {
|
|
2193
2269
|
const payload: TPayload = [
|
|
2194
2270
|
{
|