@librechat/agents 3.3.3 → 3.3.4
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.
- package/dist/cjs/agents/AgentContext.cjs +2 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +342 -109
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +19 -7
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +64 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +41 -5
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +25 -4
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +4 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +19 -2
- package/dist/cjs/messages/cache.cjs +54 -33
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +17 -43
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +315 -23
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +218 -60
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +1 -1
- package/dist/cjs/messages/prune.cjs +503 -127
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/stream.cjs +7 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +28 -14
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +54 -41
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/cjs/utils/llm.cjs +1 -1
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +307 -20
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +1514 -0
- package/dist/cjs/utils/toolContent.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +2 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +349 -116
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +19 -7
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +65 -5
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +41 -5
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +25 -4
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +7 -7
- package/dist/esm/messages/cache.mjs +55 -34
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +17 -43
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +305 -24
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +218 -61
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +1 -1
- package/dist/esm/messages/prune.mjs +500 -128
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/stream.mjs +7 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +28 -14
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +54 -41
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +6 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/esm/utils/llm.mjs +1 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +307 -21
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +1503 -0
- package/dist/esm/utils/toolContent.mjs.map +1 -0
- package/dist/types/graphs/Graph.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +27 -1
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openai/utils/index.d.ts +2 -0
- package/dist/types/llm/openrouter/index.d.ts +5 -0
- package/dist/types/messages/contextPruning.d.ts +2 -1
- package/dist/types/messages/core.d.ts +36 -0
- package/dist/types/messages/format.d.ts +6 -0
- package/dist/types/messages/prune.d.ts +16 -12
- package/dist/types/types/stream.d.ts +1 -1
- package/dist/types/utils/tokens.d.ts +7 -0
- package/dist/types/utils/toolContent.d.ts +107 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
- package/src/agents/AgentContext.ts +1 -0
- package/src/graphs/Graph.ts +698 -200
- package/src/graphs/MultiAgentGraph.ts +6 -1
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
- package/src/llm/anthropic/utils/message_inputs.ts +158 -12
- package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
- package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
- package/src/llm/bedrock/utils/message_inputs.ts +86 -13
- package/src/llm/google/utils/common.test.ts +75 -9
- package/src/llm/google/utils/common.ts +34 -3
- package/src/llm/invoke.test.ts +488 -0
- package/src/llm/invoke.ts +196 -2
- package/src/llm/openai/index.ts +106 -6
- package/src/llm/openai/utils/index.ts +109 -53
- package/src/llm/openai/utils/messages.test.ts +330 -1
- package/src/llm/openrouter/index.ts +12 -2
- package/src/llm/openrouter/reasoning.test.ts +314 -0
- package/src/messages/__tests__/observationMasking.test.ts +93 -2
- package/src/messages/cache.tail.test.ts +193 -0
- package/src/messages/cache.test.ts +113 -0
- package/src/messages/cache.ts +92 -60
- package/src/messages/contextPruning.test.ts +184 -0
- package/src/messages/contextPruning.ts +49 -42
- package/src/messages/core.ts +653 -40
- package/src/messages/ensureThinkingBlock.test.ts +49 -1
- package/src/messages/foldToollessToolBlocks.test.ts +163 -5
- package/src/messages/format.ts +363 -91
- package/src/messages/formatAgentMessages.test.ts +604 -14
- package/src/messages/formatAgentMessages.tools.test.ts +103 -0
- package/src/messages/prune.ts +996 -183
- package/src/specs/prune.test.ts +1083 -6
- package/src/specs/summarization-unit.test.ts +105 -0
- package/src/specs/tokens.test.ts +609 -32
- package/src/stream.ts +23 -15
- package/src/summarization/__tests__/node.test.ts +77 -0
- package/src/summarization/node.ts +46 -18
- package/src/tools/ToolNode.ts +147 -68
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
- package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
- package/src/tools/__tests__/directToolHooks.test.ts +255 -7
- package/src/tools/toolOutputReferences.ts +6 -0
- package/src/types/stream.ts +1 -1
- package/src/utils/llm.test.ts +18 -0
- package/src/utils/llm.ts +4 -1
- package/src/utils/tokens.ts +586 -31
- package/src/utils/toolContent.test.ts +1432 -0
- package/src/utils/toolContent.ts +2707 -0
|
@@ -390,7 +390,9 @@ function _convertLangChainContentToPart(
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
if (content.type === 'text') {
|
|
393
|
-
return
|
|
393
|
+
return typeof content.text === 'string' && content.text !== ''
|
|
394
|
+
? { text: content.text }
|
|
395
|
+
: undefined;
|
|
394
396
|
} else if (content.type === 'executableCode') {
|
|
395
397
|
return { executableCode: content.executableCode };
|
|
396
398
|
} else if (content.type === 'codeExecutionResult') {
|
|
@@ -429,10 +431,14 @@ function _convertLangChainContentToPart(
|
|
|
429
431
|
} else if (content.type === 'media') {
|
|
430
432
|
return messageContentMedia(content);
|
|
431
433
|
} else if (content.type === 'tool_use') {
|
|
434
|
+
const functionId = getGoogleFunctionId(
|
|
435
|
+
typeof content.id === 'string' ? content.id : undefined
|
|
436
|
+
);
|
|
432
437
|
return {
|
|
433
438
|
functionCall: {
|
|
434
439
|
name: content.name,
|
|
435
440
|
args: content.input,
|
|
441
|
+
...(functionId != null ? { id: functionId } : {}),
|
|
436
442
|
},
|
|
437
443
|
};
|
|
438
444
|
} else if (
|
|
@@ -553,7 +559,28 @@ export function convertMessageContentToParts(
|
|
|
553
559
|
});
|
|
554
560
|
}
|
|
555
561
|
|
|
556
|
-
|
|
562
|
+
const parsedFunctionCallIds = new Set(
|
|
563
|
+
functionCalls.flatMap((part) => {
|
|
564
|
+
const functionCall = part.functionCall as GoogleFunctionCallWithId;
|
|
565
|
+
return functionCall.id != null ? [functionCall.id] : [];
|
|
566
|
+
})
|
|
567
|
+
);
|
|
568
|
+
const parsedFunctionCallNames = new Set(
|
|
569
|
+
functionCalls.map((part) => part.functionCall.name)
|
|
570
|
+
);
|
|
571
|
+
const contentWithoutParsedMirrors = messageParts.filter((part) => {
|
|
572
|
+
if (!('functionCall' in part) || part.functionCall == null) {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
const functionCall = part.functionCall as GoogleFunctionCallWithId;
|
|
576
|
+
return !(
|
|
577
|
+
(functionCall.id != null && parsedFunctionCallIds.has(functionCall.id)) ||
|
|
578
|
+
(functionCall.id == null &&
|
|
579
|
+
parsedFunctionCallNames.has(functionCall.name))
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
return [...contentWithoutParsedMirrors, ...functionCalls];
|
|
557
584
|
}
|
|
558
585
|
|
|
559
586
|
export function convertBaseMessagesToContent(
|
|
@@ -665,7 +692,11 @@ export function dropUnsupportedModelTurnPrefill(
|
|
|
665
692
|
contents: Content[] | undefined,
|
|
666
693
|
model?: string
|
|
667
694
|
): Content[] | undefined {
|
|
668
|
-
if (
|
|
695
|
+
if (
|
|
696
|
+
contents == null ||
|
|
697
|
+
contents.length === 0 ||
|
|
698
|
+
!rejectsModelTurnPrefill(model)
|
|
699
|
+
) {
|
|
669
700
|
return contents;
|
|
670
701
|
}
|
|
671
702
|
let end = contents.length;
|
package/src/llm/invoke.test.ts
CHANGED
|
@@ -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
|