@librechat/agents 3.3.10 → 3.3.11

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 (46) hide show
  1. package/dist/cjs/graphs/Graph.cjs +2 -2
  2. package/dist/cjs/langfuseToolOutputTracing.cjs +228 -16
  3. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  4. package/dist/cjs/llm/init.cjs +1 -1
  5. package/dist/cjs/llm/invoke.cjs +14 -7
  6. package/dist/cjs/llm/invoke.cjs.map +1 -1
  7. package/dist/cjs/llm/openai/index.cjs +188 -11
  8. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  9. package/dist/cjs/main.cjs +4 -3
  10. package/dist/cjs/messages/core.cjs +592 -27
  11. package/dist/cjs/messages/core.cjs.map +1 -1
  12. package/dist/cjs/run.cjs +1 -1
  13. package/dist/cjs/stream.cjs +2 -2
  14. package/dist/cjs/tools/ToolNode.cjs +1 -1
  15. package/dist/cjs/tools/search/tool.cjs +1 -1
  16. package/dist/cjs/utils/index.cjs +1 -1
  17. package/dist/esm/graphs/Graph.mjs +2 -2
  18. package/dist/esm/langfuseToolOutputTracing.mjs +228 -16
  19. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  20. package/dist/esm/llm/init.mjs +1 -1
  21. package/dist/esm/llm/invoke.mjs +14 -7
  22. package/dist/esm/llm/invoke.mjs.map +1 -1
  23. package/dist/esm/llm/openai/index.mjs +190 -13
  24. package/dist/esm/llm/openai/index.mjs.map +1 -1
  25. package/dist/esm/main.mjs +5 -5
  26. package/dist/esm/messages/core.mjs +592 -28
  27. package/dist/esm/messages/core.mjs.map +1 -1
  28. package/dist/esm/run.mjs +1 -1
  29. package/dist/esm/stream.mjs +2 -2
  30. package/dist/esm/tools/ToolNode.mjs +1 -1
  31. package/dist/esm/tools/search/tool.mjs +1 -1
  32. package/dist/esm/utils/index.mjs +1 -1
  33. package/dist/types/langfuseToolOutputTracing.d.ts +1 -0
  34. package/dist/types/llm/invoke.d.ts +1 -1
  35. package/dist/types/messages/core.d.ts +11 -6
  36. package/package.json +1 -1
  37. package/src/langfuseToolOutputTracing.ts +410 -14
  38. package/src/llm/custom-chat-models.smoke.test.ts +747 -0
  39. package/src/llm/invoke.test.ts +98 -0
  40. package/src/llm/invoke.ts +34 -23
  41. package/src/llm/openai/index.ts +334 -25
  42. package/src/llm/openai/llm.spec.ts +107 -6
  43. package/src/messages/core.ts +1290 -42
  44. package/src/messages/formatAgentMessages.test.ts +2623 -0
  45. package/src/specs/langfuse-tool-output-tracing.test.ts +887 -0
  46. package/src/specs/preemptSeal.test.ts +374 -5
@@ -18,6 +18,7 @@ import type * as t from '@/types';
18
18
  import { _convertMessagesToAnthropicPayload } from '@/llm/anthropic/utils/message_inputs';
19
19
  import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
20
20
  import { convertMessageContentToParts } from '@/llm/google/utils/common';
21
+ import { _convertMessagesToOpenAIParams } from '@/llm/openai/utils';
21
22
  import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
22
23
  import { toLangChainContent } from '@/messages/langchain';
23
24
  import { Constants, Providers } from '@/common';
@@ -827,6 +828,103 @@ describe('tryFallbackProviders applies the same lazy annotation transform', () =
827
828
  jest.dontMock('@/llm/init');
828
829
  jest.resetModules();
829
830
  });
831
+
832
+ it('neutralizes preempted Responses history before an OpenAI Chat fallback', async () => {
833
+ const reasoning = {
834
+ id: 'rs_fallback',
835
+ type: 'reasoning',
836
+ status: 'completed',
837
+ summary: [],
838
+ encrypted_content: 'opaque-fallback-reasoning',
839
+ };
840
+ const toolOutput = {
841
+ id: 'ci_fallback',
842
+ type: 'code_interpreter_call',
843
+ status: 'completed',
844
+ code: 'print("fallback result")',
845
+ outputs: [{ type: 'logs', logs: 'fallback result' }],
846
+ };
847
+ const translatedMessage = new AIMessage({
848
+ content: [{ type: 'text', text: 'Partial answer.' }],
849
+ additional_kwargs: {
850
+ reasoning,
851
+ tool_outputs: [toolOutput],
852
+ },
853
+ response_metadata: { model_provider: 'openai' },
854
+ });
855
+ const message = new AIMessage({
856
+ contentBlocks: [
857
+ ...translatedMessage.contentBlocks,
858
+ {
859
+ type: 'non_standard',
860
+ value: {
861
+ id: 'rs_bare_fallback',
862
+ type: 'reasoning',
863
+ summary: [],
864
+ },
865
+ },
866
+ {
867
+ type: 'image',
868
+ mimeType: 'image/png',
869
+ data: 'AA==',
870
+ id: 'ig_fallback',
871
+ metadata: { status: 'completed' },
872
+ },
873
+ ],
874
+ additional_kwargs: {
875
+ reasoning,
876
+ tool_outputs: [toolOutput],
877
+ },
878
+ response_metadata: {
879
+ model_provider: 'openai',
880
+ output_version: 'v1',
881
+ preempted: true,
882
+ },
883
+ });
884
+ const { invokeMessages, model } = buildCapturingModel();
885
+ model._useResponsesApi = () => false;
886
+ jest.doMock('@/llm/init', () => ({
887
+ initializeModel: (): unknown => model,
888
+ }));
889
+ jest.resetModules();
890
+ const { tryFallbackProviders: freshTry } = (await import(
891
+ '@/llm/invoke'
892
+ )) as { tryFallbackProviders: typeof tryFallbackProviders };
893
+
894
+ await freshTry({
895
+ fallbacks: [
896
+ {
897
+ provider: Providers.OPENAI,
898
+ clientOptions: { model: 'gpt-5.6' },
899
+ },
900
+ ],
901
+ messages: [message],
902
+ primaryError: new Error('primary failed'),
903
+ });
904
+
905
+ const sent = invokeMessages[invokeMessages.length - 1][0] as AIMessage;
906
+ expect(sent).not.toBe(message);
907
+ expect(sent.content).toEqual([
908
+ { type: 'text', text: 'Partial answer.' },
909
+ { type: 'text', text: expect.stringContaining('fallback result') },
910
+ ]);
911
+ expect(sent.additional_kwargs).toEqual({});
912
+ expect(JSON.stringify(sent.toJSON())).not.toMatch(/rs_|ci_|ig_/);
913
+ const chatPayload = _convertMessagesToOpenAIParams([sent], 'gpt-5.6');
914
+ expect(chatPayload).toEqual([
915
+ {
916
+ role: 'assistant',
917
+ content: sent.content,
918
+ },
919
+ ]);
920
+ expect(JSON.stringify(chatPayload)).not.toMatch(/rs_|ci_|ig_/);
921
+ expect(JSON.stringify(message.toJSON())).toContain('rs_fallback');
922
+ expect(JSON.stringify(message.toJSON())).toContain('ci_fallback');
923
+ expect(JSON.stringify(message.toJSON())).toContain('ig_fallback');
924
+
925
+ jest.dontMock('@/llm/init');
926
+ jest.resetModules();
927
+ });
830
928
  });
831
929
 
832
930
  describe('invocation attribution metadata', () => {
package/src/llm/invoke.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { concat } from '@langchain/core/utils/stream';
2
2
  import { AIMessageChunk } from '@langchain/core/messages';
3
+ import { getCallbackManagerForConfig } from '@langchain/core/runnables';
3
4
  import {
4
5
  CallbackManager,
5
6
  CallbackManagerForLLMRun,
6
7
  type Callbacks,
7
8
  } from '@langchain/core/callbacks/manager';
8
- import { getCallbackManagerForConfig } from '@langchain/core/runnables';
9
9
  import type { Serialized } from '@langchain/core/load/serializable';
10
- import type { ChatGeneration } from '@langchain/core/outputs';
11
10
  import type { RunnableConfig } from '@langchain/core/runnables';
11
+ import type { ChatGeneration } from '@langchain/core/outputs';
12
12
  import type { ToolCall } from '@langchain/core/messages/tool';
13
13
  import type { BaseMessage } from '@langchain/core/messages';
14
14
  import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
@@ -24,28 +24,28 @@ import {
24
24
  projectStructuredToolOutputsToText,
25
25
  projectToolStreamContentForProvider,
26
26
  } from '@/messages/core';
27
+ import {
28
+ modifyDeltaProperties,
29
+ coalesceAdjacentUserTurns,
30
+ strictAlternationProviders,
31
+ appendPredecessorHandoffCue,
32
+ removePredecessorHandoffCue,
33
+ } from '@/messages';
27
34
  import {
28
35
  stripAnthropicCacheControl,
29
36
  stripBedrockCacheControl,
30
37
  } from '@/messages/cache';
38
+ import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
39
+ import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
31
40
  import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
32
41
  import { assertNotTruncatedToolCall } from '@/llm/truncation';
33
- import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
34
42
  import { manualToolStreamProviders } from '@/llm/providers';
35
- import { appendCallbacks } from '@/utils/callbacks';
43
+ import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
36
44
  import { safeDispatchCustomEvent } from '@/utils/events';
37
45
  import { getContextOverflowInfo } from '@/utils/errors';
38
- import {
39
- modifyDeltaProperties,
40
- coalesceAdjacentUserTurns,
41
- strictAlternationProviders,
42
- appendPredecessorHandoffCue,
43
- removePredecessorHandoffCue,
44
- } from '@/messages';
46
+ import { appendCallbacks } from '@/utils/callbacks';
45
47
  import { canSealPreempt } from '@/llm/preempt';
46
- import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
47
48
  import { initializeModel } from '@/llm/init';
48
- import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
49
49
 
50
50
  /**
51
51
  * Context passed to `attemptInvoke`. Matches the subset of Graph that
@@ -174,8 +174,17 @@ export function projectMessagesForProvider({
174
174
  maxToolResultChars?: number;
175
175
  callOptions?: unknown;
176
176
  }): BaseMessage[] {
177
- const providerInputMessages = projectToolStreamContentForProvider(messages);
178
- if (usesNativeOpenAIResponses(model, provider, callOptions)) {
177
+ const nativeOpenAIResponses = usesNativeOpenAIResponses(
178
+ model,
179
+ provider,
180
+ callOptions
181
+ );
182
+ const providerInputMessages = projectToolStreamContentForProvider(
183
+ messages,
184
+ nativeOpenAIResponses ? 'native' : 'fallback',
185
+ maxToolResultChars
186
+ );
187
+ if (nativeOpenAIResponses) {
179
188
  return projectOpenAIResponsesToolMessageContent(
180
189
  stripAnthropicCacheControl(
181
190
  stripBedrockCacheControl(providerInputMessages)
@@ -433,15 +442,19 @@ function synthesizeSealedUsage(
433
442
  const inputTokens =
434
443
  (countSealedTokens(context, metadata, prompt) ?? 0) +
435
444
  sealedInstructionOverhead(context, metadata);
436
- chunk.usage_metadata = {
445
+ const usageMetadata = {
437
446
  input_tokens: inputTokens,
438
447
  output_tokens: outputTokens,
439
448
  total_tokens: inputTokens + outputTokens,
440
449
  };
441
- chunk.response_metadata = {
450
+ chunk.usage_metadata = usageMetadata;
451
+ chunk.lc_kwargs.usage_metadata = usageMetadata;
452
+ const responseMetadata = {
442
453
  ...chunk.response_metadata,
443
454
  estimated_usage: true,
444
455
  };
456
+ chunk.response_metadata = responseMetadata;
457
+ chunk.lc_kwargs.response_metadata = responseMetadata;
445
458
  }
446
459
 
447
460
  function getMessageText(chunk: AIMessageChunk): string {
@@ -677,11 +690,7 @@ export async function attemptInvoke(
677
690
  });
678
691
  const registry = context?.getOrCreateToolOutputRegistry();
679
692
  const runId = config?.configurable?.run_id as string | undefined;
680
- const annotated = annotateMessagesForLLM(
681
- invocationMessages,
682
- registry,
683
- runId
684
- );
693
+ const annotated = annotateMessagesForLLM(invocationMessages, registry, runId);
685
694
  /**
686
695
  * Keyed on the provider ACTUALLY serving this call, not the agent's primary.
687
696
  * `createCallModel` normalizes for the primary, but `tryFallbackProviders`
@@ -860,10 +869,12 @@ export async function attemptInvoke(
860
869
  }
861
870
 
862
871
  if (preempted && finalChunk != null) {
863
- finalChunk.response_metadata = {
872
+ const responseMetadata = {
864
873
  ...finalChunk.response_metadata,
865
874
  preempted: true,
866
875
  };
876
+ finalChunk.response_metadata = responseMetadata;
877
+ finalChunk.lc_kwargs.response_metadata = responseMetadata;
867
878
  await endSealedModelRun(
868
879
  context,
869
880
  finalChunk,
@@ -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' };
@@ -386,6 +395,10 @@ function isResponseMessage(
386
395
  return item.type === 'message';
387
396
  }
388
397
 
398
+ function isResponseInputRole(role: string): boolean {
399
+ return role === 'system' || role === 'developer' || role === 'user';
400
+ }
401
+
389
402
  /** Only `input_text`/`input_image`/`input_file` accept a Responses breakpoint;
390
403
  * `output_text`/`refusal` (replayed assistant blocks) are rejected with a 400. */
391
404
  function isCacheableResponsePart(part: unknown): part is CacheableResponsePart {
@@ -459,11 +472,7 @@ export function addResponseCacheBreakpoints(
459
472
  /** Only input roles take a Responses breakpoint. Assistant/tool turns
460
473
  * carry output content (string or output_text) that the API rejects
461
474
  * under an input marker, so they're never eligible. */
462
- if (
463
- item.role !== 'system' &&
464
- item.role !== 'developer' &&
465
- item.role !== 'user'
466
- ) {
475
+ if (!isResponseInputRole(item.role)) {
467
476
  return false;
468
477
  }
469
478
  const content = item.content as
@@ -568,6 +577,278 @@ function isResponsesStream(
568
577
  return Symbol.asyncIterator in result;
569
578
  }
570
579
 
580
+ const RESPONSES_REPLAY_OUTPUT_ITEM_TYPES = new Set([
581
+ 'local_shell_call_output',
582
+ 'shell_call_output',
583
+ 'apply_patch_call_output',
584
+ 'program_output',
585
+ ]);
586
+
587
+ function isResponsesReplayOutputItem(item: unknown): boolean {
588
+ return (
589
+ typeof item === 'object' &&
590
+ item != null &&
591
+ 'type' in item &&
592
+ typeof item.type === 'string' &&
593
+ RESPONSES_REPLAY_OUTPUT_ITEM_TYPES.has(item.type)
594
+ );
595
+ }
596
+
597
+ /**
598
+ * LangChain's Responses converter places the authoritative terminal output in
599
+ * response_metadata.output. Its chunk merge has no way to delete provisional
600
+ * tool_outputs or replay-position sidecars, so remove those preemption-only
601
+ * captures once that terminal output arrives. An interrupted stream has no
602
+ * terminal chunk and keeps the captures for replay.
603
+ */
604
+ class ResponsesReplayAIMessageChunk extends AIMessageChunk {
605
+ override get lc_id(): string[] {
606
+ return [...this.lc_namespace, AIMessageChunk.lc_name()];
607
+ }
608
+
609
+ override concat(chunk: AIMessageChunk): this {
610
+ const combined = super.concat(chunk);
611
+ if (!Array.isArray(chunk.response_metadata.output)) {
612
+ return combined;
613
+ }
614
+ delete combined.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY];
615
+ const toolOutputs = combined.additional_kwargs.tool_outputs;
616
+ if (!Array.isArray(toolOutputs)) {
617
+ return combined;
618
+ }
619
+ const retainedToolOutputs = toolOutputs.filter(
620
+ (item) => !isResponsesReplayOutputItem(item)
621
+ );
622
+ if (retainedToolOutputs.length === toolOutputs.length) {
623
+ return combined;
624
+ }
625
+ if (retainedToolOutputs.length > 0) {
626
+ combined.additional_kwargs.tool_outputs = retainedToolOutputs;
627
+ } else {
628
+ delete combined.additional_kwargs.tool_outputs;
629
+ }
630
+ return combined;
631
+ }
632
+ }
633
+
634
+ function makeResponsesReplayAggregationSafe(
635
+ chunk: ChatGenerationChunk
636
+ ): ChatGenerationChunk {
637
+ if (!AIMessageChunk.isInstance(chunk.message)) {
638
+ return chunk;
639
+ }
640
+ const message = chunk.message;
641
+ chunk.message = new ResponsesReplayAIMessageChunk({
642
+ id: message.id,
643
+ name: message.name,
644
+ content: message.content,
645
+ additional_kwargs: message.additional_kwargs,
646
+ response_metadata: message.response_metadata,
647
+ tool_calls: message.tool_calls,
648
+ invalid_tool_calls: message.invalid_tool_calls,
649
+ tool_call_chunks: message.tool_call_chunks,
650
+ usage_metadata: message.usage_metadata,
651
+ });
652
+ return chunk;
653
+ }
654
+
655
+ function remapResponsesTextBlockIndex(
656
+ chunk: ChatGenerationChunk,
657
+ event: OpenAIClient.Responses.ResponseStreamEvent,
658
+ textBlockIndices: Map<string, number>
659
+ ): void {
660
+ const position = iife(() => {
661
+ if (
662
+ event.type === 'response.output_text.delta' ||
663
+ event.type === 'response.output_text.annotation.added'
664
+ ) {
665
+ return {
666
+ contentIndex: event.content_index,
667
+ outputIndex: event.output_index,
668
+ };
669
+ }
670
+ if (
671
+ event.type === 'response.output_item.added' &&
672
+ event.item.type === 'message'
673
+ ) {
674
+ return { contentIndex: 0, outputIndex: event.output_index };
675
+ }
676
+ return undefined;
677
+ });
678
+ if (position == null || !Array.isArray(chunk.message.content)) {
679
+ return;
680
+ }
681
+ const key = `${position.outputIndex}:${position.contentIndex}`;
682
+ let blockIndex = textBlockIndices.get(key);
683
+ if (blockIndex == null) {
684
+ blockIndex = textBlockIndices.size;
685
+ textBlockIndices.set(key, blockIndex);
686
+ }
687
+ const content = chunk.message.content.map((block) =>
688
+ typeof block === 'object' && block.type === 'text'
689
+ ? { ...block, index: blockIndex }
690
+ : block
691
+ );
692
+ chunk.message.content = content;
693
+ chunk.message.lc_kwargs.content = content;
694
+ }
695
+
696
+ function convertDroppedResponsesReplayOutput(
697
+ event: OpenAIClient.Responses.ResponseStreamEvent
698
+ ): ChatGenerationChunk | null {
699
+ if (event.type !== 'response.output_item.done') {
700
+ return null;
701
+ }
702
+ if (event.item.type === 'reasoning') {
703
+ // Added/summary events already stream id, type, and summary. Only merge
704
+ // terminal fields here so chunk concatenation does not duplicate summary.
705
+ return new ChatGenerationChunk({
706
+ text: '',
707
+ message: new AIMessageChunk({
708
+ content: [],
709
+ additional_kwargs: {
710
+ reasoning: {
711
+ status: event.item.status,
712
+ ...(typeof event.item.encrypted_content === 'string'
713
+ ? { encrypted_content: event.item.encrypted_content }
714
+ : {}),
715
+ },
716
+ },
717
+ response_metadata: { model_provider: 'openai' },
718
+ }),
719
+ });
720
+ }
721
+ if (!RESPONSES_REPLAY_OUTPUT_ITEM_TYPES.has(event.item.type)) {
722
+ return null;
723
+ }
724
+ return new ChatGenerationChunk({
725
+ text: '',
726
+ message: new AIMessageChunk({
727
+ content: [],
728
+ additional_kwargs: { tool_outputs: [event.item] },
729
+ response_metadata: { model_provider: 'openai' },
730
+ }),
731
+ });
732
+ }
733
+
734
+ function attachResponsesReplayPosition(
735
+ chunk: ChatGenerationChunk,
736
+ event: OpenAIClient.Responses.ResponseStreamEvent,
737
+ seenPositions: Set<string>
738
+ ): void {
739
+ let position: ResponsesReplayPosition | undefined;
740
+ if (event.type === 'response.output_text.delta' && event.delta.length > 0) {
741
+ position = {
742
+ contentIndex: event.content_index,
743
+ itemId: event.item_id,
744
+ kind: 'text',
745
+ outputIndex: event.output_index,
746
+ };
747
+ } else if (
748
+ event.type === 'response.output_item.added' &&
749
+ event.item.type === 'message' &&
750
+ typeof event.item.id === 'string' &&
751
+ event.item.id.length > 0
752
+ ) {
753
+ position = {
754
+ itemId: event.item.id,
755
+ kind: 'message',
756
+ outputIndex: event.output_index,
757
+ };
758
+ } else if (
759
+ event.type === 'response.output_item.added' &&
760
+ event.item.type === 'reasoning' &&
761
+ typeof event.item.id === 'string' &&
762
+ event.item.id.length > 0
763
+ ) {
764
+ position = {
765
+ itemId: event.item.id,
766
+ kind: 'reasoning',
767
+ outputIndex: event.output_index,
768
+ };
769
+ } else if (
770
+ event.type === 'response.output_item.done' &&
771
+ (RESPONSES_REPLAY_OUTPUT_ITEM_TYPES.has(event.item.type) ||
772
+ Array.isArray(chunk.message.additional_kwargs.tool_outputs))
773
+ ) {
774
+ let itemId: string | undefined;
775
+ if (typeof event.item.id === 'string' && event.item.id.length > 0) {
776
+ itemId = event.item.id;
777
+ } else if (
778
+ 'call_id' in event.item &&
779
+ typeof event.item.call_id === 'string' &&
780
+ event.item.call_id.length > 0
781
+ ) {
782
+ itemId = event.item.call_id;
783
+ }
784
+ if (itemId != null) {
785
+ position = {
786
+ itemId,
787
+ kind: 'output',
788
+ outputIndex: event.output_index,
789
+ };
790
+ }
791
+ }
792
+ if (position == null) {
793
+ return;
794
+ }
795
+ const positionKey = `${position.kind}:${position.itemId}:${position.outputIndex}:${position.contentIndex ?? ''}`;
796
+ if (seenPositions.has(positionKey)) {
797
+ return;
798
+ }
799
+ seenPositions.add(positionKey);
800
+ const existing = chunk.message.additional_kwargs[
801
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
802
+ ] as unknown;
803
+ const additionalKwargs = {
804
+ ...chunk.message.additional_kwargs,
805
+ [OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
806
+ ...(Array.isArray(existing) ? existing : []),
807
+ position,
808
+ ],
809
+ };
810
+ chunk.message.additional_kwargs = additionalKwargs;
811
+ chunk.message.lc_kwargs.additional_kwargs = additionalKwargs;
812
+ }
813
+
814
+ async function* convertLibreChatResponsesStream(
815
+ stream: AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>,
816
+ options: ResponsesStreamChunkOptions,
817
+ runManager?: CallbackManagerForLLMRun
818
+ ): AsyncGenerator<ChatGenerationChunk> {
819
+ const seenReplayPositions = new Set<string>();
820
+ const responsesTextBlockIndices = new Map<string, number>();
821
+ try {
822
+ for await (const event of stream) {
823
+ options.signal?.throwIfAborted();
824
+ const convertedChunk =
825
+ convertResponsesDeltaToChatGenerationChunk(event) ??
826
+ convertDroppedResponsesReplayOutput(event);
827
+ if (convertedChunk == null) {
828
+ continue;
829
+ }
830
+ const chunk = makeResponsesReplayAggregationSafe(convertedChunk);
831
+ remapResponsesTextBlockIndex(chunk, event, responsesTextBlockIndices);
832
+ attachResponsesReplayPosition(chunk, event, seenReplayPositions);
833
+ attachCacheWriteUsage(chunk.message);
834
+ await runManager?.handleLLMNewToken(
835
+ chunk.text || '',
836
+ {
837
+ prompt: options.promptIndex ?? 0,
838
+ completion: 0,
839
+ },
840
+ undefined,
841
+ undefined,
842
+ undefined,
843
+ { chunk }
844
+ );
845
+ yield chunk;
846
+ }
847
+ } catch (e) {
848
+ throw wrapOpenAIClientError(e);
849
+ }
850
+ }
851
+
571
852
  function createUsageMetadata(
572
853
  usage?: OpenAIClient.Completions.CompletionUsage
573
854
  ): UsageMetadata {
@@ -1754,14 +2035,20 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
1754
2035
  options: this['ParsedCallOptions'],
1755
2036
  runManager?: CallbackManagerForLLMRun
1756
2037
  ): 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
- }
2038
+ const projectedMessages = projectOpenAIResponsesProviderMessages(messages);
2039
+ const stream = await this.completionWithRetry(
2040
+ {
2041
+ ...this.invocationParams(options),
2042
+ input: convertMessagesToResponsesInput({
2043
+ messages: projectedMessages,
2044
+ zdrEnabled: this.zdrEnabled ?? false,
2045
+ model: this.model,
2046
+ }),
2047
+ stream: true,
2048
+ },
2049
+ options
2050
+ );
2051
+ yield* convertLibreChatResponsesStream(stream, options, runManager);
1765
2052
  }
1766
2053
 
1767
2054
  async *_streamChatModelEvents(
@@ -1988,7 +2275,11 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
1988
2275
  options: this['ParsedCallOptions'],
1989
2276
  runManager?: CallbackManagerForLLMRun
1990
2277
  ): Promise<ChatResult> {
1991
- const result = await super._generate(messages, options, runManager);
2278
+ const result = await super._generate(
2279
+ projectOpenAIResponsesProviderMessages(messages),
2280
+ options,
2281
+ runManager
2282
+ );
1992
2283
  for (const generation of result.generations) {
1993
2284
  attachCacheWriteUsage(generation.message);
1994
2285
  }
@@ -2000,14 +2291,32 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
2000
2291
  options: this['ParsedCallOptions'],
2001
2292
  runManager?: CallbackManagerForLLMRun
2002
2293
  ): AsyncGenerator<ChatGenerationChunk> {
2003
- for await (const chunk of super._streamResponseChunks(
2004
- messages,
2294
+ const projectedMessages = projectOpenAIResponsesProviderMessages(messages);
2295
+ const stream = await this.completionWithRetry(
2296
+ {
2297
+ ...this.invocationParams(options),
2298
+ input: convertMessagesToResponsesInput({
2299
+ messages: projectedMessages,
2300
+ zdrEnabled: this.zdrEnabled ?? false,
2301
+ model: this.model,
2302
+ }),
2303
+ stream: true,
2304
+ },
2305
+ options
2306
+ );
2307
+ yield* convertLibreChatResponsesStream(stream, options, runManager);
2308
+ }
2309
+
2310
+ async *_streamChatModelEvents(
2311
+ messages: BaseMessage[],
2312
+ options: this['ParsedCallOptions'],
2313
+ runManager?: CallbackManagerForLLMRun
2314
+ ): AsyncGenerator<ChatModelStreamEvent> {
2315
+ yield* super._streamChatModelEvents(
2316
+ projectOpenAIResponsesProviderMessages(messages),
2005
2317
  options,
2006
2318
  runManager
2007
- )) {
2008
- attachCacheWriteUsage(chunk.message);
2009
- yield chunk;
2010
- }
2319
+ );
2011
2320
  }
2012
2321
 
2013
2322
  protected _getReasoningParams(