@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
|
@@ -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
|
+
});
|