@librechat/agents 3.2.46 → 3.2.52

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 (57) hide show
  1. package/dist/cjs/graphs/MultiAgentGraph.cjs +6 -6
  2. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/index.cjs +4 -3
  4. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/utils/tools.cjs +2 -1
  6. package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
  7. package/dist/cjs/llm/bedrock/cachePoints.cjs +28 -0
  8. package/dist/cjs/llm/bedrock/cachePoints.cjs.map +1 -0
  9. package/dist/cjs/llm/bedrock/index.cjs +10 -1
  10. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  11. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  12. package/dist/cjs/run.cjs +21 -3
  13. package/dist/cjs/run.cjs.map +1 -1
  14. package/dist/cjs/tools/ToolNode.cjs +26 -5
  15. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  16. package/dist/esm/graphs/MultiAgentGraph.mjs +7 -7
  17. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  18. package/dist/esm/llm/anthropic/index.mjs +4 -3
  19. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  20. package/dist/esm/llm/anthropic/utils/tools.mjs +2 -1
  21. package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
  22. package/dist/esm/llm/bedrock/cachePoints.mjs +28 -0
  23. package/dist/esm/llm/bedrock/cachePoints.mjs.map +1 -0
  24. package/dist/esm/llm/bedrock/index.mjs +10 -1
  25. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  26. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  27. package/dist/esm/run.mjs +21 -3
  28. package/dist/esm/run.mjs.map +1 -1
  29. package/dist/esm/tools/ToolNode.mjs +26 -5
  30. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  31. package/dist/types/llm/anthropic/utils/tools.d.ts +1 -1
  32. package/dist/types/llm/bedrock/cachePoints.d.ts +9 -0
  33. package/dist/types/run.d.ts +15 -1
  34. package/dist/types/tools/ToolNode.d.ts +9 -2
  35. package/package.json +16 -21
  36. package/src/graphs/MultiAgentGraph.ts +7 -12
  37. package/src/llm/anthropic/index.ts +13 -2
  38. package/src/llm/anthropic/inherited-content-utils.spec.ts +244 -0
  39. package/src/llm/anthropic/inherited-stream-events.spec.ts +752 -0
  40. package/src/llm/anthropic/inherited-strict.spec.ts +302 -0
  41. package/src/llm/anthropic/llm.spec.ts +65 -0
  42. package/src/llm/anthropic/utils/tools.ts +7 -1
  43. package/src/llm/bedrock/cachePoints.ts +86 -0
  44. package/src/llm/bedrock/index.ts +9 -0
  45. package/src/llm/bedrock/inherited-cache.spec.ts +144 -0
  46. package/src/llm/bedrock/inherited.spec.ts +724 -0
  47. package/src/llm/google/inherited-stream-events.spec.ts +350 -0
  48. package/src/llm/openai/inherited-deepseek.spec.ts +347 -0
  49. package/src/llm/openai/inherited-xai.spec.ts +416 -0
  50. package/src/llm/openai/llm.spec.ts +1568 -0
  51. package/src/llm/openrouter/index.ts +1 -3
  52. package/src/llm/vertexai/inherited-stream-events.spec.ts +271 -0
  53. package/src/run.ts +31 -3
  54. package/src/scripts/handoff-test.ts +5 -6
  55. package/src/tools/ToolNode.ts +43 -5
  56. package/src/tools/__tests__/ToolNode.runtimeState.test.ts +120 -0
  57. package/src/tools/__tests__/hitl.test.ts +162 -0
@@ -0,0 +1,416 @@
1
+ // Inherited xAI specs for the LibreChat OpenAI fork, ported from two
2
+ // upstream-derived suites (each adapted to the fork's actual surface):
3
+ //
4
+ // 1. completions — Inherited from @langchain/xai@1.4.3
5
+ // chat_models/tests/completions.test.ts.
6
+ // xAI 1.4.x splits the model into chat_models/{completions,responses} +
7
+ // converters (like openai 1.5.x). Our fork's `ChatXAI` (from `@/llm/openai`)
8
+ // extends the upstream top-level `ChatXAI`, which IS the completions class
9
+ // (`ChatXAI extends ChatOpenAICompletions`), so the completions-path behavior
10
+ // tested upstream lives directly on the instance (no `.completions`
11
+ // sub-delegate, unlike the fork's `ChatOpenAI`). The live-search /
12
+ // invocationParams / `_hasBuiltInTools` / `_getEffectiveSearchParameters`
13
+ // surface is inherited verbatim and exercised here against the fork.
14
+ //
15
+ // 2. stream events — Inherited from @langchain/xai@1.4.3
16
+ // chat_models/tests/chat_models_stream_events.test.ts.
17
+ // Upstream extends `ChatXAI` (completions) and drives `streamEvents` with
18
+ // vitest custom matchers (`toHaveStreamText` / `toHaveStreamReasoning` /
19
+ // `toHaveStreamToolCalls`) that are unavailable in jest. The fork inherits
20
+ // `_streamChatModelEvents` from langchain-core, so we keep these as a parity
21
+ // check: construct a `MockStreamChatXAI` overriding `completionWithRetry`
22
+ // (mirroring the upstream mock + custom-chat-models.smoke.test.ts), wrap
23
+ // `_streamChatModelEvents` in a `ChatModelStream`, and assert on the same
24
+ // promise-backed sub-streams (`.text` / `.reasoning` / `.toolCalls`) that
25
+ // the vitest matchers wrap.
26
+ //
27
+ // Import notes (per task rules):
28
+ // - The model class is imported from `@/llm/openai` (our fork), NOT
29
+ // `@langchain/xai`.
30
+ // - `isXAIBuiltInTool` and the `ChatXAICompletionsInvocationParams` type are
31
+ // re-exported by `@langchain/xai`'s main entry and imported from there
32
+ // (the fork does not re-wrap them).
33
+ // - The live-search tool constants/types (`XAI_LIVE_SEARCH_TOOL_NAME`,
34
+ // `XAI_LIVE_SEARCH_TOOL_TYPE`, `XAILiveSearchTool`, `XAISearchParameters`)
35
+ // are NOT exported from `@langchain/xai` (only `.` is in package `exports`,
36
+ // and they live behind unexposed subpaths). They are reproduced locally from
37
+ // `dist/tools/live_search.d.ts` + `dist/live_search.d.ts` to keep the
38
+ // fixtures faithful without reaching into deep import paths.
39
+ //
40
+ // Where the fork intentionally diverges from upstream we assert OUR behavior and
41
+ // note the diff inline. UNIT only (mocked transport); the upstream live cases are
42
+ // dropped with reasons below.
43
+ //
44
+ // Dropped (inherited): the upstream suite has no `it.skip`/live integration
45
+ // cases — every upstream case is deterministic and ported here. No xAI Responses
46
+ // API cases existed in either source, so none needed a probe/drop.
47
+
48
+ import { isXAIBuiltInTool } from '@langchain/xai';
49
+ import { describe, test, expect, beforeEach } from '@jest/globals';
50
+ import { ChatModelStream } from '@langchain/core/language_models/stream';
51
+ import {
52
+ openAIReasoningTextChunks,
53
+ openAITextOnlyChunks,
54
+ openAIToolCallChunks,
55
+ } from '@langchain/core/testing';
56
+ import type { ChatModelStreamEvent } from '@langchain/core/language_models/event';
57
+ import type { ChatXAICompletionsInvocationParams } from '@langchain/xai';
58
+ import { ChatXAI } from '@/llm/openai';
59
+
60
+ const TEST_MODEL = process.env.XAI_TEST_MODEL ?? 'grok-3-fast';
61
+
62
+ // Local copies of the (unexported) live-search tool literals, verbatim from
63
+ // @langchain/xai@1.4.3 dist/tools/live_search.d.ts.
64
+ const XAI_LIVE_SEARCH_TOOL_NAME = 'live_search' as const;
65
+ const XAI_LIVE_SEARCH_TOOL_TYPE = 'live_search_deprecated_20251215' as const;
66
+
67
+ type XAILiveSearchTool = {
68
+ name: typeof XAI_LIVE_SEARCH_TOOL_NAME;
69
+ type: typeof XAI_LIVE_SEARCH_TOOL_TYPE;
70
+ };
71
+
72
+ // Subset of the (unexported) XAISearchParameters shape used by these fixtures.
73
+ type XAISearchParameters = NonNullable<
74
+ ChatXAICompletionsInvocationParams['search_parameters']
75
+ >;
76
+
77
+ type ParsedCallOptions = ChatXAI['ParsedCallOptions'];
78
+
79
+ // `clientConfig` and the protected search helpers are reached the way upstream
80
+ // reaches them (an internal cast); typed structurally to avoid `any`.
81
+ type ChatXAIInternals = {
82
+ clientConfig: { baseURL?: string };
83
+ searchParameters?: XAISearchParameters;
84
+ _getEffectiveSearchParameters(
85
+ options?: ParsedCallOptions
86
+ ): XAISearchParameters | undefined;
87
+ _hasBuiltInTools(tools?: unknown[]): boolean;
88
+ };
89
+
90
+ const internals = (model: ChatXAI): ChatXAIInternals =>
91
+ model as unknown as ChatXAIInternals;
92
+
93
+ beforeEach(() => {
94
+ process.env.XAI_API_KEY = 'foo';
95
+ });
96
+
97
+ describe('inherited xAI completions (chat_models/tests/completions.test.ts)', () => {
98
+ describe('baseURL configuration', () => {
99
+ test('should use default baseURL when not specified', () => {
100
+ const model = new ChatXAI();
101
+ expect(internals(model).clientConfig.baseURL).toBe('https://api.x.ai/v1');
102
+ });
103
+
104
+ test('should use custom baseURL when provided', () => {
105
+ const model = new ChatXAI({
106
+ baseURL: 'https://custom.api.example.com/v1',
107
+ });
108
+ expect(internals(model).clientConfig.baseURL).toBe(
109
+ 'https://custom.api.example.com/v1'
110
+ );
111
+ });
112
+ });
113
+
114
+ // Fork divergence: the serialized `lc_id` reflects the LibreChat subclass name
115
+ // (`LibreChatXAI` via `lc_name()`), not upstream's `ChatXAI`. Everything else
116
+ // (the `model` kwarg, secret stripping) is identical, so we assert OUR id.
117
+ describe('Serialization', () => {
118
+ test('serializes model + apiKey (lc_id = LibreChatXAI)', () => {
119
+ delete process.env.XAI_API_KEY;
120
+ const model = new ChatXAI({ model: TEST_MODEL, apiKey: 'bar' });
121
+ expect(JSON.stringify(model)).toEqual(
122
+ `{"lc":1,"type":"constructor","id":["langchain","chat_models","xai","LibreChatXAI"],"kwargs":{"model":"${TEST_MODEL}"}}`
123
+ );
124
+ });
125
+
126
+ test('serializes with no params (default model grok-3-fast)', () => {
127
+ const model = new ChatXAI();
128
+ expect(JSON.stringify(model)).toEqual(
129
+ '{"lc":1,"type":"constructor","id":["langchain","chat_models","xai","LibreChatXAI"],"kwargs":{"model":"grok-3-fast"}}'
130
+ );
131
+ });
132
+
133
+ test('serializes with model shorthand', () => {
134
+ // The string-shorthand constructor is inherited from the upstream base but
135
+ // is narrowed away by the fork's field type, so cast to reach that path.
136
+ const model = new ChatXAI(
137
+ TEST_MODEL as unknown as ConstructorParameters<typeof ChatXAI>[0]
138
+ );
139
+ expect(JSON.stringify(model)).toEqual(
140
+ `{"lc":1,"type":"constructor","id":["langchain","chat_models","xai","LibreChatXAI"],"kwargs":{"model":"${TEST_MODEL}"}}`
141
+ );
142
+ });
143
+ });
144
+
145
+ describe('Server Tool Calling', () => {
146
+ describe('isXAIBuiltInTool', () => {
147
+ test('should identify live_search as a built-in tool', () => {
148
+ const liveSearchTool: XAILiveSearchTool = {
149
+ name: XAI_LIVE_SEARCH_TOOL_NAME,
150
+ type: XAI_LIVE_SEARCH_TOOL_TYPE,
151
+ };
152
+ expect(isXAIBuiltInTool(liveSearchTool)).toBe(true);
153
+ });
154
+
155
+ test('should not identify function tools as built-in', () => {
156
+ const functionTool = {
157
+ type: 'function',
158
+ function: {
159
+ name: 'get_weather',
160
+ description: 'Get the weather',
161
+ parameters: { type: 'object', properties: {} },
162
+ },
163
+ };
164
+ expect(isXAIBuiltInTool(functionTool as never)).toBe(false);
165
+ });
166
+
167
+ test('should not identify invalid objects as built-in', () => {
168
+ expect(isXAIBuiltInTool(null as never)).toBe(false);
169
+ expect(isXAIBuiltInTool(undefined as never)).toBe(false);
170
+ expect(isXAIBuiltInTool({} as never)).toBe(false);
171
+ expect(isXAIBuiltInTool({ type: 'other' } as never)).toBe(false);
172
+ });
173
+ });
174
+
175
+ describe('ChatXAI with searchParameters', () => {
176
+ test('should store searchParameters from constructor', () => {
177
+ const searchParams: XAISearchParameters = {
178
+ mode: 'auto',
179
+ max_search_results: 5,
180
+ };
181
+ const model = new ChatXAI({ searchParameters: searchParams });
182
+ expect(internals(model).searchParameters).toEqual(searchParams);
183
+ });
184
+
185
+ test('should have undefined searchParameters by default', () => {
186
+ const model = new ChatXAI();
187
+ expect(internals(model).searchParameters).toBeUndefined();
188
+ });
189
+
190
+ test('should merge search parameters correctly', () => {
191
+ const model = new ChatXAI({
192
+ searchParameters: { mode: 'auto', max_search_results: 5 },
193
+ });
194
+
195
+ const effectiveParams = internals(model)._getEffectiveSearchParameters({
196
+ searchParameters: {
197
+ max_search_results: 10,
198
+ from_date: '2024-01-01',
199
+ },
200
+ } as ParsedCallOptions);
201
+
202
+ expect(effectiveParams).toEqual({
203
+ mode: 'auto',
204
+ max_search_results: 10,
205
+ from_date: '2024-01-01',
206
+ });
207
+ });
208
+ });
209
+
210
+ describe('invocationParams with server tools', () => {
211
+ test('should add search_parameters when live_search tool is bound', () => {
212
+ const model = new ChatXAI();
213
+
214
+ const params: ChatXAICompletionsInvocationParams =
215
+ model.invocationParams({
216
+ tools: [
217
+ {
218
+ type: XAI_LIVE_SEARCH_TOOL_TYPE,
219
+ name: XAI_LIVE_SEARCH_TOOL_NAME,
220
+ },
221
+ ],
222
+ } as unknown as ParsedCallOptions);
223
+
224
+ expect(params.search_parameters).toBeDefined();
225
+ expect(params.search_parameters?.mode).toBe('auto');
226
+ });
227
+
228
+ test('should add search_parameters from call options', () => {
229
+ const model = new ChatXAI();
230
+
231
+ const params: ChatXAICompletionsInvocationParams =
232
+ model.invocationParams({
233
+ searchParameters: {
234
+ mode: 'on',
235
+ max_search_results: 10,
236
+ from_date: '2024-01-01',
237
+ },
238
+ } as unknown as ParsedCallOptions);
239
+
240
+ expect(params.search_parameters).toEqual({
241
+ mode: 'on',
242
+ max_search_results: 10,
243
+ from_date: '2024-01-01',
244
+ });
245
+ });
246
+
247
+ test('should include sources in search_parameters when provided', () => {
248
+ const model = new ChatXAI();
249
+
250
+ const params: ChatXAICompletionsInvocationParams =
251
+ model.invocationParams({
252
+ searchParameters: {
253
+ mode: 'on',
254
+ sources: [
255
+ { type: 'web', allowed_websites: ['x.ai'] },
256
+ { type: 'news', excluded_websites: ['bbc.co.uk'] },
257
+ { type: 'x', included_x_handles: ['xai'] },
258
+ { type: 'rss', links: ['https://example.com/feed.rss'] },
259
+ ],
260
+ },
261
+ } as unknown as ParsedCallOptions);
262
+
263
+ expect(params.search_parameters).toBeDefined();
264
+ expect(params.search_parameters?.sources).toEqual([
265
+ { type: 'web', allowed_websites: ['x.ai'] },
266
+ { type: 'news', excluded_websites: ['bbc.co.uk'] },
267
+ { type: 'x', included_x_handles: ['xai'] },
268
+ { type: 'rss', links: ['https://example.com/feed.rss'] },
269
+ ]);
270
+ });
271
+
272
+ test('should omit sources field when none are configured', () => {
273
+ const model = new ChatXAI();
274
+
275
+ const params: ChatXAICompletionsInvocationParams =
276
+ model.invocationParams({
277
+ searchParameters: { mode: 'auto' },
278
+ } as unknown as ParsedCallOptions);
279
+
280
+ expect(params.search_parameters).toEqual({ mode: 'auto' });
281
+ expect(
282
+ Object.prototype.hasOwnProperty.call(
283
+ params.search_parameters as NonNullable<
284
+ ChatXAICompletionsInvocationParams['search_parameters']
285
+ >,
286
+ 'sources'
287
+ )
288
+ ).toBe(false);
289
+ });
290
+
291
+ test('should merge instance and call option search parameters', () => {
292
+ const model = new ChatXAI({
293
+ searchParameters: {
294
+ mode: 'auto',
295
+ max_search_results: 5,
296
+ return_citations: true,
297
+ },
298
+ });
299
+
300
+ const params: ChatXAICompletionsInvocationParams =
301
+ model.invocationParams({
302
+ searchParameters: { max_search_results: 10 },
303
+ } as unknown as ParsedCallOptions);
304
+
305
+ expect(params.search_parameters).toEqual({
306
+ mode: 'auto',
307
+ max_search_results: 10,
308
+ return_citations: true,
309
+ });
310
+ });
311
+
312
+ test('should not add search_parameters when no search config is present', () => {
313
+ const model = new ChatXAI();
314
+
315
+ const params: ChatXAICompletionsInvocationParams =
316
+ model.invocationParams({} as ParsedCallOptions);
317
+
318
+ expect(params.search_parameters).toBeUndefined();
319
+ });
320
+ });
321
+
322
+ describe('_hasBuiltInTools', () => {
323
+ test('should return true when live_search tool is present', () => {
324
+ const model = new ChatXAI();
325
+ const result = internals(model)._hasBuiltInTools([
326
+ { type: XAI_LIVE_SEARCH_TOOL_TYPE, name: XAI_LIVE_SEARCH_TOOL_NAME },
327
+ { type: 'function', function: { name: 'test', parameters: {} } },
328
+ ]);
329
+ expect(result).toBe(true);
330
+ });
331
+
332
+ test('should return false when no built-in tools are present', () => {
333
+ const model = new ChatXAI();
334
+ const result = internals(model)._hasBuiltInTools([
335
+ { type: 'function', function: { name: 'test', parameters: {} } },
336
+ ]);
337
+ expect(result).toBe(false);
338
+ });
339
+
340
+ test('should return false for undefined or empty tools', () => {
341
+ const model = new ChatXAI();
342
+ expect(internals(model)._hasBuiltInTools(undefined)).toBe(false);
343
+ expect(internals(model)._hasBuiltInTools([])).toBe(false);
344
+ });
345
+ });
346
+ });
347
+ });
348
+
349
+ // Parity check for the inherited native `streamEvents` protocol. Upstream's
350
+ // MockStreamChatXAI overrides `completionWithRetry` to feed raw OpenAI chunks;
351
+ // our fork's `ChatXAI` exposes the same method directly (it extends the
352
+ // completions class), so the mock transplants verbatim.
353
+ type RawChunks = ReturnType<typeof openAITextOnlyChunks>;
354
+
355
+ class MockStreamChatXAI extends ChatXAI {
356
+ private readonly mockChunks: RawChunks;
357
+
358
+ constructor(chunks: RawChunks) {
359
+ super({ apiKey: 'fake-key', model: 'grok-3', streaming: true });
360
+ this.mockChunks = chunks;
361
+ }
362
+
363
+ override async completionWithRetry(): Promise<never> {
364
+ const chunks = this.mockChunks;
365
+ return {
366
+ async *[Symbol.asyncIterator]() {
367
+ for (const chunk of chunks) {
368
+ yield chunk;
369
+ }
370
+ },
371
+ } as never;
372
+ }
373
+ }
374
+
375
+ function streamEvents(model: ChatXAI): AsyncGenerator<ChatModelStreamEvent> {
376
+ return (
377
+ model as unknown as {
378
+ _streamChatModelEvents(
379
+ messages: unknown[],
380
+ options: Record<string, unknown>
381
+ ): AsyncGenerator<ChatModelStreamEvent>;
382
+ }
383
+ )._streamChatModelEvents([], {});
384
+ }
385
+
386
+ describe('inherited xAI streamEvents (chat_models/tests/chat_models_stream_events.test.ts)', () => {
387
+ // Re-expressed from upstream's vitest matchers
388
+ // (`toHaveStreamText` / `toHaveStreamReasoning` / `toHaveStreamToolCalls`),
389
+ // which wrap exactly the `ChatModelStream` sub-streams asserted here.
390
+ test('streams text', async () => {
391
+ const stream = new ChatModelStream(
392
+ streamEvents(new MockStreamChatXAI(openAITextOnlyChunks()))
393
+ );
394
+ expect(await stream.text).toBe('Hello world');
395
+ });
396
+
397
+ test('streams reasoning', async () => {
398
+ const stream = new ChatModelStream(
399
+ streamEvents(new MockStreamChatXAI(openAIReasoningTextChunks()))
400
+ );
401
+ expect(await stream.reasoning).toBe('Let me reason...');
402
+ });
403
+
404
+ test('streams tool calls', async () => {
405
+ const stream = new ChatModelStream(
406
+ streamEvents(new MockStreamChatXAI(openAIToolCallChunks()))
407
+ );
408
+ const calls = await stream.toolCalls;
409
+ expect(calls).toEqual([
410
+ expect.objectContaining({
411
+ name: 'web_search',
412
+ args: { query: 'weather' },
413
+ }),
414
+ ]);
415
+ });
416
+ });