@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
@@ -14,6 +14,7 @@ import {
14
14
  import type { BaseMessage } from '@langchain/core/messages';
15
15
  import type { MessageContentComplex, TPayload } from '@/types';
16
16
  import {
17
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY,
17
18
  convertMessagesToContent,
18
19
  formatAnthropicArtifactContent,
19
20
  formatArtifactPayload,
@@ -22,6 +23,7 @@ import {
22
23
  projectComputerCallOutputsToText,
23
24
  projectOpenAIToolMessageContent,
24
25
  projectOpenAIResponsesToolMessageContent,
26
+ projectToolStreamContentForProvider,
25
27
  } from './core';
26
28
  import {
27
29
  _convertMessagesToOpenAIParams,
@@ -42,6 +44,13 @@ type AnthropicPayloadBlock = {
42
44
  type: string;
43
45
  };
44
46
 
47
+ const CODE_INTERPRETER_REPLAY_EXTRAS = {
48
+ librechatServerToolResult: { toolName: 'code_interpreter' },
49
+ } as const;
50
+ const IMAGE_GENERATION_REPLAY_EXTRAS = {
51
+ librechatServerToolResult: { toolName: 'image_generation' },
52
+ } as const;
53
+
45
54
  const getAnthropicPayloadBlocks = (
46
55
  content: unknown
47
56
  ): AnthropicPayloadBlock[] => {
@@ -1753,6 +1762,2620 @@ describe('formatAgentMessages', () => {
1753
1762
  });
1754
1763
  });
1755
1764
 
1765
+ it('neutralizes nonterminal Responses references on preempted provider input', () => {
1766
+ const message = new AIMessage({
1767
+ id: 'msg_interrupted',
1768
+ content: 'Partial answer.',
1769
+ additional_kwargs: {
1770
+ reasoning: {
1771
+ id: 'rs_interrupted',
1772
+ type: 'reasoning',
1773
+ summary: [],
1774
+ },
1775
+ tool_outputs: [
1776
+ {
1777
+ id: 'ci_interrupted',
1778
+ type: 'code_interpreter_call',
1779
+ status: 'completed',
1780
+ },
1781
+ ],
1782
+ __openai_function_call_ids__: { call_1: 'fc_interrupted' },
1783
+ __openai_custom_tool_call_ids__: { call_2: 'ctc_interrupted' },
1784
+ retained: 'application metadata',
1785
+ },
1786
+ response_metadata: {
1787
+ id: 'resp_interrupted',
1788
+ model_provider: 'openai',
1789
+ output: [],
1790
+ tool_outputs: [{ id: 'ci_interrupted' }],
1791
+ model_name: 'gpt-5.6',
1792
+ preempted: true,
1793
+ },
1794
+ });
1795
+
1796
+ const messages = [message];
1797
+ const unsafeProviderInput = convertMessagesToResponsesInput({
1798
+ messages,
1799
+ zdrEnabled: false,
1800
+ model: 'gpt-5.6',
1801
+ });
1802
+ const projected = projectOpenAIResponsesToolMessageContent(messages);
1803
+ const projectedMessage = projected[0] as AIMessage;
1804
+ const providerInput = convertMessagesToResponsesInput({
1805
+ messages: projected,
1806
+ zdrEnabled: false,
1807
+ model: 'gpt-5.6',
1808
+ });
1809
+
1810
+ expect(projected).not.toBe(messages);
1811
+ expect(projectedMessage).not.toBe(message);
1812
+ expect(Object.getPrototypeOf(projectedMessage)).toBe(
1813
+ Object.getPrototypeOf(message)
1814
+ );
1815
+ expect(projectedMessage.id).toBeUndefined();
1816
+ expect(projectedMessage.content).toBe('Partial answer.');
1817
+ expect(projectedMessage.additional_kwargs).toEqual({
1818
+ retained: 'application metadata',
1819
+ });
1820
+ expect(projectedMessage.response_metadata).toEqual({
1821
+ model_provider: 'openai',
1822
+ model_name: 'gpt-5.6',
1823
+ preempted: true,
1824
+ });
1825
+ const unsafeSerializedInput = JSON.stringify(unsafeProviderInput);
1826
+ const serializedInput = JSON.stringify(providerInput);
1827
+ expect(unsafeSerializedInput).toContain('rs_interrupted');
1828
+ expect(unsafeSerializedInput).toContain('ci_interrupted');
1829
+ expect(serializedInput).toContain('Partial answer.');
1830
+ expect(serializedInput).not.toContain('interrupted');
1831
+ expect(message.id).toBe('msg_interrupted');
1832
+ expect(message.additional_kwargs.reasoning).toBeDefined();
1833
+ expect(message.response_metadata).toHaveProperty('output');
1834
+
1835
+ const projectedAgain = projectOpenAIResponsesToolMessageContent(projected);
1836
+ expect(projectedAgain).toBe(projected);
1837
+ expect(projectedAgain[0]).toBe(projectedMessage);
1838
+ });
1839
+
1840
+ it('retains self-contained encrypted reasoning on preempted Responses input', () => {
1841
+ const reasoning = {
1842
+ id: 'rs_encrypted',
1843
+ type: 'reasoning',
1844
+ status: 'completed',
1845
+ summary: [],
1846
+ encrypted_content: 'opaque-reasoning',
1847
+ };
1848
+ const message = new AIMessage({
1849
+ id: 'msg_interrupted',
1850
+ content: 'Partial answer.',
1851
+ additional_kwargs: { reasoning },
1852
+ response_metadata: { model_provider: 'openai', preempted: true },
1853
+ });
1854
+
1855
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
1856
+ const projectedMessage = projected as AIMessage;
1857
+ const providerInput = convertMessagesToResponsesInput({
1858
+ messages: [projected],
1859
+ zdrEnabled: false,
1860
+ model: 'gpt-5.6',
1861
+ });
1862
+
1863
+ expect(projectedMessage.additional_kwargs.reasoning).toBe(reasoning);
1864
+ expect(providerInput).toContainEqual(
1865
+ expect.objectContaining({
1866
+ id: 'rs_encrypted',
1867
+ type: 'reasoning',
1868
+ encrypted_content: 'opaque-reasoning',
1869
+ })
1870
+ );
1871
+ });
1872
+
1873
+ it.each([
1874
+ [
1875
+ 'Responses projector',
1876
+ (messages: BaseMessage[]) =>
1877
+ projectOpenAIResponsesToolMessageContent(messages),
1878
+ ],
1879
+ [
1880
+ 'common native projector',
1881
+ (messages: BaseMessage[]) =>
1882
+ projectToolStreamContentForProvider(messages, 'native'),
1883
+ ],
1884
+ ])(
1885
+ 'does not classify provider-neutral v1 Chat content as Responses through the %s',
1886
+ (_name, project) => {
1887
+ const applicationBlock = {
1888
+ type: 'non_standard' as const,
1889
+ value: { source: 'application' },
1890
+ };
1891
+ const message = new AIMessage({
1892
+ content: [{ type: 'text', text: 'Chat answer.' }, applicationBlock],
1893
+ response_metadata: {
1894
+ model_provider: 'openai',
1895
+ output_version: 'v1',
1896
+ preempted: true,
1897
+ },
1898
+ });
1899
+ const messages = [message];
1900
+
1901
+ const projected = project(messages);
1902
+
1903
+ expect(projected).toBe(messages);
1904
+ expect(projected[0]).toBe(message);
1905
+ expect(message.content).toEqual([
1906
+ { type: 'text', text: 'Chat answer.' },
1907
+ applicationBlock,
1908
+ ]);
1909
+ }
1910
+ );
1911
+
1912
+ it('normalizes Responses text metadata before OpenAI Chat fallback replay', () => {
1913
+ const annotatedText = {
1914
+ type: 'text' as const,
1915
+ text: 'Cited answer.',
1916
+ annotations: [
1917
+ {
1918
+ type: 'citation' as const,
1919
+ url: 'https://example.com/source',
1920
+ title: 'Source',
1921
+ },
1922
+ ],
1923
+ extras: { phase: 'final_answer' },
1924
+ index: 0,
1925
+ };
1926
+ const message = new AIMessage({
1927
+ content: [annotatedText],
1928
+ response_metadata: {
1929
+ model_provider: 'openai',
1930
+ output: [],
1931
+ output_version: 'v1',
1932
+ preempted: true,
1933
+ },
1934
+ });
1935
+
1936
+ const native = projectToolStreamContentForProvider([message], 'native');
1937
+ const fallback = projectToolStreamContentForProvider([message], 'fallback');
1938
+ const fallbackMessage = fallback[0] as AIMessage;
1939
+
1940
+ expect((native[0] as AIMessage).content).toEqual([annotatedText]);
1941
+ expect(fallbackMessage.content).toEqual([
1942
+ { type: 'text', text: 'Cited answer.' },
1943
+ ]);
1944
+ expect(_convertMessagesToOpenAIParams(fallback)).toEqual([
1945
+ {
1946
+ role: 'assistant',
1947
+ content: [{ type: 'text', text: 'Cited answer.' }],
1948
+ },
1949
+ ]);
1950
+ expect(message.content).toEqual([annotatedText]);
1951
+
1952
+ const fallbackAgain = projectToolStreamContentForProvider(
1953
+ fallback,
1954
+ 'fallback'
1955
+ );
1956
+ expect(fallbackAgain).toBe(fallback);
1957
+ expect(fallbackAgain[0]).toBe(fallbackMessage);
1958
+ });
1959
+
1960
+ it.each([
1961
+ [
1962
+ 'Responses projector',
1963
+ (messages: BaseMessage[]) =>
1964
+ projectOpenAIResponsesToolMessageContent(messages),
1965
+ ],
1966
+ [
1967
+ 'common native projector',
1968
+ (messages: BaseMessage[]) =>
1969
+ projectToolStreamContentForProvider(messages, 'native'),
1970
+ ],
1971
+ ])('sanitizes native v0 fragments through the %s', (_name, project) => {
1972
+ const emptyText = { type: 'text' as const, text: '' };
1973
+ const partialText = { type: 'text' as const, text: 'Partial answer.' };
1974
+ const bareReasoning = {
1975
+ type: 'reasoning' as const,
1976
+ reasoning: 'summary',
1977
+ };
1978
+ const message = new AIMessage({
1979
+ id: 'resp_fragmented',
1980
+ content: [emptyText, partialText, bareReasoning],
1981
+ response_metadata: {
1982
+ model_provider: 'openai',
1983
+ preempted: true,
1984
+ },
1985
+ });
1986
+ const messages = [message];
1987
+
1988
+ const projected = project(messages);
1989
+ const projectedMessage = projected[0] as AIMessage;
1990
+
1991
+ expect(projected).not.toBe(messages);
1992
+ expect(projectedMessage.content).toEqual([partialText]);
1993
+ expect(projectedMessage.response_metadata.output_version).toBeUndefined();
1994
+ expect(message.content).toEqual([emptyText, partialText, bareReasoning]);
1995
+
1996
+ const projectedAgain = project(projected);
1997
+ expect(projectedAgain).toBe(projected);
1998
+ expect(projectedAgain[0]).toBe(projectedMessage);
1999
+ });
2000
+
2001
+ it.each([
2002
+ [
2003
+ 'Responses projector',
2004
+ (messages: BaseMessage[]) =>
2005
+ projectOpenAIResponsesToolMessageContent(messages),
2006
+ ],
2007
+ [
2008
+ 'common native projector',
2009
+ (messages: BaseMessage[]) =>
2010
+ projectToolStreamContentForProvider(messages, 'native'),
2011
+ ],
2012
+ ])(
2013
+ 'sanitizes unpromoted translated v0 content through the %s',
2014
+ (_name, project) => {
2015
+ const emptyText = { type: 'text' as const, text: '' };
2016
+ const partialText = { type: 'text' as const, text: 'Partial answer.' };
2017
+ const applicationImage = {
2018
+ type: 'image' as const,
2019
+ mimeType: 'image/png',
2020
+ data: 'AQ==',
2021
+ };
2022
+ const bareReasoning = {
2023
+ type: 'reasoning' as const,
2024
+ reasoning: 'unfinished summary',
2025
+ };
2026
+ const message = new AIMessage({
2027
+ id: 'msg_unpromoted',
2028
+ content: [emptyText, partialText, applicationImage, bareReasoning],
2029
+ additional_kwargs: {
2030
+ tool_outputs: [
2031
+ {
2032
+ id: 'ig_unusable',
2033
+ type: 'image_generation_call',
2034
+ status: 'in_progress',
2035
+ result: 'AA==',
2036
+ },
2037
+ ],
2038
+ },
2039
+ response_metadata: {
2040
+ model_provider: 'openai',
2041
+ preempted: true,
2042
+ },
2043
+ });
2044
+ const messages = [message];
2045
+
2046
+ const projected = project(messages);
2047
+ const projectedMessage = projected[0] as AIMessage;
2048
+
2049
+ expect(projected).not.toBe(messages);
2050
+ expect(projectedMessage.id).toBeUndefined();
2051
+ expect(projectedMessage.content).toEqual([partialText, applicationImage]);
2052
+ expect(projectedMessage.additional_kwargs).not.toHaveProperty(
2053
+ 'tool_outputs'
2054
+ );
2055
+ expect(projectedMessage.response_metadata.output_version).toBeUndefined();
2056
+ expect(JSON.stringify(projectedMessage.toJSON())).not.toContain(
2057
+ 'ig_unusable'
2058
+ );
2059
+ expect(message.content).toEqual([
2060
+ emptyText,
2061
+ partialText,
2062
+ applicationImage,
2063
+ bareReasoning,
2064
+ ]);
2065
+
2066
+ const projectedAgain = project(projected);
2067
+ expect(projectedAgain).toBe(projected);
2068
+ expect(projectedAgain[0]).toBe(projectedMessage);
2069
+ }
2070
+ );
2071
+
2072
+ it('recognizes response_metadata.tool_outputs as a Responses marker', () => {
2073
+ const message = new AIMessage({
2074
+ content: 'Partial answer.',
2075
+ response_metadata: {
2076
+ model_provider: 'openai',
2077
+ preempted: true,
2078
+ tool_outputs: [{ id: 'ci_metadata_only' }],
2079
+ },
2080
+ });
2081
+
2082
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
2083
+ const projectedMessage = projected[0] as AIMessage;
2084
+
2085
+ expect(projectedMessage).not.toBe(message);
2086
+ expect(projectedMessage.content).toBe('Partial answer.');
2087
+ expect(projectedMessage.response_metadata).not.toHaveProperty(
2088
+ 'tool_outputs'
2089
+ );
2090
+ expect(JSON.stringify(projectedMessage.toJSON())).not.toContain(
2091
+ 'ci_metadata_only'
2092
+ );
2093
+ expect(message.response_metadata).toHaveProperty('tool_outputs');
2094
+ });
2095
+
2096
+ it('neutralizes terminal Responses ids when request retention is unknown', () => {
2097
+ const message = new AIMessage({
2098
+ id: 'msg_completed',
2099
+ content: 'Complete answer.',
2100
+ additional_kwargs: {
2101
+ reasoning: {
2102
+ id: 'rs_completed',
2103
+ type: 'reasoning',
2104
+ summary: [],
2105
+ },
2106
+ },
2107
+ response_metadata: {
2108
+ model_provider: 'openai',
2109
+ preempted: true,
2110
+ status: 'completed',
2111
+ output: [
2112
+ {
2113
+ id: 'msg_completed',
2114
+ type: 'message',
2115
+ role: 'assistant',
2116
+ status: 'completed',
2117
+ content: [
2118
+ {
2119
+ type: 'output_text',
2120
+ text: 'Complete answer.',
2121
+ annotations: [],
2122
+ },
2123
+ ],
2124
+ },
2125
+ ],
2126
+ },
2127
+ });
2128
+
2129
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
2130
+ const projectedMessage = projected[0] as AIMessage;
2131
+ const providerInput = convertMessagesToResponsesInput({
2132
+ messages: projected,
2133
+ zdrEnabled: false,
2134
+ model: 'gpt-5.6',
2135
+ });
2136
+
2137
+ expect(projectedMessage).not.toBe(message);
2138
+ expect(projectedMessage.id).toBeUndefined();
2139
+ expect(projectedMessage.additional_kwargs.reasoning).toBeUndefined();
2140
+ expect(projectedMessage.response_metadata).toEqual({
2141
+ model_provider: 'openai',
2142
+ preempted: true,
2143
+ status: 'completed',
2144
+ });
2145
+ expect(JSON.stringify(providerInput)).toContain('Complete answer.');
2146
+ expect(JSON.stringify(providerInput)).not.toContain('msg_completed');
2147
+ expect(JSON.stringify(providerInput)).not.toContain('rs_completed');
2148
+ expect(message.response_metadata).toHaveProperty('output');
2149
+ });
2150
+
2151
+ it.each(['additional_kwargs', 'response_metadata'] as const)(
2152
+ 'preserves completed v0 generated images from %s without provider ids',
2153
+ (imageSource) => {
2154
+ const events: Parameters<
2155
+ typeof convertResponsesDeltaToChatGenerationChunk
2156
+ >[0][] = [
2157
+ {
2158
+ type: 'response.output_item.done',
2159
+ sequence_number: 0,
2160
+ output_index: 0,
2161
+ item: {
2162
+ id: 'ig_interrupted',
2163
+ type: 'image_generation_call',
2164
+ status: 'completed',
2165
+ result: 'AA==',
2166
+ },
2167
+ },
2168
+ {
2169
+ type: 'response.output_item.added',
2170
+ sequence_number: 1,
2171
+ output_index: 1,
2172
+ item: {
2173
+ id: 'msg_interrupted',
2174
+ type: 'message',
2175
+ role: 'assistant',
2176
+ status: 'in_progress',
2177
+ content: [],
2178
+ },
2179
+ },
2180
+ {
2181
+ type: 'response.output_text.delta',
2182
+ sequence_number: 2,
2183
+ output_index: 1,
2184
+ content_index: 0,
2185
+ item_id: 'msg_interrupted',
2186
+ delta: 'Partial answer.',
2187
+ logprobs: [],
2188
+ },
2189
+ ];
2190
+ let streamedMessage: AIMessageChunk | undefined;
2191
+ for (const event of events) {
2192
+ const generation = convertResponsesDeltaToChatGenerationChunk(event);
2193
+ if (generation == null) {
2194
+ continue;
2195
+ }
2196
+ streamedMessage =
2197
+ streamedMessage == null
2198
+ ? (generation.message as AIMessageChunk)
2199
+ : (streamedMessage.concat(
2200
+ generation.message as AIMessageChunk
2201
+ ) as AIMessageChunk);
2202
+ }
2203
+ expect(streamedMessage).toBeDefined();
2204
+ const toolOutputs = streamedMessage!.additional_kwargs.tool_outputs;
2205
+ const message =
2206
+ imageSource === 'additional_kwargs'
2207
+ ? streamedMessage!
2208
+ : new AIMessage({
2209
+ content: 'Partial answer.',
2210
+ response_metadata: {
2211
+ model_provider: 'openai',
2212
+ output: toolOutputs,
2213
+ },
2214
+ });
2215
+ message.response_metadata.preempted = true;
2216
+
2217
+ const unsafeProviderInput = convertMessagesToResponsesInput({
2218
+ messages: [message],
2219
+ zdrEnabled: false,
2220
+ model: 'gpt-5.6',
2221
+ });
2222
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
2223
+ const projectedMessage = projected[0] as AIMessage;
2224
+
2225
+ expect(JSON.stringify(unsafeProviderInput)).toContain('ig_interrupted');
2226
+ expect(projectedMessage.response_metadata).toMatchObject({
2227
+ model_provider: 'openai',
2228
+ output_version: 'v1',
2229
+ preempted: true,
2230
+ });
2231
+ expect(projectedMessage.content).toEqual(
2232
+ expect.arrayContaining([
2233
+ expect.objectContaining({ type: 'text', text: 'Partial answer.' }),
2234
+ {
2235
+ type: 'image',
2236
+ mimeType: 'image/png',
2237
+ data: 'AA==',
2238
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2239
+ },
2240
+ ])
2241
+ );
2242
+ expect(JSON.stringify(projectedMessage.toJSON())).not.toContain(
2243
+ 'ig_interrupted'
2244
+ );
2245
+ expect(JSON.stringify(message.toJSON())).toContain('ig_interrupted');
2246
+
2247
+ for (const zdrEnabled of [false, true]) {
2248
+ const providerInput = convertMessagesToResponsesInput({
2249
+ messages: projected,
2250
+ zdrEnabled,
2251
+ model: 'gpt-5.6',
2252
+ });
2253
+ expect(providerInput).toEqual(
2254
+ expect.arrayContaining([
2255
+ expect.objectContaining({
2256
+ type: 'message',
2257
+ role: 'assistant',
2258
+ content: expect.arrayContaining([
2259
+ expect.objectContaining({
2260
+ type: 'output_text',
2261
+ text: 'Partial answer.',
2262
+ }),
2263
+ {
2264
+ type: 'input_image',
2265
+ detail: 'auto',
2266
+ image_url: 'data:image/png;base64,AA==',
2267
+ },
2268
+ ]),
2269
+ }),
2270
+ ])
2271
+ );
2272
+ expect(JSON.stringify(providerInput)).not.toContain(
2273
+ 'image_generation_call'
2274
+ );
2275
+ expect(JSON.stringify(providerInput)).not.toContain('ig_interrupted');
2276
+ }
2277
+
2278
+ const projectedAgain =
2279
+ projectOpenAIResponsesToolMessageContent(projected);
2280
+ expect(projectedAgain).toBe(projected);
2281
+ expect(projectedAgain[0]).toBe(projectedMessage);
2282
+ }
2283
+ );
2284
+
2285
+ it('keeps completed v1 generated images in their Responses output positions', () => {
2286
+ const firstGeneratedImage = {
2287
+ id: 'ig_before_text',
2288
+ type: 'image_generation_call',
2289
+ status: 'completed',
2290
+ result: 'AA==',
2291
+ };
2292
+ const secondGeneratedImage = {
2293
+ id: 'ig_between_text',
2294
+ type: 'image_generation_call',
2295
+ status: 'completed',
2296
+ result: 'AQ==',
2297
+ };
2298
+ const message = new AIMessage({
2299
+ content: [
2300
+ {
2301
+ type: 'image',
2302
+ mimeType: 'image/png',
2303
+ data: firstGeneratedImage.result,
2304
+ id: firstGeneratedImage.id,
2305
+ metadata: { status: 'completed' },
2306
+ },
2307
+ { type: 'text', text: 'The image above is the first result.' },
2308
+ {
2309
+ type: 'image',
2310
+ mimeType: 'image/png',
2311
+ data: secondGeneratedImage.result,
2312
+ id: secondGeneratedImage.id,
2313
+ metadata: { status: 'completed' },
2314
+ },
2315
+ { type: 'text', text: 'The second image is directly above.' },
2316
+ ],
2317
+ response_metadata: {
2318
+ model_provider: 'openai',
2319
+ output_version: 'v1',
2320
+ preempted: true,
2321
+ output: [
2322
+ firstGeneratedImage,
2323
+ {
2324
+ id: 'msg_first_narration',
2325
+ type: 'message',
2326
+ role: 'assistant',
2327
+ status: 'completed',
2328
+ content: [
2329
+ {
2330
+ type: 'output_text',
2331
+ text: 'The image above is the first result.',
2332
+ annotations: [],
2333
+ },
2334
+ ],
2335
+ },
2336
+ secondGeneratedImage,
2337
+ {
2338
+ id: 'msg_second_narration',
2339
+ type: 'message',
2340
+ role: 'assistant',
2341
+ status: 'in_progress',
2342
+ content: [
2343
+ {
2344
+ type: 'output_text',
2345
+ text: 'The second image is directly above.',
2346
+ annotations: [],
2347
+ },
2348
+ ],
2349
+ },
2350
+ ],
2351
+ },
2352
+ });
2353
+
2354
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
2355
+ const projectedMessage = projected[0] as AIMessage;
2356
+ const providerInput = convertMessagesToResponsesInput({
2357
+ messages: projected,
2358
+ zdrEnabled: false,
2359
+ model: 'gpt-5.6',
2360
+ });
2361
+
2362
+ expect(projectedMessage.content).toEqual([
2363
+ {
2364
+ type: 'image',
2365
+ mimeType: 'image/png',
2366
+ data: 'AA==',
2367
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2368
+ },
2369
+ { type: 'text', text: 'The image above is the first result.' },
2370
+ {
2371
+ type: 'image',
2372
+ mimeType: 'image/png',
2373
+ data: 'AQ==',
2374
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2375
+ },
2376
+ { type: 'text', text: 'The second image is directly above.' },
2377
+ ]);
2378
+ expect(providerInput).toEqual([
2379
+ {
2380
+ type: 'message',
2381
+ role: 'assistant',
2382
+ content: [
2383
+ {
2384
+ type: 'input_image',
2385
+ detail: 'auto',
2386
+ image_url: 'data:image/png;base64,AA==',
2387
+ },
2388
+ {
2389
+ type: 'output_text',
2390
+ text: 'The image above is the first result.',
2391
+ annotations: [],
2392
+ },
2393
+ {
2394
+ type: 'input_image',
2395
+ detail: 'auto',
2396
+ image_url: 'data:image/png;base64,AQ==',
2397
+ },
2398
+ {
2399
+ type: 'output_text',
2400
+ text: 'The second image is directly above.',
2401
+ annotations: [],
2402
+ },
2403
+ ],
2404
+ },
2405
+ ]);
2406
+ expect(JSON.stringify(projectedMessage.toJSON())).not.toMatch(
2407
+ /ig_(before|between)_text/
2408
+ );
2409
+ expect(message.response_metadata).toHaveProperty('output');
2410
+
2411
+ const projectedAgain = projectOpenAIResponsesToolMessageContent(projected);
2412
+ expect(projectedAgain).toBe(projected);
2413
+ expect(projectedAgain[0]).toBe(projectedMessage);
2414
+ });
2415
+
2416
+ it.each([
2417
+ ['v0', 'jpeg', '/9j/4AAQSkZJRg==', 'image/jpeg'],
2418
+ ['v1', 'jpeg', '/9j/4AAQSkZJRg==', 'image/jpeg'],
2419
+ ['v0', 'webp', 'UklGRgAAAABXRUJQ', 'image/webp'],
2420
+ ['v1', 'webp', 'UklGRgAAAABXRUJQ', 'image/webp'],
2421
+ ] as const)(
2422
+ 'preserves %s generated %s media type during replay',
2423
+ (outputVersion, _format, data, mimeType) => {
2424
+ const generatedImage = {
2425
+ id: `ig_${outputVersion}_${_format}`,
2426
+ type: 'image_generation_call',
2427
+ status: 'completed',
2428
+ result: data,
2429
+ };
2430
+ const v0Message = new AIMessage({
2431
+ content: [{ type: 'text', text: 'Generated image.' }],
2432
+ additional_kwargs: { tool_outputs: [generatedImage] },
2433
+ response_metadata: {
2434
+ model_provider: 'openai',
2435
+ preempted: true,
2436
+ },
2437
+ });
2438
+ const message =
2439
+ outputVersion === 'v0'
2440
+ ? v0Message
2441
+ : new AIMessage({
2442
+ contentBlocks: v0Message.contentBlocks,
2443
+ additional_kwargs: v0Message.additional_kwargs,
2444
+ response_metadata: {
2445
+ model_provider: 'openai',
2446
+ output_version: 'v1',
2447
+ preempted: true,
2448
+ },
2449
+ });
2450
+ const originalSerialized = JSON.stringify(message.toJSON());
2451
+
2452
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
2453
+ const projectedMessage = projected[0] as AIMessage;
2454
+ const providerInput = convertMessagesToResponsesInput({
2455
+ messages: projected,
2456
+ zdrEnabled: false,
2457
+ model: 'gpt-5.6',
2458
+ });
2459
+
2460
+ expect(projectedMessage.content).toEqual(
2461
+ expect.arrayContaining([
2462
+ {
2463
+ type: 'image',
2464
+ mimeType,
2465
+ data,
2466
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2467
+ },
2468
+ ])
2469
+ );
2470
+ expect(JSON.stringify(providerInput)).toContain(
2471
+ `data:${mimeType};base64,${data}`
2472
+ );
2473
+ expect(JSON.stringify(providerInput)).not.toContain(generatedImage.id);
2474
+ expect(JSON.stringify(providerInput)).not.toContain(
2475
+ 'librechatServerToolResult'
2476
+ );
2477
+ expect(JSON.stringify(providerInput)).not.toContain('extras');
2478
+
2479
+ const fallback = projectToolStreamContentForProvider(
2480
+ [message],
2481
+ 'fallback'
2482
+ );
2483
+ expect((fallback[0] as AIMessage).content).toEqual([
2484
+ { type: 'text', text: 'Generated image.' },
2485
+ ]);
2486
+ expect(_convertMessagesToOpenAIParams(fallback)).toEqual([
2487
+ {
2488
+ role: 'assistant',
2489
+ content: [{ type: 'text', text: 'Generated image.' }],
2490
+ },
2491
+ ]);
2492
+ expect(JSON.stringify(fallback[0].toJSON())).not.toContain(
2493
+ 'librechatServerToolResult'
2494
+ );
2495
+ expect(JSON.stringify(message.toJSON())).toBe(originalSerialized);
2496
+ expect(originalSerialized).toContain(generatedImage.id);
2497
+ expect(originalSerialized).not.toContain('librechatServerToolResult');
2498
+
2499
+ const projectedAgain =
2500
+ projectOpenAIResponsesToolMessageContent(projected);
2501
+ expect(projectedAgain).toBe(projected);
2502
+ expect(projectedAgain[0]).toBe(projectedMessage);
2503
+ }
2504
+ );
2505
+
2506
+ it.each([
2507
+ ['in_progress', 'AA=='],
2508
+ ['completed', ''],
2509
+ ] as const)(
2510
+ 'does not promote %s v0 generated image data %j',
2511
+ (status, result) => {
2512
+ const message = new AIMessage({
2513
+ content: 'Partial answer.',
2514
+ additional_kwargs: {
2515
+ tool_outputs: [
2516
+ {
2517
+ id: 'ig_unusable',
2518
+ type: 'image_generation_call',
2519
+ status,
2520
+ result,
2521
+ },
2522
+ ],
2523
+ },
2524
+ response_metadata: {
2525
+ model_provider: 'openai',
2526
+ preempted: true,
2527
+ },
2528
+ });
2529
+
2530
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
2531
+ const projectedMessage = projected as AIMessage;
2532
+ const providerInput = convertMessagesToResponsesInput({
2533
+ messages: [projected],
2534
+ zdrEnabled: false,
2535
+ model: 'gpt-5.6',
2536
+ });
2537
+
2538
+ expect(projectedMessage.response_metadata.output_version).toBeUndefined();
2539
+ expect(JSON.stringify(providerInput)).toContain('Partial answer.');
2540
+ expect(JSON.stringify(providerInput)).not.toContain('input_image');
2541
+ expect(JSON.stringify(providerInput)).not.toContain('ig_unusable');
2542
+ }
2543
+ );
2544
+
2545
+ it('uses response output over stale v0 generated-image tool outputs', () => {
2546
+ const message = new AIMessage({
2547
+ content: [{ type: 'text', text: 'Partial answer.' }],
2548
+ additional_kwargs: {
2549
+ tool_outputs: [
2550
+ {
2551
+ id: 'ig_stale',
2552
+ type: 'image_generation_call',
2553
+ status: 'in_progress',
2554
+ result: 'AQ==',
2555
+ },
2556
+ ],
2557
+ },
2558
+ response_metadata: {
2559
+ model_provider: 'openai',
2560
+ output: [
2561
+ {
2562
+ id: 'ig_authoritative',
2563
+ type: 'image_generation_call',
2564
+ status: 'completed',
2565
+ result: 'AA==',
2566
+ },
2567
+ ],
2568
+ preempted: true,
2569
+ },
2570
+ });
2571
+
2572
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
2573
+ const projectedMessage = projected as AIMessage;
2574
+ const providerInput = convertMessagesToResponsesInput({
2575
+ messages: [projected],
2576
+ zdrEnabled: false,
2577
+ model: 'gpt-5.6',
2578
+ });
2579
+
2580
+ expect(projectedMessage.content).toEqual([
2581
+ { type: 'text', text: 'Partial answer.' },
2582
+ {
2583
+ type: 'image',
2584
+ mimeType: 'image/png',
2585
+ data: 'AA==',
2586
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2587
+ },
2588
+ ]);
2589
+ expect(JSON.stringify(providerInput)).toContain(
2590
+ 'data:image/png;base64,AA=='
2591
+ );
2592
+ expect(JSON.stringify(providerInput)).not.toContain('AQ==');
2593
+ expect(JSON.stringify(providerInput)).not.toContain('ig_stale');
2594
+ expect(JSON.stringify(providerInput)).not.toContain('ig_authoritative');
2595
+ });
2596
+
2597
+ it('preserves unrelated self-contained image blocks during v0 promotion', () => {
2598
+ const applicationImage = {
2599
+ type: 'image' as const,
2600
+ mimeType: 'image/png',
2601
+ data: 'AQ==',
2602
+ metadata: { status: 'ready' },
2603
+ };
2604
+ const message = new AIMessage({
2605
+ content: [{ type: 'text', text: 'Partial answer.' }, applicationImage],
2606
+ additional_kwargs: {
2607
+ tool_outputs: [
2608
+ {
2609
+ id: 'ig_interrupted',
2610
+ type: 'image_generation_call',
2611
+ status: 'completed',
2612
+ result: 'AA==',
2613
+ },
2614
+ ],
2615
+ },
2616
+ response_metadata: {
2617
+ model_provider: 'openai',
2618
+ preempted: true,
2619
+ },
2620
+ });
2621
+
2622
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
2623
+ const projectedMessage = projected as AIMessage;
2624
+
2625
+ expect(projectedMessage.content).toEqual([
2626
+ { type: 'text', text: 'Partial answer.' },
2627
+ applicationImage,
2628
+ {
2629
+ type: 'image',
2630
+ mimeType: 'image/png',
2631
+ data: 'AA==',
2632
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2633
+ },
2634
+ ]);
2635
+ expect(JSON.stringify(projectedMessage.toJSON())).not.toContain(
2636
+ 'ig_interrupted'
2637
+ );
2638
+ });
2639
+
2640
+ it.each([
2641
+ [
2642
+ 'data URL',
2643
+ { url: 'data:image/png;base64,AQ==' },
2644
+ 'data:image/png;base64,AQ==',
2645
+ ],
2646
+ [
2647
+ 'HTTPS URL',
2648
+ { url: 'https://example.com/application-image.png' },
2649
+ 'https://example.com/application-image.png',
2650
+ ],
2651
+ [
2652
+ 'file ID',
2653
+ { fileId: 'file_application_image' },
2654
+ '"file_id":"file_application_image"',
2655
+ ],
2656
+ ])(
2657
+ 'preserves an unrelated v0 application image backed by a %s',
2658
+ (_label, source, providerFragment) => {
2659
+ const applicationImage = {
2660
+ type: 'image' as const,
2661
+ ...source,
2662
+ metadata: { source: 'application' },
2663
+ };
2664
+ const providerImage = {
2665
+ type: 'image' as const,
2666
+ id: 'ig_url_provider',
2667
+ url: 'data:image/png;base64,AA==',
2668
+ metadata: { status: 'completed' },
2669
+ };
2670
+ const message = new AIMessage({
2671
+ content: [
2672
+ applicationImage,
2673
+ { type: 'text', text: 'Partial answer.' },
2674
+ providerImage,
2675
+ ],
2676
+ additional_kwargs: {
2677
+ tool_outputs: [
2678
+ {
2679
+ id: providerImage.id,
2680
+ type: 'image_generation_call',
2681
+ status: 'completed',
2682
+ result: 'AA==',
2683
+ },
2684
+ ],
2685
+ },
2686
+ response_metadata: {
2687
+ model_provider: 'openai',
2688
+ preempted: true,
2689
+ },
2690
+ });
2691
+
2692
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
2693
+ const providerInput = convertMessagesToResponsesInput({
2694
+ messages: [projected],
2695
+ zdrEnabled: false,
2696
+ model: 'gpt-5.6',
2697
+ });
2698
+
2699
+ expect(projected.content).toEqual([
2700
+ applicationImage,
2701
+ { type: 'text', text: 'Partial answer.' },
2702
+ {
2703
+ type: 'image',
2704
+ mimeType: 'image/png',
2705
+ data: 'AA==',
2706
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2707
+ },
2708
+ ]);
2709
+ expect(JSON.stringify(projected.toJSON())).not.toContain(
2710
+ providerImage.id
2711
+ );
2712
+ expect(JSON.stringify(providerInput)).toContain(providerFragment);
2713
+ }
2714
+ );
2715
+
2716
+ it('ignores dropped v0 text placeholders when restoring image positions', () => {
2717
+ const applicationImage = {
2718
+ type: 'image' as const,
2719
+ mimeType: 'image/png',
2720
+ data: 'AQ==',
2721
+ };
2722
+ const message = new AIMessage({
2723
+ content: [
2724
+ { type: 'text', text: '' },
2725
+ applicationImage,
2726
+ { type: 'text', text: 'Later narration.' },
2727
+ ],
2728
+ additional_kwargs: {
2729
+ tool_outputs: [
2730
+ {
2731
+ id: 'ig_after_placeholder',
2732
+ type: 'image_generation_call',
2733
+ status: 'completed',
2734
+ result: 'AA==',
2735
+ },
2736
+ ],
2737
+ },
2738
+ response_metadata: {
2739
+ model_provider: 'openai',
2740
+ preempted: true,
2741
+ },
2742
+ });
2743
+
2744
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
2745
+
2746
+ expect(projected.content).toEqual([
2747
+ applicationImage,
2748
+ { type: 'text', text: 'Later narration.' },
2749
+ {
2750
+ type: 'image',
2751
+ mimeType: 'image/png',
2752
+ data: 'AA==',
2753
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2754
+ },
2755
+ ]);
2756
+ });
2757
+
2758
+ it.each([
2759
+ ['an earlier replay item before a leading image', true],
2760
+ ['a trailing image before a later replay item', false],
2761
+ ] as const)('keeps %s during v0 promotion', (_label, imageFirst) => {
2762
+ const applicationImage = {
2763
+ type: 'image' as const,
2764
+ mimeType: 'image/png',
2765
+ data: 'AQ==',
2766
+ };
2767
+ const narration = { type: 'text' as const, text: 'Narration.' };
2768
+ const generatedImage = {
2769
+ type: 'image' as const,
2770
+ mimeType: 'image/png',
2771
+ data: 'Ag==',
2772
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2773
+ };
2774
+ const generatedImageId = `ig_${imageFirst ? 'before' : 'after'}_image`;
2775
+ const message = new AIMessage({
2776
+ content: imageFirst
2777
+ ? [applicationImage, narration]
2778
+ : [narration, applicationImage],
2779
+ additional_kwargs: {
2780
+ tool_outputs: [
2781
+ {
2782
+ id: generatedImageId,
2783
+ type: 'image_generation_call',
2784
+ status: 'completed',
2785
+ result: generatedImage.data,
2786
+ },
2787
+ ],
2788
+ [OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: imageFirst
2789
+ ? [
2790
+ {
2791
+ itemId: generatedImageId,
2792
+ kind: 'output',
2793
+ outputIndex: 0,
2794
+ },
2795
+ {
2796
+ itemId: 'msg_after_image',
2797
+ kind: 'text',
2798
+ outputIndex: 1,
2799
+ contentIndex: 0,
2800
+ },
2801
+ ]
2802
+ : [
2803
+ {
2804
+ itemId: 'msg_before_image',
2805
+ kind: 'text',
2806
+ outputIndex: 0,
2807
+ contentIndex: 0,
2808
+ },
2809
+ {
2810
+ itemId: generatedImageId,
2811
+ kind: 'output',
2812
+ outputIndex: 1,
2813
+ },
2814
+ ],
2815
+ },
2816
+ response_metadata: {
2817
+ model_provider: 'openai',
2818
+ preempted: true,
2819
+ },
2820
+ });
2821
+
2822
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
2823
+
2824
+ expect(projected.content).toEqual(
2825
+ imageFirst
2826
+ ? [generatedImage, applicationImage, narration]
2827
+ : [narration, applicationImage, generatedImage]
2828
+ );
2829
+ });
2830
+
2831
+ it('restores v0 image positions before appending a generated image', () => {
2832
+ const imageA = {
2833
+ type: 'image' as const,
2834
+ mimeType: 'image/png',
2835
+ data: 'AQ==',
2836
+ };
2837
+ const imageB = {
2838
+ type: 'image' as const,
2839
+ mimeType: 'image/png',
2840
+ data: 'Ag==',
2841
+ };
2842
+ const imageC = {
2843
+ type: 'image' as const,
2844
+ mimeType: 'image/png',
2845
+ data: 'Aw==',
2846
+ };
2847
+ const message = new AIMessage({
2848
+ content: [
2849
+ imageA,
2850
+ { type: 'text', text: 'First.' },
2851
+ imageB,
2852
+ { type: 'text', text: 'Second.' },
2853
+ imageC,
2854
+ ],
2855
+ additional_kwargs: {
2856
+ tool_outputs: [
2857
+ {
2858
+ id: 'ig_ordered',
2859
+ type: 'image_generation_call',
2860
+ status: 'completed',
2861
+ result: 'BA==',
2862
+ },
2863
+ ],
2864
+ },
2865
+ response_metadata: {
2866
+ model_provider: 'openai',
2867
+ preempted: true,
2868
+ },
2869
+ });
2870
+
2871
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
2872
+ const projectedMessage = projected[0] as AIMessage;
2873
+ const providerInput = convertMessagesToResponsesInput({
2874
+ messages: projected,
2875
+ zdrEnabled: false,
2876
+ model: 'gpt-5.6',
2877
+ });
2878
+
2879
+ expect(projectedMessage.content).toEqual([
2880
+ imageA,
2881
+ { type: 'text', text: 'First.' },
2882
+ imageB,
2883
+ { type: 'text', text: 'Second.' },
2884
+ imageC,
2885
+ {
2886
+ type: 'image',
2887
+ mimeType: 'image/png',
2888
+ data: 'BA==',
2889
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2890
+ },
2891
+ ]);
2892
+ expect(providerInput).toEqual([
2893
+ {
2894
+ type: 'message',
2895
+ role: 'assistant',
2896
+ content: [
2897
+ {
2898
+ type: 'input_image',
2899
+ detail: 'auto',
2900
+ image_url: 'data:image/png;base64,AQ==',
2901
+ },
2902
+ { type: 'output_text', text: 'First.', annotations: [] },
2903
+ {
2904
+ type: 'input_image',
2905
+ detail: 'auto',
2906
+ image_url: 'data:image/png;base64,Ag==',
2907
+ },
2908
+ { type: 'output_text', text: 'Second.', annotations: [] },
2909
+ {
2910
+ type: 'input_image',
2911
+ detail: 'auto',
2912
+ image_url: 'data:image/png;base64,Aw==',
2913
+ },
2914
+ {
2915
+ type: 'input_image',
2916
+ detail: 'auto',
2917
+ image_url: 'data:image/png;base64,BA==',
2918
+ },
2919
+ ],
2920
+ },
2921
+ ]);
2922
+ expect(JSON.stringify(projectedMessage.toJSON())).not.toContain(
2923
+ 'ig_ordered'
2924
+ );
2925
+ expect(message.content).toEqual([
2926
+ imageA,
2927
+ { type: 'text', text: 'First.' },
2928
+ imageB,
2929
+ { type: 'text', text: 'Second.' },
2930
+ imageC,
2931
+ ]);
2932
+
2933
+ const projectedAgain = projectOpenAIResponsesToolMessageContent(projected);
2934
+ expect(projectedAgain).toBe(projected);
2935
+ expect(projectedAgain[0]).toBe(projectedMessage);
2936
+ });
2937
+
2938
+ it('keeps encrypted reasoning before image-only v0 content', () => {
2939
+ const reasoning = {
2940
+ id: 'rs_image_only',
2941
+ type: 'reasoning',
2942
+ status: 'completed',
2943
+ summary: [],
2944
+ encrypted_content: 'opaque-image-reasoning',
2945
+ };
2946
+ const applicationImage = {
2947
+ type: 'image' as const,
2948
+ mimeType: 'image/png',
2949
+ data: 'AQ==',
2950
+ };
2951
+ const message = new AIMessage({
2952
+ content: [applicationImage],
2953
+ additional_kwargs: {
2954
+ reasoning,
2955
+ [OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
2956
+ {
2957
+ itemId: reasoning.id,
2958
+ kind: 'reasoning',
2959
+ outputIndex: 0,
2960
+ },
2961
+ {
2962
+ itemId: 'msg_image_only',
2963
+ kind: 'message',
2964
+ outputIndex: 1,
2965
+ },
2966
+ {
2967
+ itemId: 'ig_image_only',
2968
+ kind: 'output',
2969
+ outputIndex: 2,
2970
+ },
2971
+ ],
2972
+ tool_outputs: [
2973
+ {
2974
+ id: 'ig_image_only',
2975
+ type: 'image_generation_call',
2976
+ status: 'completed',
2977
+ result: 'Ag==',
2978
+ },
2979
+ ],
2980
+ },
2981
+ response_metadata: {
2982
+ model_provider: 'openai',
2983
+ preempted: true,
2984
+ },
2985
+ });
2986
+
2987
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
2988
+
2989
+ expect(projected.content).toEqual([
2990
+ { type: 'non_standard', value: reasoning },
2991
+ applicationImage,
2992
+ {
2993
+ type: 'image',
2994
+ mimeType: 'image/png',
2995
+ data: 'Ag==',
2996
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
2997
+ },
2998
+ ]);
2999
+ });
3000
+
3001
+ it('does not collapse identical application images during v0 promotion', () => {
3002
+ const applicationImage = {
3003
+ type: 'image' as const,
3004
+ mimeType: 'image/png',
3005
+ data: 'AA==',
3006
+ };
3007
+ const providerImage = {
3008
+ ...applicationImage,
3009
+ id: 'ig_duplicate',
3010
+ metadata: { status: 'completed' },
3011
+ };
3012
+ const message = new AIMessage({
3013
+ content: [applicationImage, applicationImage, providerImage],
3014
+ additional_kwargs: {
3015
+ tool_outputs: [
3016
+ {
3017
+ id: 'ig_duplicate',
3018
+ type: 'image_generation_call',
3019
+ status: 'completed',
3020
+ result: 'AA==',
3021
+ },
3022
+ ],
3023
+ },
3024
+ response_metadata: {
3025
+ model_provider: 'openai',
3026
+ preempted: true,
3027
+ },
3028
+ });
3029
+
3030
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
3031
+
3032
+ expect(projected.content).toEqual([
3033
+ applicationImage,
3034
+ applicationImage,
3035
+ {
3036
+ type: 'image',
3037
+ mimeType: 'image/png',
3038
+ data: 'AA==',
3039
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
3040
+ },
3041
+ ]);
3042
+ });
3043
+
3044
+ it('keeps an id-less completed application image with generated bytes', () => {
3045
+ const applicationImage = {
3046
+ type: 'image' as const,
3047
+ mimeType: 'image/png',
3048
+ data: 'AA==',
3049
+ metadata: { source: 'application', status: 'completed' },
3050
+ };
3051
+ const message = new AIMessage({
3052
+ content: [applicationImage],
3053
+ additional_kwargs: {
3054
+ tool_outputs: [
3055
+ {
3056
+ id: 'ig_same_bytes',
3057
+ type: 'image_generation_call',
3058
+ status: 'completed',
3059
+ result: 'AA==',
3060
+ },
3061
+ ],
3062
+ },
3063
+ response_metadata: {
3064
+ model_provider: 'openai',
3065
+ preempted: true,
3066
+ },
3067
+ });
3068
+
3069
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
3070
+
3071
+ expect(projected.content).toEqual([
3072
+ applicationImage,
3073
+ {
3074
+ type: 'image',
3075
+ mimeType: 'image/png',
3076
+ data: 'AA==',
3077
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
3078
+ },
3079
+ ]);
3080
+ });
3081
+
3082
+ it('deduplicates repeated authoritative generated-image items by id', () => {
3083
+ const generatedImage = {
3084
+ id: 'ig_duplicate_output',
3085
+ type: 'image_generation_call',
3086
+ status: 'completed',
3087
+ result: 'AA==',
3088
+ };
3089
+ const message = new AIMessage({
3090
+ content: [{ type: 'text', text: 'Partial answer.' }],
3091
+ additional_kwargs: {
3092
+ tool_outputs: [generatedImage, { ...generatedImage }],
3093
+ },
3094
+ response_metadata: {
3095
+ model_provider: 'openai',
3096
+ preempted: true,
3097
+ },
3098
+ });
3099
+
3100
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
3101
+
3102
+ expect(projected.content).toEqual([
3103
+ { type: 'text', text: 'Partial answer.' },
3104
+ {
3105
+ type: 'image',
3106
+ mimeType: 'image/png',
3107
+ data: 'AA==',
3108
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
3109
+ },
3110
+ ]);
3111
+ });
3112
+
3113
+ it.each(['v0', 'v1'] as const)(
3114
+ 'marks native %s code-interpreter data-url image replay without changing provider payload',
3115
+ (outputVersion) => {
3116
+ const dataUrl = 'data:image/png;base64,AA==';
3117
+ const toolOutput = {
3118
+ id: `ci_inline_${outputVersion}`,
3119
+ type: 'code_interpreter_call',
3120
+ status: 'completed',
3121
+ code: 'display(chart)',
3122
+ outputs: [{ type: 'image', url: dataUrl }],
3123
+ };
3124
+ const v0Message = new AIMessage({
3125
+ content: [{ type: 'text', text: 'Partial answer.' }],
3126
+ additional_kwargs: { tool_outputs: [toolOutput] },
3127
+ response_metadata: {
3128
+ model_provider: 'openai',
3129
+ preempted: true,
3130
+ },
3131
+ });
3132
+ const message =
3133
+ outputVersion === 'v0'
3134
+ ? v0Message
3135
+ : new AIMessage({
3136
+ contentBlocks: v0Message.contentBlocks,
3137
+ additional_kwargs: v0Message.additional_kwargs,
3138
+ response_metadata: {
3139
+ model_provider: 'openai',
3140
+ output_version: 'v1',
3141
+ preempted: true,
3142
+ },
3143
+ });
3144
+ const originalSerialized = JSON.stringify(message.toJSON());
3145
+
3146
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
3147
+ const projectedMessage = projected[0] as AIMessage;
3148
+ const providerInput = convertMessagesToResponsesInput({
3149
+ messages: projected,
3150
+ zdrEnabled: false,
3151
+ model: 'gpt-5.6',
3152
+ });
3153
+
3154
+ expect(projectedMessage.content).toEqual([
3155
+ { type: 'text', text: 'Partial answer.' },
3156
+ {
3157
+ type: 'image',
3158
+ url: dataUrl,
3159
+ extras: CODE_INTERPRETER_REPLAY_EXTRAS,
3160
+ },
3161
+ ]);
3162
+ expect(providerInput).toEqual([
3163
+ {
3164
+ type: 'message',
3165
+ role: 'assistant',
3166
+ content: [
3167
+ {
3168
+ type: 'output_text',
3169
+ text: 'Partial answer.',
3170
+ annotations: [],
3171
+ },
3172
+ {
3173
+ type: 'input_image',
3174
+ detail: 'auto',
3175
+ image_url: dataUrl,
3176
+ },
3177
+ ],
3178
+ },
3179
+ ]);
3180
+ expect(JSON.stringify(providerInput)).not.toContain(
3181
+ 'librechatServerToolResult'
3182
+ );
3183
+ expect(JSON.stringify(providerInput)).not.toContain('extras');
3184
+
3185
+ const fallback = projectToolStreamContentForProvider(
3186
+ [message],
3187
+ 'fallback'
3188
+ );
3189
+ const fallbackMessage = fallback[0] as AIMessage;
3190
+ expect(fallbackMessage.content).toEqual([
3191
+ { type: 'text', text: 'Partial answer.' },
3192
+ {
3193
+ type: 'text',
3194
+ text: JSON.stringify({
3195
+ serverToolResult: {
3196
+ librechatResponsesReplay: true,
3197
+ toolName: 'code_interpreter',
3198
+ status: 'success',
3199
+ output: {
3200
+ type: 'code_interpreter_image',
3201
+ url: dataUrl,
3202
+ },
3203
+ },
3204
+ }),
3205
+ },
3206
+ ]);
3207
+ expect(_convertMessagesToOpenAIParams(fallback)).toEqual([
3208
+ {
3209
+ role: 'assistant',
3210
+ content: fallbackMessage.content,
3211
+ },
3212
+ ]);
3213
+ expect(JSON.stringify(fallbackMessage.toJSON())).not.toContain('extras');
3214
+ expect(JSON.stringify(fallbackMessage.toJSON())).not.toContain(
3215
+ 'librechatServerToolResult'
3216
+ );
3217
+ expect(JSON.stringify(message.toJSON())).toBe(originalSerialized);
3218
+ expect(originalSerialized).toContain(toolOutput.id);
3219
+ expect(originalSerialized).not.toContain('librechatServerToolResult');
3220
+
3221
+ const projectedAgain =
3222
+ projectOpenAIResponsesToolMessageContent(projected);
3223
+ expect(projectedAgain).toBe(projected);
3224
+ expect(projectedAgain[0]).toBe(projectedMessage);
3225
+ }
3226
+ );
3227
+
3228
+ it.each(['v0', 'v1'] as const)(
3229
+ 'preserves completed %s server-tool results without replay ids',
3230
+ (outputVersion) => {
3231
+ const toolOutput = {
3232
+ id: 'ci_completed',
3233
+ type: 'code_interpreter_call',
3234
+ status: 'completed',
3235
+ code: 'print("computed")',
3236
+ outputs: [
3237
+ { type: 'logs', logs: `computed${'x'.repeat(1_000)}` },
3238
+ {
3239
+ type: 'image',
3240
+ url: 'https://example.com/ephemeral-chart.png',
3241
+ },
3242
+ ],
3243
+ };
3244
+ const v0Message = new AIMessage({
3245
+ content: [{ type: 'text', text: 'Partial answer.' }],
3246
+ additional_kwargs: { tool_outputs: [toolOutput] },
3247
+ response_metadata: {
3248
+ model_provider: 'openai',
3249
+ preempted: true,
3250
+ },
3251
+ });
3252
+ const message =
3253
+ outputVersion === 'v0'
3254
+ ? v0Message
3255
+ : new AIMessage({
3256
+ contentBlocks: v0Message.contentBlocks,
3257
+ additional_kwargs: v0Message.additional_kwargs,
3258
+ response_metadata: {
3259
+ model_provider: 'openai',
3260
+ output_version: 'v1',
3261
+ preempted: true,
3262
+ },
3263
+ });
3264
+
3265
+ const projected = projectOpenAIResponsesToolMessageContent(
3266
+ [message],
3267
+ 400
3268
+ );
3269
+ const projectedMessage = projected[0] as AIMessage;
3270
+ const providerInput = convertMessagesToResponsesInput({
3271
+ messages: projected,
3272
+ zdrEnabled: false,
3273
+ model: 'gpt-5.6',
3274
+ });
3275
+
3276
+ expect(projectedMessage.response_metadata.output_version).toBe('v1');
3277
+ expect(projectedMessage.content).toEqual([
3278
+ { type: 'text', text: 'Partial answer.' },
3279
+ {
3280
+ type: 'text',
3281
+ text: expect.stringContaining('computed'),
3282
+ extras: {
3283
+ librechatServerToolResult: { toolName: 'code_interpreter' },
3284
+ },
3285
+ },
3286
+ {
3287
+ type: 'text',
3288
+ text: expect.stringContaining(
3289
+ 'https://example.com/ephemeral-chart.png'
3290
+ ),
3291
+ extras: {
3292
+ librechatServerToolResult: { toolName: 'code_interpreter' },
3293
+ },
3294
+ },
3295
+ ]);
3296
+ expect(
3297
+ (projectedMessage.content[1] as { text: string }).text.length
3298
+ ).toBeLessThanOrEqual(400);
3299
+ const serializedProviderInput = JSON.stringify(providerInput);
3300
+ expect(serializedProviderInput).toContain('computed');
3301
+ expect(serializedProviderInput).toContain('ephemeral-chart.png');
3302
+ expect(serializedProviderInput).not.toContain('ci_completed');
3303
+ expect(serializedProviderInput).not.toContain('function_call_output');
3304
+ expect(serializedProviderInput).not.toContain('server_tool_call');
3305
+ expect(JSON.stringify(message.toJSON())).toContain('ci_completed');
3306
+
3307
+ const projectedAgain =
3308
+ projectOpenAIResponsesToolMessageContent(projected);
3309
+ expect(projectedAgain).toBe(projected);
3310
+ expect(projectedAgain[0]).toBe(projectedMessage);
3311
+ }
3312
+ );
3313
+
3314
+ it('uses captured output positions for translated v0 server-tool results', () => {
3315
+ const toolOutputs = [
3316
+ {
3317
+ id: 'ci_position_item',
3318
+ type: 'code_interpreter_call',
3319
+ status: 'completed',
3320
+ code: 'print("CODE_BEFORE_TEXT")',
3321
+ outputs: [{ type: 'logs', logs: 'CODE_BEFORE_TEXT' }],
3322
+ },
3323
+ {
3324
+ id: 'fs_position_item',
3325
+ type: 'file_search_call',
3326
+ status: 'completed',
3327
+ queries: ['positioned file result'],
3328
+ results: [
3329
+ {
3330
+ file_id: 'file_without_provider_replay',
3331
+ filename: 'position.txt',
3332
+ score: 1,
3333
+ text: 'FILE_BETWEEN_TEXT',
3334
+ },
3335
+ ],
3336
+ },
3337
+ {
3338
+ id: 'ws_position_item',
3339
+ type: 'web_search_call',
3340
+ status: 'completed',
3341
+ action: { type: 'search', query: 'positioned web result' },
3342
+ results: [{ title: 'WEB_BETWEEN_TEXT' }],
3343
+ },
3344
+ {
3345
+ id: 'ts_position_item',
3346
+ type: 'tool_search_output',
3347
+ status: 'completed',
3348
+ tools: [
3349
+ {
3350
+ type: 'function',
3351
+ name: 'TOOL_BETWEEN_TEXT',
3352
+ description: 'positioned tool-search result',
3353
+ parameters: { type: 'object', properties: {} },
3354
+ },
3355
+ ],
3356
+ },
3357
+ ];
3358
+ const message = new AIMessage({
3359
+ content: [
3360
+ { type: 'text', text: 'First narration.' },
3361
+ { type: 'text', text: 'Second narration.' },
3362
+ ],
3363
+ additional_kwargs: {
3364
+ tool_outputs: toolOutputs,
3365
+ [OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
3366
+ { itemId: 'ci_position_item', kind: 'output', outputIndex: 0 },
3367
+ {
3368
+ itemId: 'msg_first_position',
3369
+ kind: 'text',
3370
+ outputIndex: 1,
3371
+ contentIndex: 0,
3372
+ },
3373
+ { itemId: 'fs_position_item', kind: 'output', outputIndex: 2 },
3374
+ { itemId: 'ws_position_item', kind: 'output', outputIndex: 3 },
3375
+ { itemId: 'ts_position_item', kind: 'output', outputIndex: 4 },
3376
+ {
3377
+ itemId: 'msg_second_position',
3378
+ kind: 'text',
3379
+ outputIndex: 5,
3380
+ contentIndex: 0,
3381
+ },
3382
+ ],
3383
+ },
3384
+ response_metadata: {
3385
+ model_provider: 'openai',
3386
+ preempted: true,
3387
+ },
3388
+ });
3389
+ const originalSerialized = JSON.stringify(message.toJSON());
3390
+
3391
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
3392
+ const projectedMessage = projected[0] as AIMessage;
3393
+ const markers = [
3394
+ 'CODE_BEFORE_TEXT',
3395
+ 'FILE_BETWEEN_TEXT',
3396
+ 'WEB_BETWEEN_TEXT',
3397
+ 'TOOL_BETWEEN_TEXT',
3398
+ ];
3399
+ const orderedContent = (
3400
+ projectedMessage.content as Array<{ text?: string; type: string }>
3401
+ ).map((block) => {
3402
+ if (
3403
+ block.text === 'First narration.' ||
3404
+ block.text === 'Second narration.'
3405
+ ) {
3406
+ return block.text;
3407
+ }
3408
+ return markers.find((marker) => block.text?.includes(marker) === true);
3409
+ });
3410
+ const providerInput = convertMessagesToResponsesInput({
3411
+ messages: projected,
3412
+ zdrEnabled: false,
3413
+ model: 'gpt-5.6',
3414
+ });
3415
+ const serializedProviderInput = JSON.stringify(providerInput);
3416
+
3417
+ expect(orderedContent).toEqual([
3418
+ 'CODE_BEFORE_TEXT',
3419
+ 'First narration.',
3420
+ 'FILE_BETWEEN_TEXT',
3421
+ 'WEB_BETWEEN_TEXT',
3422
+ 'TOOL_BETWEEN_TEXT',
3423
+ 'Second narration.',
3424
+ ]);
3425
+ expect(serializedProviderInput).toContain('WEB_BETWEEN_TEXT');
3426
+ for (const toolOutput of toolOutputs) {
3427
+ expect(serializedProviderInput).not.toContain(toolOutput.id);
3428
+ }
3429
+ expect(projectedMessage.additional_kwargs).not.toHaveProperty(
3430
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
3431
+ );
3432
+ expect(JSON.stringify(message.toJSON())).toBe(originalSerialized);
3433
+
3434
+ const projectedAgain = projectOpenAIResponsesToolMessageContent(projected);
3435
+ expect(projectedAgain).toBe(projected);
3436
+ expect(projectedAgain[0]).toBe(projectedMessage);
3437
+ });
3438
+
3439
+ it.each(['v0', 'v1'] as const)(
3440
+ 'preserves terminal incomplete %s file-search and code-interpreter outputs',
3441
+ (outputVersion) => {
3442
+ const dataUrl = 'data:image/png;base64,AA==';
3443
+ const toolOutputs = [
3444
+ {
3445
+ id: `fs_incomplete_${outputVersion}`,
3446
+ type: 'file_search_call',
3447
+ status: 'incomplete',
3448
+ queries: ['partial result'],
3449
+ results: [
3450
+ {
3451
+ file_id: 'partial_file',
3452
+ filename: 'partial.txt',
3453
+ score: 0.5,
3454
+ text: 'PARTIAL_FILE_RESULT',
3455
+ },
3456
+ ],
3457
+ },
3458
+ {
3459
+ id: `ci_incomplete_${outputVersion}`,
3460
+ type: 'code_interpreter_call',
3461
+ status: 'incomplete',
3462
+ code: 'display(partial_chart)',
3463
+ outputs: [
3464
+ { type: 'logs', logs: 'PARTIAL_CODE_LOG' },
3465
+ { type: 'image', url: dataUrl },
3466
+ ],
3467
+ },
3468
+ ];
3469
+ const message = new AIMessage({
3470
+ content: [{ type: 'text', text: 'Partial narration.' }],
3471
+ additional_kwargs: {
3472
+ tool_outputs: toolOutputs,
3473
+ [OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
3474
+ {
3475
+ itemId: toolOutputs[0].id,
3476
+ kind: 'output',
3477
+ outputIndex: 0,
3478
+ },
3479
+ {
3480
+ itemId: toolOutputs[1].id,
3481
+ kind: 'output',
3482
+ outputIndex: 1,
3483
+ },
3484
+ {
3485
+ itemId: 'msg_incomplete',
3486
+ kind: 'text',
3487
+ outputIndex: 2,
3488
+ contentIndex: 0,
3489
+ },
3490
+ ],
3491
+ },
3492
+ response_metadata: {
3493
+ model_provider: 'openai',
3494
+ ...(outputVersion === 'v1' ? { output_version: 'v1' } : {}),
3495
+ preempted: true,
3496
+ },
3497
+ });
3498
+ const originalSerialized = JSON.stringify(message.toJSON());
3499
+
3500
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
3501
+ const projectedMessage = projected[0] as AIMessage;
3502
+ const providerInput = convertMessagesToResponsesInput({
3503
+ messages: projected,
3504
+ zdrEnabled: false,
3505
+ model: 'gpt-5.6',
3506
+ });
3507
+ const serialized = JSON.stringify(projectedMessage.content);
3508
+ const serializedProviderInput = JSON.stringify(providerInput);
3509
+ const serverResults = (
3510
+ projectedMessage.content as Array<{ text?: string; type: string }>
3511
+ )
3512
+ .filter(
3513
+ (block) =>
3514
+ block.type === 'text' &&
3515
+ block.text?.startsWith('{"serverToolResult":') === true
3516
+ )
3517
+ .map(
3518
+ (block) =>
3519
+ (
3520
+ JSON.parse(block.text!) as {
3521
+ serverToolResult: { output: unknown; status: string };
3522
+ }
3523
+ ).serverToolResult
3524
+ );
3525
+
3526
+ expect(serverResults).toEqual([
3527
+ expect.objectContaining({ status: 'error' }),
3528
+ expect.objectContaining({ status: 'error' }),
3529
+ ]);
3530
+ expect(serialized).toContain('PARTIAL_FILE_RESULT');
3531
+ expect(serialized).toContain('PARTIAL_CODE_LOG');
3532
+ expect(projectedMessage.content).toEqual(
3533
+ expect.arrayContaining([
3534
+ {
3535
+ type: 'image',
3536
+ url: dataUrl,
3537
+ extras: CODE_INTERPRETER_REPLAY_EXTRAS,
3538
+ },
3539
+ ])
3540
+ );
3541
+ expect(serializedProviderInput).toContain(dataUrl);
3542
+ expect(serializedProviderInput).not.toContain(toolOutputs[0].id);
3543
+ expect(serializedProviderInput).not.toContain(toolOutputs[1].id);
3544
+ expect(JSON.stringify(message.toJSON())).toBe(originalSerialized);
3545
+
3546
+ const projectedAgain =
3547
+ projectOpenAIResponsesToolMessageContent(projected);
3548
+ expect(projectedAgain).toBe(projected);
3549
+ expect(projectedAgain[0]).toBe(projectedMessage);
3550
+ }
3551
+ );
3552
+
3553
+ it.each([
3554
+ ['logs then image', ['logs', 'image']],
3555
+ ['image then logs', ['image', 'logs']],
3556
+ ] as const)(
3557
+ 'preserves code-interpreter %s order at its cross-item position',
3558
+ (_label, outputOrder) => {
3559
+ const dataUrl = 'data:image/png;base64,AQ==';
3560
+ const outputs = outputOrder.map((type) =>
3561
+ type === 'logs'
3562
+ ? { type: 'logs', logs: 'ORDERED_CODE_LOG' }
3563
+ : { type: 'image', url: dataUrl }
3564
+ );
3565
+ const message = new AIMessage({
3566
+ content: [
3567
+ { type: 'text', text: 'First narration.' },
3568
+ { type: 'text', text: 'Second narration.' },
3569
+ ],
3570
+ additional_kwargs: {
3571
+ tool_outputs: [
3572
+ {
3573
+ id: 'ci_ordered_outputs',
3574
+ type: 'code_interpreter_call',
3575
+ status: 'completed',
3576
+ code: 'display(result)',
3577
+ outputs,
3578
+ },
3579
+ ],
3580
+ [OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
3581
+ {
3582
+ itemId: 'msg_order_first',
3583
+ kind: 'text',
3584
+ outputIndex: 0,
3585
+ contentIndex: 0,
3586
+ },
3587
+ {
3588
+ itemId: 'ci_ordered_outputs',
3589
+ kind: 'output',
3590
+ outputIndex: 1,
3591
+ },
3592
+ {
3593
+ itemId: 'msg_order_second',
3594
+ kind: 'text',
3595
+ outputIndex: 2,
3596
+ contentIndex: 0,
3597
+ },
3598
+ ],
3599
+ },
3600
+ response_metadata: {
3601
+ model_provider: 'openai',
3602
+ preempted: true,
3603
+ },
3604
+ });
3605
+
3606
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
3607
+ const projectedMessage = projected[0] as AIMessage;
3608
+ const orderedContent = (
3609
+ projectedMessage.content as Array<{
3610
+ text?: string;
3611
+ type: string;
3612
+ url?: string;
3613
+ }>
3614
+ ).map((block) => {
3615
+ if (
3616
+ block.text === 'First narration.' ||
3617
+ block.text === 'Second narration.'
3618
+ ) {
3619
+ return block.text;
3620
+ }
3621
+ if (block.type === 'image' && block.url === dataUrl) {
3622
+ return 'image';
3623
+ }
3624
+ return block.text?.includes('ORDERED_CODE_LOG') === true
3625
+ ? 'logs'
3626
+ : undefined;
3627
+ });
3628
+
3629
+ expect(orderedContent).toEqual([
3630
+ 'First narration.',
3631
+ ...outputOrder,
3632
+ 'Second narration.',
3633
+ ]);
3634
+ expect(JSON.stringify(projectedMessage.toJSON())).not.toContain(
3635
+ 'ci_ordered_outputs'
3636
+ );
3637
+ }
3638
+ );
3639
+
3640
+ it('uses response output over stale v0 server-tool results', () => {
3641
+ const makeCodeOutput = (id: string, logs: string) => ({
3642
+ id,
3643
+ type: 'code_interpreter_call',
3644
+ status: 'completed',
3645
+ code: `print(${JSON.stringify(logs)})`,
3646
+ outputs: [{ type: 'logs', logs }],
3647
+ });
3648
+ const message = new AIMessage({
3649
+ content: [{ type: 'text', text: 'Partial answer.' }],
3650
+ additional_kwargs: {
3651
+ tool_outputs: [makeCodeOutput('ci_stale', 'stale result')],
3652
+ },
3653
+ response_metadata: {
3654
+ model_provider: 'openai',
3655
+ output: [makeCodeOutput('ci_authoritative', 'current result')],
3656
+ preempted: true,
3657
+ },
3658
+ });
3659
+
3660
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
3661
+ const serialized = JSON.stringify(projected.content);
3662
+
3663
+ expect(serialized).toContain('current result');
3664
+ expect(serialized).not.toContain('stale result');
3665
+ expect(serialized).not.toContain('ci_authoritative');
3666
+ expect(serialized).not.toContain('ci_stale');
3667
+ });
3668
+
3669
+ it.each(['v0', 'v1'] as const)(
3670
+ 'preserves embedded %s MCP output without its provider id',
3671
+ (outputVersion) => {
3672
+ const mcpOutput = {
3673
+ id: 'mcp_provider_item',
3674
+ type: 'mcp_call',
3675
+ name: 'lookup',
3676
+ server_label: 'documents',
3677
+ arguments: '{"query":"answer"}',
3678
+ status: 'completed',
3679
+ output: 'MCP answer',
3680
+ };
3681
+ const v0Message = new AIMessage({
3682
+ content: [{ type: 'text', text: 'Partial answer.' }],
3683
+ additional_kwargs: { tool_outputs: [mcpOutput] },
3684
+ response_metadata: {
3685
+ model_provider: 'openai',
3686
+ preempted: true,
3687
+ },
3688
+ });
3689
+ const message =
3690
+ outputVersion === 'v0'
3691
+ ? v0Message
3692
+ : new AIMessage({
3693
+ contentBlocks: v0Message.contentBlocks,
3694
+ additional_kwargs: v0Message.additional_kwargs,
3695
+ response_metadata: {
3696
+ model_provider: 'openai',
3697
+ output_version: 'v1',
3698
+ preempted: true,
3699
+ },
3700
+ });
3701
+
3702
+ const [projected] = projectOpenAIResponsesToolMessageContent([message]);
3703
+ const projectedMessage = projected as AIMessage;
3704
+ const serialized = JSON.stringify(projected.content);
3705
+
3706
+ expect(projectedMessage.response_metadata.output_version).toBe('v1');
3707
+ expect(serialized).toContain('MCP answer');
3708
+ expect(serialized).toContain('documents');
3709
+ expect(serialized).not.toContain('mcp_provider_item');
3710
+ expect(JSON.stringify(message.toJSON())).toContain('mcp_provider_item');
3711
+ }
3712
+ );
3713
+
3714
+ it.each(['v0', 'v1'] as const)(
3715
+ 'preserves captured %s Responses results that LangChain does not convert',
3716
+ (outputVersion) => {
3717
+ const toolOutputs = [
3718
+ {
3719
+ id: 'local_output_item',
3720
+ type: 'local_shell_call_output',
3721
+ status: 'incomplete',
3722
+ output: 'local shell partial output',
3723
+ },
3724
+ {
3725
+ id: 'shell_output_item',
3726
+ call_id: 'shell_call_id',
3727
+ type: 'shell_call_output',
3728
+ status: 'incomplete',
3729
+ output: [
3730
+ {
3731
+ stdout: 'shell partial output',
3732
+ stderr: '',
3733
+ outcome: { type: 'exit', exit_code: 0 },
3734
+ },
3735
+ ],
3736
+ },
3737
+ {
3738
+ id: 'patch_output_item',
3739
+ call_id: 'patch_call_id',
3740
+ type: 'apply_patch_call_output',
3741
+ status: 'failed',
3742
+ output: 'patch failed output',
3743
+ },
3744
+ {
3745
+ id: 'program_output_item',
3746
+ call_id: 'program_call_id',
3747
+ type: 'program_output',
3748
+ status: 'incomplete',
3749
+ result: 'program partial output',
3750
+ },
3751
+ {
3752
+ id: 'mcp_list_item',
3753
+ type: 'mcp_list_tools',
3754
+ server_label: 'documents',
3755
+ tools: [
3756
+ {
3757
+ name: 'lookup',
3758
+ description: 'Look up a document',
3759
+ input_schema: { type: 'object' },
3760
+ },
3761
+ ],
3762
+ error: 'listing was incomplete',
3763
+ },
3764
+ ];
3765
+ const v0Message = new AIMessage({
3766
+ content: [{ type: 'text', text: 'Partial answer.' }],
3767
+ additional_kwargs: { tool_outputs: toolOutputs },
3768
+ response_metadata: {
3769
+ model_provider: 'openai',
3770
+ preempted: true,
3771
+ },
3772
+ });
3773
+ const message =
3774
+ outputVersion === 'v0'
3775
+ ? v0Message
3776
+ : new AIMessage({
3777
+ contentBlocks: v0Message.contentBlocks,
3778
+ additional_kwargs: v0Message.additional_kwargs,
3779
+ response_metadata: {
3780
+ model_provider: 'openai',
3781
+ output_version: 'v1',
3782
+ preempted: true,
3783
+ },
3784
+ });
3785
+
3786
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
3787
+ const projectedMessage = projected[0] as AIMessage;
3788
+ const serialized = JSON.stringify(projectedMessage.content);
3789
+ const providerInput = convertMessagesToResponsesInput({
3790
+ messages: projected,
3791
+ zdrEnabled: false,
3792
+ model: 'gpt-5.6',
3793
+ });
3794
+ const serializedProviderInput = JSON.stringify(providerInput);
3795
+ const replayStatuses = (
3796
+ projectedMessage.content as Array<{ text?: string; type: string }>
3797
+ )
3798
+ .filter(
3799
+ (block) =>
3800
+ block.type === 'text' &&
3801
+ block.text?.startsWith('{"serverToolResult":') === true
3802
+ )
3803
+ .map(
3804
+ (block) =>
3805
+ (
3806
+ JSON.parse(block.text!) as {
3807
+ serverToolResult: { status: string };
3808
+ }
3809
+ ).serverToolResult.status
3810
+ );
3811
+
3812
+ expect(projectedMessage.response_metadata.output_version).toBe('v1');
3813
+ expect(serialized).toContain('local shell partial output');
3814
+ expect(serialized).toContain('shell partial output');
3815
+ expect(serialized).toContain('patch failed output');
3816
+ expect(serialized).toContain('program partial output');
3817
+ expect(serialized).toContain('listing was incomplete');
3818
+ expect(serialized).toContain('lookup');
3819
+ expect(replayStatuses).toEqual(Array(5).fill('error'));
3820
+ expect(serializedProviderInput).not.toContain('local_output_item');
3821
+ expect(serializedProviderInput).not.toContain('shell_call_id');
3822
+ expect(serializedProviderInput).not.toContain('patch_call_id');
3823
+ expect(serializedProviderInput).not.toContain('program_call_id');
3824
+ expect(serializedProviderInput).not.toContain('mcp_list_item');
3825
+ expect(serializedProviderInput).not.toContain('shell_call_output');
3826
+ expect(serializedProviderInput).not.toContain('apply_patch_call_output');
3827
+ expect(serializedProviderInput).not.toContain('program_output');
3828
+ }
3829
+ );
3830
+
3831
+ it('replays captured server-tool results at their output positions', () => {
3832
+ const makeResult = (id: string, output: string) => ({
3833
+ id,
3834
+ type: 'local_shell_call_output',
3835
+ status: 'completed',
3836
+ output,
3837
+ });
3838
+ const results = [
3839
+ makeResult('result_after', 'after result'),
3840
+ {
3841
+ call_id: 'patch_without_item_id',
3842
+ type: 'apply_patch_call_output',
3843
+ status: 'completed',
3844
+ output: 'no-id patch result',
3845
+ },
3846
+ makeResult('result_before', 'before result'),
3847
+ makeResult('result_middle', 'middle result'),
3848
+ ];
3849
+ const message = new AIMessage({
3850
+ content: [
3851
+ { type: 'text', text: 'First narration.' },
3852
+ { type: 'text', text: 'Second narration.' },
3853
+ ],
3854
+ additional_kwargs: {
3855
+ tool_outputs: results,
3856
+ [OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
3857
+ {
3858
+ itemId: 'result_after',
3859
+ kind: 'output',
3860
+ outputIndex: 5,
3861
+ },
3862
+ {
3863
+ itemId: 'msg_first',
3864
+ kind: 'text',
3865
+ outputIndex: 1,
3866
+ contentIndex: 0,
3867
+ },
3868
+ {
3869
+ itemId: 'result_before',
3870
+ kind: 'output',
3871
+ outputIndex: 0,
3872
+ },
3873
+ {
3874
+ itemId: 'msg_first',
3875
+ kind: 'text',
3876
+ outputIndex: 1,
3877
+ contentIndex: 0,
3878
+ },
3879
+ {
3880
+ itemId: 'result_middle',
3881
+ kind: 'output',
3882
+ outputIndex: 3,
3883
+ },
3884
+ {
3885
+ itemId: 'patch_without_item_id',
3886
+ kind: 'output',
3887
+ outputIndex: 2,
3888
+ },
3889
+ {
3890
+ itemId: 'msg_second',
3891
+ kind: 'text',
3892
+ outputIndex: 4,
3893
+ contentIndex: 0,
3894
+ },
3895
+ ],
3896
+ },
3897
+ response_metadata: {
3898
+ model_provider: 'openai',
3899
+ preempted: true,
3900
+ },
3901
+ });
3902
+
3903
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
3904
+ const projectedMessage = projected[0] as AIMessage;
3905
+ const orderedText = (
3906
+ projectedMessage.content as Array<{
3907
+ extras?: { librechatServerToolResult?: unknown };
3908
+ text?: string;
3909
+ type: string;
3910
+ }>
3911
+ ).map((block) => {
3912
+ if (block.extras?.librechatServerToolResult != null) {
3913
+ return (
3914
+ JSON.parse(block.text!) as {
3915
+ serverToolResult: { output: string };
3916
+ }
3917
+ ).serverToolResult.output;
3918
+ }
3919
+ return block.text;
3920
+ });
3921
+
3922
+ expect(orderedText).toEqual([
3923
+ 'before result',
3924
+ 'First narration.',
3925
+ 'no-id patch result',
3926
+ 'middle result',
3927
+ 'Second narration.',
3928
+ 'after result',
3929
+ ]);
3930
+ expect(projectedMessage.additional_kwargs).not.toHaveProperty(
3931
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
3932
+ );
3933
+ expect(JSON.stringify(projectedMessage.toJSON())).not.toMatch(/result_/);
3934
+ expect(JSON.stringify(projectedMessage.toJSON())).not.toContain(
3935
+ 'patch_without_item_id'
3936
+ );
3937
+ expect(JSON.stringify(projectedMessage.toJSON())).not.toContain(
3938
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
3939
+ );
3940
+
3941
+ const projectedAgain = projectOpenAIResponsesToolMessageContent(projected);
3942
+ expect(projectedAgain).toBe(projected);
3943
+ expect(projectedAgain[0]).toBe(projectedMessage);
3944
+ });
3945
+
3946
+ it('derives id-less server-result positions from authoritative output', () => {
3947
+ const output = [
3948
+ {
3949
+ type: 'local_shell_call_output',
3950
+ status: 'completed',
3951
+ output: 'IDLESS_BEFORE_TEXT',
3952
+ },
3953
+ {
3954
+ id: 'msg_authoritative_first',
3955
+ type: 'message',
3956
+ role: 'assistant',
3957
+ status: 'completed',
3958
+ content: [
3959
+ {
3960
+ type: 'output_text',
3961
+ text: 'First narration.',
3962
+ annotations: [],
3963
+ },
3964
+ ],
3965
+ },
3966
+ {
3967
+ id: 'msg_authoritative_empty',
3968
+ type: 'message',
3969
+ role: 'assistant',
3970
+ status: 'completed',
3971
+ content: [
3972
+ {
3973
+ type: 'output_text',
3974
+ text: '',
3975
+ annotations: [],
3976
+ },
3977
+ ],
3978
+ },
3979
+ {
3980
+ type: 'program_output',
3981
+ status: 'completed',
3982
+ result: 'IDLESS_BETWEEN_TEXT',
3983
+ },
3984
+ {
3985
+ id: 'msg_authoritative_second',
3986
+ type: 'message',
3987
+ role: 'assistant',
3988
+ status: 'in_progress',
3989
+ content: [
3990
+ {
3991
+ type: 'output_text',
3992
+ text: 'Second narration.',
3993
+ annotations: [],
3994
+ },
3995
+ ],
3996
+ },
3997
+ ];
3998
+ const message = new AIMessage({
3999
+ content: [
4000
+ { type: 'text', text: 'First narration.' },
4001
+ { type: 'text', text: 'Second narration.' },
4002
+ ],
4003
+ response_metadata: {
4004
+ model_provider: 'openai',
4005
+ output,
4006
+ output_version: 'v1',
4007
+ preempted: true,
4008
+ },
4009
+ });
4010
+ const originalSerialized = JSON.stringify(message.toJSON());
4011
+
4012
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
4013
+ const projectedMessage = projected[0] as AIMessage;
4014
+ const orderedContent = (
4015
+ projectedMessage.content as Array<{ text?: string; type: string }>
4016
+ ).map((block) => {
4017
+ if (
4018
+ block.text === 'First narration.' ||
4019
+ block.text === 'Second narration.'
4020
+ ) {
4021
+ return block.text;
4022
+ }
4023
+ if (block.text?.includes('IDLESS_BEFORE_TEXT') === true) {
4024
+ return 'IDLESS_BEFORE_TEXT';
4025
+ }
4026
+ return block.text?.includes('IDLESS_BETWEEN_TEXT') === true
4027
+ ? 'IDLESS_BETWEEN_TEXT'
4028
+ : undefined;
4029
+ });
4030
+ const providerInput = convertMessagesToResponsesInput({
4031
+ messages: projected,
4032
+ zdrEnabled: false,
4033
+ model: 'gpt-5.6',
4034
+ });
4035
+
4036
+ expect(orderedContent).toEqual([
4037
+ 'IDLESS_BEFORE_TEXT',
4038
+ 'First narration.',
4039
+ 'IDLESS_BETWEEN_TEXT',
4040
+ 'Second narration.',
4041
+ ]);
4042
+ expect(JSON.stringify(providerInput)).not.toContain(
4043
+ 'local_shell_call_output'
4044
+ );
4045
+ expect(JSON.stringify(providerInput)).not.toContain('program_output');
4046
+ expect(JSON.stringify(message.toJSON())).toBe(originalSerialized);
4047
+
4048
+ const projectedAgain = projectOpenAIResponsesToolMessageContent(projected);
4049
+ expect(projectedAgain).toBe(projected);
4050
+ expect(projectedAgain[0]).toBe(projectedMessage);
4051
+ });
4052
+
4053
+ it.each(['v0', 'v1'] as const)(
4054
+ 'replays encrypted %s reasoning at its captured output position',
4055
+ (outputVersion) => {
4056
+ const reasoning = {
4057
+ id: `rs_positioned_${outputVersion}`,
4058
+ type: 'reasoning',
4059
+ status: 'completed',
4060
+ summary: [],
4061
+ encrypted_content: `opaque-positioned-${outputVersion}`,
4062
+ };
4063
+ const serverResult = {
4064
+ id: `local_before_reasoning_${outputVersion}`,
4065
+ type: 'local_shell_call_output',
4066
+ status: 'completed',
4067
+ output: 'RESULT_BEFORE_REASONING',
4068
+ };
4069
+ const message = new AIMessage({
4070
+ content: [
4071
+ { type: 'reasoning', reasoning: 'unfinished summary' },
4072
+ { type: 'text', text: 'Narration after reasoning.' },
4073
+ ],
4074
+ additional_kwargs: {
4075
+ reasoning,
4076
+ tool_outputs: [serverResult],
4077
+ [OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
4078
+ {
4079
+ itemId: serverResult.id,
4080
+ kind: 'output',
4081
+ outputIndex: 0,
4082
+ },
4083
+ {
4084
+ itemId: reasoning.id,
4085
+ kind: 'reasoning',
4086
+ outputIndex: 1,
4087
+ },
4088
+ {
4089
+ itemId: 'msg_after_reasoning',
4090
+ kind: 'text',
4091
+ outputIndex: 2,
4092
+ contentIndex: 0,
4093
+ },
4094
+ ],
4095
+ },
4096
+ response_metadata: {
4097
+ model_provider: 'openai',
4098
+ ...(outputVersion === 'v1' ? { output_version: 'v1' } : {}),
4099
+ preempted: true,
4100
+ },
4101
+ });
4102
+ const originalSerialized = JSON.stringify(message.toJSON());
4103
+
4104
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
4105
+ const projectedMessage = projected[0] as AIMessage;
4106
+ const replayOrder = (
4107
+ projectedMessage.content as Array<{
4108
+ text?: string;
4109
+ type: string;
4110
+ value?: unknown;
4111
+ }>
4112
+ ).map((block) => {
4113
+ if (block.type === 'non_standard' && block.value === reasoning) {
4114
+ return 'reasoning';
4115
+ }
4116
+ if (block.text === 'Narration after reasoning.') {
4117
+ return 'narration';
4118
+ }
4119
+ return block.text?.includes('RESULT_BEFORE_REASONING') === true
4120
+ ? 'result'
4121
+ : undefined;
4122
+ });
4123
+ const providerInput = convertMessagesToResponsesInput({
4124
+ messages: projected,
4125
+ zdrEnabled: false,
4126
+ model: 'gpt-5.6',
4127
+ });
4128
+ const serializedProviderInput = JSON.stringify(providerInput);
4129
+ const resultIndex = serializedProviderInput.indexOf(
4130
+ 'RESULT_BEFORE_REASONING'
4131
+ );
4132
+ const reasoningIndex = serializedProviderInput.indexOf(reasoning.id);
4133
+ const narrationIndex = serializedProviderInput.indexOf(
4134
+ 'Narration after reasoning.'
4135
+ );
4136
+
4137
+ expect(replayOrder).toEqual(['result', 'reasoning', 'narration']);
4138
+ expect(resultIndex).toBeGreaterThanOrEqual(0);
4139
+ expect(resultIndex).toBeLessThan(reasoningIndex);
4140
+ expect(reasoningIndex).toBeLessThan(narrationIndex);
4141
+ expect(serializedProviderInput).not.toContain(serverResult.id);
4142
+ expect(projectedMessage.additional_kwargs.reasoning).toBe(reasoning);
4143
+ expect(projectedMessage.additional_kwargs).not.toHaveProperty(
4144
+ OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
4145
+ );
4146
+ expect(JSON.stringify(message.toJSON())).toBe(originalSerialized);
4147
+
4148
+ const projectedAgain =
4149
+ projectOpenAIResponsesToolMessageContent(projected);
4150
+ expect(projectedAgain).toBe(projected);
4151
+ expect(projectedAgain[0]).toBe(projectedMessage);
4152
+ }
4153
+ );
4154
+
4155
+ it('neutralizes provider references duplicated into v1 content blocks', () => {
4156
+ const reasoning = {
4157
+ id: 'rs_interrupted',
4158
+ type: 'reasoning',
4159
+ status: 'in_progress',
4160
+ summary: [],
4161
+ };
4162
+ const toolOutputs = [
4163
+ {
4164
+ id: 'ci_interrupted',
4165
+ type: 'code_interpreter_call',
4166
+ status: 'completed',
4167
+ code: 'print(1)',
4168
+ outputs: [{ type: 'logs', logs: '1' }],
4169
+ },
4170
+ {
4171
+ type: 'image_generation_call',
4172
+ id: 'ig_interrupted',
4173
+ status: 'completed',
4174
+ result: 'AA==',
4175
+ },
4176
+ ];
4177
+ const providerMessage = new AIMessage({
4178
+ content: [{ type: 'text', text: 'Partial answer.' }],
4179
+ additional_kwargs: { reasoning, tool_outputs: toolOutputs },
4180
+ response_metadata: { model_provider: 'openai' },
4181
+ });
4182
+ const message = new AIMessage({
4183
+ contentBlocks: providerMessage.contentBlocks,
4184
+ additional_kwargs: providerMessage.additional_kwargs,
4185
+ response_metadata: {
4186
+ id: 'resp_interrupted',
4187
+ model_provider: 'openai',
4188
+ output_version: 'v1',
4189
+ preempted: true,
4190
+ },
4191
+ });
4192
+
4193
+ expect(message.content).toEqual([
4194
+ { type: 'reasoning', reasoning: '' },
4195
+ { type: 'text', text: 'Partial answer.' },
4196
+ {
4197
+ type: 'server_tool_call',
4198
+ id: 'ci_interrupted',
4199
+ name: 'code_interpreter',
4200
+ args: { code: 'print(1)' },
4201
+ },
4202
+ {
4203
+ type: 'server_tool_call_result',
4204
+ toolCallId: 'ci_interrupted',
4205
+ status: 'success',
4206
+ output: {
4207
+ type: 'code_interpreter_output',
4208
+ returnCode: 0,
4209
+ stderr: undefined,
4210
+ stdout: '1',
4211
+ },
4212
+ },
4213
+ {
4214
+ type: 'image',
4215
+ mimeType: 'image/png',
4216
+ data: 'AA==',
4217
+ id: 'ig_interrupted',
4218
+ metadata: { status: 'completed' },
4219
+ },
4220
+ {
4221
+ type: 'non_standard',
4222
+ value: toolOutputs[1],
4223
+ },
4224
+ ]);
4225
+ expect(message.additional_kwargs.tool_outputs).toBe(toolOutputs);
4226
+
4227
+ const unsafeProviderInput = convertMessagesToResponsesInput({
4228
+ messages: [message],
4229
+ zdrEnabled: false,
4230
+ model: 'gpt-5.6',
4231
+ });
4232
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
4233
+ const projectedMessage = projected[0] as AIMessage;
4234
+ const providerInput = convertMessagesToResponsesInput({
4235
+ messages: projected,
4236
+ zdrEnabled: false,
4237
+ model: 'gpt-5.6',
4238
+ });
4239
+
4240
+ expect(unsafeProviderInput).toContainEqual(
4241
+ expect.objectContaining({ type: 'reasoning' })
4242
+ );
4243
+ expect(JSON.stringify(unsafeProviderInput)).toContain('ci_interrupted');
4244
+ expect(JSON.stringify(unsafeProviderInput)).toContain(
4245
+ 'image_generation_call'
4246
+ );
4247
+ expect(JSON.stringify(providerInput)).toContain('Partial answer.');
4248
+ expect(JSON.stringify(providerInput)).toContain(
4249
+ 'data:image/png;base64,AA=='
4250
+ );
4251
+ expect(JSON.stringify(providerInput)).not.toContain('rs_interrupted');
4252
+ expect(JSON.stringify(providerInput)).not.toContain('ci_interrupted');
4253
+ expect(JSON.stringify(providerInput)).not.toContain(
4254
+ 'image_generation_call'
4255
+ );
4256
+ expect(projectedMessage.content).toEqual([
4257
+ { type: 'text', text: 'Partial answer.' },
4258
+ {
4259
+ type: 'text',
4260
+ text: JSON.stringify({
4261
+ serverToolResult: {
4262
+ librechatResponsesReplay: true,
4263
+ toolName: 'code_interpreter',
4264
+ status: 'success',
4265
+ output: {
4266
+ type: 'code_interpreter_output',
4267
+ returnCode: 0,
4268
+ stdout: '1',
4269
+ },
4270
+ },
4271
+ }),
4272
+ extras: {
4273
+ librechatServerToolResult: { toolName: 'code_interpreter' },
4274
+ },
4275
+ },
4276
+ {
4277
+ type: 'image',
4278
+ mimeType: 'image/png',
4279
+ data: 'AA==',
4280
+ extras: IMAGE_GENERATION_REPLAY_EXTRAS,
4281
+ },
4282
+ ]);
4283
+ expect(projectedMessage.additional_kwargs).toEqual({});
4284
+ const serialized = JSON.stringify(projectedMessage.toJSON());
4285
+ expect(serialized).toContain('Partial answer.');
4286
+ expect(serialized).toContain('AA==');
4287
+ expect(serialized).not.toContain('rs_interrupted');
4288
+ expect(serialized).not.toContain('ci_interrupted');
4289
+ expect(serialized).not.toContain('image_generation_call');
4290
+ expect(JSON.stringify(message.toJSON())).toContain('rs_interrupted');
4291
+ expect(JSON.stringify(message.toJSON())).toContain('ci_interrupted');
4292
+
4293
+ const fallback = projectToolStreamContentForProvider([message], 'fallback');
4294
+ const fallbackMessage = fallback[0] as AIMessage;
4295
+ expect(fallbackMessage.content).toEqual([
4296
+ { type: 'text', text: 'Partial answer.' },
4297
+ {
4298
+ type: 'text',
4299
+ text: JSON.stringify({
4300
+ serverToolResult: {
4301
+ librechatResponsesReplay: true,
4302
+ toolName: 'code_interpreter',
4303
+ status: 'success',
4304
+ output: {
4305
+ type: 'code_interpreter_output',
4306
+ returnCode: 0,
4307
+ stdout: '1',
4308
+ },
4309
+ },
4310
+ }),
4311
+ },
4312
+ ]);
4313
+ expect(_convertMessagesToOpenAIParams(fallback)).toEqual([
4314
+ {
4315
+ role: 'assistant',
4316
+ content: fallbackMessage.content,
4317
+ },
4318
+ ]);
4319
+ expect(JSON.stringify(fallbackMessage.toJSON())).not.toContain('extras');
4320
+ expect(JSON.stringify(fallbackMessage.toJSON())).not.toContain(
4321
+ 'librechatServerToolResult'
4322
+ );
4323
+
4324
+ const projectedAgain = projectOpenAIResponsesToolMessageContent(projected);
4325
+ expect(projectedAgain).toBe(projected);
4326
+ expect(projectedAgain[0]).toBe(projectedMessage);
4327
+ });
4328
+
4329
+ it('retains exactly one self-contained encrypted reasoning item in v1', () => {
4330
+ const reasoning = {
4331
+ id: 'rs_encrypted',
4332
+ type: 'reasoning',
4333
+ status: 'completed',
4334
+ summary: [],
4335
+ encrypted_content: 'opaque-reasoning',
4336
+ };
4337
+ const message = new AIMessage({
4338
+ content: [
4339
+ {
4340
+ type: 'reasoning',
4341
+ reasoning: 'summary',
4342
+ },
4343
+ { type: 'text', text: 'Partial answer.' },
4344
+ ],
4345
+ additional_kwargs: { reasoning },
4346
+ response_metadata: {
4347
+ model_provider: 'openai',
4348
+ output_version: 'v1',
4349
+ preempted: true,
4350
+ },
4351
+ });
4352
+
4353
+ const projected = projectOpenAIResponsesToolMessageContent([message]);
4354
+ const projectedMessage = projected[0] as AIMessage;
4355
+ const providerInput = convertMessagesToResponsesInput({
4356
+ messages: projected,
4357
+ zdrEnabled: false,
4358
+ model: 'gpt-5.6',
4359
+ });
4360
+
4361
+ expect(projectedMessage.content).toEqual([
4362
+ { type: 'non_standard', value: reasoning },
4363
+ { type: 'text', text: 'Partial answer.' },
4364
+ ]);
4365
+ expect(
4366
+ providerInput.filter(
4367
+ (item) => item.type === 'reasoning' && item.id === 'rs_encrypted'
4368
+ )
4369
+ ).toEqual([
4370
+ expect.objectContaining({
4371
+ id: 'rs_encrypted',
4372
+ type: 'reasoning',
4373
+ encrypted_content: 'opaque-reasoning',
4374
+ }),
4375
+ ]);
4376
+ expect(JSON.stringify(providerInput)).toContain('Partial answer.');
4377
+ });
4378
+
1756
4379
  it('preserves Responses computer outputs handled as provider-native media', () => {
1757
4380
  const computerCall = new AIMessage({
1758
4381
  content: '',