@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,350 @@
1
+ // Inherited stream-events specs for the LibreChat Google fork
2
+ // (`CustomChatGoogleGenerativeAI` from `@/llm/google`), merged from two
3
+ // upstream-derived suites adapted to the fork's actual surface:
4
+ //
5
+ // 1. converter — Inherited from @langchain/google-genai@2.2.0
6
+ // src/utils/stream_events.test.ts (tests `convertGoogleGenAIStream`,
7
+ // the raw Gemini stream -> `ChatModelStreamEvent` converter).
8
+ // `convertGoogleGenAIStream` is NOT exported from the package root and the
9
+ // deep subpath (`dist/utils/stream_events.js`) is blocked by the package
10
+ // `exports` map (`ERR_PACKAGE_PATH_NOT_EXPORTED`), so it cannot be imported
11
+ // and unit-tested directly. Instead each converter case is routed through
12
+ // the fork's inherited `_streamChatModelEvents` (which calls the converter
13
+ // internally) and asserted on the collected `ChatModelStreamEvent[]`.
14
+ //
15
+ // 2. stream events — Inherited from @langchain/google-genai@2.2.0
16
+ // src/tests/chat_models_stream_events.test.ts (tests
17
+ // `ChatGoogleGenerativeAI.streamEvents()` typed sub-streams).
18
+ // The fork overrides the legacy `_streamResponseChunks` / `_generate` /
19
+ // `invocationParams` but NOT `_streamChatModelEvents`, so it inherits the
20
+ // native streamEvents protocol. The Google transport is mocked the way
21
+ // `src/llm/google/llm.spec.ts` accesses it: by spying on the private
22
+ // `client.generateContentStream`. The upstream suite used vitest custom
23
+ // matchers (`toHaveStreamText` / `toHaveStreamReasoning` /
24
+ // `toHaveStreamToolCalls` / `toHaveStreamUsage`) unavailable in jest; they
25
+ // are re-expressed against the public `ChatModelStream` sub-streams
26
+ // (`.text` / `.reasoning` / `.toolCalls` / `.usage`) that those matchers
27
+ // wrap.
28
+
29
+ import { HumanMessage, SystemMessage } from '@langchain/core/messages';
30
+ import { describe, test, expect, jest, afterEach } from '@jest/globals';
31
+ import { ChatModelStream } from '@langchain/core/language_models/stream';
32
+ import type { ChatModelStreamEvent } from '@langchain/core/language_models/event';
33
+ import type { GenerateContentRequest } from '@google/generative-ai';
34
+ import type { BaseMessage } from '@langchain/core/messages';
35
+ import { CustomChatGoogleGenerativeAI } from '@/llm/google';
36
+
37
+ type GeminiStreamChunk = Record<string, unknown>;
38
+
39
+ type TestGoogleGenAIClient = {
40
+ systemInstruction?: unknown;
41
+ generateContentStream: (
42
+ request: GenerateContentRequest,
43
+ options?: unknown
44
+ ) => Promise<{ stream: AsyncIterable<GeminiStreamChunk> }>;
45
+ };
46
+
47
+ type StreamEventsModel = {
48
+ _streamChatModelEvents: (
49
+ messages: BaseMessage[],
50
+ options: Record<string, unknown>
51
+ ) => AsyncGenerator<ChatModelStreamEvent>;
52
+ };
53
+
54
+ function getTestClient(
55
+ model: CustomChatGoogleGenerativeAI
56
+ ): TestGoogleGenAIClient {
57
+ return (model as unknown as { client: TestGoogleGenAIClient }).client;
58
+ }
59
+
60
+ function toStream(
61
+ chunks: GeminiStreamChunk[]
62
+ ): AsyncIterable<GeminiStreamChunk> {
63
+ return (async function* () {
64
+ for (const chunk of chunks) {
65
+ yield chunk;
66
+ }
67
+ })();
68
+ }
69
+
70
+ function newModel(): CustomChatGoogleGenerativeAI {
71
+ return new CustomChatGoogleGenerativeAI({
72
+ apiKey: 'fake-key',
73
+ model: 'gemini-2.0-flash',
74
+ });
75
+ }
76
+
77
+ function mockGoogleGenAI(
78
+ stream: AsyncIterable<GeminiStreamChunk>
79
+ ): CustomChatGoogleGenerativeAI {
80
+ const model = newModel();
81
+ jest
82
+ .spyOn(getTestClient(model), 'generateContentStream')
83
+ .mockResolvedValue({ stream });
84
+ return model;
85
+ }
86
+
87
+ function streamEvents(
88
+ model: CustomChatGoogleGenerativeAI,
89
+ messages: BaseMessage[] = [new HumanMessage('Hello')],
90
+ options: Record<string, unknown> = {}
91
+ ): AsyncGenerator<ChatModelStreamEvent> {
92
+ return (model as unknown as StreamEventsModel)._streamChatModelEvents(
93
+ messages,
94
+ options
95
+ );
96
+ }
97
+
98
+ async function collectEvents(
99
+ chunks: GeminiStreamChunk[],
100
+ options: Record<string, unknown> = {}
101
+ ): Promise<ChatModelStreamEvent[]> {
102
+ const events: ChatModelStreamEvent[] = [];
103
+ for await (const event of streamEvents(
104
+ mockGoogleGenAI(toStream(chunks)),
105
+ [new HumanMessage('Hello')],
106
+ options
107
+ )) {
108
+ events.push(event);
109
+ }
110
+ return events;
111
+ }
112
+
113
+ afterEach(() => {
114
+ jest.restoreAllMocks();
115
+ });
116
+
117
+ // Routed from upstream `convertGoogleGenAIStream` unit tests: the converter is
118
+ // not importable from the fork, so each case drives the inherited
119
+ // `_streamChatModelEvents` (which invokes the converter) and asserts the
120
+ // emitted `ChatModelStreamEvent[]`.
121
+ describe('convertGoogleGenAIStream (via inherited _streamChatModelEvents)', () => {
122
+ test('text-only streaming', async () => {
123
+ const events = await collectEvents([
124
+ { candidates: [{ content: { parts: [{ text: 'Hello' }] } }] },
125
+ { candidates: [{ content: { parts: [{ text: ' world' }] } }] },
126
+ ]);
127
+
128
+ expect(
129
+ events.find((e) => e.event === 'content-block-finish')
130
+ ).toMatchObject({
131
+ content: { text: 'Hello world' },
132
+ });
133
+ });
134
+
135
+ test('maps Gemini finish reasons', async () => {
136
+ const lengthEvents = await collectEvents([
137
+ {
138
+ candidates: [
139
+ {
140
+ content: { parts: [{ text: 'Hello' }] },
141
+ finishReason: 'MAX_TOKENS',
142
+ },
143
+ ],
144
+ },
145
+ ]);
146
+ expect(
147
+ lengthEvents.find((e) => e.event === 'message-finish')
148
+ ).toMatchObject({ reason: 'length' });
149
+
150
+ const filterEvents = await collectEvents([
151
+ {
152
+ candidates: [
153
+ {
154
+ content: { parts: [{ text: 'Hello' }] },
155
+ finishReason: 'SAFETY',
156
+ },
157
+ ],
158
+ },
159
+ ]);
160
+ expect(
161
+ filterEvents.find((e) => e.event === 'message-finish')
162
+ ).toMatchObject({ reason: 'content_filter' });
163
+ });
164
+
165
+ test('thought parts map to reasoning', async () => {
166
+ const events = await collectEvents([
167
+ {
168
+ candidates: [
169
+ {
170
+ content: { parts: [{ text: 'Let me think', thought: true }] },
171
+ },
172
+ ],
173
+ },
174
+ ]);
175
+
176
+ expect(
177
+ events.find(
178
+ (e) =>
179
+ e.event === 'content-block-finish' &&
180
+ (
181
+ e as Extract<
182
+ ChatModelStreamEvent,
183
+ { event: 'content-block-finish' }
184
+ >
185
+ ).content.type === 'reasoning'
186
+ )
187
+ ).toMatchObject({
188
+ content: { reasoning: 'Let me think' },
189
+ });
190
+ });
191
+ });
192
+
193
+ function geminiTextStream(): AsyncIterable<GeminiStreamChunk> {
194
+ return toStream([
195
+ { candidates: [{ content: { parts: [{ text: 'Hello' }] } }] },
196
+ { candidates: [{ content: { parts: [{ text: ' world' }] } }] },
197
+ ]);
198
+ }
199
+
200
+ function geminiReasoningStream(): AsyncIterable<GeminiStreamChunk> {
201
+ return toStream([
202
+ {
203
+ candidates: [
204
+ { content: { parts: [{ text: 'Let me reason...', thought: true }] } },
205
+ ],
206
+ },
207
+ ]);
208
+ }
209
+
210
+ function geminiToolStream(): AsyncIterable<GeminiStreamChunk> {
211
+ return toStream([
212
+ { candidates: [{ content: { parts: [{ text: 'Let me search.' }] } }] },
213
+ {
214
+ candidates: [
215
+ {
216
+ content: {
217
+ parts: [
218
+ {
219
+ functionCall: {
220
+ name: 'web_search',
221
+ args: { query: 'weather' },
222
+ },
223
+ },
224
+ ],
225
+ },
226
+ },
227
+ ],
228
+ },
229
+ ]);
230
+ }
231
+
232
+ function geminiUsageStream(): AsyncIterable<GeminiStreamChunk> {
233
+ return toStream([
234
+ {
235
+ usageMetadata: {
236
+ promptTokenCount: 10,
237
+ candidatesTokenCount: 4,
238
+ totalTokenCount: 14,
239
+ },
240
+ candidates: [{ content: { parts: [{ text: 'Hi' }] } }],
241
+ },
242
+ ]);
243
+ }
244
+
245
+ // Re-expressed from upstream `ChatGoogleGenerativeAI.streamEvents` describe.
246
+ // The vitest matchers (`toHaveStreamText` etc.) are unavailable in jest, so the
247
+ // cases assert the `ChatModelStream` sub-streams those matchers wrap.
248
+ describe('CustomChatGoogleGenerativeAI.streamEvents (sub-stream assertions)', () => {
249
+ test('streams text', async () => {
250
+ const stream = new ChatModelStream(
251
+ streamEvents(mockGoogleGenAI(geminiTextStream())) as never
252
+ );
253
+ expect(await stream.text).toBe('Hello world');
254
+ });
255
+
256
+ test('streams reasoning', async () => {
257
+ const stream = new ChatModelStream(
258
+ streamEvents(mockGoogleGenAI(geminiReasoningStream())) as never
259
+ );
260
+ expect(await stream.reasoning).toBe('Let me reason...');
261
+ });
262
+
263
+ test('streams tool calls', async () => {
264
+ const stream = new ChatModelStream(
265
+ streamEvents(mockGoogleGenAI(geminiToolStream())) as never
266
+ );
267
+ const calls = await stream.toolCalls;
268
+ expect(calls).toEqual([
269
+ expect.objectContaining({
270
+ name: 'web_search',
271
+ args: { query: 'weather' },
272
+ }),
273
+ ]);
274
+ });
275
+
276
+ test('streams usage', async () => {
277
+ const stream = new ChatModelStream(
278
+ streamEvents(
279
+ mockGoogleGenAI(geminiUsageStream()),
280
+ [new HumanMessage('Hello')],
281
+ { streamUsage: true }
282
+ ) as never
283
+ );
284
+ expect(await stream.usage).toMatchObject({
285
+ input_tokens: 10,
286
+ output_tokens: 4,
287
+ total_tokens: 14,
288
+ });
289
+ });
290
+
291
+ test('passes system instructions per streamEvents request', async () => {
292
+ const model = newModel();
293
+ const generateContentStream = jest
294
+ .spyOn(getTestClient(model), 'generateContentStream')
295
+ .mockResolvedValue({ stream: geminiTextStream() });
296
+
297
+ const stream = new ChatModelStream(
298
+ streamEvents(model, [
299
+ new SystemMessage('StreamV2 system instruction'),
300
+ new HumanMessage('Hello'),
301
+ ]) as never
302
+ );
303
+ expect(await stream.text).toBe('Hello world');
304
+
305
+ const [[request]] = generateContentStream.mock.calls;
306
+ expect(request.systemInstruction).toEqual({
307
+ role: 'system',
308
+ parts: [{ text: 'StreamV2 system instruction' }],
309
+ });
310
+ expect(request.contents).toEqual([
311
+ { role: 'user', parts: [{ text: 'Hello' }] },
312
+ ]);
313
+ expect(getTestClient(model).systemInstruction).toBeUndefined();
314
+ });
315
+
316
+ // FORK DIVERGENCE (not a bug): upstream's `_streamChatModelEvents` request
317
+ // builder sets `request.systemInstruction` per call and leaves
318
+ // `client.systemInstruction` undefined. The fork's overridden legacy
319
+ // `_streamResponseChunks` (used by `.stream()`) keeps the old
320
+ // `@google/generative-ai` convention instead: it assigns the system message
321
+ // to `client.systemInstruction` and omits it from the request. The system
322
+ // instruction is still applied correctly, just via a different mechanism, so
323
+ // this case asserts the fork's actual behavior rather than upstream's.
324
+ test('passes system instructions per stream request', async () => {
325
+ const model = newModel();
326
+ const generateContentStream = jest
327
+ .spyOn(getTestClient(model), 'generateContentStream')
328
+ .mockResolvedValue({ stream: geminiTextStream() });
329
+
330
+ const stream = await model.stream([
331
+ new SystemMessage('Stream system instruction'),
332
+ new HumanMessage('Hello'),
333
+ ]);
334
+ const chunks: string[] = [];
335
+ for await (const chunk of stream) {
336
+ chunks.push(chunk.text);
337
+ }
338
+
339
+ const [[request]] = generateContentStream.mock.calls;
340
+ expect(chunks.join('')).toBe('Hello world');
341
+ expect(request.systemInstruction).toBeUndefined();
342
+ expect(request.contents).toEqual([
343
+ { role: 'user', parts: [{ text: 'Hello' }] },
344
+ ]);
345
+ expect(getTestClient(model).systemInstruction).toEqual({
346
+ role: 'system',
347
+ parts: [{ text: 'Stream system instruction' }],
348
+ });
349
+ });
350
+ });
@@ -0,0 +1,347 @@
1
+ // Inherited DeepSeek specs for the LibreChat `@langchain/agents` fork, ported
2
+ // from three upstream @langchain/deepseek@1.1.3 suites and adapted to the
3
+ // fork's actual surface (`ChatDeepSeek` from `@/llm/openai`):
4
+ //
5
+ // 1. reasoning — Inherited from @langchain/deepseek@1.1.3
6
+ // src/tests/chat_models_reasoning.test.ts.
7
+ // Upstream drives `model.stream("hi")` through a `configuration.fetch`
8
+ // SSE mock. Our fork heavily overrides the legacy streaming path
9
+ // (`_streamResponseChunks` -> `_streamResponseChunksWithReasoning`) to
10
+ // split `<think>` fallback tags into `additional_kwargs.reasoning_content`.
11
+ // We exercise that override directly by overriding `completionWithRetry`
12
+ // (the `deepseek.test.ts` harness pattern) and feeding OpenAI-shaped
13
+ // chunks. The fork's `<think>` parser reproduces upstream's observable
14
+ // content/reasoning split exactly for every deterministic case below.
15
+ //
16
+ // 2. stream events — Inherited from @langchain/deepseek@1.1.3
17
+ // src/tests/chat_models_stream_events.test.ts.
18
+ // Our `ChatDeepSeek` does NOT override `_streamChatModelEvents`; it
19
+ // inherits the native `ChatModelStreamEvent` protocol from upstream's
20
+ // ChatOpenAICompletions, a separate code path from the legacy
21
+ // `_streamResponseChunks` override. These are kept as a parity check.
22
+ // Upstream's vitest custom matchers (`toHaveStreamText` /
23
+ // `toHaveStreamReasoning` / `toHaveStreamToolCalls`) do not exist in jest;
24
+ // they are re-expressed against the public `ChatModelStream` sub-streams
25
+ // (`.text` / `.reasoning` / `.toolCalls`) that those matchers wrap.
26
+ //
27
+ // 3. standard unit tests — Inherited from @langchain/deepseek@1.1.3
28
+ // src/tests/chat_models.standard.test.ts.
29
+ // Upstream runs `ChatModelUnitTests` from `@langchain/standard-tests/vitest`.
30
+ // That package is vitest-only and is not installed in this repo, so the
31
+ // harness class itself is dropped. Its deterministic, fork-relevant
32
+ // assertions (api-key precedence, tool-calling, structured output, model
33
+ // field) are re-expressed inline against the fork.
34
+
35
+ import { describe, it, test, expect } from '@jest/globals';
36
+ import { AIMessageChunk, HumanMessage } from '@langchain/core/messages';
37
+ import { ChatModelStream } from '@langchain/core/language_models/stream';
38
+ import {
39
+ openAIReasoningTextChunks,
40
+ openAITextOnlyChunks,
41
+ openAIToolCallChunks,
42
+ } from '@langchain/core/testing';
43
+ import type { ChatModelStreamEvent } from '@langchain/core/language_models/event';
44
+ import type { OpenAIClient } from '@langchain/openai';
45
+ import { ChatDeepSeek } from '@/llm/openai';
46
+
47
+ type RawChunk = OpenAIClient.Chat.Completions.ChatCompletionChunk;
48
+
49
+ function createContentChunk(content: string): RawChunk {
50
+ return {
51
+ id: 'chatcmpl-123',
52
+ object: 'chat.completion.chunk',
53
+ created: 0,
54
+ model: 'deepseek-chat',
55
+ choices: [
56
+ {
57
+ index: 0,
58
+ delta: { content },
59
+ finish_reason: null,
60
+ logprobs: null,
61
+ },
62
+ ],
63
+ };
64
+ }
65
+
66
+ function createStopChunk(): RawChunk {
67
+ return {
68
+ id: 'chatcmpl-123',
69
+ object: 'chat.completion.chunk',
70
+ created: 0,
71
+ model: 'deepseek-chat',
72
+ choices: [
73
+ {
74
+ index: 0,
75
+ delta: {},
76
+ finish_reason: 'stop',
77
+ logprobs: null,
78
+ },
79
+ ],
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Mirrors the `deepseek.test.ts` / smoke-test transport mock: override
85
+ * `completionWithRetry` so the legacy streaming path consumes fixed chunks.
86
+ */
87
+ class MockStreamChatDeepSeek extends ChatDeepSeek {
88
+ constructor(private readonly chunks: RawChunk[]) {
89
+ super({ apiKey: 'fake-key', model: 'deepseek-chat', streaming: true });
90
+ }
91
+
92
+ async completionWithRetry(
93
+ request: OpenAIClient.Chat.ChatCompletionCreateParamsStreaming,
94
+ requestOptions?: OpenAIClient.RequestOptions
95
+ ): Promise<AsyncIterable<RawChunk>>;
96
+ async completionWithRetry(
97
+ request: OpenAIClient.Chat.ChatCompletionCreateParamsNonStreaming,
98
+ requestOptions?: OpenAIClient.RequestOptions
99
+ ): Promise<OpenAIClient.Chat.Completions.ChatCompletion>;
100
+ async completionWithRetry(): Promise<
101
+ AsyncIterable<RawChunk> | OpenAIClient.Chat.Completions.ChatCompletion
102
+ > {
103
+ const chunks = this.chunks;
104
+ return {
105
+ async *[Symbol.asyncIterator](): AsyncGenerator<RawChunk> {
106
+ for (const chunk of chunks) {
107
+ yield chunk;
108
+ }
109
+ },
110
+ };
111
+ }
112
+ }
113
+
114
+ type AggregatedStream = { content: string; reasoning: string };
115
+
116
+ async function streamThinkContents(
117
+ contents: string[]
118
+ ): Promise<AggregatedStream> {
119
+ const chunks = [...contents.map(createContentChunk), createStopChunk()];
120
+ const model = new MockStreamChatDeepSeek(chunks);
121
+
122
+ const collected: AIMessageChunk[] = [];
123
+ for await (const chunk of await model.stream([new HumanMessage('hi')])) {
124
+ collected.push(chunk);
125
+ }
126
+
127
+ const content = collected
128
+ .map((chunk) => (typeof chunk.content === 'string' ? chunk.content : ''))
129
+ .join('');
130
+ const reasoning = collected
131
+ .map((chunk) => (chunk.additional_kwargs.reasoning_content as string) ?? '')
132
+ .join('');
133
+ return { content, reasoning };
134
+ }
135
+
136
+ function toDeepSeekChunks(
137
+ chunks: ReturnType<typeof openAITextOnlyChunks>
138
+ ): RawChunk[] {
139
+ return chunks.map((chunk) => ({
140
+ ...chunk,
141
+ id: chunk.id ?? 'chatcmpl-test',
142
+ object: 'chat.completion.chunk',
143
+ created: 0,
144
+ model: chunk.model ?? 'deepseek-chat',
145
+ service_tier: null,
146
+ choices: (chunk.choices ?? []).map((choice) => ({
147
+ ...choice,
148
+ delta: choice.delta ?? {},
149
+ })) as RawChunk['choices'],
150
+ })) as RawChunk[];
151
+ }
152
+
153
+ type StreamEventsModel = {
154
+ _streamChatModelEvents: (
155
+ messages: unknown[],
156
+ options: Record<string, unknown>
157
+ ) => AsyncGenerator<ChatModelStreamEvent>;
158
+ };
159
+
160
+ function streamEvents(
161
+ model: ChatDeepSeek
162
+ ): AsyncGenerator<ChatModelStreamEvent> {
163
+ return (model as unknown as StreamEventsModel)._streamChatModelEvents([], {});
164
+ }
165
+
166
+ describe('ChatDeepSeek (inherited @langchain/deepseek@1.1.3 reasoning)', () => {
167
+ // Dropped (inherited): `new ChatDeepSeek("deepseek-chat", {...})` shorthand —
168
+ // the fork constructor only accepts a single fields object
169
+ // (ConstructorParameters<typeof OriginalChatDeepSeek>[0]); the positional
170
+ // (model, fields) overload does not type-check. Model-field init is covered
171
+ // in the standard-unit-tests describe below.
172
+
173
+ test('separates <think> tags into reasoning_content', async () => {
174
+ const { content, reasoning } = await streamThinkContents([
175
+ '<think>',
176
+ 'thinking process...',
177
+ '</think>',
178
+ 'Hello world',
179
+ ]);
180
+
181
+ // Parity with upstream: tags stripped, thought routed to reasoning.
182
+ expect(content).toBe('Hello world');
183
+ expect(reasoning).toBe('thinking process...');
184
+ });
185
+
186
+ test('handles multiple think blocks and content before/after', async () => {
187
+ const { content, reasoning } = await streamThinkContents([
188
+ 'Start ',
189
+ '<think>',
190
+ 'Reason 1',
191
+ '</think>',
192
+ ' Middle ',
193
+ '<think>',
194
+ 'Reason 2',
195
+ '</think>',
196
+ ' End',
197
+ ]);
198
+
199
+ // Parity with upstream.
200
+ expect(content).toBe('Start Middle End');
201
+ expect(reasoning).toBe('Reason 1Reason 2');
202
+ });
203
+
204
+ test('handles unclosed think tags (flush at end)', async () => {
205
+ const { reasoning } = await streamThinkContents([
206
+ 'Start ',
207
+ '<think>',
208
+ 'Unclosed thought',
209
+ ]);
210
+
211
+ // Parity with upstream: trailing open block flushes as reasoning.
212
+ expect(reasoning).toBe('Unclosed thought');
213
+ });
214
+
215
+ test('handles split tags across chunks', async () => {
216
+ const { reasoning } = await streamThinkContents([
217
+ '<th',
218
+ 'ink>Thought',
219
+ '</th',
220
+ 'ink>',
221
+ ]);
222
+
223
+ // Parity with upstream: partial-tag buffering reassembles boundaries.
224
+ expect(reasoning).toBe('Thought');
225
+ });
226
+
227
+ test('handles empty think blocks', async () => {
228
+ const { content, reasoning } = await streamThinkContents([
229
+ 'Before ',
230
+ '<think>',
231
+ '</think>',
232
+ ' After',
233
+ ]);
234
+
235
+ // Parity with upstream.
236
+ expect(content).toBe('Before After');
237
+ expect(reasoning).toBe('');
238
+ });
239
+
240
+ test('handles nested think tags (inner treated as reasoning text)', async () => {
241
+ const { content, reasoning } = await streamThinkContents([
242
+ '<think>',
243
+ 'Outer ',
244
+ '<think>',
245
+ 'Inner',
246
+ '</think>',
247
+ ' Content',
248
+ ]);
249
+
250
+ // Parity with upstream: first </think> closes the outer block; the inner
251
+ // <think> is kept verbatim inside the reasoning text.
252
+ expect(content).toBe(' Content');
253
+ expect(reasoning).toBe('Outer <think>Inner');
254
+ });
255
+
256
+ test('handles malformed tags gracefully', async () => {
257
+ const { content } = await streamThinkContents([
258
+ '</think>',
259
+ 'Text ',
260
+ '<think',
261
+ ' more',
262
+ ]);
263
+
264
+ // Parity with upstream's loose assertion: orphan/incomplete tags are
265
+ // surfaced as content. The fork emits the full
266
+ // "</think>Text <think more" verbatim.
267
+ expect(content).toContain('Text');
268
+ });
269
+ });
270
+
271
+ describe('ChatDeepSeek.streamEvents (inherited @langchain/deepseek@1.1.3, native path)', () => {
272
+ // The fork inherits `_streamChatModelEvents` from upstream's completions
273
+ // model (no override), so the native ChatModelStreamEvent protocol behaves
274
+ // identically — kept as a parity check.
275
+
276
+ test('streams text', async () => {
277
+ const model = new MockStreamChatDeepSeek(
278
+ toDeepSeekChunks(openAITextOnlyChunks())
279
+ );
280
+ const stream = new ChatModelStream(streamEvents(model));
281
+ expect(await stream.text).toBe('Hello world');
282
+ });
283
+
284
+ test('streams reasoning', async () => {
285
+ const model = new MockStreamChatDeepSeek(
286
+ toDeepSeekChunks(openAIReasoningTextChunks())
287
+ );
288
+ const stream = new ChatModelStream(streamEvents(model));
289
+ expect(await stream.reasoning).toBe('Let me reason...');
290
+ });
291
+
292
+ test('streams tool calls', async () => {
293
+ const model = new MockStreamChatDeepSeek(
294
+ toDeepSeekChunks(openAIToolCallChunks())
295
+ );
296
+ const stream = new ChatModelStream(streamEvents(model));
297
+ const calls = await stream.toolCalls;
298
+ expect(calls).toEqual([
299
+ expect.objectContaining({
300
+ name: 'web_search',
301
+ args: { query: 'weather' },
302
+ }),
303
+ ]);
304
+ });
305
+ });
306
+
307
+ describe('ChatDeepSeek (inherited @langchain/deepseek@1.1.3 standard unit tests)', () => {
308
+ // Re-expressed from upstream's `ChatModelUnitTests` standard suite, which
309
+ // imports `@langchain/standard-tests/vitest` (vitest-only, not installed).
310
+ // The harness class is dropped; its deterministic assertions are inlined.
311
+
312
+ it('initializes the model field from constructor args', () => {
313
+ process.env.DEEPSEEK_API_KEY = 'test';
314
+ const model = new ChatDeepSeek({ model: 'deepseek-chat', apiKey: 'test' });
315
+ expect(model.model).toBe('deepseek-chat');
316
+ });
317
+
318
+ it('reads the api key from the apiKey field when the env var is unset', () => {
319
+ process.env.DEEPSEEK_API_KEY = '';
320
+ const model = new ChatDeepSeek({
321
+ apiKey: 'arg-key',
322
+ model: 'deepseek-chat',
323
+ });
324
+ expect(model.apiKey).toBe('arg-key');
325
+ process.env.DEEPSEEK_API_KEY = 'test';
326
+ });
327
+
328
+ it('throws when no api key is provided via field or env', () => {
329
+ process.env.DEEPSEEK_API_KEY = '';
330
+ expect(() => new ChatDeepSeek({ model: 'deepseek-chat' })).toThrow(
331
+ /API key not found/i
332
+ );
333
+ process.env.DEEPSEEK_API_KEY = 'test';
334
+ });
335
+
336
+ it('supports tool calling (bindTools) and structured output', () => {
337
+ process.env.DEEPSEEK_API_KEY = 'test';
338
+ const model = new ChatDeepSeek({ model: 'deepseek-chat', apiKey: 'test' });
339
+ expect(typeof model.bindTools).toBe('function');
340
+ expect(typeof model.withStructuredOutput).toBe('function');
341
+ expect(model.bindTools([])).toBeDefined();
342
+ });
343
+ });
344
+
345
+ // Dropped (inherited): live `.invoke()` / `.stream()`-to-API cases — none of
346
+ // the three source suites contained network-backed cases; all upstream
347
+ // scenarios were deterministic mocked transport.