@librechat/agents 3.3.3 → 3.3.5

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 (274) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +2 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +21 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +10 -1
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +793 -111
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/hooks/HookRegistry.cjs +25 -0
  12. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  13. package/dist/cjs/hooks/index.cjs +12 -0
  14. package/dist/cjs/hooks/index.cjs.map +1 -1
  15. package/dist/cjs/hooks/types.cjs +1 -0
  16. package/dist/cjs/hooks/types.cjs.map +1 -1
  17. package/dist/cjs/langfuse.cjs +8 -0
  18. package/dist/cjs/langfuse.cjs.map +1 -1
  19. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  20. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  21. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +38 -13
  22. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  24. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  25. package/dist/cjs/llm/invoke.cjs +331 -5
  26. package/dist/cjs/llm/invoke.cjs.map +1 -1
  27. package/dist/cjs/llm/openai/index.cjs +41 -5
  28. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  29. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  30. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  31. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  32. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  33. package/dist/cjs/llm/preempt.cjs +132 -0
  34. package/dist/cjs/llm/preempt.cjs.map +1 -0
  35. package/dist/cjs/main.cjs +36 -5
  36. package/dist/cjs/messages/alternation.cjs +92 -0
  37. package/dist/cjs/messages/alternation.cjs.map +1 -0
  38. package/dist/cjs/messages/cache.cjs +54 -33
  39. package/dist/cjs/messages/cache.cjs.map +1 -1
  40. package/dist/cjs/messages/contextPruning.cjs +17 -43
  41. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  42. package/dist/cjs/messages/core.cjs +315 -23
  43. package/dist/cjs/messages/core.cjs.map +1 -1
  44. package/dist/cjs/messages/format.cjs +290 -60
  45. package/dist/cjs/messages/format.cjs.map +1 -1
  46. package/dist/cjs/messages/handoffCue.cjs +63 -0
  47. package/dist/cjs/messages/handoffCue.cjs.map +1 -0
  48. package/dist/cjs/messages/index.cjs +4 -1
  49. package/dist/cjs/messages/injected.cjs +60 -0
  50. package/dist/cjs/messages/injected.cjs.map +1 -0
  51. package/dist/cjs/messages/prune.cjs +503 -127
  52. package/dist/cjs/messages/prune.cjs.map +1 -1
  53. package/dist/cjs/run.cjs +80 -7
  54. package/dist/cjs/run.cjs.map +1 -1
  55. package/dist/cjs/session/handlers.cjs +18 -9
  56. package/dist/cjs/session/handlers.cjs.map +1 -1
  57. package/dist/cjs/stream.cjs +35 -10
  58. package/dist/cjs/stream.cjs.map +1 -1
  59. package/dist/cjs/summarization/node.cjs +28 -14
  60. package/dist/cjs/summarization/node.cjs.map +1 -1
  61. package/dist/cjs/tools/ToolNode.cjs +56 -63
  62. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  63. package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
  64. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
  65. package/dist/cjs/tools/search/tool.cjs +9 -1
  66. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  67. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  68. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  69. package/dist/cjs/utils/index.cjs +1 -1
  70. package/dist/cjs/utils/llm.cjs +1 -1
  71. package/dist/cjs/utils/llm.cjs.map +1 -1
  72. package/dist/cjs/utils/tokens.cjs +307 -20
  73. package/dist/cjs/utils/tokens.cjs.map +1 -1
  74. package/dist/cjs/utils/toolContent.cjs +1514 -0
  75. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  76. package/dist/esm/agents/AgentContext.mjs +2 -1
  77. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  78. package/dist/esm/common/constants.mjs +19 -1
  79. package/dist/esm/common/constants.mjs.map +1 -1
  80. package/dist/esm/events.mjs +10 -1
  81. package/dist/esm/events.mjs.map +1 -1
  82. package/dist/esm/graphs/Graph.mjs +798 -116
  83. package/dist/esm/graphs/Graph.mjs.map +1 -1
  84. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  85. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  86. package/dist/esm/hooks/HookRegistry.mjs +25 -0
  87. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  88. package/dist/esm/hooks/index.mjs +12 -1
  89. package/dist/esm/hooks/index.mjs.map +1 -1
  90. package/dist/esm/hooks/types.mjs +1 -0
  91. package/dist/esm/hooks/types.mjs.map +1 -1
  92. package/dist/esm/langfuse.mjs +9 -1
  93. package/dist/esm/langfuse.mjs.map +1 -1
  94. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  95. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  96. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +38 -13
  97. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  98. package/dist/esm/llm/google/utils/common.mjs +19 -7
  99. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  100. package/dist/esm/llm/invoke.mjs +332 -8
  101. package/dist/esm/llm/invoke.mjs.map +1 -1
  102. package/dist/esm/llm/openai/index.mjs +41 -5
  103. package/dist/esm/llm/openai/index.mjs.map +1 -1
  104. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  105. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  106. package/dist/esm/llm/openrouter/index.mjs +4 -1
  107. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  108. package/dist/esm/llm/preempt.mjs +131 -0
  109. package/dist/esm/llm/preempt.mjs.map +1 -0
  110. package/dist/esm/main.mjs +14 -11
  111. package/dist/esm/messages/alternation.mjs +91 -0
  112. package/dist/esm/messages/alternation.mjs.map +1 -0
  113. package/dist/esm/messages/cache.mjs +55 -34
  114. package/dist/esm/messages/cache.mjs.map +1 -1
  115. package/dist/esm/messages/contextPruning.mjs +17 -43
  116. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  117. package/dist/esm/messages/core.mjs +305 -24
  118. package/dist/esm/messages/core.mjs.map +1 -1
  119. package/dist/esm/messages/format.mjs +290 -61
  120. package/dist/esm/messages/format.mjs.map +1 -1
  121. package/dist/esm/messages/handoffCue.mjs +61 -0
  122. package/dist/esm/messages/handoffCue.mjs.map +1 -0
  123. package/dist/esm/messages/index.mjs +4 -1
  124. package/dist/esm/messages/injected.mjs +60 -0
  125. package/dist/esm/messages/injected.mjs.map +1 -0
  126. package/dist/esm/messages/prune.mjs +500 -128
  127. package/dist/esm/messages/prune.mjs.map +1 -1
  128. package/dist/esm/run.mjs +80 -7
  129. package/dist/esm/run.mjs.map +1 -1
  130. package/dist/esm/session/handlers.mjs +19 -10
  131. package/dist/esm/session/handlers.mjs.map +1 -1
  132. package/dist/esm/stream.mjs +34 -11
  133. package/dist/esm/stream.mjs.map +1 -1
  134. package/dist/esm/summarization/node.mjs +28 -14
  135. package/dist/esm/summarization/node.mjs.map +1 -1
  136. package/dist/esm/tools/ToolNode.mjs +56 -63
  137. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  138. package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
  139. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
  140. package/dist/esm/tools/search/tool.mjs +9 -1
  141. package/dist/esm/tools/search/tool.mjs.map +1 -1
  142. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  143. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  144. package/dist/esm/utils/index.mjs +1 -1
  145. package/dist/esm/utils/llm.mjs +1 -1
  146. package/dist/esm/utils/llm.mjs.map +1 -1
  147. package/dist/esm/utils/tokens.mjs +307 -21
  148. package/dist/esm/utils/tokens.mjs.map +1 -1
  149. package/dist/esm/utils/toolContent.mjs +1503 -0
  150. package/dist/esm/utils/toolContent.mjs.map +1 -0
  151. package/dist/types/common/constants.d.ts +18 -0
  152. package/dist/types/graphs/Graph.d.ts +139 -2
  153. package/dist/types/hooks/HookRegistry.d.ts +15 -0
  154. package/dist/types/hooks/index.d.ts +12 -1
  155. package/dist/types/hooks/types.d.ts +45 -6
  156. package/dist/types/index.d.ts +1 -1
  157. package/dist/types/llm/invoke.d.ts +34 -1
  158. package/dist/types/llm/openai/index.d.ts +4 -0
  159. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  160. package/dist/types/llm/openrouter/index.d.ts +5 -0
  161. package/dist/types/llm/preempt.d.ts +40 -0
  162. package/dist/types/messages/alternation.d.ts +23 -0
  163. package/dist/types/messages/contextPruning.d.ts +2 -1
  164. package/dist/types/messages/core.d.ts +36 -0
  165. package/dist/types/messages/format.d.ts +6 -0
  166. package/dist/types/messages/handoffCue.d.ts +40 -0
  167. package/dist/types/messages/index.d.ts +3 -0
  168. package/dist/types/messages/injected.d.ts +3 -0
  169. package/dist/types/messages/prune.d.ts +16 -12
  170. package/dist/types/run.d.ts +7 -0
  171. package/dist/types/stream.d.ts +14 -0
  172. package/dist/types/tools/ToolNode.d.ts +0 -7
  173. package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
  174. package/dist/types/tools/search/types.d.ts +31 -2
  175. package/dist/types/types/graph.d.ts +8 -1
  176. package/dist/types/types/run.d.ts +65 -0
  177. package/dist/types/types/stream.d.ts +1 -26
  178. package/dist/types/utils/tokens.d.ts +7 -0
  179. package/dist/types/utils/toolContent.d.ts +107 -0
  180. package/package.json +1 -1
  181. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  182. package/src/agents/AgentContext.ts +1 -0
  183. package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
  184. package/src/common/constants.ts +21 -0
  185. package/src/events.ts +15 -1
  186. package/src/graphs/Graph.ts +1261 -198
  187. package/src/graphs/MultiAgentGraph.ts +6 -1
  188. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1139 -4
  189. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
  190. package/src/hooks/HookRegistry.ts +40 -0
  191. package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
  192. package/src/hooks/index.ts +16 -2
  193. package/src/hooks/types.ts +47 -3
  194. package/src/index.ts +1 -1
  195. package/src/langfuse.ts +26 -1
  196. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  197. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  198. package/src/llm/bedrock/utils/message_inputs.test.ts +131 -1
  199. package/src/llm/bedrock/utils/message_inputs.ts +100 -24
  200. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
  201. package/src/llm/google/utils/common.test.ts +75 -9
  202. package/src/llm/google/utils/common.ts +34 -3
  203. package/src/llm/invoke.alternation.test.ts +87 -0
  204. package/src/llm/invoke.handoffCue.test.ts +112 -0
  205. package/src/llm/invoke.test.ts +488 -0
  206. package/src/llm/invoke.ts +611 -8
  207. package/src/llm/openai/index.ts +106 -6
  208. package/src/llm/openai/utils/index.ts +109 -53
  209. package/src/llm/openai/utils/messages.test.ts +330 -1
  210. package/src/llm/openrouter/index.ts +12 -2
  211. package/src/llm/openrouter/reasoning.test.ts +314 -0
  212. package/src/llm/preempt.test.ts +323 -0
  213. package/src/llm/preempt.ts +178 -0
  214. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  215. package/src/messages/alternation.test.ts +212 -0
  216. package/src/messages/alternation.ts +112 -0
  217. package/src/messages/cache.tail.test.ts +193 -0
  218. package/src/messages/cache.test.ts +113 -0
  219. package/src/messages/cache.ts +92 -60
  220. package/src/messages/contextPruning.test.ts +184 -0
  221. package/src/messages/contextPruning.ts +49 -42
  222. package/src/messages/core.ts +653 -40
  223. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  224. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  225. package/src/messages/format.ts +454 -91
  226. package/src/messages/formatAgentMessages.steer.test.ts +267 -0
  227. package/src/messages/formatAgentMessages.test.ts +604 -14
  228. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  229. package/src/messages/handoffCue.test.ts +96 -0
  230. package/src/messages/handoffCue.ts +78 -0
  231. package/src/messages/index.ts +3 -0
  232. package/src/messages/injected.test.ts +90 -0
  233. package/src/messages/injected.ts +74 -0
  234. package/src/messages/prune.ts +996 -183
  235. package/src/run.ts +91 -6
  236. package/src/scripts/preempt-probe.ts +330 -0
  237. package/src/scripts/preempt-scenarios.ts +388 -0
  238. package/src/session/handlers.ts +32 -12
  239. package/src/specs/handoffCue.test.ts +165 -0
  240. package/src/specs/langfuse-callbacks.test.ts +352 -2
  241. package/src/specs/preemptSeal.test.ts +309 -0
  242. package/src/specs/prune.test.ts +1083 -6
  243. package/src/specs/summarization-unit.test.ts +105 -0
  244. package/src/specs/tokens.test.ts +609 -32
  245. package/src/stream.dispatch.test.ts +63 -0
  246. package/src/stream.ts +59 -23
  247. package/src/summarization/__tests__/node.test.ts +77 -0
  248. package/src/summarization/node.ts +46 -18
  249. package/src/tools/ToolNode.ts +150 -99
  250. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  251. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  252. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  253. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  254. package/src/tools/search/keenable-scraper.test.ts +153 -0
  255. package/src/tools/search/keenable-scraper.ts +137 -0
  256. package/src/tools/search/tool.ts +13 -2
  257. package/src/tools/search/types.ts +50 -3
  258. package/src/tools/toolOutputReferences.ts +6 -0
  259. package/src/types/graph.ts +12 -1
  260. package/src/types/run.ts +67 -0
  261. package/src/types/stream.ts +1 -42
  262. package/src/utils/llm.test.ts +18 -0
  263. package/src/utils/llm.ts +4 -1
  264. package/src/utils/tokens.ts +586 -31
  265. package/src/utils/toolContent.test.ts +1432 -0
  266. package/src/utils/toolContent.ts +2707 -0
  267. package/dist/cjs/splitStream.cjs +0 -151
  268. package/dist/cjs/splitStream.cjs.map +0 -1
  269. package/dist/esm/splitStream.mjs +0 -150
  270. package/dist/esm/splitStream.mjs.map +0 -1
  271. package/dist/types/mockStream.d.ts +0 -32
  272. package/dist/types/splitStream.d.ts +0 -37
  273. package/src/mockStream.ts +0 -99
  274. package/src/splitStream.ts +0 -234
@@ -7,13 +7,22 @@ import {
7
7
  HumanMessage,
8
8
  ToolMessage,
9
9
  } from '@langchain/core/messages';
10
+ import {
11
+ convertMessagesToCompletionsMessageParams,
12
+ convertMessagesToResponsesInput,
13
+ tools as openAITools,
14
+ } from '@langchain/openai';
10
15
  import type { StructuredToolInterface } from '@langchain/core/tools';
11
16
  import type { BaseMessage } from '@langchain/core/messages';
12
17
  import type * as t from '@/types';
18
+ import { _convertMessagesToAnthropicPayload } from '@/llm/anthropic/utils/message_inputs';
13
19
  import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
20
+ import { convertMessageContentToParts } from '@/llm/google/utils/common';
14
21
  import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
22
+ import { toLangChainContent } from '@/messages/langchain';
15
23
  import { Constants, Providers } from '@/common';
16
24
  import { ToolNode } from '@/tools/ToolNode';
25
+ import { ChatOpenAI } from '@/llm/openai';
17
26
 
18
27
  /**
19
28
  * Minimal stub model shape `attemptInvoke` reads. Either `invoke` or
@@ -21,6 +30,9 @@ import { ToolNode } from '@/tools/ToolNode';
21
30
  * extending the real `BaseChatModel` would pull in too much surface.
22
31
  */
23
32
  type StubModel = {
33
+ _useResponsesApi?: (options?: unknown) => boolean;
34
+ defaultOptions?: unknown;
35
+ last?: unknown;
24
36
  invoke?: (messages: BaseMessage[], config?: unknown) => Promise<AIMessage>;
25
37
  stream?: (
26
38
  messages: BaseMessage[],
@@ -164,6 +176,482 @@ describe('attemptInvoke applies lazy ref annotation', () => {
164
176
  expect(invokeMessages[0][0].content).toBe('output');
165
177
  });
166
178
 
179
+ it('replaces native computer screenshots for non-Responses providers', async () => {
180
+ const screenshot = `data:image/png;base64,${'A'.repeat(100_000)}`;
181
+ const computerOutput = new ToolMessage({
182
+ content: screenshot,
183
+ tool_call_id: 'computer-output',
184
+ additional_kwargs: { type: 'computer_call_output' },
185
+ });
186
+ const { invokeMessages, model } = buildCapturingModel();
187
+
188
+ await attemptInvoke({
189
+ model: model as t.ChatModel,
190
+ messages: [computerOutput],
191
+ provider: Providers.ANTHROPIC,
192
+ });
193
+
194
+ expect(invokeMessages[0][0].content).toBe(
195
+ '[Computer screenshot omitted for this provider]'
196
+ );
197
+ expect(computerOutput.content).toBe(screenshot);
198
+ });
199
+
200
+ it('scopes cache blocks to providers that support them', async () => {
201
+ const cachedOutput = new ToolMessage({
202
+ content: [
203
+ {
204
+ type: 'text',
205
+ text: 'result body',
206
+ cache_control: { type: 'ephemeral' },
207
+ },
208
+ ] as ToolMessage['content'],
209
+ tool_call_id: 'call_cached_tool',
210
+ });
211
+ const messages = [
212
+ new AIMessage({
213
+ content: '',
214
+ tool_calls: [{ id: 'call_cached_tool', name: 'search', args: {} }],
215
+ }),
216
+ cachedOutput,
217
+ ];
218
+ const openRouter = buildCapturingModel();
219
+ const openAI = buildCapturingModel();
220
+ const anthropic = buildCapturingModel();
221
+ const google = buildCapturingModel();
222
+
223
+ await attemptInvoke({
224
+ model: openRouter.model as t.ChatModel,
225
+ messages,
226
+ provider: Providers.OPENROUTER,
227
+ });
228
+ await attemptInvoke({
229
+ model: openAI.model as t.ChatModel,
230
+ messages,
231
+ provider: Providers.OPENAI,
232
+ });
233
+ await attemptInvoke({
234
+ model: anthropic.model as t.ChatModel,
235
+ messages,
236
+ provider: Providers.ANTHROPIC,
237
+ });
238
+ await attemptInvoke({
239
+ model: google.model as t.ChatModel,
240
+ messages,
241
+ provider: Providers.GOOGLE,
242
+ });
243
+
244
+ expect(openRouter.invokeMessages[0][1].content).toEqual(
245
+ cachedOutput.content
246
+ );
247
+ expect(openAI.invokeMessages[0][1].content).toBe('result body');
248
+ expect(anthropic.invokeMessages[0][1].content).toEqual(
249
+ cachedOutput.content
250
+ );
251
+ expect(google.invokeMessages[0][1].content).toBe('result body');
252
+ expect(cachedOutput.content).toEqual([
253
+ {
254
+ type: 'text',
255
+ text: 'result body',
256
+ cache_control: { type: 'ephemeral' },
257
+ },
258
+ ]);
259
+ });
260
+
261
+ it('removes foreign cache markers from official OpenAI Chat payloads', async () => {
262
+ const messages = [
263
+ new HumanMessage({
264
+ content: toLangChainContent([
265
+ {
266
+ type: 'text',
267
+ text: 'question',
268
+ cache_control: { type: 'ephemeral' },
269
+ },
270
+ ]),
271
+ }),
272
+ new AIMessage({
273
+ content: toLangChainContent([
274
+ {
275
+ type: 'text',
276
+ text: 'answer',
277
+ cache_control: { type: 'ephemeral' },
278
+ },
279
+ ]),
280
+ }),
281
+ ];
282
+ const { invokeMessages, model } = buildCapturingModel();
283
+
284
+ await attemptInvoke({
285
+ model: model as t.ChatModel,
286
+ messages,
287
+ provider: Providers.OPENAI,
288
+ });
289
+
290
+ const payload = convertMessagesToCompletionsMessageParams({
291
+ messages: invokeMessages[0],
292
+ model: 'gpt-4o',
293
+ });
294
+ expect(JSON.stringify(payload)).not.toContain('cache_control');
295
+ expect(JSON.stringify(messages)).toContain('cache_control');
296
+ });
297
+
298
+ it('removes Bedrock cache points before an Anthropic fallback', async () => {
299
+ const messages = [
300
+ new HumanMessage('search'),
301
+ new AIMessage({
302
+ content: '',
303
+ tool_calls: [{ id: 'call_bedrock_cache', name: 'search', args: {} }],
304
+ }),
305
+ new ToolMessage({
306
+ content: [
307
+ { type: 'text', text: 'result body' },
308
+ { cachePoint: { type: 'default' } },
309
+ ] as ToolMessage['content'],
310
+ tool_call_id: 'call_bedrock_cache',
311
+ }),
312
+ ];
313
+ const { invokeMessages, model } = buildCapturingModel();
314
+
315
+ await attemptInvoke({
316
+ model: model as t.ChatModel,
317
+ messages,
318
+ provider: Providers.ANTHROPIC,
319
+ });
320
+
321
+ expect((invokeMessages[0][2] as ToolMessage).content).toBe('result body');
322
+ expect(() =>
323
+ _convertMessagesToAnthropicPayload(invokeMessages[0])
324
+ ).not.toThrow();
325
+ expect(JSON.stringify(invokeMessages[0])).not.toContain('cachePoint');
326
+ expect(JSON.stringify(messages)).toContain('cachePoint');
327
+ });
328
+
329
+ it('preserves error tool metadata while stripping foreign cache markers', async () => {
330
+ const cachedError = new ToolMessage({
331
+ content: toLangChainContent([
332
+ {
333
+ type: 'text',
334
+ text: 'boom',
335
+ cache_control: { type: 'ephemeral' },
336
+ },
337
+ ]),
338
+ tool_call_id: 'call_error',
339
+ name: 'search',
340
+ status: 'error',
341
+ artifact: { source: 'test' },
342
+ metadata: { trace: 'trace-1' },
343
+ });
344
+ const messages = [
345
+ new AIMessage({
346
+ content: '',
347
+ tool_calls: [{ id: 'call_error', name: 'search', args: {} }],
348
+ }),
349
+ cachedError,
350
+ ];
351
+ const { invokeMessages, model } = buildCapturingModel();
352
+
353
+ await attemptInvoke({
354
+ model: model as t.ChatModel,
355
+ messages,
356
+ provider: Providers.GOOGLE,
357
+ });
358
+
359
+ const sent = invokeMessages[0][1] as ToolMessage;
360
+ expect(sent).toMatchObject({
361
+ content: 'boom',
362
+ status: 'error',
363
+ artifact: { source: 'test' },
364
+ metadata: { trace: 'trace-1' },
365
+ });
366
+ expect(convertMessageContentToParts(sent, false, messages)).toEqual([
367
+ expect.objectContaining({
368
+ functionResponse: expect.objectContaining({
369
+ response: { error: { details: 'boom' } },
370
+ }),
371
+ }),
372
+ ]);
373
+ expect(cachedError.content).not.toBe('boom');
374
+ });
375
+
376
+ it('removes per-block cache markers from OpenRouter Responses input', async () => {
377
+ const messages = [
378
+ new HumanMessage({
379
+ content: toLangChainContent([
380
+ {
381
+ type: 'text',
382
+ text: 'question',
383
+ cache_control: { type: 'ephemeral' },
384
+ },
385
+ ]),
386
+ }),
387
+ ];
388
+ const { invokeMessages, model } = buildCapturingModel();
389
+ model.last = { _useResponsesApi: () => true };
390
+
391
+ await attemptInvoke({
392
+ model: model as t.ChatModel,
393
+ messages,
394
+ provider: Providers.OPENROUTER,
395
+ });
396
+
397
+ const input = convertMessagesToResponsesInput({
398
+ messages: invokeMessages[0],
399
+ model: 'openai/gpt-5',
400
+ zdrEnabled: false,
401
+ });
402
+ expect(JSON.stringify(input)).not.toContain('cache_control');
403
+ expect(JSON.stringify(messages)).toContain('cache_control');
404
+ });
405
+
406
+ it.each([
407
+ [Providers.OPENAI, false],
408
+ [Providers.OPENAI, true],
409
+ [Providers.OPENROUTER, false],
410
+ [Providers.OPENROUTER, true],
411
+ [Providers.ANTHROPIC, false],
412
+ [Providers.BEDROCK, false],
413
+ [Providers.GOOGLE, false],
414
+ ])(
415
+ 'drops incomplete streamed tool-input fragments for %s (Responses=%s)',
416
+ async (provider, responses) => {
417
+ let typeGetterCalls = 0;
418
+ const accessorBlock = {};
419
+ Object.defineProperty(accessorBlock, 'type', {
420
+ enumerable: true,
421
+ get() {
422
+ typeGetterCalls++;
423
+ throw new Error('type getter must not run');
424
+ },
425
+ });
426
+ const proxyBlock = new Proxy(
427
+ {},
428
+ {
429
+ getOwnPropertyDescriptor() {
430
+ throw new Error('proxy descriptor trap must not run');
431
+ },
432
+ }
433
+ );
434
+ const streamedToolCall = new AIMessage({
435
+ content: toLangChainContent([
436
+ { type: 'text', text: 'calling' },
437
+ {
438
+ type: 'tool_use',
439
+ id: 'call_streamed',
440
+ name: 'search',
441
+ input: '',
442
+ index: 0,
443
+ },
444
+ {
445
+ type: 'text',
446
+ index: 0,
447
+ input: '{"query":"records"}',
448
+ },
449
+ accessorBlock,
450
+ proxyBlock,
451
+ ]),
452
+ tool_calls: [
453
+ {
454
+ id: 'call_streamed',
455
+ name: 'search',
456
+ args: { query: 'records' },
457
+ type: 'tool_call',
458
+ },
459
+ ],
460
+ });
461
+ const messages = [
462
+ new HumanMessage('run the search'),
463
+ streamedToolCall,
464
+ new ToolMessage({
465
+ content: 'result',
466
+ tool_call_id: 'call_streamed',
467
+ name: 'search',
468
+ }),
469
+ ];
470
+ const { invokeMessages, model } = buildCapturingModel();
471
+ if (responses) {
472
+ model.last = { _useResponsesApi: () => true };
473
+ }
474
+
475
+ await attemptInvoke({
476
+ model: model as t.ChatModel,
477
+ messages,
478
+ provider,
479
+ });
480
+
481
+ const sent = invokeMessages[0][1] as AIMessage;
482
+ expect(sent.content).toEqual([
483
+ { type: 'text', text: 'calling' },
484
+ {
485
+ type: 'tool_use',
486
+ id: 'call_streamed',
487
+ name: 'search',
488
+ input: '',
489
+ index: 0,
490
+ },
491
+ ]);
492
+ expect(sent.tool_calls).toEqual(streamedToolCall.tool_calls);
493
+ expect(typeGetterCalls).toBe(0);
494
+ expect(streamedToolCall.content).toHaveLength(5);
495
+ }
496
+ );
497
+
498
+ it.each([Providers.OPENAI, Providers.OPENROUTER])(
499
+ 'canonicalizes computer screenshots for an actual %s Responses attempt',
500
+ async (provider) => {
501
+ const { invokeMessages, model } = buildCapturingModel();
502
+ model.last = { _useResponsesApi: () => true };
503
+ const computerCall = new AIMessage({
504
+ content: '',
505
+ response_metadata: {
506
+ output: [
507
+ {
508
+ type: 'computer_call',
509
+ call_id: 'computer-output',
510
+ action: { type: 'screenshot' },
511
+ },
512
+ ],
513
+ },
514
+ });
515
+ const computerOutput = new ToolMessage({
516
+ content: 'data:image/png;base64,AA==',
517
+ tool_call_id: 'computer-output',
518
+ additional_kwargs: { type: 'computer_call_output' },
519
+ });
520
+
521
+ await attemptInvoke({
522
+ model: model as t.ChatModel,
523
+ messages: [computerCall, computerOutput],
524
+ provider,
525
+ });
526
+
527
+ expect(invokeMessages[0][1].content).toEqual([
528
+ {
529
+ type: 'input_image',
530
+ image_url: 'data:image/png;base64,AA==',
531
+ },
532
+ ]);
533
+ }
534
+ );
535
+
536
+ it('merges per-call options when selecting Chat versus Responses', async () => {
537
+ const computerCall = new AIMessage({
538
+ content: '',
539
+ response_metadata: {
540
+ output: [
541
+ {
542
+ type: 'computer_call',
543
+ call_id: 'call_per_call_computer',
544
+ action: { type: 'screenshot' },
545
+ },
546
+ ],
547
+ },
548
+ });
549
+ const computerOutput = new ToolMessage({
550
+ content: 'data:image/png;base64,AA==',
551
+ tool_call_id: 'call_per_call_computer',
552
+ additional_kwargs: { type: 'computer_call_output' },
553
+ });
554
+ const responses = buildCapturingModel();
555
+ responses.model.defaultOptions = { tools: [] };
556
+ responses.model._useResponsesApi = (options) =>
557
+ Array.isArray((options as { tools?: unknown[] } | undefined)?.tools) &&
558
+ ((options as { tools: unknown[] }).tools.length ?? 0) > 0;
559
+
560
+ await attemptInvoke(
561
+ {
562
+ model: responses.model as t.ChatModel,
563
+ messages: [computerCall, computerOutput],
564
+ provider: Providers.OPENAI,
565
+ },
566
+ {
567
+ tools: [{ type: 'computer_20251124' }],
568
+ } as unknown as Parameters<typeof attemptInvoke>[1]
569
+ );
570
+
571
+ expect(responses.invokeMessages[0][1].content).toEqual([
572
+ {
573
+ type: 'input_image',
574
+ image_url: 'data:image/png;base64,AA==',
575
+ },
576
+ ]);
577
+
578
+ const chat = buildCapturingModel();
579
+ chat.model.defaultOptions = {
580
+ tools: [{ type: 'computer_20251124' }],
581
+ };
582
+ chat.model._useResponsesApi = responses.model._useResponsesApi;
583
+
584
+ await attemptInvoke(
585
+ {
586
+ model: chat.model as t.ChatModel,
587
+ messages: [computerCall, computerOutput],
588
+ provider: Providers.OPENAI,
589
+ },
590
+ { tools: [] } as unknown as Parameters<typeof attemptInvoke>[1]
591
+ );
592
+
593
+ expect(chat.invokeMessages[0][1].content).toBe(
594
+ '[Computer screenshot omitted for this provider]'
595
+ );
596
+ });
597
+
598
+ it('detects Responses selected by a bound built-in computer tool', async () => {
599
+ const boundModel = new ChatOpenAI({
600
+ apiKey: 'test-key',
601
+ model: 'computer-use-preview',
602
+ useResponsesApi: false,
603
+ }).bindTools([
604
+ openAITools.computerUse({
605
+ displayWidth: 1024,
606
+ displayHeight: 768,
607
+ environment: 'browser',
608
+ execute: async () => 'data:image/png;base64,AA==',
609
+ }),
610
+ ]);
611
+ const invokeMessages: BaseMessage[][] = [];
612
+ Object.defineProperty(boundModel, 'stream', {
613
+ configurable: true,
614
+ value: undefined,
615
+ });
616
+ Object.defineProperty(boundModel, 'invoke', {
617
+ configurable: true,
618
+ value: jest.fn(async (messages: BaseMessage[]) => {
619
+ invokeMessages.push(messages);
620
+ return new AIMessage({ content: 'ok' });
621
+ }),
622
+ });
623
+ const computerCall = new AIMessage({
624
+ content: '',
625
+ response_metadata: {
626
+ output: [
627
+ {
628
+ type: 'computer_call',
629
+ call_id: 'bound-computer-output',
630
+ action: { type: 'screenshot' },
631
+ },
632
+ ],
633
+ },
634
+ });
635
+ const computerOutput = new ToolMessage({
636
+ content: 'data:image/png;base64,AA==',
637
+ tool_call_id: 'bound-computer-output',
638
+ additional_kwargs: { type: 'computer_call_output' },
639
+ });
640
+
641
+ await attemptInvoke({
642
+ model: boundModel as t.ChatModel,
643
+ messages: [computerCall, computerOutput],
644
+ provider: Providers.OPENAI,
645
+ });
646
+
647
+ expect(invokeMessages[0][1].content).toEqual([
648
+ {
649
+ type: 'input_image',
650
+ image_url: 'data:image/png;base64,AA==',
651
+ },
652
+ ]);
653
+ });
654
+
167
655
  it('skips annotation for stale _refKey not present in current run registry (cross-run scenario)', async () => {
168
656
  const registry = new ToolOutputReferenceRegistry();
169
657
  // run-3 registry holds tool0turn0 - the current run's live ref