@librechat/agents 3.2.46 → 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/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/types/llm/anthropic/utils/tools.d.ts +1 -1
- package/dist/types/llm/bedrock/cachePoints.d.ts +9 -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
|
@@ -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
|
+
});
|