@librechat/agents 3.1.85 → 3.1.87

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 (166) hide show
  1. package/README.md +69 -0
  2. package/dist/cjs/agents/AgentContext.cjs +7 -2
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/events.cjs +23 -0
  5. package/dist/cjs/events.cjs.map +1 -1
  6. package/dist/cjs/graphs/Graph.cjs +133 -18
  7. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  10. package/dist/cjs/llm/anthropic/index.cjs +251 -53
  11. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  12. package/dist/cjs/llm/init.cjs +1 -5
  13. package/dist/cjs/llm/init.cjs.map +1 -1
  14. package/dist/cjs/llm/openai/index.cjs +113 -24
  15. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  16. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openrouter/index.cjs +3 -1
  18. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  19. package/dist/cjs/main.cjs +18 -5
  20. package/dist/cjs/main.cjs.map +1 -1
  21. package/dist/cjs/openai/index.cjs +253 -0
  22. package/dist/cjs/openai/index.cjs.map +1 -0
  23. package/dist/cjs/responses/index.cjs +448 -0
  24. package/dist/cjs/responses/index.cjs.map +1 -0
  25. package/dist/cjs/run.cjs +108 -7
  26. package/dist/cjs/run.cjs.map +1 -1
  27. package/dist/cjs/session/AgentSession.cjs +1057 -0
  28. package/dist/cjs/session/AgentSession.cjs.map +1 -0
  29. package/dist/cjs/session/JsonlSessionStore.cjs +425 -0
  30. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -0
  31. package/dist/cjs/session/handlers.cjs +221 -0
  32. package/dist/cjs/session/handlers.cjs.map +1 -0
  33. package/dist/cjs/session/ids.cjs +22 -0
  34. package/dist/cjs/session/ids.cjs.map +1 -0
  35. package/dist/cjs/session/messageSerialization.cjs +179 -0
  36. package/dist/cjs/session/messageSerialization.cjs.map +1 -0
  37. package/dist/cjs/stream.cjs +472 -11
  38. package/dist/cjs/stream.cjs.map +1 -1
  39. package/dist/cjs/summarization/node.cjs +1 -1
  40. package/dist/cjs/summarization/node.cjs.map +1 -1
  41. package/dist/cjs/tools/ToolNode.cjs +177 -59
  42. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  43. package/dist/cjs/tools/eagerEventExecution.cjs +113 -0
  44. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -0
  45. package/dist/cjs/tools/handlers.cjs +1 -1
  46. package/dist/cjs/tools/handlers.cjs.map +1 -1
  47. package/dist/cjs/tools/streamedToolCallSeals.cjs +42 -0
  48. package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -0
  49. package/dist/esm/agents/AgentContext.mjs +7 -2
  50. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  51. package/dist/esm/events.mjs +23 -1
  52. package/dist/esm/events.mjs.map +1 -1
  53. package/dist/esm/graphs/Graph.mjs +133 -18
  54. package/dist/esm/graphs/Graph.mjs.map +1 -1
  55. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  56. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  57. package/dist/esm/llm/anthropic/index.mjs +251 -53
  58. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  59. package/dist/esm/llm/init.mjs +1 -5
  60. package/dist/esm/llm/init.mjs.map +1 -1
  61. package/dist/esm/llm/openai/index.mjs +113 -25
  62. package/dist/esm/llm/openai/index.mjs.map +1 -1
  63. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  64. package/dist/esm/llm/openrouter/index.mjs +4 -2
  65. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  66. package/dist/esm/main.mjs +5 -1
  67. package/dist/esm/main.mjs.map +1 -1
  68. package/dist/esm/openai/index.mjs +246 -0
  69. package/dist/esm/openai/index.mjs.map +1 -0
  70. package/dist/esm/responses/index.mjs +440 -0
  71. package/dist/esm/responses/index.mjs.map +1 -0
  72. package/dist/esm/run.mjs +108 -7
  73. package/dist/esm/run.mjs.map +1 -1
  74. package/dist/esm/session/AgentSession.mjs +1054 -0
  75. package/dist/esm/session/AgentSession.mjs.map +1 -0
  76. package/dist/esm/session/JsonlSessionStore.mjs +422 -0
  77. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -0
  78. package/dist/esm/session/handlers.mjs +219 -0
  79. package/dist/esm/session/handlers.mjs.map +1 -0
  80. package/dist/esm/session/ids.mjs +17 -0
  81. package/dist/esm/session/ids.mjs.map +1 -0
  82. package/dist/esm/session/messageSerialization.mjs +173 -0
  83. package/dist/esm/session/messageSerialization.mjs.map +1 -0
  84. package/dist/esm/stream.mjs +473 -12
  85. package/dist/esm/stream.mjs.map +1 -1
  86. package/dist/esm/summarization/node.mjs +1 -1
  87. package/dist/esm/summarization/node.mjs.map +1 -1
  88. package/dist/esm/tools/ToolNode.mjs +177 -59
  89. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  90. package/dist/esm/tools/eagerEventExecution.mjs +107 -0
  91. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -0
  92. package/dist/esm/tools/handlers.mjs +1 -1
  93. package/dist/esm/tools/handlers.mjs.map +1 -1
  94. package/dist/esm/tools/streamedToolCallSeals.mjs +36 -0
  95. package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -0
  96. package/dist/types/events.d.ts +1 -0
  97. package/dist/types/graphs/Graph.d.ts +24 -9
  98. package/dist/types/index.d.ts +1 -0
  99. package/dist/types/llm/openai/index.d.ts +1 -0
  100. package/dist/types/openai/index.d.ts +75 -0
  101. package/dist/types/responses/index.d.ts +97 -0
  102. package/dist/types/run.d.ts +2 -0
  103. package/dist/types/session/AgentSession.d.ts +32 -0
  104. package/dist/types/session/JsonlSessionStore.d.ts +67 -0
  105. package/dist/types/session/handlers.d.ts +8 -0
  106. package/dist/types/session/ids.d.ts +4 -0
  107. package/dist/types/session/index.d.ts +5 -0
  108. package/dist/types/session/messageSerialization.d.ts +7 -0
  109. package/dist/types/session/types.d.ts +191 -0
  110. package/dist/types/tools/ToolNode.d.ts +12 -1
  111. package/dist/types/tools/eagerEventExecution.d.ts +23 -0
  112. package/dist/types/tools/streamedToolCallSeals.d.ts +13 -0
  113. package/dist/types/types/hitl.d.ts +4 -0
  114. package/dist/types/types/run.d.ts +11 -1
  115. package/dist/types/types/tools.d.ts +36 -0
  116. package/package.json +19 -2
  117. package/src/__tests__/stream.eagerEventExecution.test.ts +2458 -0
  118. package/src/agents/AgentContext.ts +7 -2
  119. package/src/agents/__tests__/AgentContext.test.ts +254 -5
  120. package/src/events.ts +29 -0
  121. package/src/graphs/Graph.ts +224 -50
  122. package/src/graphs/MultiAgentGraph.ts +1 -1
  123. package/src/graphs/__tests__/composition.smoke.test.ts +30 -0
  124. package/src/index.ts +3 -0
  125. package/src/llm/anthropic/index.ts +356 -84
  126. package/src/llm/anthropic/llm.spec.ts +64 -0
  127. package/src/llm/custom-chat-models.smoke.test.ts +175 -4
  128. package/src/llm/openai/contentBlocks.test.ts +35 -0
  129. package/src/llm/openai/deepseek.test.ts +201 -2
  130. package/src/llm/openai/index.ts +171 -26
  131. package/src/llm/openai/utils/index.ts +22 -0
  132. package/src/llm/openrouter/index.ts +4 -2
  133. package/src/openai/__tests__/openai.test.ts +337 -0
  134. package/src/openai/index.ts +404 -0
  135. package/src/responses/__tests__/responses.test.ts +652 -0
  136. package/src/responses/index.ts +677 -0
  137. package/src/run.ts +158 -8
  138. package/src/scripts/compare_pi_vs_ours.ts +592 -173
  139. package/src/scripts/session_live.ts +548 -0
  140. package/src/session/AgentSession.ts +1432 -0
  141. package/src/session/JsonlSessionStore.ts +572 -0
  142. package/src/session/__tests__/JsonlSessionStore.test.ts +1410 -0
  143. package/src/session/__tests__/handlers.test.ts +161 -0
  144. package/src/session/handlers.ts +272 -0
  145. package/src/session/ids.ts +17 -0
  146. package/src/session/index.ts +44 -0
  147. package/src/session/messageSerialization.ts +207 -0
  148. package/src/session/types.ts +275 -0
  149. package/src/specs/custom-event-await.test.ts +89 -0
  150. package/src/specs/summarization.test.ts +1 -1
  151. package/src/stream.ts +755 -48
  152. package/src/summarization/node.ts +1 -1
  153. package/src/tools/ToolNode.ts +299 -126
  154. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +373 -0
  155. package/src/tools/__tests__/handlers.test.ts +2 -1
  156. package/src/tools/__tests__/hitl.test.ts +206 -110
  157. package/src/tools/eagerEventExecution.ts +153 -0
  158. package/src/tools/handlers.ts +8 -4
  159. package/src/tools/streamedToolCallSeals.ts +57 -0
  160. package/src/types/hitl.ts +4 -0
  161. package/src/types/run.ts +11 -0
  162. package/src/types/tools.ts +36 -0
  163. package/dist/cjs/llm/text.cjs +0 -69
  164. package/dist/cjs/llm/text.cjs.map +0 -1
  165. package/dist/esm/llm/text.mjs +0 -67
  166. package/dist/esm/llm/text.mjs.map +0 -1
@@ -3,13 +3,15 @@ import {
3
3
  AIMessageChunk,
4
4
  HumanMessage,
5
5
  } from '@langchain/core/messages';
6
+ import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
6
7
  import type { OpenAIChatInput, OpenAIClient } from '@langchain/openai';
7
- import type { ChatOpenRouterCallOptions } from '@/llm/openrouter';
8
- import type { CustomAnthropicInput } from '@/llm/anthropic';
8
+ import type { ChatGenerationChunk } from '@langchain/core/outputs';
9
9
  import type {
10
10
  ChatAnthropicToolType,
11
11
  AnthropicMCPServerURLDefinition,
12
12
  } from '@/llm/anthropic/types';
13
+ import type { ChatOpenRouterCallOptions } from '@/llm/openrouter';
14
+ import type { CustomAnthropicInput } from '@/llm/anthropic';
13
15
  import {
14
16
  ChatXAI,
15
17
  ChatOpenAI,
@@ -80,6 +82,19 @@ type OpenAIStreamEvent = {
80
82
  type OpenAIStreamItem =
81
83
  | OpenAIClient.Chat.Completions.ChatCompletionChunk
82
84
  | OpenAIStreamEvent;
85
+ type FetchOutcome = 'resolved' | 'rejected' | 'pending';
86
+ type AbortableFetchCapture = {
87
+ fetch: typeof fetch;
88
+ getSignal: () => AbortSignal | undefined;
89
+ };
90
+ type FetchTimeoutClient = {
91
+ fetchWithTimeout: (
92
+ url: RequestInfo,
93
+ init: RequestInit | undefined,
94
+ ms: number,
95
+ controller: AbortController
96
+ ) => Promise<Response>;
97
+ };
83
98
  type MockableCompletionCreate = (
84
99
  request: unknown,
85
100
  options?: unknown
@@ -133,6 +148,18 @@ type CompletionUsageWithCacheWrite = Omit<
133
148
  };
134
149
  type OpenAIStreamModel = ChatOpenAI | AzureChatOpenAI;
135
150
 
151
+ class CallbackTestChatOpenRouter extends ChatOpenRouter {
152
+ streamChunksWithCallbacks(
153
+ runManager?: CallbackManagerForLLMRun
154
+ ): AsyncGenerator<ChatGenerationChunk> {
155
+ return this._streamResponseChunks(
156
+ [new HumanMessage('hi')],
157
+ {} as this['ParsedCallOptions'],
158
+ runManager
159
+ );
160
+ }
161
+ }
162
+
136
163
  const baseAzureFields = {
137
164
  azureOpenAIApiKey: 'test-azure-key',
138
165
  azureOpenAIApiVersion: '2024-10-21',
@@ -140,6 +167,55 @@ const baseAzureFields = {
140
167
  azureOpenAIApiDeploymentName: 'test-deployment',
141
168
  };
142
169
 
170
+ const waitForFetchOutcome = (
171
+ promise: Promise<Response>,
172
+ timeoutMs = 100
173
+ ): Promise<FetchOutcome> =>
174
+ Promise.race([
175
+ promise.then(
176
+ () => 'resolved' as const,
177
+ () => 'rejected' as const
178
+ ),
179
+ new Promise<'pending'>((resolve) => {
180
+ setTimeout(() => resolve('pending'), timeoutMs);
181
+ }),
182
+ ]);
183
+
184
+ const createAbortableFetch = (): AbortableFetchCapture => {
185
+ let requestSignal: AbortSignal | undefined;
186
+ return {
187
+ fetch: async (_url, init): Promise<Response> =>
188
+ new Promise<Response>((_resolve, reject) => {
189
+ requestSignal = init?.signal ?? undefined;
190
+ requestSignal?.addEventListener(
191
+ 'abort',
192
+ () => reject(new Error('Aborted')),
193
+ { once: true }
194
+ );
195
+ }),
196
+ getSignal: () => requestSignal,
197
+ };
198
+ };
199
+
200
+ const expectFetchTimeoutAbort = async (
201
+ client: FetchTimeoutClient,
202
+ capturedFetch: AbortableFetchCapture,
203
+ url: string
204
+ ): Promise<void> => {
205
+ const controller = new AbortController();
206
+
207
+ const response = client.fetchWithTimeout(
208
+ url,
209
+ { method: 'post' },
210
+ 10,
211
+ controller
212
+ );
213
+
214
+ await expect(waitForFetchOutcome(response)).resolves.toBe('rejected');
215
+ expect(controller.signal.aborted).toBe(true);
216
+ expect(capturedFetch.getSignal()?.aborted).toBe(true);
217
+ };
218
+
143
219
  const baseBedrockFields = {
144
220
  region: 'us-east-1',
145
221
  credentials: {
@@ -711,8 +787,7 @@ describe('custom chat model class smoke tests', () => {
711
787
  }
712
788
 
713
789
  const usageChunk = chunks.find(
714
- (chunk) =>
715
- chunk.usage_metadata?.input_token_details?.cache_creation === 5
790
+ (chunk) => chunk.usage_metadata?.input_token_details?.cache_creation === 5
716
791
  );
717
792
  expect(usageChunk?.usage_metadata).toEqual({
718
793
  input_tokens: 11,
@@ -730,6 +805,44 @@ describe('custom chat model class smoke tests', () => {
730
805
  });
731
806
  });
732
807
 
808
+ it('emits OpenRouter callbacks before an early stream break', async () => {
809
+ const model = new CallbackTestChatOpenRouter({
810
+ model: 'openai/gpt-4o-mini',
811
+ apiKey: 'test-key',
812
+ _lc_stream_delay: 1,
813
+ } as OpenRouterFields & { _lc_stream_delay: number });
814
+ const completions = (model as unknown as StreamingCompletionBackedModel)
815
+ .completions;
816
+ const textChunks: string[] = [];
817
+ const callbackTokens: string[] = [];
818
+
819
+ async function* streamChunks(): AsyncGenerator<OpenAIClient.Chat.Completions.ChatCompletionChunk> {
820
+ yield createOpenAIStreamChunk('alpha beta gamma');
821
+ }
822
+
823
+ completions.completionWithRetry = async (): Promise<
824
+ AsyncIterable<OpenAIClient.Chat.Completions.ChatCompletionChunk>
825
+ > => streamChunks();
826
+
827
+ const runManager = {
828
+ handleLLMNewToken(token: string): void {
829
+ if (token !== '') {
830
+ callbackTokens.push(token);
831
+ }
832
+ },
833
+ } as unknown as CallbackManagerForLLMRun;
834
+
835
+ for await (const chunk of model.streamChunksWithCallbacks(runManager)) {
836
+ if (chunk.text !== '') {
837
+ textChunks.push(chunk.text);
838
+ }
839
+ break;
840
+ }
841
+
842
+ expect(textChunks).toEqual(['alpha beta gamma']);
843
+ expect(callbackTokens).toEqual(textChunks);
844
+ });
845
+
733
846
  it('keeps Anthropic output, residency, compaction, and stream-delay options', () => {
734
847
  const contextManagement = {
735
848
  edits: [
@@ -748,6 +861,10 @@ describe('custom chat model class smoke tests', () => {
748
861
  contextManagement,
749
862
  _lc_stream_delay: 8,
750
863
  });
864
+ const defaultModel = new CustomAnthropic({
865
+ model: 'claude-sonnet-4-5-20250929',
866
+ apiKey: 'test-key',
867
+ });
751
868
 
752
869
  const params = model.invocationParams({
753
870
  outputConfig: { effort: 'low' },
@@ -756,6 +873,7 @@ describe('custom chat model class smoke tests', () => {
756
873
 
757
874
  expect(CustomAnthropic.lc_name()).toBe('LibreChatAnthropic');
758
875
  expect(model._lc_stream_delay).toBe(8);
876
+ expect(defaultModel._lc_stream_delay).toBe(25);
759
877
  expect(params.output_config).toEqual({ effort: 'low' });
760
878
  expect(params.inference_geo).toBe('eu');
761
879
  expect(params.context_management).toEqual(contextManagement);
@@ -909,4 +1027,57 @@ describe('custom chat model class smoke tests', () => {
909
1027
  expect(method).toBe('PATCH');
910
1028
  expect(client.abortHandler).toBeDefined();
911
1029
  });
1030
+
1031
+ it('aborts custom OpenAI fetches when the request timeout elapses', async () => {
1032
+ const capturedFetch = createAbortableFetch();
1033
+ const client = new CustomOpenAIClient({
1034
+ apiKey: 'test-key',
1035
+ fetch: capturedFetch.fetch,
1036
+ });
1037
+
1038
+ await expectFetchTimeoutAbort(
1039
+ client,
1040
+ capturedFetch,
1041
+ 'https://example.test/v1/chat/completions'
1042
+ );
1043
+ });
1044
+
1045
+ it('aborts custom Azure OpenAI fetches when the request timeout elapses', async () => {
1046
+ const capturedFetch = createAbortableFetch();
1047
+ const client = new CustomAzureOpenAIClient({
1048
+ apiKey: 'test-azure-key',
1049
+ apiVersion: '2024-10-21',
1050
+ baseURL: 'https://example.test/openai/deployments/test-deployment',
1051
+ fetch: capturedFetch.fetch,
1052
+ });
1053
+
1054
+ await expectFetchTimeoutAbort(
1055
+ client,
1056
+ capturedFetch,
1057
+ 'https://example.test/openai/deployments/test-deployment/chat/completions'
1058
+ );
1059
+ });
1060
+
1061
+ it('propagates caller abort signals to custom OpenAI fetches', async () => {
1062
+ const capturedFetch = createAbortableFetch();
1063
+ const client = new CustomOpenAIClient({
1064
+ apiKey: 'test-key',
1065
+ fetch: capturedFetch.fetch,
1066
+ });
1067
+ const callerController = new AbortController();
1068
+ const requestController = new AbortController();
1069
+
1070
+ const response = client.fetchWithTimeout(
1071
+ 'https://example.test/v1/chat/completions',
1072
+ { method: 'post', signal: callerController.signal },
1073
+ 1000,
1074
+ requestController
1075
+ );
1076
+
1077
+ callerController.abort();
1078
+
1079
+ await expect(waitForFetchOutcome(response)).resolves.toBe('rejected');
1080
+ expect(requestController.signal.aborted).toBe(true);
1081
+ expect(capturedFetch.getSignal()?.aborted).toBe(true);
1082
+ });
912
1083
  });
@@ -4,6 +4,12 @@ import {
4
4
  AIMessageChunk,
5
5
  type ContentBlock,
6
6
  } from '@langchain/core/messages';
7
+ import {
8
+ STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
9
+ STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
10
+ OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
11
+ } from '@/tools/streamedToolCallSeals';
12
+ import { _convertOpenAIResponsesDeltaToBaseMessageChunk } from './utils';
7
13
 
8
14
  describe('OpenAI content block translator compatibility', () => {
9
15
  describe('Chat Completions', () => {
@@ -107,6 +113,35 @@ describe('OpenAI content block translator compatibility', () => {
107
113
  });
108
114
 
109
115
  describe('Responses', () => {
116
+ test('marks Responses function call arguments done as an explicit tool-call seal', () => {
117
+ const chunk = _convertOpenAIResponsesDeltaToBaseMessageChunk({
118
+ type: 'response.function_call_arguments.done',
119
+ sequence_number: 3,
120
+ item_id: 'fc_123',
121
+ output_index: 1,
122
+ name: 'search',
123
+ arguments: '{"query":"weather"}',
124
+ } as Parameters<typeof _convertOpenAIResponsesDeltaToBaseMessageChunk>[0]);
125
+ const message = chunk?.message as AIMessageChunk | undefined;
126
+
127
+ expect(message?.tool_call_chunks).toEqual([
128
+ {
129
+ type: 'tool_call_chunk',
130
+ name: 'search',
131
+ args: '{"query":"weather"}',
132
+ index: 1,
133
+ },
134
+ ]);
135
+ expect(message?.response_metadata).toMatchObject({
136
+ [STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY]:
137
+ OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
138
+ [STREAMED_TOOL_CALL_SEAL_METADATA_KEY]: {
139
+ kind: 'single',
140
+ index: 1,
141
+ },
142
+ });
143
+ });
144
+
110
145
  test('translates Responses messages to v1 content blocks', () => {
111
146
  const code = ['print(', 'hello', ')'].join(String.fromCharCode(39));
112
147
  const responseTextBlock = {
@@ -1,4 +1,5 @@
1
1
  import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
2
+ import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
2
3
  import type { ChatGenerationChunk } from '@langchain/core/outputs';
3
4
  import type { BaseMessage } from '@langchain/core/messages';
4
5
  import type { OpenAIClient } from '@langchain/openai';
@@ -66,6 +67,16 @@ class CapturingChatDeepSeek extends ChatDeepSeek {
66
67
  signal,
67
68
  } as this['ParsedCallOptions']);
68
69
  }
70
+
71
+ streamChunksWithCallbacks(
72
+ runManager?: CallbackManagerForLLMRun
73
+ ): AsyncGenerator<ChatGenerationChunk> {
74
+ return this._streamResponseChunks(
75
+ [new HumanMessage('hi')],
76
+ {} as this['ParsedCallOptions'],
77
+ runManager
78
+ );
79
+ }
69
80
  }
70
81
 
71
82
  function createToolContextMessages(): BaseMessage[] {
@@ -111,7 +122,10 @@ function createCompletionStreamChunks(): OpenAIChatCompletionChunk[] {
111
122
  ];
112
123
  }
113
124
 
114
- function createContentChunk(content: string): OpenAIChatCompletionChunk {
125
+ function createContentChunk(
126
+ content: string,
127
+ logprobs: OpenAIChatCompletionChunk['choices'][number]['logprobs'] = null
128
+ ): OpenAIChatCompletionChunk {
115
129
  return {
116
130
  id: 'chatcmpl-deepseek-test',
117
131
  object: 'chat.completion.chunk',
@@ -125,7 +139,7 @@ function createContentChunk(content: string): OpenAIChatCompletionChunk {
125
139
  content,
126
140
  },
127
141
  finish_reason: null,
128
- logprobs: null,
142
+ logprobs,
129
143
  },
130
144
  ],
131
145
  };
@@ -489,4 +503,189 @@ describe('ChatDeepSeek', () => {
489
503
 
490
504
  await expect(iterator.next()).rejects.toThrow('AbortError');
491
505
  });
506
+
507
+ it('does not yield a delayed DeepSeek chunk after abort', async () => {
508
+ const controller = new AbortController();
509
+ const model = new CapturingChatDeepSeek(
510
+ {
511
+ apiKey: 'test-key',
512
+ model: 'deepseek-v4-pro',
513
+ streaming: true,
514
+ _lc_stream_delay: 1000,
515
+ },
516
+ [createContentChunk('first '), createContentChunk('second')]
517
+ );
518
+ const stream = model.streamChunksWithSignal(controller.signal);
519
+ const iterator = stream[Symbol.asyncIterator]();
520
+
521
+ await expect(iterator.next()).resolves.toEqual(
522
+ expect.objectContaining({
523
+ done: false,
524
+ value: expect.objectContaining({
525
+ text: 'first ',
526
+ }),
527
+ })
528
+ );
529
+
530
+ const delayedChunk = iterator.next();
531
+ await Promise.resolve();
532
+ controller.abort(new Error('AbortError: User aborted request.'));
533
+
534
+ await expect(delayedChunk).rejects.toThrow('AbortError');
535
+ });
536
+
537
+ it('splits large delayed DeepSeek text chunks', async () => {
538
+ const model = new CapturingChatDeepSeek(
539
+ {
540
+ apiKey: 'test-key',
541
+ model: 'deepseek-v4-pro',
542
+ streaming: true,
543
+ _lc_stream_delay: 1,
544
+ },
545
+ [createContentChunk('alpha beta gamma')]
546
+ );
547
+ const textChunks: string[] = [];
548
+
549
+ for await (const chunk of model.streamChunksWithSignal(
550
+ new AbortController().signal
551
+ )) {
552
+ if (chunk.text) {
553
+ textChunks.push(chunk.text);
554
+ }
555
+ }
556
+
557
+ expect(textChunks).toEqual(['alpha ', 'beta ', 'gamma']);
558
+ });
559
+
560
+ it('keeps delayed DeepSeek logprob chunks intact', async () => {
561
+ const logprobs = { content: [], refusal: null } as NonNullable<
562
+ OpenAIChatCompletionChunk['choices'][number]['logprobs']
563
+ >;
564
+ const model = new CapturingChatDeepSeek(
565
+ {
566
+ apiKey: 'test-key',
567
+ model: 'deepseek-v4-pro',
568
+ streaming: true,
569
+ logprobs: true,
570
+ _lc_stream_delay: 1,
571
+ },
572
+ [createContentChunk('alpha beta gamma', logprobs)]
573
+ );
574
+ const chunks: ChatGenerationChunk[] = [];
575
+
576
+ for await (const chunk of model.streamChunksWithSignal(
577
+ new AbortController().signal
578
+ )) {
579
+ if (chunk.text !== '') {
580
+ chunks.push(chunk);
581
+ }
582
+ }
583
+
584
+ expect(chunks.map((chunk) => chunk.text)).toEqual(['alpha beta gamma']);
585
+ expect(chunks[0].generationInfo?.logprobs).toBe(logprobs);
586
+ });
587
+
588
+ it('emits callbacks for split delayed DeepSeek text chunks', async () => {
589
+ const model = new CapturingChatDeepSeek(
590
+ {
591
+ apiKey: 'test-key',
592
+ model: 'deepseek-v4-pro',
593
+ streaming: true,
594
+ _lc_stream_delay: 1,
595
+ },
596
+ [createContentChunk('alpha beta gamma')]
597
+ );
598
+ const textChunks: string[] = [];
599
+ const callbackTokens: string[] = [];
600
+
601
+ const stream = await model.stream([new HumanMessage('hi')], {
602
+ callbacks: [
603
+ {
604
+ handleLLMNewToken(token: string): void {
605
+ if (token !== '') {
606
+ callbackTokens.push(token);
607
+ }
608
+ },
609
+ },
610
+ ],
611
+ });
612
+
613
+ for await (const chunk of stream) {
614
+ if (typeof chunk.content === 'string' && chunk.content !== '') {
615
+ textChunks.push(chunk.content);
616
+ }
617
+ }
618
+
619
+ expect(textChunks).toEqual(['alpha ', 'beta ', 'gamma']);
620
+ expect(callbackTokens).toEqual(textChunks);
621
+ });
622
+
623
+ it('emits a delayed DeepSeek callback before an early stream break', async () => {
624
+ const model = new CapturingChatDeepSeek(
625
+ {
626
+ apiKey: 'test-key',
627
+ model: 'deepseek-v4-pro',
628
+ streaming: true,
629
+ _lc_stream_delay: 1,
630
+ },
631
+ [createContentChunk('alpha beta gamma')]
632
+ );
633
+ const textChunks: string[] = [];
634
+ const callbackTokens: string[] = [];
635
+ const runManager = {
636
+ handleLLMNewToken(token: string): void {
637
+ if (token !== '') {
638
+ callbackTokens.push(token);
639
+ }
640
+ },
641
+ } as unknown as CallbackManagerForLLMRun;
642
+
643
+ for await (const chunk of model.streamChunksWithCallbacks(runManager)) {
644
+ if (chunk.text !== '') {
645
+ textChunks.push(chunk.text);
646
+ }
647
+ break;
648
+ }
649
+
650
+ expect(textChunks).toEqual(['alpha ']);
651
+ expect(callbackTokens).toEqual(textChunks);
652
+ });
653
+
654
+ it('counts consumer work toward delayed DeepSeek cadence', async () => {
655
+ const model = new CapturingChatDeepSeek(
656
+ {
657
+ apiKey: 'test-key',
658
+ model: 'deepseek-v4-pro',
659
+ streaming: true,
660
+ _lc_stream_delay: 100,
661
+ },
662
+ [createContentChunk('first '), createContentChunk('second')]
663
+ );
664
+ const stream = model.streamChunksWithSignal(new AbortController().signal);
665
+ const iterator = stream[Symbol.asyncIterator]();
666
+
667
+ await expect(iterator.next()).resolves.toEqual(
668
+ expect.objectContaining({
669
+ done: false,
670
+ value: expect.objectContaining({
671
+ text: 'first ',
672
+ }),
673
+ })
674
+ );
675
+
676
+ await new Promise<void>((resolve) => {
677
+ setTimeout(resolve, 125);
678
+ });
679
+ const started = Date.now();
680
+
681
+ await expect(iterator.next()).resolves.toEqual(
682
+ expect.objectContaining({
683
+ done: false,
684
+ value: expect.objectContaining({
685
+ text: 'second',
686
+ }),
687
+ })
688
+ );
689
+ expect(Date.now() - started).toBeLessThan(50);
690
+ });
492
691
  });