@librechat/agents 3.3.10 → 3.3.12

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 (211) hide show
  1. package/dist/cjs/graphs/Graph.cjs +248 -27
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/langfuseToolOutputTracing.cjs +228 -16
  4. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  5. package/dist/cjs/llm/bedrock/index.cjs +13 -2
  6. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  7. package/dist/cjs/llm/init.cjs +1 -1
  8. package/dist/cjs/llm/invoke.cjs +160 -43
  9. package/dist/cjs/llm/invoke.cjs.map +1 -1
  10. package/dist/cjs/llm/openai/index.cjs +190 -13
  11. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  12. package/dist/cjs/llm/streamLimits.cjs +723 -0
  13. package/dist/cjs/llm/streamLimits.cjs.map +1 -0
  14. package/dist/cjs/main.cjs +12 -3
  15. package/dist/cjs/messages/core.cjs +592 -27
  16. package/dist/cjs/messages/core.cjs.map +1 -1
  17. package/dist/cjs/run.cjs +7 -3
  18. package/dist/cjs/run.cjs.map +1 -1
  19. package/dist/cjs/session/AgentSession.cjs +4 -1
  20. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  21. package/dist/cjs/stream.cjs +105 -14
  22. package/dist/cjs/stream.cjs.map +1 -1
  23. package/dist/cjs/summarization/node.cjs +157 -37
  24. package/dist/cjs/summarization/node.cjs.map +1 -1
  25. package/dist/cjs/tools/BashExecutor.cjs +3 -2
  26. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  27. package/dist/cjs/tools/CodeExecutor.cjs +4 -3
  28. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  29. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -3
  30. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  31. package/dist/cjs/tools/ToolNode.cjs +77 -4
  32. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  33. package/dist/cjs/tools/ToolSearch.cjs +3 -2
  34. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  35. package/dist/cjs/tools/search/crw-scraper.cjs +7 -1
  36. package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -1
  37. package/dist/cjs/tools/search/crw-search.cjs +3 -1
  38. package/dist/cjs/tools/search/crw-search.cjs.map +1 -1
  39. package/dist/cjs/tools/search/firecrawl.cjs +7 -1
  40. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  41. package/dist/cjs/tools/search/keenable-scraper.cjs +7 -1
  42. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -1
  43. package/dist/cjs/tools/search/keenable-search.cjs +3 -1
  44. package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
  45. package/dist/cjs/tools/search/rerankers.cjs +26 -8
  46. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  47. package/dist/cjs/tools/search/search.cjs +30 -10
  48. package/dist/cjs/tools/search/search.cjs.map +1 -1
  49. package/dist/cjs/tools/search/serper-scraper.cjs +7 -1
  50. package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
  51. package/dist/cjs/tools/search/tavily-scraper.cjs +7 -1
  52. package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
  53. package/dist/cjs/tools/search/tavily-search.cjs +3 -1
  54. package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
  55. package/dist/cjs/tools/search/tool.cjs +17 -3
  56. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  57. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +54 -3
  58. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  59. package/dist/cjs/utils/index.cjs +2 -1
  60. package/dist/cjs/utils/misc.cjs +12 -0
  61. package/dist/cjs/utils/misc.cjs.map +1 -1
  62. package/dist/cjs/utils/proxy.cjs +63 -0
  63. package/dist/cjs/utils/proxy.cjs.map +1 -0
  64. package/dist/esm/graphs/Graph.mjs +247 -26
  65. package/dist/esm/graphs/Graph.mjs.map +1 -1
  66. package/dist/esm/langfuseToolOutputTracing.mjs +228 -16
  67. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  68. package/dist/esm/llm/bedrock/index.mjs +13 -2
  69. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  70. package/dist/esm/llm/init.mjs +1 -1
  71. package/dist/esm/llm/invoke.mjs +160 -43
  72. package/dist/esm/llm/invoke.mjs.map +1 -1
  73. package/dist/esm/llm/openai/index.mjs +192 -15
  74. package/dist/esm/llm/openai/index.mjs.map +1 -1
  75. package/dist/esm/llm/streamLimits.mjs +704 -0
  76. package/dist/esm/llm/streamLimits.mjs.map +1 -0
  77. package/dist/esm/main.mjs +8 -6
  78. package/dist/esm/messages/core.mjs +592 -28
  79. package/dist/esm/messages/core.mjs.map +1 -1
  80. package/dist/esm/run.mjs +7 -3
  81. package/dist/esm/run.mjs.map +1 -1
  82. package/dist/esm/session/AgentSession.mjs +4 -1
  83. package/dist/esm/session/AgentSession.mjs.map +1 -1
  84. package/dist/esm/stream.mjs +105 -14
  85. package/dist/esm/stream.mjs.map +1 -1
  86. package/dist/esm/summarization/node.mjs +157 -37
  87. package/dist/esm/summarization/node.mjs.map +1 -1
  88. package/dist/esm/tools/BashExecutor.mjs +3 -2
  89. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  90. package/dist/esm/tools/CodeExecutor.mjs +4 -3
  91. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  92. package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -3
  93. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  94. package/dist/esm/tools/ToolNode.mjs +77 -4
  95. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  96. package/dist/esm/tools/ToolSearch.mjs +3 -2
  97. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  98. package/dist/esm/tools/search/crw-scraper.mjs +7 -1
  99. package/dist/esm/tools/search/crw-scraper.mjs.map +1 -1
  100. package/dist/esm/tools/search/crw-search.mjs +3 -1
  101. package/dist/esm/tools/search/crw-search.mjs.map +1 -1
  102. package/dist/esm/tools/search/firecrawl.mjs +7 -1
  103. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  104. package/dist/esm/tools/search/keenable-scraper.mjs +7 -1
  105. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -1
  106. package/dist/esm/tools/search/keenable-search.mjs +3 -1
  107. package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
  108. package/dist/esm/tools/search/rerankers.mjs +26 -8
  109. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  110. package/dist/esm/tools/search/search.mjs +30 -10
  111. package/dist/esm/tools/search/search.mjs.map +1 -1
  112. package/dist/esm/tools/search/serper-scraper.mjs +7 -1
  113. package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
  114. package/dist/esm/tools/search/tavily-scraper.mjs +7 -1
  115. package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
  116. package/dist/esm/tools/search/tavily-search.mjs +3 -1
  117. package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
  118. package/dist/esm/tools/search/tool.mjs +17 -3
  119. package/dist/esm/tools/search/tool.mjs.map +1 -1
  120. package/dist/esm/tools/subagent/SubagentExecutor.mjs +54 -3
  121. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  122. package/dist/esm/utils/index.mjs +2 -1
  123. package/dist/esm/utils/misc.mjs +12 -1
  124. package/dist/esm/utils/misc.mjs.map +1 -1
  125. package/dist/esm/utils/proxy.mjs +62 -0
  126. package/dist/esm/utils/proxy.mjs.map +1 -0
  127. package/dist/types/graphs/Graph.d.ts +53 -1
  128. package/dist/types/index.d.ts +2 -0
  129. package/dist/types/langfuseToolOutputTracing.d.ts +1 -0
  130. package/dist/types/llm/invoke.d.ts +26 -5
  131. package/dist/types/llm/openai/index.d.ts +3 -0
  132. package/dist/types/llm/streamLimits.d.ts +314 -0
  133. package/dist/types/messages/core.d.ts +11 -6
  134. package/dist/types/run.d.ts +1 -0
  135. package/dist/types/summarization/node.d.ts +27 -2
  136. package/dist/types/tools/BashExecutor.d.ts +2 -2
  137. package/dist/types/tools/CodeExecutor.d.ts +3 -3
  138. package/dist/types/tools/ToolNode.d.ts +11 -1
  139. package/dist/types/tools/search/crw-scraper.d.ts +2 -0
  140. package/dist/types/tools/search/firecrawl.d.ts +2 -0
  141. package/dist/types/tools/search/keenable-scraper.d.ts +2 -0
  142. package/dist/types/tools/search/rerankers.d.ts +9 -5
  143. package/dist/types/tools/search/serper-scraper.d.ts +2 -0
  144. package/dist/types/tools/search/tavily-scraper.d.ts +2 -0
  145. package/dist/types/tools/search/types.d.ts +28 -33
  146. package/dist/types/tools/subagent/SubagentExecutor.d.ts +44 -0
  147. package/dist/types/types/graph.d.ts +7 -1
  148. package/dist/types/types/run.d.ts +46 -1
  149. package/dist/types/types/tools.d.ts +21 -0
  150. package/dist/types/utils/index.d.ts +1 -0
  151. package/dist/types/utils/misc.d.ts +7 -0
  152. package/dist/types/utils/proxy.d.ts +31 -0
  153. package/package.json +2 -1
  154. package/src/__tests__/stream.eagerArgsDivergence.test.ts +158 -0
  155. package/src/__tests__/stream.eagerEventExecution.test.ts +1 -0
  156. package/src/__tests__/stream.streamLimits.test.ts +1982 -0
  157. package/src/graphs/Graph.ts +302 -27
  158. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +234 -0
  159. package/src/index.ts +11 -0
  160. package/src/langfuseToolOutputTracing.ts +410 -14
  161. package/src/llm/bedrock/index.ts +22 -2
  162. package/src/llm/custom-chat-models.smoke.test.ts +747 -0
  163. package/src/llm/invoke.streamLimits.test.ts +142 -0
  164. package/src/llm/invoke.test.ts +187 -1
  165. package/src/llm/invoke.ts +231 -43
  166. package/src/llm/openai/cacheWriteTokens.test.ts +112 -0
  167. package/src/llm/openai/index.ts +348 -31
  168. package/src/llm/openai/llm.spec.ts +107 -6
  169. package/src/llm/streamLimits.test.ts +450 -0
  170. package/src/llm/streamLimits.ts +1158 -0
  171. package/src/messages/core.ts +1290 -42
  172. package/src/messages/formatAgentMessages.test.ts +2623 -0
  173. package/src/run.ts +4 -0
  174. package/src/session/AgentSession.ts +5 -0
  175. package/src/specs/langfuse-tool-output-tracing.test.ts +887 -0
  176. package/src/specs/preemptSeal.test.ts +374 -5
  177. package/src/stream.ts +141 -6
  178. package/src/summarization/__tests__/node.test.ts +269 -0
  179. package/src/summarization/chunkHandler.test.ts +196 -0
  180. package/src/summarization/node.ts +203 -6
  181. package/src/tools/BashExecutor.ts +4 -3
  182. package/src/tools/CodeExecutor.ts +5 -4
  183. package/src/tools/ProgrammaticToolCalling.ts +7 -5
  184. package/src/tools/ToolNode.ts +109 -6
  185. package/src/tools/ToolSearch.ts +4 -3
  186. package/src/tools/__tests__/BashExecutor.test.ts +2 -2
  187. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +2 -4
  188. package/src/tools/__tests__/SubagentExecutor.test.ts +126 -0
  189. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +389 -0
  190. package/src/tools/__tests__/ToolNode.streamLimits.test.ts +69 -0
  191. package/src/tools/search/crw-scraper.ts +6 -0
  192. package/src/tools/search/crw-search.ts +6 -1
  193. package/src/tools/search/firecrawl.ts +6 -0
  194. package/src/tools/search/http-agent.test.ts +133 -0
  195. package/src/tools/search/keenable-scraper.ts +6 -0
  196. package/src/tools/search/keenable-search.ts +6 -1
  197. package/src/tools/search/rerankers.ts +36 -10
  198. package/src/tools/search/search.ts +29 -11
  199. package/src/tools/search/serper-scraper.ts +6 -0
  200. package/src/tools/search/tavily-scraper.ts +6 -0
  201. package/src/tools/search/tavily-search.ts +2 -0
  202. package/src/tools/search/tool.ts +16 -0
  203. package/src/tools/search/types.ts +31 -33
  204. package/src/tools/subagent/SubagentExecutor.ts +96 -3
  205. package/src/types/graph.ts +7 -0
  206. package/src/types/run.ts +49 -1
  207. package/src/types/tools.ts +21 -0
  208. package/src/utils/index.ts +1 -0
  209. package/src/utils/misc.ts +19 -0
  210. package/src/utils/proxy.test.ts +176 -0
  211. package/src/utils/proxy.ts +93 -0
@@ -1,3 +1,4 @@
1
+ import { convertMessagesToResponsesInput } from '@langchain/openai';
1
2
  import {
2
3
  AIMessage,
3
4
  AIMessageChunk,
@@ -21,6 +22,10 @@ import {
21
22
  CustomOpenAIClient,
22
23
  CustomAzureOpenAIClient,
23
24
  } from '@/llm/openai';
25
+ import {
26
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY,
27
+ projectOpenAIResponsesToolMessageContent,
28
+ } from '@/messages/core';
24
29
  import { CustomChatGoogleGenerativeAI } from '@/llm/google';
25
30
  import { CustomChatBedrockConverse } from '@/llm/bedrock';
26
31
  import { ChatOpenRouter } from '@/llm/openrouter';
@@ -37,6 +42,29 @@ type OpenAIResponsesDelegate = {
37
42
  client?: unknown;
38
43
  _getClientOptions: (options?: OpenAIRequestOptions) => OpenAIRequestOptions;
39
44
  };
45
+ type StreamingOpenAIResponsesDelegate = OpenAIResponsesDelegate & {
46
+ completionWithRetry: (
47
+ request: OpenAIClient.Responses.ResponseCreateParamsStreaming
48
+ ) => Promise<AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>>;
49
+ };
50
+ type MockableResponsesDelegate = OpenAIResponsesDelegate & {
51
+ completionWithRetry: (request: {
52
+ input?: unknown;
53
+ stream?: boolean;
54
+ }) => Promise<unknown>;
55
+ _generate: (
56
+ messages: AIMessage[],
57
+ options: Record<string, unknown>
58
+ ) => Promise<unknown>;
59
+ _streamChatModelEvents: (
60
+ messages: AIMessage[],
61
+ options: Record<string, unknown>
62
+ ) => AsyncGenerator<unknown>;
63
+ _streamResponseChunks: (
64
+ messages: AIMessage[],
65
+ options: Record<string, unknown>
66
+ ) => AsyncGenerator<ChatGenerationChunk>;
67
+ };
40
68
  type AnthropicCallOptions = Parameters<
41
69
  CustomAnthropic['invocationParams']
42
70
  >[0] & {
@@ -183,6 +211,107 @@ const baseAzureFields = {
183
211
  azureOpenAIApiDeploymentName: 'test-deployment',
184
212
  };
185
213
 
214
+ function createPersistedPreemptedAzureMessage(): AIMessage {
215
+ return new AIMessage({
216
+ id: 'msg_persisted_azure',
217
+ content: [{ type: 'text', text: 'Partial Azure answer.' }],
218
+ additional_kwargs: {
219
+ reasoning: {
220
+ id: 'rs_persisted_azure',
221
+ type: 'reasoning',
222
+ status: 'in_progress',
223
+ summary: [],
224
+ },
225
+ tool_outputs: [
226
+ {
227
+ id: 'local_persisted_azure',
228
+ type: 'local_shell_call_output',
229
+ status: 'completed',
230
+ output: 'AZURE_SERVER_RESULT',
231
+ },
232
+ ],
233
+ [OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
234
+ {
235
+ itemId: 'local_persisted_azure',
236
+ kind: 'output',
237
+ outputIndex: 0,
238
+ },
239
+ {
240
+ itemId: 'rs_persisted_azure',
241
+ kind: 'reasoning',
242
+ outputIndex: 1,
243
+ },
244
+ {
245
+ itemId: 'msg_persisted_azure',
246
+ kind: 'text',
247
+ outputIndex: 2,
248
+ contentIndex: 0,
249
+ },
250
+ ],
251
+ },
252
+ response_metadata: {
253
+ id: 'resp_persisted_azure',
254
+ model_provider: 'openai',
255
+ preempted: true,
256
+ },
257
+ });
258
+ }
259
+
260
+ function expectNeutralizedAzureResponsesRequest(request: unknown): void {
261
+ const serialized = JSON.stringify(request);
262
+ expect(serialized).toContain('AZURE_SERVER_RESULT');
263
+ expect(serialized.match(/AZURE_SERVER_RESULT/g)).toHaveLength(1);
264
+ expect(serialized).toContain('serverToolResult');
265
+ expect(serialized).not.toContain('msg_persisted_azure');
266
+ expect(serialized).not.toContain('rs_persisted_azure');
267
+ expect(serialized).not.toContain('local_persisted_azure');
268
+ expect(serialized).not.toContain('resp_persisted_azure');
269
+ expect(serialized).not.toContain(OPENAI_RESPONSES_REPLAY_POSITIONS_KEY);
270
+ expect(serialized).not.toContain('local_shell_call_output');
271
+ }
272
+
273
+ function createCompletedAzureResponse(): OpenAIClient.Responses.Response {
274
+ return {
275
+ id: 'resp_fresh_azure',
276
+ created_at: 0,
277
+ error: null,
278
+ incomplete_details: null,
279
+ instructions: null,
280
+ metadata: {},
281
+ model: 'gpt-5',
282
+ object: 'response',
283
+ output: [
284
+ {
285
+ id: 'msg_fresh_azure',
286
+ type: 'message',
287
+ role: 'assistant',
288
+ status: 'completed',
289
+ content: [
290
+ {
291
+ type: 'output_text',
292
+ text: 'Fresh Azure answer.',
293
+ annotations: [],
294
+ },
295
+ ],
296
+ },
297
+ ],
298
+ output_text: 'Fresh Azure answer.',
299
+ parallel_tool_calls: true,
300
+ status: 'completed',
301
+ temperature: null,
302
+ tool_choice: 'auto',
303
+ tools: [],
304
+ top_p: null,
305
+ usage: {
306
+ input_tokens: 1,
307
+ input_tokens_details: { cached_tokens: 0, cache_write_tokens: 0 },
308
+ output_tokens: 1,
309
+ output_tokens_details: { reasoning_tokens: 0 },
310
+ total_tokens: 2,
311
+ },
312
+ } as OpenAIClient.Responses.Response;
313
+ }
314
+
186
315
  const waitForFetchOutcome = (
187
316
  promise: Promise<Response>,
188
317
  timeoutMs = 100
@@ -401,6 +530,569 @@ describe('custom chat model class smoke tests', () => {
401
530
  );
402
531
  });
403
532
 
533
+ it('retains replayable Responses output items before a terminal event', async () => {
534
+ const model = new ChatOpenAI({
535
+ model: 'gpt-5',
536
+ apiKey: 'test-key',
537
+ useResponsesApi: true,
538
+ });
539
+ const responses = (
540
+ model as unknown as { responses: StreamingOpenAIResponsesDelegate }
541
+ ).responses;
542
+ const items = [
543
+ {
544
+ id: 'local_output_item',
545
+ type: 'local_shell_call_output',
546
+ status: 'completed',
547
+ output: 'local output',
548
+ },
549
+ {
550
+ id: 'shell_output_item',
551
+ call_id: 'shell_call_id',
552
+ type: 'shell_call_output',
553
+ status: 'completed',
554
+ max_output_length: 1_000,
555
+ output: [
556
+ {
557
+ stdout: 'shell output',
558
+ stderr: '',
559
+ outcome: { type: 'exit', exit_code: 0 },
560
+ },
561
+ ],
562
+ },
563
+ {
564
+ id: 'patch_output_item',
565
+ call_id: 'patch_call_id',
566
+ type: 'apply_patch_call_output',
567
+ status: 'completed',
568
+ output: 'patch output',
569
+ },
570
+ {
571
+ id: 'program_output_item',
572
+ call_id: 'program_call_id',
573
+ type: 'program_output',
574
+ status: 'completed',
575
+ result: 'program output',
576
+ },
577
+ ] as const;
578
+ responses.completionWithRetry = async () =>
579
+ (async function* () {
580
+ for (let i = 0; i < items.length; i++) {
581
+ yield {
582
+ type: 'response.output_item.done',
583
+ sequence_number: i,
584
+ output_index: i,
585
+ item: items[i],
586
+ } as unknown as OpenAIClient.Responses.ResponseStreamEvent;
587
+ }
588
+ yield {
589
+ type: 'response.output_item.added',
590
+ sequence_number: items.length,
591
+ output_index: items.length,
592
+ item: {
593
+ id: 'msg_partial',
594
+ type: 'message',
595
+ role: 'assistant',
596
+ status: 'in_progress',
597
+ content: [],
598
+ },
599
+ } as OpenAIClient.Responses.ResponseStreamEvent;
600
+ yield {
601
+ type: 'response.output_text.delta',
602
+ sequence_number: items.length + 1,
603
+ output_index: items.length,
604
+ content_index: 0,
605
+ item_id: 'msg_partial',
606
+ delta: 'Partial ',
607
+ logprobs: [],
608
+ } as OpenAIClient.Responses.ResponseStreamEvent;
609
+ yield {
610
+ type: 'response.output_text.delta',
611
+ sequence_number: items.length + 2,
612
+ output_index: items.length,
613
+ content_index: 0,
614
+ item_id: 'msg_partial',
615
+ delta: 'answer.',
616
+ logprobs: [],
617
+ } as OpenAIClient.Responses.ResponseStreamEvent;
618
+ })();
619
+
620
+ const chunks: AIMessageChunk[] = [];
621
+ const stream = await model.stream([new HumanMessage('run tools')]);
622
+ for await (const chunk of stream) {
623
+ chunks.push(chunk as AIMessageChunk);
624
+ }
625
+ const combined = chunks.reduce<AIMessageChunk | undefined>(
626
+ (current, chunk) =>
627
+ current == null ? chunk : (current.concat(chunk) as AIMessageChunk),
628
+ undefined
629
+ );
630
+
631
+ expect(chunks).toHaveLength(items.length + 3);
632
+ expect(combined?.text).toBe('Partial answer.');
633
+ expect(combined?.additional_kwargs.tool_outputs).toEqual(items);
634
+ expect(
635
+ combined?.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]
636
+ ).toEqual([
637
+ ...items.map((item, outputIndex) => ({
638
+ itemId: item.id,
639
+ kind: 'output',
640
+ outputIndex,
641
+ })),
642
+ {
643
+ itemId: 'msg_partial',
644
+ kind: 'message',
645
+ outputIndex: items.length,
646
+ },
647
+ {
648
+ contentIndex: 0,
649
+ itemId: 'msg_partial',
650
+ kind: 'text',
651
+ outputIndex: items.length,
652
+ },
653
+ ]);
654
+ });
655
+
656
+ it('rehydrates one-chunk Responses replay ordering from constructor kwargs', async () => {
657
+ const model = new ChatOpenAI({
658
+ model: 'gpt-5',
659
+ apiKey: 'test-key',
660
+ useResponsesApi: true,
661
+ });
662
+ const responses = (
663
+ model as unknown as { responses: StreamingOpenAIResponsesDelegate }
664
+ ).responses;
665
+ responses.completionWithRetry = async () =>
666
+ (async function* () {
667
+ yield {
668
+ type: 'response.output_text.delta',
669
+ sequence_number: 0,
670
+ output_index: 3,
671
+ content_index: 2,
672
+ item_id: 'msg_one_chunk',
673
+ delta: 'Partial answer.',
674
+ logprobs: [],
675
+ } as OpenAIClient.Responses.ResponseStreamEvent;
676
+ })();
677
+
678
+ const stream = await model.stream([new HumanMessage('answer')]);
679
+ const result = await stream.next();
680
+ expect(result.done).toBe(false);
681
+ const chunk = result.value as AIMessageChunk;
682
+ const serializedFields = JSON.parse(
683
+ JSON.stringify(chunk.lc_kwargs)
684
+ ) as ConstructorParameters<typeof AIMessageChunk>[0];
685
+ const rehydrated = new AIMessageChunk(serializedFields);
686
+
687
+ expect(chunk.content).toEqual([
688
+ { type: 'text', text: 'Partial answer.', index: 0 },
689
+ ]);
690
+ expect(rehydrated.content).toEqual([
691
+ { type: 'text', text: 'Partial answer.', index: 0 },
692
+ ]);
693
+ expect(
694
+ rehydrated.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]
695
+ ).toEqual([
696
+ {
697
+ contentIndex: 2,
698
+ itemId: 'msg_one_chunk',
699
+ kind: 'text',
700
+ outputIndex: 3,
701
+ },
702
+ ]);
703
+ });
704
+
705
+ it.each(['response.completed', 'response.incomplete'] as const)(
706
+ 'removes provisional replay outputs after %s supplies the authoritative output',
707
+ async (terminalType) => {
708
+ const model = new ChatOpenAI({
709
+ model: 'gpt-5',
710
+ apiKey: 'test-key',
711
+ useResponsesApi: true,
712
+ });
713
+ const responses = (
714
+ model as unknown as { responses: StreamingOpenAIResponsesDelegate }
715
+ ).responses;
716
+ const streamedToolOutput = {
717
+ id: 'web_search_item',
718
+ type: 'web_search_call',
719
+ status: 'completed',
720
+ } as const;
721
+ const provisionalReplayOutput = {
722
+ id: 'local_output_item',
723
+ type: 'local_shell_call_output',
724
+ status: 'completed',
725
+ output: 'stale provisional local output',
726
+ } as const;
727
+ const authoritativeReplayOutput = {
728
+ ...provisionalReplayOutput,
729
+ output: 'unique authoritative local output',
730
+ } as const;
731
+ responses.completionWithRetry = async () =>
732
+ (async function* () {
733
+ yield {
734
+ type: 'response.output_item.done',
735
+ sequence_number: 0,
736
+ output_index: 0,
737
+ item: streamedToolOutput,
738
+ } as unknown as OpenAIClient.Responses.ResponseStreamEvent;
739
+ yield {
740
+ type: 'response.output_item.done',
741
+ sequence_number: 1,
742
+ output_index: 1,
743
+ item: provisionalReplayOutput,
744
+ } as unknown as OpenAIClient.Responses.ResponseStreamEvent;
745
+ yield {
746
+ type: terminalType,
747
+ sequence_number: 2,
748
+ response: {
749
+ id: 'resp_terminal',
750
+ model: 'gpt-5',
751
+ output: [streamedToolOutput, authoritativeReplayOutput],
752
+ status:
753
+ terminalType === 'response.completed'
754
+ ? 'completed'
755
+ : 'incomplete',
756
+ usage: null,
757
+ },
758
+ } as unknown as OpenAIClient.Responses.ResponseStreamEvent;
759
+ })();
760
+
761
+ let tracedResult: unknown;
762
+ const chunks: AIMessageChunk[] = [];
763
+ const stream = await model.stream([new HumanMessage('run tools')], {
764
+ callbacks: [
765
+ {
766
+ handleLLMEnd(result: unknown): void {
767
+ tracedResult = result;
768
+ },
769
+ },
770
+ ],
771
+ });
772
+ for await (const chunk of stream) {
773
+ chunks.push(chunk as AIMessageChunk);
774
+ }
775
+ const combined = chunks.reduce<AIMessageChunk | undefined>(
776
+ (current, chunk) =>
777
+ current == null ? chunk : (current.concat(chunk) as AIMessageChunk),
778
+ undefined
779
+ );
780
+
781
+ expect(combined?.additional_kwargs.tool_outputs).toEqual([
782
+ streamedToolOutput,
783
+ ]);
784
+ expect(combined?.response_metadata.output).toEqual([
785
+ streamedToolOutput,
786
+ authoritativeReplayOutput,
787
+ ]);
788
+ expect(combined?.toJSON()).toEqual(
789
+ expect.objectContaining({
790
+ id: ['langchain_core', 'messages', 'AIMessageChunk'],
791
+ })
792
+ );
793
+ expect(
794
+ JSON.stringify(combined).match(/unique authoritative local output/g)
795
+ ).toHaveLength(1);
796
+ expect(JSON.stringify(combined)).not.toContain(
797
+ 'stale provisional local output'
798
+ );
799
+ expect(JSON.stringify(combined?.lc_kwargs)).not.toContain(
800
+ 'stale provisional local output'
801
+ );
802
+ expect(combined?.lc_kwargs.additional_kwargs).toEqual(
803
+ expect.objectContaining({ tool_outputs: [streamedToolOutput] })
804
+ );
805
+ expect(combined?.additional_kwargs).not.toHaveProperty(
806
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
807
+ );
808
+ expect(combined?.lc_kwargs.additional_kwargs).not.toHaveProperty(
809
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
810
+ );
811
+ expect(
812
+ JSON.stringify(tracedResult).match(/unique authoritative local output/g)
813
+ ).toHaveLength(1);
814
+ expect(JSON.stringify(tracedResult)).not.toContain(
815
+ 'stale provisional local output'
816
+ );
817
+ expect(JSON.stringify(tracedResult)).not.toContain(
818
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
819
+ );
820
+ }
821
+ );
822
+
823
+ it('removes replay positions on a text-only terminal response', async () => {
824
+ const model = new ChatOpenAI({
825
+ model: 'gpt-5',
826
+ apiKey: 'test-key',
827
+ useResponsesApi: true,
828
+ });
829
+ const responses = (
830
+ model as unknown as { responses: StreamingOpenAIResponsesDelegate }
831
+ ).responses;
832
+ const messageItem = {
833
+ id: 'msg_terminal',
834
+ type: 'message',
835
+ role: 'assistant',
836
+ status: 'completed',
837
+ content: [
838
+ {
839
+ type: 'output_text',
840
+ text: 'Complete answer.',
841
+ annotations: [],
842
+ logprobs: [],
843
+ },
844
+ ],
845
+ } as const;
846
+ responses.completionWithRetry = async () =>
847
+ (async function* () {
848
+ yield {
849
+ type: 'response.output_text.delta',
850
+ sequence_number: 0,
851
+ output_index: 0,
852
+ content_index: 0,
853
+ item_id: messageItem.id,
854
+ delta: messageItem.content[0].text,
855
+ logprobs: [],
856
+ } as OpenAIClient.Responses.ResponseStreamEvent;
857
+ yield {
858
+ type: 'response.completed',
859
+ sequence_number: 1,
860
+ response: {
861
+ id: 'resp_text_terminal',
862
+ model: 'gpt-5',
863
+ output: [messageItem],
864
+ status: 'completed',
865
+ usage: null,
866
+ },
867
+ } as unknown as OpenAIClient.Responses.ResponseStreamEvent;
868
+ })();
869
+
870
+ let combined: AIMessageChunk | undefined;
871
+ const stream = await model.stream([new HumanMessage('answer')]);
872
+ for await (const chunk of stream) {
873
+ combined =
874
+ combined == null
875
+ ? (chunk as AIMessageChunk)
876
+ : (combined.concat(chunk as AIMessageChunk) as AIMessageChunk);
877
+ }
878
+
879
+ expect(combined?.text).toBe('Complete answer.');
880
+ expect(combined?.response_metadata.output).toEqual([messageItem]);
881
+ expect(combined?.additional_kwargs).not.toHaveProperty(
882
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
883
+ );
884
+ expect(combined?.lc_kwargs.additional_kwargs).not.toHaveProperty(
885
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
886
+ );
887
+ });
888
+
889
+ it('keeps a raw server result between distinct Responses output messages', async () => {
890
+ const model = new ChatOpenAI({
891
+ model: 'gpt-5',
892
+ apiKey: 'test-key',
893
+ useResponsesApi: true,
894
+ });
895
+ const responses = (
896
+ model as unknown as { responses: StreamingOpenAIResponsesDelegate }
897
+ ).responses;
898
+ responses.completionWithRetry = async () =>
899
+ (async function* () {
900
+ yield {
901
+ type: 'response.output_text.delta',
902
+ sequence_number: 0,
903
+ output_index: 0,
904
+ content_index: 0,
905
+ item_id: 'msg_first',
906
+ delta: 'First narration.',
907
+ logprobs: [],
908
+ } as OpenAIClient.Responses.ResponseStreamEvent;
909
+ yield {
910
+ type: 'response.output_item.done',
911
+ sequence_number: 1,
912
+ output_index: 1,
913
+ item: {
914
+ id: 'local_output_item',
915
+ type: 'local_shell_call_output',
916
+ status: 'completed',
917
+ output: 'middle server result',
918
+ },
919
+ } as OpenAIClient.Responses.ResponseStreamEvent;
920
+ yield {
921
+ type: 'response.output_text.delta',
922
+ sequence_number: 2,
923
+ output_index: 2,
924
+ content_index: 0,
925
+ item_id: 'msg_second',
926
+ delta: 'Second narration.',
927
+ logprobs: [],
928
+ } as OpenAIClient.Responses.ResponseStreamEvent;
929
+ })();
930
+
931
+ let combined: AIMessageChunk | undefined;
932
+ const stream = await model.stream([new HumanMessage('run a server tool')]);
933
+ for await (const chunk of stream) {
934
+ combined =
935
+ combined == null
936
+ ? (chunk as AIMessageChunk)
937
+ : (combined.concat(chunk as AIMessageChunk) as AIMessageChunk);
938
+ }
939
+ expect(combined).toBeDefined();
940
+ combined!.response_metadata.preempted = true;
941
+
942
+ const [projected] = projectOpenAIResponsesToolMessageContent([combined!]);
943
+ const providerInput = convertMessagesToResponsesInput({
944
+ messages: [projected],
945
+ model: 'gpt-5',
946
+ zdrEnabled: false,
947
+ });
948
+ const serialized = JSON.stringify(providerInput);
949
+ const firstIndex = serialized.indexOf('First narration.');
950
+ const resultIndex = serialized.indexOf('middle server result');
951
+ const secondIndex = serialized.indexOf('Second narration.');
952
+
953
+ expect(firstIndex).toBeGreaterThanOrEqual(0);
954
+ expect(firstIndex).toBeLessThan(resultIndex);
955
+ expect(resultIndex).toBeLessThan(secondIndex);
956
+ });
957
+
958
+ it('keeps streamed generated images and server results in cross-item order', async () => {
959
+ const model = new ChatOpenAI({
960
+ model: 'gpt-5',
961
+ apiKey: 'test-key',
962
+ useResponsesApi: true,
963
+ });
964
+ const responses = (
965
+ model as unknown as { responses: StreamingOpenAIResponsesDelegate }
966
+ ).responses;
967
+ responses.completionWithRetry = async () =>
968
+ (async function* () {
969
+ yield {
970
+ type: 'response.output_text.delta',
971
+ sequence_number: 0,
972
+ output_index: 0,
973
+ content_index: 0,
974
+ item_id: 'msg_first',
975
+ delta: 'First narration.',
976
+ logprobs: [],
977
+ } as OpenAIClient.Responses.ResponseStreamEvent;
978
+ yield {
979
+ type: 'response.output_item.done',
980
+ sequence_number: 1,
981
+ output_index: 1,
982
+ item: {
983
+ id: 'ig_middle',
984
+ type: 'image_generation_call',
985
+ status: 'completed',
986
+ result: 'AA==',
987
+ },
988
+ } as OpenAIClient.Responses.ResponseStreamEvent;
989
+ yield {
990
+ type: 'response.output_item.done',
991
+ sequence_number: 2,
992
+ output_index: 2,
993
+ item: {
994
+ id: 'local_middle',
995
+ type: 'local_shell_call_output',
996
+ status: 'completed',
997
+ output: 'middle server result',
998
+ },
999
+ } as OpenAIClient.Responses.ResponseStreamEvent;
1000
+ yield {
1001
+ type: 'response.output_item.added',
1002
+ sequence_number: 3,
1003
+ output_index: 3,
1004
+ item: {
1005
+ id: 'rs_middle',
1006
+ type: 'reasoning',
1007
+ status: 'in_progress',
1008
+ summary: [],
1009
+ },
1010
+ } as OpenAIClient.Responses.ResponseStreamEvent;
1011
+ yield {
1012
+ type: 'response.output_item.done',
1013
+ sequence_number: 4,
1014
+ output_index: 3,
1015
+ item: {
1016
+ id: 'rs_middle',
1017
+ type: 'reasoning',
1018
+ status: 'incomplete',
1019
+ summary: [],
1020
+ encrypted_content: 'opaque-middle-reasoning',
1021
+ },
1022
+ } as OpenAIClient.Responses.ResponseStreamEvent;
1023
+ yield {
1024
+ type: 'response.output_text.delta',
1025
+ sequence_number: 5,
1026
+ output_index: 4,
1027
+ content_index: 0,
1028
+ item_id: 'msg_second',
1029
+ delta: 'Second narration.',
1030
+ logprobs: [],
1031
+ } as OpenAIClient.Responses.ResponseStreamEvent;
1032
+ })();
1033
+
1034
+ let combined: AIMessageChunk | undefined;
1035
+ const stream = await model.stream([new HumanMessage('generate and run')]);
1036
+ for await (const chunk of stream) {
1037
+ combined =
1038
+ combined == null
1039
+ ? (chunk as AIMessageChunk)
1040
+ : (combined.concat(chunk as AIMessageChunk) as AIMessageChunk);
1041
+ }
1042
+ expect(combined).toBeDefined();
1043
+ expect(combined!.additional_kwargs.reasoning).toEqual({
1044
+ id: 'rs_middle',
1045
+ type: 'reasoning',
1046
+ status: 'incomplete',
1047
+ summary: [],
1048
+ encrypted_content: 'opaque-middle-reasoning',
1049
+ });
1050
+ expect(
1051
+ combined!.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]
1052
+ ).toEqual([
1053
+ {
1054
+ contentIndex: 0,
1055
+ itemId: 'msg_first',
1056
+ kind: 'text',
1057
+ outputIndex: 0,
1058
+ },
1059
+ { itemId: 'ig_middle', kind: 'output', outputIndex: 1 },
1060
+ { itemId: 'local_middle', kind: 'output', outputIndex: 2 },
1061
+ { itemId: 'rs_middle', kind: 'reasoning', outputIndex: 3 },
1062
+ {
1063
+ contentIndex: 0,
1064
+ itemId: 'msg_second',
1065
+ kind: 'text',
1066
+ outputIndex: 4,
1067
+ },
1068
+ ]);
1069
+ combined!.response_metadata.preempted = true;
1070
+
1071
+ const [projected] = projectOpenAIResponsesToolMessageContent([combined!]);
1072
+ const providerInput = convertMessagesToResponsesInput({
1073
+ messages: [projected],
1074
+ model: 'gpt-5',
1075
+ zdrEnabled: false,
1076
+ });
1077
+ const serialized = JSON.stringify(providerInput);
1078
+ const firstIndex = serialized.indexOf('First narration.');
1079
+ const imageIndex = serialized.indexOf('data:image/png;base64,AA==');
1080
+ const resultIndex = serialized.indexOf('middle server result');
1081
+ const reasoningIndex = serialized.indexOf('opaque-middle-reasoning');
1082
+ const secondIndex = serialized.indexOf('Second narration.');
1083
+
1084
+ expect(firstIndex).toBeGreaterThanOrEqual(0);
1085
+ expect(firstIndex).toBeLessThan(imageIndex);
1086
+ expect(imageIndex).toBeLessThan(resultIndex);
1087
+ expect(resultIndex).toBeLessThan(reasoningIndex);
1088
+ expect(reasoningIndex).toBeLessThan(secondIndex);
1089
+ expect(serialized).not.toContain('ig_middle');
1090
+ expect(serialized).not.toContain('local_middle');
1091
+ expect(projected.additional_kwargs).not.toHaveProperty(
1092
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
1093
+ );
1094
+ });
1095
+
404
1096
  it('keeps Azure client customization and gates reasoning to reasoning models', () => {
405
1097
  const model = new AzureChatOpenAI({
406
1098
  ...baseAzureFields,
@@ -444,6 +1136,61 @@ describe('custom chat model class smoke tests', () => {
444
1136
  expect(nonReasoningModel.getReasoningParams()).toBeUndefined();
445
1137
  });
446
1138
 
1139
+ it('sanitizes persisted preempted history on direct Azure Responses streams', async () => {
1140
+ const model = new AzureChatOpenAI({
1141
+ ...baseAzureFields,
1142
+ });
1143
+ model.model = 'gpt-5';
1144
+ const responses = (
1145
+ model as unknown as { responses: MockableResponsesDelegate }
1146
+ ).responses;
1147
+ const requests: unknown[] = [];
1148
+ responses.completionWithRetry = async (request) => {
1149
+ requests.push(request);
1150
+ return (async function* () {})();
1151
+ };
1152
+ const message = createPersistedPreemptedAzureMessage();
1153
+ const originalSerialized = JSON.stringify(message.toJSON());
1154
+
1155
+ for await (const _chunk of responses._streamResponseChunks([message], {})) {
1156
+ // The empty mock stream intentionally yields no chunks.
1157
+ }
1158
+ for await (const _event of responses._streamChatModelEvents(
1159
+ [message],
1160
+ {}
1161
+ )) {
1162
+ // The empty mock stream intentionally yields no events.
1163
+ }
1164
+
1165
+ expect(requests).toHaveLength(2);
1166
+ for (const request of requests) {
1167
+ expectNeutralizedAzureResponsesRequest(request);
1168
+ }
1169
+ expect(JSON.stringify(message.toJSON())).toBe(originalSerialized);
1170
+ });
1171
+
1172
+ it('sanitizes persisted preempted history on direct Azure Responses generation', async () => {
1173
+ const model = new AzureChatOpenAI({
1174
+ ...baseAzureFields,
1175
+ });
1176
+ model.model = 'gpt-5';
1177
+ const responses = (
1178
+ model as unknown as { responses: MockableResponsesDelegate }
1179
+ ).responses;
1180
+ let request: unknown;
1181
+ responses.completionWithRetry = async (nextRequest) => {
1182
+ request = nextRequest;
1183
+ return createCompletedAzureResponse();
1184
+ };
1185
+ const message = createPersistedPreemptedAzureMessage();
1186
+ const originalSerialized = JSON.stringify(message.toJSON());
1187
+
1188
+ await responses._generate([message], {});
1189
+
1190
+ expectNeutralizedAzureResponsesRequest(request);
1191
+ expect(JSON.stringify(message.toJSON())).toBe(originalSerialized);
1192
+ });
1193
+
447
1194
  it('keeps DeepSeek, Moonshot, and xAI on LibreChat wrapper semantics', () => {
448
1195
  const deepSeek = new ChatDeepSeek({
449
1196
  model: 'deepseek-chat',