@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.
Files changed (44) hide show
  1. package/dist/cjs/llm/anthropic/index.cjs +4 -3
  2. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/utils/tools.cjs +2 -1
  4. package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
  5. package/dist/cjs/llm/bedrock/cachePoints.cjs +28 -0
  6. package/dist/cjs/llm/bedrock/cachePoints.cjs.map +1 -0
  7. package/dist/cjs/llm/bedrock/index.cjs +10 -1
  8. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  9. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  10. package/dist/cjs/messages/format.cjs +1 -1
  11. package/dist/cjs/messages/format.cjs.map +1 -1
  12. package/dist/esm/llm/anthropic/index.mjs +4 -3
  13. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  14. package/dist/esm/llm/anthropic/utils/tools.mjs +2 -1
  15. package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
  16. package/dist/esm/llm/bedrock/cachePoints.mjs +28 -0
  17. package/dist/esm/llm/bedrock/cachePoints.mjs.map +1 -0
  18. package/dist/esm/llm/bedrock/index.mjs +10 -1
  19. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  20. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  21. package/dist/esm/messages/format.mjs +1 -1
  22. package/dist/esm/messages/format.mjs.map +1 -1
  23. package/dist/types/llm/anthropic/utils/tools.d.ts +1 -1
  24. package/dist/types/llm/bedrock/cachePoints.d.ts +9 -0
  25. package/dist/types/messages/format.d.ts +4 -0
  26. package/package.json +16 -21
  27. package/src/llm/anthropic/index.ts +13 -2
  28. package/src/llm/anthropic/inherited-content-utils.spec.ts +244 -0
  29. package/src/llm/anthropic/inherited-stream-events.spec.ts +752 -0
  30. package/src/llm/anthropic/inherited-strict.spec.ts +302 -0
  31. package/src/llm/anthropic/llm.spec.ts +65 -0
  32. package/src/llm/anthropic/utils/tools.ts +7 -1
  33. package/src/llm/bedrock/cachePoints.ts +86 -0
  34. package/src/llm/bedrock/index.ts +9 -0
  35. package/src/llm/bedrock/inherited-cache.spec.ts +144 -0
  36. package/src/llm/bedrock/inherited.spec.ts +724 -0
  37. package/src/llm/google/inherited-stream-events.spec.ts +350 -0
  38. package/src/llm/openai/inherited-deepseek.spec.ts +347 -0
  39. package/src/llm/openai/inherited-xai.spec.ts +416 -0
  40. package/src/llm/openai/llm.spec.ts +1568 -0
  41. package/src/llm/openrouter/index.ts +1 -3
  42. package/src/llm/vertexai/inherited-stream-events.spec.ts +271 -0
  43. package/src/messages/format.ts +6 -1
  44. 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
- // @ts-expect-error - OpenRouter reasoning extends OpenAI Reasoning with additional
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
+ });
@@ -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: options?.provider === Providers.DEEPSEEK,
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
  {