@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
@@ -16,7 +16,10 @@ import {
16
16
  import {
17
17
  getEndpoint,
18
18
  OpenAIClient,
19
+ wrapOpenAIClientError,
19
20
  getHeadersWithUserAgent,
21
+ convertMessagesToResponsesInput,
22
+ convertResponsesDeltaToChatGenerationChunk,
20
23
  ChatOpenAI as OriginalChatOpenAI,
21
24
  ChatOpenAIResponses as OriginalChatOpenAIResponses,
22
25
  ChatOpenAICompletions as OriginalChatOpenAICompletions,
@@ -34,9 +37,15 @@ import type { BindToolsInput } from '@langchain/core/language_models/chat_models
34
37
  import type { ChatGeneration, ChatResult } from '@langchain/core/outputs';
35
38
  import type { ChatXAIInput } from '@langchain/xai';
36
39
  import type * as t from '@langchain/openai';
40
+ import type { ResponsesReplayPosition } from '@/messages/core';
37
41
  import type { SeenScalarMetadata } from './streamMetadata';
38
42
  import type { HeaderValue, HeadersLike } from './types';
39
43
  import type { PromptCacheTtl } from '@/messages/cache';
44
+ import {
45
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY,
46
+ projectOpenAIResponsesToolMessageContent,
47
+ projectToolStreamContentForProvider,
48
+ } from '@/messages/core';
40
49
  import {
41
50
  buildAnthropicCacheControl,
42
51
  resolvePromptCacheTtl,
@@ -47,12 +56,8 @@ import {
47
56
  STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
48
57
  OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER,
49
58
  } from '@/tools/streamedToolCallSeals';
50
- import {
51
- projectOpenAIResponsesToolMessageContent,
52
- projectToolStreamContentForProvider,
53
- } from '@/messages/core';
54
- import { INTENT_ARG, isIntentLabelProperty } from '@/tools/intentArg';
55
59
  import { isReasoningModel, _convertMessagesToOpenAIParams } from './utils';
60
+ import { INTENT_ARG, isIntentLabelProperty } from '@/tools/intentArg';
56
61
  import { dropRepeatedScalarMetadata } from './streamMetadata';
57
62
 
58
63
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@@ -224,6 +229,10 @@ type ResponsesRequest =
224
229
  type ResponsesResult =
225
230
  | AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>
226
231
  | OpenAIClient.Responses.Response;
232
+ type ResponsesStreamChunkOptions = {
233
+ promptIndex?: number;
234
+ signal?: AbortSignal;
235
+ };
227
236
  type CacheableChatPart = {
228
237
  type: 'text' | 'image_url' | 'input_audio' | 'file' | 'refusal';
229
238
  prompt_cache_breakpoint?: { mode: 'explicit' };
@@ -236,7 +245,15 @@ type CacheableResponsePart = (
236
245
  ) & {
237
246
  prompt_cache_breakpoint?: { mode: 'explicit' };
238
247
  };
239
- type ResponsesUsageWithCacheWrite = OpenAIClient.Responses.ResponseUsage & {
248
+ // `Omit` before re-adding `input_tokens_details` as optional matters: the SDK's own
249
+ // `ResponseUsage` declares it required (true for OpenAI itself), so a plain intersection
250
+ // would keep it required in the merged type despite the `?:` here — masking, at the type
251
+ // level, that OpenAI-*compatible* servers (e.g. mlx_vlm.server) may omit it entirely.
252
+ // Mirrors `CompletionUsageWithCacheWrite`'s handling of the analogous Completions API field.
253
+ type ResponsesUsageWithCacheWrite = Omit<
254
+ OpenAIClient.Responses.ResponseUsage,
255
+ 'input_tokens_details'
256
+ > & {
240
257
  input_tokens_details?: OpenAIClient.Responses.ResponseUsage['input_tokens_details'] & {
241
258
  cache_write_tokens?: number;
242
259
  };
@@ -386,6 +403,10 @@ function isResponseMessage(
386
403
  return item.type === 'message';
387
404
  }
388
405
 
406
+ function isResponseInputRole(role: string): boolean {
407
+ return role === 'system' || role === 'developer' || role === 'user';
408
+ }
409
+
389
410
  /** Only `input_text`/`input_image`/`input_file` accept a Responses breakpoint;
390
411
  * `output_text`/`refusal` (replayed assistant blocks) are rejected with a 400. */
391
412
  function isCacheableResponsePart(part: unknown): part is CacheableResponsePart {
@@ -459,11 +480,7 @@ export function addResponseCacheBreakpoints(
459
480
  /** Only input roles take a Responses breakpoint. Assistant/tool turns
460
481
  * carry output content (string or output_text) that the API rejects
461
482
  * under an input marker, so they're never eligible. */
462
- if (
463
- item.role !== 'system' &&
464
- item.role !== 'developer' &&
465
- item.role !== 'user'
466
- ) {
483
+ if (!isResponseInputRole(item.role)) {
467
484
  return false;
468
485
  }
469
486
  const content = item.content as
@@ -500,13 +517,13 @@ export function shouldIncludeEncryptedReasoning(
500
517
  );
501
518
  }
502
519
 
503
- function getCacheWriteTokens(message: BaseMessage): number | undefined {
520
+ export function getCacheWriteTokens(message: BaseMessage): number | undefined {
504
521
  const responseMetadata = message.response_metadata as {
505
522
  usage?: ResponsesUsageWithCacheWrite;
506
523
  metadata?: Record<string, string>;
507
524
  };
508
525
  const reported =
509
- responseMetadata.usage?.input_tokens_details.cache_write_tokens;
526
+ responseMetadata.usage?.input_tokens_details?.cache_write_tokens;
510
527
  if (reported != null) {
511
528
  return reported;
512
529
  }
@@ -518,7 +535,7 @@ function getCacheWriteTokens(message: BaseMessage): number | undefined {
518
535
  return Number.isFinite(parsed) ? parsed : undefined;
519
536
  }
520
537
 
521
- function attachCacheWriteUsage(message: BaseMessage): void {
538
+ export function attachCacheWriteUsage(message: BaseMessage): void {
522
539
  const cacheWriteTokens = getCacheWriteTokens(message);
523
540
  if (
524
541
  cacheWriteTokens == null ||
@@ -545,11 +562,11 @@ function attachCacheWriteUsage(message: BaseMessage): void {
545
562
  };
546
563
  }
547
564
 
548
- function attachCacheWriteMetadata(
565
+ export function attachCacheWriteMetadata(
549
566
  response: OpenAIClient.Responses.Response
550
567
  ): OpenAIClient.Responses.Response {
551
568
  const usage = response.usage as ResponsesUsageWithCacheWrite | undefined;
552
- const cacheWriteTokens = usage?.input_tokens_details.cache_write_tokens;
569
+ const cacheWriteTokens = usage?.input_tokens_details?.cache_write_tokens;
553
570
  if (cacheWriteTokens == null) {
554
571
  return response;
555
572
  }
@@ -568,6 +585,278 @@ function isResponsesStream(
568
585
  return Symbol.asyncIterator in result;
569
586
  }
570
587
 
588
+ const RESPONSES_REPLAY_OUTPUT_ITEM_TYPES = new Set([
589
+ 'local_shell_call_output',
590
+ 'shell_call_output',
591
+ 'apply_patch_call_output',
592
+ 'program_output',
593
+ ]);
594
+
595
+ function isResponsesReplayOutputItem(item: unknown): boolean {
596
+ return (
597
+ typeof item === 'object' &&
598
+ item != null &&
599
+ 'type' in item &&
600
+ typeof item.type === 'string' &&
601
+ RESPONSES_REPLAY_OUTPUT_ITEM_TYPES.has(item.type)
602
+ );
603
+ }
604
+
605
+ /**
606
+ * LangChain's Responses converter places the authoritative terminal output in
607
+ * response_metadata.output. Its chunk merge has no way to delete provisional
608
+ * tool_outputs or replay-position sidecars, so remove those preemption-only
609
+ * captures once that terminal output arrives. An interrupted stream has no
610
+ * terminal chunk and keeps the captures for replay.
611
+ */
612
+ class ResponsesReplayAIMessageChunk extends AIMessageChunk {
613
+ override get lc_id(): string[] {
614
+ return [...this.lc_namespace, AIMessageChunk.lc_name()];
615
+ }
616
+
617
+ override concat(chunk: AIMessageChunk): this {
618
+ const combined = super.concat(chunk);
619
+ if (!Array.isArray(chunk.response_metadata.output)) {
620
+ return combined;
621
+ }
622
+ delete combined.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY];
623
+ const toolOutputs = combined.additional_kwargs.tool_outputs;
624
+ if (!Array.isArray(toolOutputs)) {
625
+ return combined;
626
+ }
627
+ const retainedToolOutputs = toolOutputs.filter(
628
+ (item) => !isResponsesReplayOutputItem(item)
629
+ );
630
+ if (retainedToolOutputs.length === toolOutputs.length) {
631
+ return combined;
632
+ }
633
+ if (retainedToolOutputs.length > 0) {
634
+ combined.additional_kwargs.tool_outputs = retainedToolOutputs;
635
+ } else {
636
+ delete combined.additional_kwargs.tool_outputs;
637
+ }
638
+ return combined;
639
+ }
640
+ }
641
+
642
+ function makeResponsesReplayAggregationSafe(
643
+ chunk: ChatGenerationChunk
644
+ ): ChatGenerationChunk {
645
+ if (!AIMessageChunk.isInstance(chunk.message)) {
646
+ return chunk;
647
+ }
648
+ const message = chunk.message;
649
+ chunk.message = new ResponsesReplayAIMessageChunk({
650
+ id: message.id,
651
+ name: message.name,
652
+ content: message.content,
653
+ additional_kwargs: message.additional_kwargs,
654
+ response_metadata: message.response_metadata,
655
+ tool_calls: message.tool_calls,
656
+ invalid_tool_calls: message.invalid_tool_calls,
657
+ tool_call_chunks: message.tool_call_chunks,
658
+ usage_metadata: message.usage_metadata,
659
+ });
660
+ return chunk;
661
+ }
662
+
663
+ function remapResponsesTextBlockIndex(
664
+ chunk: ChatGenerationChunk,
665
+ event: OpenAIClient.Responses.ResponseStreamEvent,
666
+ textBlockIndices: Map<string, number>
667
+ ): void {
668
+ const position = iife(() => {
669
+ if (
670
+ event.type === 'response.output_text.delta' ||
671
+ event.type === 'response.output_text.annotation.added'
672
+ ) {
673
+ return {
674
+ contentIndex: event.content_index,
675
+ outputIndex: event.output_index,
676
+ };
677
+ }
678
+ if (
679
+ event.type === 'response.output_item.added' &&
680
+ event.item.type === 'message'
681
+ ) {
682
+ return { contentIndex: 0, outputIndex: event.output_index };
683
+ }
684
+ return undefined;
685
+ });
686
+ if (position == null || !Array.isArray(chunk.message.content)) {
687
+ return;
688
+ }
689
+ const key = `${position.outputIndex}:${position.contentIndex}`;
690
+ let blockIndex = textBlockIndices.get(key);
691
+ if (blockIndex == null) {
692
+ blockIndex = textBlockIndices.size;
693
+ textBlockIndices.set(key, blockIndex);
694
+ }
695
+ const content = chunk.message.content.map((block) =>
696
+ typeof block === 'object' && block.type === 'text'
697
+ ? { ...block, index: blockIndex }
698
+ : block
699
+ );
700
+ chunk.message.content = content;
701
+ chunk.message.lc_kwargs.content = content;
702
+ }
703
+
704
+ function convertDroppedResponsesReplayOutput(
705
+ event: OpenAIClient.Responses.ResponseStreamEvent
706
+ ): ChatGenerationChunk | null {
707
+ if (event.type !== 'response.output_item.done') {
708
+ return null;
709
+ }
710
+ if (event.item.type === 'reasoning') {
711
+ // Added/summary events already stream id, type, and summary. Only merge
712
+ // terminal fields here so chunk concatenation does not duplicate summary.
713
+ return new ChatGenerationChunk({
714
+ text: '',
715
+ message: new AIMessageChunk({
716
+ content: [],
717
+ additional_kwargs: {
718
+ reasoning: {
719
+ status: event.item.status,
720
+ ...(typeof event.item.encrypted_content === 'string'
721
+ ? { encrypted_content: event.item.encrypted_content }
722
+ : {}),
723
+ },
724
+ },
725
+ response_metadata: { model_provider: 'openai' },
726
+ }),
727
+ });
728
+ }
729
+ if (!RESPONSES_REPLAY_OUTPUT_ITEM_TYPES.has(event.item.type)) {
730
+ return null;
731
+ }
732
+ return new ChatGenerationChunk({
733
+ text: '',
734
+ message: new AIMessageChunk({
735
+ content: [],
736
+ additional_kwargs: { tool_outputs: [event.item] },
737
+ response_metadata: { model_provider: 'openai' },
738
+ }),
739
+ });
740
+ }
741
+
742
+ function attachResponsesReplayPosition(
743
+ chunk: ChatGenerationChunk,
744
+ event: OpenAIClient.Responses.ResponseStreamEvent,
745
+ seenPositions: Set<string>
746
+ ): void {
747
+ let position: ResponsesReplayPosition | undefined;
748
+ if (event.type === 'response.output_text.delta' && event.delta.length > 0) {
749
+ position = {
750
+ contentIndex: event.content_index,
751
+ itemId: event.item_id,
752
+ kind: 'text',
753
+ outputIndex: event.output_index,
754
+ };
755
+ } else if (
756
+ event.type === 'response.output_item.added' &&
757
+ event.item.type === 'message' &&
758
+ typeof event.item.id === 'string' &&
759
+ event.item.id.length > 0
760
+ ) {
761
+ position = {
762
+ itemId: event.item.id,
763
+ kind: 'message',
764
+ outputIndex: event.output_index,
765
+ };
766
+ } else if (
767
+ event.type === 'response.output_item.added' &&
768
+ event.item.type === 'reasoning' &&
769
+ typeof event.item.id === 'string' &&
770
+ event.item.id.length > 0
771
+ ) {
772
+ position = {
773
+ itemId: event.item.id,
774
+ kind: 'reasoning',
775
+ outputIndex: event.output_index,
776
+ };
777
+ } else if (
778
+ event.type === 'response.output_item.done' &&
779
+ (RESPONSES_REPLAY_OUTPUT_ITEM_TYPES.has(event.item.type) ||
780
+ Array.isArray(chunk.message.additional_kwargs.tool_outputs))
781
+ ) {
782
+ let itemId: string | undefined;
783
+ if (typeof event.item.id === 'string' && event.item.id.length > 0) {
784
+ itemId = event.item.id;
785
+ } else if (
786
+ 'call_id' in event.item &&
787
+ typeof event.item.call_id === 'string' &&
788
+ event.item.call_id.length > 0
789
+ ) {
790
+ itemId = event.item.call_id;
791
+ }
792
+ if (itemId != null) {
793
+ position = {
794
+ itemId,
795
+ kind: 'output',
796
+ outputIndex: event.output_index,
797
+ };
798
+ }
799
+ }
800
+ if (position == null) {
801
+ return;
802
+ }
803
+ const positionKey = `${position.kind}:${position.itemId}:${position.outputIndex}:${position.contentIndex ?? ''}`;
804
+ if (seenPositions.has(positionKey)) {
805
+ return;
806
+ }
807
+ seenPositions.add(positionKey);
808
+ const existing = chunk.message.additional_kwargs[
809
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
810
+ ] as unknown;
811
+ const additionalKwargs = {
812
+ ...chunk.message.additional_kwargs,
813
+ [OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
814
+ ...(Array.isArray(existing) ? existing : []),
815
+ position,
816
+ ],
817
+ };
818
+ chunk.message.additional_kwargs = additionalKwargs;
819
+ chunk.message.lc_kwargs.additional_kwargs = additionalKwargs;
820
+ }
821
+
822
+ async function* convertLibreChatResponsesStream(
823
+ stream: AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>,
824
+ options: ResponsesStreamChunkOptions,
825
+ runManager?: CallbackManagerForLLMRun
826
+ ): AsyncGenerator<ChatGenerationChunk> {
827
+ const seenReplayPositions = new Set<string>();
828
+ const responsesTextBlockIndices = new Map<string, number>();
829
+ try {
830
+ for await (const event of stream) {
831
+ options.signal?.throwIfAborted();
832
+ const convertedChunk =
833
+ convertResponsesDeltaToChatGenerationChunk(event) ??
834
+ convertDroppedResponsesReplayOutput(event);
835
+ if (convertedChunk == null) {
836
+ continue;
837
+ }
838
+ const chunk = makeResponsesReplayAggregationSafe(convertedChunk);
839
+ remapResponsesTextBlockIndex(chunk, event, responsesTextBlockIndices);
840
+ attachResponsesReplayPosition(chunk, event, seenReplayPositions);
841
+ attachCacheWriteUsage(chunk.message);
842
+ await runManager?.handleLLMNewToken(
843
+ chunk.text || '',
844
+ {
845
+ prompt: options.promptIndex ?? 0,
846
+ completion: 0,
847
+ },
848
+ undefined,
849
+ undefined,
850
+ undefined,
851
+ { chunk }
852
+ );
853
+ yield chunk;
854
+ }
855
+ } catch (e) {
856
+ throw wrapOpenAIClientError(e);
857
+ }
858
+ }
859
+
571
860
  function createUsageMetadata(
572
861
  usage?: OpenAIClient.Completions.CompletionUsage
573
862
  ): UsageMetadata {
@@ -1754,14 +2043,20 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
1754
2043
  options: this['ParsedCallOptions'],
1755
2044
  runManager?: CallbackManagerForLLMRun
1756
2045
  ): AsyncGenerator<ChatGenerationChunk> {
1757
- for await (const chunk of super._streamResponseChunks(
1758
- projectOpenAIResponsesProviderMessages(messages),
1759
- options,
1760
- runManager
1761
- )) {
1762
- attachCacheWriteUsage(chunk.message);
1763
- yield chunk;
1764
- }
2046
+ const projectedMessages = projectOpenAIResponsesProviderMessages(messages);
2047
+ const stream = await this.completionWithRetry(
2048
+ {
2049
+ ...this.invocationParams(options),
2050
+ input: convertMessagesToResponsesInput({
2051
+ messages: projectedMessages,
2052
+ zdrEnabled: this.zdrEnabled ?? false,
2053
+ model: this.model,
2054
+ }),
2055
+ stream: true,
2056
+ },
2057
+ options
2058
+ );
2059
+ yield* convertLibreChatResponsesStream(stream, options, runManager);
1765
2060
  }
1766
2061
 
1767
2062
  async *_streamChatModelEvents(
@@ -1988,7 +2283,11 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
1988
2283
  options: this['ParsedCallOptions'],
1989
2284
  runManager?: CallbackManagerForLLMRun
1990
2285
  ): Promise<ChatResult> {
1991
- const result = await super._generate(messages, options, runManager);
2286
+ const result = await super._generate(
2287
+ projectOpenAIResponsesProviderMessages(messages),
2288
+ options,
2289
+ runManager
2290
+ );
1992
2291
  for (const generation of result.generations) {
1993
2292
  attachCacheWriteUsage(generation.message);
1994
2293
  }
@@ -2000,14 +2299,32 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
2000
2299
  options: this['ParsedCallOptions'],
2001
2300
  runManager?: CallbackManagerForLLMRun
2002
2301
  ): AsyncGenerator<ChatGenerationChunk> {
2003
- for await (const chunk of super._streamResponseChunks(
2004
- messages,
2302
+ const projectedMessages = projectOpenAIResponsesProviderMessages(messages);
2303
+ const stream = await this.completionWithRetry(
2304
+ {
2305
+ ...this.invocationParams(options),
2306
+ input: convertMessagesToResponsesInput({
2307
+ messages: projectedMessages,
2308
+ zdrEnabled: this.zdrEnabled ?? false,
2309
+ model: this.model,
2310
+ }),
2311
+ stream: true,
2312
+ },
2313
+ options
2314
+ );
2315
+ yield* convertLibreChatResponsesStream(stream, options, runManager);
2316
+ }
2317
+
2318
+ async *_streamChatModelEvents(
2319
+ messages: BaseMessage[],
2320
+ options: this['ParsedCallOptions'],
2321
+ runManager?: CallbackManagerForLLMRun
2322
+ ): AsyncGenerator<ChatModelStreamEvent> {
2323
+ yield* super._streamChatModelEvents(
2324
+ projectOpenAIResponsesProviderMessages(messages),
2005
2325
  options,
2006
2326
  runManager
2007
- )) {
2008
- attachCacheWriteUsage(chunk.message);
2009
- yield chunk;
2010
- }
2327
+ );
2011
2328
  }
2012
2329
 
2013
2330
  protected _getReasoningParams(
@@ -82,6 +82,17 @@ type CompletionsStreamDelegate = {
82
82
  ) => AsyncGenerator<ChatGenerationChunk>;
83
83
  };
84
84
 
85
+ type ResponsesStreamDelegate = {
86
+ completionWithRetry: (
87
+ request: OpenAIClient.Responses.ResponseCreateParamsStreaming
88
+ ) => Promise<AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>>;
89
+ _streamResponseChunks: (
90
+ messages: BaseMessage[],
91
+ options: Record<string, unknown>,
92
+ runManager?: CallbackManagerForLLMRun
93
+ ) => AsyncGenerator<ChatGenerationChunk>;
94
+ };
95
+
85
96
  type InvocationParamsDelegate = {
86
97
  invocationParams: (
87
98
  options: Record<string, unknown>,
@@ -109,6 +120,9 @@ function completionsDelegateOf(model: ChatOpenAI): CompletionsDelegate {
109
120
  const completionsOf = <T>(model: ChatOpenAI): T =>
110
121
  (model as unknown as { completions: T }).completions;
111
122
 
123
+ const responsesOf = <T>(model: ChatOpenAI): T =>
124
+ (model as unknown as { responses: T }).responses;
125
+
112
126
  function newOpenAI(): ChatOpenAI {
113
127
  return new ChatOpenAI({ model: 'gpt-4o-mini', apiKey: 'test-key' });
114
128
  }
@@ -1080,6 +1094,90 @@ describe('ChatOpenAICompletions streaming usage_metadata callback', () => {
1080
1094
  });
1081
1095
  });
1082
1096
 
1097
+ describe('ChatOpenAIResponses streaming callback dispatch', () => {
1098
+ it('emits the callback before a consumer stops at the yielded chunk', async () => {
1099
+ const model = new ChatOpenAI({
1100
+ model: 'gpt-5',
1101
+ apiKey: 'test-key',
1102
+ useResponsesApi: true,
1103
+ });
1104
+ const responses = responsesOf<ResponsesStreamDelegate>(model);
1105
+ responses.completionWithRetry = async () =>
1106
+ (async function* () {
1107
+ yield {
1108
+ type: 'response.output_text.delta',
1109
+ sequence_number: 0,
1110
+ output_index: 0,
1111
+ content_index: 0,
1112
+ item_id: 'msg_callback',
1113
+ delta: 'visible chunk',
1114
+ logprobs: [],
1115
+ } as OpenAIClient.Responses.ResponseStreamEvent;
1116
+ })();
1117
+
1118
+ const events: string[] = [];
1119
+ const runManager = {
1120
+ handleLLMNewToken(token: string): void {
1121
+ events.push(`callback:${token}`);
1122
+ },
1123
+ } as unknown as CallbackManagerForLLMRun;
1124
+
1125
+ for await (const chunk of responses._streamResponseChunks(
1126
+ [new HumanMessage('test')],
1127
+ {},
1128
+ runManager
1129
+ )) {
1130
+ events.push(`yield:${chunk.text}`);
1131
+ break;
1132
+ }
1133
+
1134
+ expect(events).toEqual(['callback:visible chunk', 'yield:visible chunk']);
1135
+ });
1136
+
1137
+ it('propagates an abort received with the next buffered event', async () => {
1138
+ const model = new ChatOpenAI({
1139
+ model: 'gpt-5',
1140
+ apiKey: 'test-key',
1141
+ useResponsesApi: true,
1142
+ });
1143
+ const responses = responsesOf<ResponsesStreamDelegate>(model);
1144
+ const controller = new AbortController();
1145
+ const abortReason = new Error('buffered Responses stream aborted');
1146
+ responses.completionWithRetry = async () =>
1147
+ (async function* () {
1148
+ yield {
1149
+ type: 'response.output_text.delta',
1150
+ sequence_number: 0,
1151
+ output_index: 0,
1152
+ content_index: 0,
1153
+ item_id: 'msg_abort',
1154
+ delta: 'partial',
1155
+ logprobs: [],
1156
+ } as OpenAIClient.Responses.ResponseStreamEvent;
1157
+ controller.abort(abortReason);
1158
+ yield {
1159
+ type: 'response.output_text.delta',
1160
+ sequence_number: 1,
1161
+ output_index: 0,
1162
+ content_index: 0,
1163
+ item_id: 'msg_abort',
1164
+ delta: ' must not complete normally',
1165
+ logprobs: [],
1166
+ } as OpenAIClient.Responses.ResponseStreamEvent;
1167
+ })();
1168
+
1169
+ const stream = responses._streamResponseChunks([new HumanMessage('test')], {
1170
+ signal: controller.signal,
1171
+ });
1172
+
1173
+ await expect(stream.next()).resolves.toMatchObject({
1174
+ done: false,
1175
+ value: { text: 'partial' },
1176
+ });
1177
+ await expect(stream.next()).rejects.toBe(abortReason);
1178
+ });
1179
+ });
1180
+
1083
1181
  describe('ChatOpenAICompletions reasoning_content compatibility', () => {
1084
1182
  it('should preserve reasoning_content on streamed assistant chunks', async () => {
1085
1183
  const model = new ChatOpenAI({
@@ -1237,11 +1335,15 @@ describe('ChatOpenAICompletions strict tools for structured output', () => {
1237
1335
  }) as unknown as {
1238
1336
  tools?: { function: { parameters: { properties: object } } }[];
1239
1337
  };
1240
- return Object.keys(params.tools?.[0]?.function.parameters.properties ?? {});
1338
+ return Object.keys(
1339
+ params.tools?.[0]?.function.parameters.properties ?? {}
1340
+ );
1241
1341
  }
1242
1342
 
1243
1343
  it('drops the optional label when strict is auto-enabled (invalid otherwise)', () => {
1244
- expect(toolProperties({ response_format: jsonSchemaResponseFormat })).toEqual(['query']);
1344
+ expect(
1345
+ toolProperties({ response_format: jsonSchemaResponseFormat })
1346
+ ).toEqual(['query']);
1245
1347
  });
1246
1348
 
1247
1349
  it('keeps the label on non-strict requests', () => {
@@ -1305,10 +1407,9 @@ describe('ChatOpenAICompletions strict tools for structured output', () => {
1305
1407
  }) as unknown as {
1306
1408
  tools?: { function: { parameters: { properties: object } } }[];
1307
1409
  };
1308
- expect(Object.keys(params.tools?.[0]?.function.parameters.properties ?? {})).toEqual([
1309
- 'intent',
1310
- 'title',
1311
- ]);
1410
+ expect(
1411
+ Object.keys(params.tools?.[0]?.function.parameters.properties ?? {})
1412
+ ).toEqual(['intent', 'title']);
1312
1413
  });
1313
1414
  });
1314
1415
  });