@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
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { type OpenAI as OpenAIClient } from 'openai';
|
|
4
4
|
import { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
5
5
|
import {
|
|
6
|
-
convertLangChainToolCallToOpenAI,
|
|
7
6
|
makeInvalidToolCall,
|
|
8
7
|
parseToolCall,
|
|
9
8
|
} from '@langchain/core/output_parsers/openai_tools';
|
|
@@ -37,11 +36,23 @@ import type {
|
|
|
37
36
|
ChatOpenAIReasoningSummary,
|
|
38
37
|
} from '@langchain/openai';
|
|
39
38
|
import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
|
|
39
|
+
import {
|
|
40
|
+
getBoundedCacheControlledTextToolContent,
|
|
41
|
+
getComputerCallOutputScreenshot,
|
|
42
|
+
isComputerCallOutputMessage,
|
|
43
|
+
serializeStructuredValueBounded,
|
|
44
|
+
} from '@/utils/toolContent';
|
|
40
45
|
import {
|
|
41
46
|
STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
|
|
42
47
|
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
43
48
|
OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
|
|
44
49
|
} from '@/tools/streamedToolCallSeals';
|
|
50
|
+
import {
|
|
51
|
+
calculateMaxToolCallInputChars,
|
|
52
|
+
projectToolCallInputs,
|
|
53
|
+
serializeToolCallInput,
|
|
54
|
+
} from '@/messages/prune';
|
|
55
|
+
import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
|
|
45
56
|
import { toLangChainContent } from '@/messages/langchain';
|
|
46
57
|
|
|
47
58
|
export type { OpenAICallOptions, OpenAIChatInput };
|
|
@@ -83,6 +94,32 @@ type OpenAIRoleEnum =
|
|
|
83
94
|
type OpenAICompletionParam =
|
|
84
95
|
OpenAIClient.Chat.Completions.ChatCompletionMessageParam;
|
|
85
96
|
|
|
97
|
+
const MAX_PROVIDER_TOOL_CALL_INPUT_CHARS = calculateMaxToolCallInputChars();
|
|
98
|
+
|
|
99
|
+
function convertLangChainToolCallToBoundedOpenAI(toolCall: ToolCall): {
|
|
100
|
+
id: string;
|
|
101
|
+
type: 'function';
|
|
102
|
+
function: {
|
|
103
|
+
name: string;
|
|
104
|
+
arguments: string;
|
|
105
|
+
};
|
|
106
|
+
} {
|
|
107
|
+
if (toolCall.id == null) {
|
|
108
|
+
throw new Error('All OpenAI tool calls must have an "id" field.');
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
id: toolCall.id,
|
|
112
|
+
type: 'function',
|
|
113
|
+
function: {
|
|
114
|
+
name: toolCall.name,
|
|
115
|
+
arguments: serializeToolCallInput(
|
|
116
|
+
toolCall.args,
|
|
117
|
+
MAX_PROVIDER_TOOL_CALL_INPUT_CHARS
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
function extractGenericMessageCustomRole(message: ChatMessage) {
|
|
87
124
|
if (
|
|
88
125
|
message.role !== 'system' &&
|
|
@@ -300,6 +337,8 @@ export interface ConvertMessagesOptions {
|
|
|
300
337
|
includeReasoningDetails?: boolean;
|
|
301
338
|
/** Convert reasoning_details to content blocks for Claude (requires content array format) */
|
|
302
339
|
convertReasoningDetailsToContent?: boolean;
|
|
340
|
+
/** Preserve OpenRouter's canonical cache-decorated tool text block. */
|
|
341
|
+
preserveToolCacheControl?: boolean;
|
|
303
342
|
}
|
|
304
343
|
|
|
305
344
|
// Used in LangSmith, export is important here
|
|
@@ -308,9 +347,13 @@ export function _convertMessagesToOpenAIParams(
|
|
|
308
347
|
model?: string,
|
|
309
348
|
options?: ConvertMessagesOptions
|
|
310
349
|
): OpenAICompletionParam[] {
|
|
350
|
+
const projectedMessages = projectToolCallInputs(
|
|
351
|
+
messages,
|
|
352
|
+
MAX_PROVIDER_TOOL_CALL_INPUT_CHARS
|
|
353
|
+
);
|
|
311
354
|
let hasReasoningToolCallContext = false;
|
|
312
355
|
// TODO: Function messages do not support array content, fix cast
|
|
313
|
-
return
|
|
356
|
+
return projectedMessages.flatMap((message) => {
|
|
314
357
|
let role = messageToOpenAIRole(message);
|
|
315
358
|
if (role === 'system' && isReasoningModel(model)) {
|
|
316
359
|
role = 'developer';
|
|
@@ -318,22 +361,40 @@ export function _convertMessagesToOpenAIParams(
|
|
|
318
361
|
|
|
319
362
|
let hasAnthropicThinkingBlock: boolean = false;
|
|
320
363
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
364
|
+
let content: unknown;
|
|
365
|
+
if (
|
|
366
|
+
role === 'tool' &&
|
|
367
|
+
typeof message.content !== 'string' &&
|
|
368
|
+
!isComputerCallOutputMessage(message)
|
|
369
|
+
) {
|
|
370
|
+
content =
|
|
371
|
+
options?.preserveToolCacheControl === true
|
|
372
|
+
? getBoundedCacheControlledTextToolContent(
|
|
373
|
+
message.content,
|
|
374
|
+
HARD_MAX_TOOL_RESULT_CHARS
|
|
375
|
+
)
|
|
376
|
+
: undefined;
|
|
377
|
+
content ??= serializeStructuredValueBounded(
|
|
378
|
+
message.content,
|
|
379
|
+
HARD_MAX_TOOL_RESULT_CHARS
|
|
380
|
+
).content;
|
|
381
|
+
} else if (typeof message.content === 'string') {
|
|
382
|
+
content = message.content;
|
|
383
|
+
} else {
|
|
384
|
+
content = message.content.map((m) => {
|
|
385
|
+
if ('type' in m && m.type === 'thinking') {
|
|
386
|
+
hasAnthropicThinkingBlock = true;
|
|
335
387
|
return m;
|
|
336
|
-
}
|
|
388
|
+
}
|
|
389
|
+
if (isDataContentBlock(m)) {
|
|
390
|
+
return convertToProviderContentBlock(
|
|
391
|
+
m,
|
|
392
|
+
completionsApiContentBlockConverter
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return m;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
337
398
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
338
399
|
const completionParam: Record<string, any> = {
|
|
339
400
|
role,
|
|
@@ -351,7 +412,7 @@ export function _convertMessagesToOpenAIParams(
|
|
|
351
412
|
if (isAIMessage(message) && !!message.tool_calls?.length) {
|
|
352
413
|
messageHasToolCalls = true;
|
|
353
414
|
completionParam.tool_calls = message.tool_calls.map(
|
|
354
|
-
|
|
415
|
+
convertLangChainToolCallToBoundedOpenAI
|
|
355
416
|
);
|
|
356
417
|
completionParam.content = hasAnthropicThinkingBlock ? content : '';
|
|
357
418
|
if (
|
|
@@ -527,7 +588,11 @@ export function _convertMessagesToOpenAIResponsesParams(
|
|
|
527
588
|
model?: string,
|
|
528
589
|
zdrEnabled?: boolean
|
|
529
590
|
): ResponsesInputItem[] {
|
|
530
|
-
|
|
591
|
+
const projectedMessages = projectToolCallInputs(
|
|
592
|
+
messages,
|
|
593
|
+
MAX_PROVIDER_TOOL_CALL_INPUT_CHARS
|
|
594
|
+
);
|
|
595
|
+
return projectedMessages.flatMap(
|
|
531
596
|
(lcMsg): ResponsesInputItem | ResponsesInputItem[] => {
|
|
532
597
|
const additional_kwargs =
|
|
533
598
|
lcMsg.additional_kwargs as BaseMessageFields['additional_kwargs'] & {
|
|
@@ -552,44 +617,29 @@ export function _convertMessagesToOpenAIResponsesParams(
|
|
|
552
617
|
|
|
553
618
|
// Handle computer call output
|
|
554
619
|
if (additional_kwargs.type === 'computer_call_output') {
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
image_url: toolMessage.content,
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (Array.isArray(toolMessage.content)) {
|
|
564
|
-
const oaiScreenshot = toolMessage.content.find(
|
|
565
|
-
(i) => i.type === 'computer_screenshot'
|
|
566
|
-
) as { type: 'computer_screenshot'; image_url: string };
|
|
567
|
-
|
|
568
|
-
if (oaiScreenshot) return oaiScreenshot;
|
|
569
|
-
|
|
570
|
-
const lcImage = toolMessage.content.find(
|
|
571
|
-
(i) => i.type === 'image_url'
|
|
572
|
-
) as MessageContentImageUrl;
|
|
573
|
-
|
|
574
|
-
if (lcImage) {
|
|
575
|
-
return {
|
|
576
|
-
type: 'computer_screenshot' as const,
|
|
577
|
-
image_url:
|
|
578
|
-
typeof lcImage.image_url === 'string'
|
|
579
|
-
? lcImage.image_url
|
|
580
|
-
: lcImage.image_url.url,
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
620
|
+
const screenshot = getComputerCallOutputScreenshot(
|
|
621
|
+
toolMessage.content
|
|
622
|
+
);
|
|
623
|
+
if (screenshot == null) {
|
|
585
624
|
throw new Error('Invalid computer call output');
|
|
586
|
-
}
|
|
625
|
+
}
|
|
587
626
|
|
|
588
|
-
|
|
627
|
+
const output: OpenAIClient.Responses.ResponseComputerToolCallOutputScreenshot =
|
|
628
|
+
'image_url' in screenshot
|
|
629
|
+
? {
|
|
630
|
+
type: 'computer_screenshot',
|
|
631
|
+
image_url: screenshot.image_url,
|
|
632
|
+
}
|
|
633
|
+
: {
|
|
634
|
+
type: 'computer_screenshot',
|
|
635
|
+
file_id: screenshot.file_id,
|
|
636
|
+
};
|
|
637
|
+
const computerCallOutput: ResponsesInputItem = {
|
|
589
638
|
type: 'computer_call_output',
|
|
590
639
|
output,
|
|
591
640
|
call_id: toolMessage.tool_call_id,
|
|
592
641
|
};
|
|
642
|
+
return computerCallOutput;
|
|
593
643
|
}
|
|
594
644
|
|
|
595
645
|
return {
|
|
@@ -598,7 +648,10 @@ export function _convertMessagesToOpenAIResponsesParams(
|
|
|
598
648
|
id: toolMessage.id?.startsWith('fc_') ? toolMessage.id : undefined,
|
|
599
649
|
output:
|
|
600
650
|
typeof toolMessage.content !== 'string'
|
|
601
|
-
?
|
|
651
|
+
? serializeStructuredValueBounded(
|
|
652
|
+
toolMessage.content,
|
|
653
|
+
HARD_MAX_TOOL_RESULT_CHARS
|
|
654
|
+
).content
|
|
602
655
|
: toolMessage.content,
|
|
603
656
|
};
|
|
604
657
|
}
|
|
@@ -682,7 +735,10 @@ export function _convertMessagesToOpenAIResponsesParams(
|
|
|
682
735
|
(toolCall): ResponsesInputItem => ({
|
|
683
736
|
type: 'function_call',
|
|
684
737
|
name: toolCall.name,
|
|
685
|
-
arguments:
|
|
738
|
+
arguments: serializeToolCallInput(
|
|
739
|
+
toolCall.args,
|
|
740
|
+
MAX_PROVIDER_TOOL_CALL_INPUT_CHARS
|
|
741
|
+
),
|
|
686
742
|
call_id: toolCall.id!,
|
|
687
743
|
...(zdrEnabled ? { id: functionCallIds?.[toolCall.id!] } : {}),
|
|
688
744
|
})
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
_convertMessagesToOpenAIParams,
|
|
4
|
+
_convertMessagesToOpenAIResponsesParams,
|
|
5
|
+
} from './index';
|
|
6
|
+
import { calculateMaxToolCallInputChars } from '@/messages/prune';
|
|
7
|
+
import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
|
|
3
8
|
|
|
4
9
|
describe('_convertMessagesToOpenAIParams', () => {
|
|
5
10
|
it('includes reasoning_content for assistant messages in tool-call context when requested', () => {
|
|
@@ -156,4 +161,328 @@ describe('_convertMessagesToOpenAIParams', () => {
|
|
|
156
161
|
})
|
|
157
162
|
);
|
|
158
163
|
});
|
|
164
|
+
|
|
165
|
+
it('bounds structured function-tool output for Chat and Responses adapters', () => {
|
|
166
|
+
let toJSONCalls = 0;
|
|
167
|
+
const toolMessage = new ToolMessage({
|
|
168
|
+
content: [
|
|
169
|
+
{ type: 'text', text: 'rendered chart' },
|
|
170
|
+
{
|
|
171
|
+
type: 'image_url',
|
|
172
|
+
image_url: {
|
|
173
|
+
url: `data:image/png;base64,${'A'.repeat(
|
|
174
|
+
HARD_MAX_TOOL_RESULT_CHARS + 1_000
|
|
175
|
+
)}`,
|
|
176
|
+
},
|
|
177
|
+
toJSON() {
|
|
178
|
+
toJSONCalls++;
|
|
179
|
+
return { expanded: 'B'.repeat(HARD_MAX_TOOL_RESULT_CHARS * 2) };
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
tool_call_id: 'call_structured',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const chat = _convertMessagesToOpenAIParams([toolMessage]);
|
|
187
|
+
const responses = _convertMessagesToOpenAIResponsesParams([toolMessage]);
|
|
188
|
+
const chatContent = chat[0].content;
|
|
189
|
+
|
|
190
|
+
expect(typeof chatContent).toBe('string');
|
|
191
|
+
expect((chatContent as string).length).toBeLessThanOrEqual(
|
|
192
|
+
HARD_MAX_TOOL_RESULT_CHARS
|
|
193
|
+
);
|
|
194
|
+
expect(responses[0]).toMatchObject({
|
|
195
|
+
type: 'function_call_output',
|
|
196
|
+
call_id: 'call_structured',
|
|
197
|
+
output: chatContent,
|
|
198
|
+
});
|
|
199
|
+
expect(toJSONCalls).toBe(0);
|
|
200
|
+
expect(Array.isArray(toolMessage.content)).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('preserves canonical OpenRouter cache blocks only when requested', () => {
|
|
204
|
+
const toolMessage = new ToolMessage({
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: 'text',
|
|
208
|
+
text: 'result body',
|
|
209
|
+
cache_control: { type: 'ephemeral', ttl: '1h' },
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
tool_call_id: 'call_cached',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const defaultContent = _convertMessagesToOpenAIParams([toolMessage])[0]
|
|
216
|
+
.content;
|
|
217
|
+
const openRouterContent = _convertMessagesToOpenAIParams(
|
|
218
|
+
[toolMessage],
|
|
219
|
+
'anthropic/claude-sonnet-4',
|
|
220
|
+
{ preserveToolCacheControl: true }
|
|
221
|
+
)[0].content;
|
|
222
|
+
|
|
223
|
+
expect(typeof defaultContent).toBe('string');
|
|
224
|
+
expect(openRouterContent).toEqual(toolMessage.content);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('bounds direct structured tool-call args without invoking accessors or toJSON', () => {
|
|
228
|
+
const maxInputChars = calculateMaxToolCallInputChars();
|
|
229
|
+
let getterCalls = 0;
|
|
230
|
+
let toJSONCalls = 0;
|
|
231
|
+
const args: Record<string, unknown> = {
|
|
232
|
+
query: 'safe',
|
|
233
|
+
payload: 'x'.repeat(maxInputChars + 1_000),
|
|
234
|
+
};
|
|
235
|
+
Object.defineProperty(args, 'expanded', {
|
|
236
|
+
enumerable: true,
|
|
237
|
+
get() {
|
|
238
|
+
getterCalls++;
|
|
239
|
+
return 'y'.repeat(maxInputChars * 2);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
args.self = args;
|
|
243
|
+
args.toJSON = (): Record<string, string> => {
|
|
244
|
+
toJSONCalls++;
|
|
245
|
+
return { payload: 'z'.repeat(maxInputChars * 2) };
|
|
246
|
+
};
|
|
247
|
+
const message = new AIMessage({
|
|
248
|
+
content: '',
|
|
249
|
+
tool_calls: [
|
|
250
|
+
{
|
|
251
|
+
id: 'call_adversarial',
|
|
252
|
+
name: 'lookup',
|
|
253
|
+
args,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const chat = _convertMessagesToOpenAIParams([message]);
|
|
259
|
+
const responses = _convertMessagesToOpenAIResponsesParams([message]);
|
|
260
|
+
const chatArguments = (
|
|
261
|
+
chat[0] as {
|
|
262
|
+
tool_calls: Array<{ function: { arguments: string } }>;
|
|
263
|
+
}
|
|
264
|
+
).tool_calls[0].function.arguments;
|
|
265
|
+
const responseArguments = (
|
|
266
|
+
responses.find((item) => item.type === 'function_call') as {
|
|
267
|
+
arguments: string;
|
|
268
|
+
}
|
|
269
|
+
).arguments;
|
|
270
|
+
|
|
271
|
+
expect(chatArguments).toBe(responseArguments);
|
|
272
|
+
expect(chatArguments.length).toBeLessThanOrEqual(maxInputChars);
|
|
273
|
+
expect(() => JSON.parse(chatArguments)).not.toThrow();
|
|
274
|
+
expect(getterCalls).toBe(0);
|
|
275
|
+
expect(toJSONCalls).toBe(0);
|
|
276
|
+
expect(args.self).toBe(args);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('bounds and synchronizes raw Chat and Responses function-call arguments', () => {
|
|
280
|
+
const maxInputChars = calculateMaxToolCallInputChars();
|
|
281
|
+
const rawArguments = `{"payload":"${'x'.repeat(maxInputChars + 1_000)}"}`;
|
|
282
|
+
const rawOnlyMessage = new AIMessage({
|
|
283
|
+
content: '',
|
|
284
|
+
additional_kwargs: {
|
|
285
|
+
tool_calls: [
|
|
286
|
+
{
|
|
287
|
+
id: 'call_raw',
|
|
288
|
+
type: 'function',
|
|
289
|
+
function: { name: 'lookup', arguments: rawArguments },
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
response_metadata: {
|
|
294
|
+
output: [
|
|
295
|
+
{
|
|
296
|
+
type: 'function_call',
|
|
297
|
+
id: 'fc_raw',
|
|
298
|
+
call_id: 'call_raw',
|
|
299
|
+
name: 'lookup',
|
|
300
|
+
arguments: rawArguments,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const chat = _convertMessagesToOpenAIParams([rawOnlyMessage]);
|
|
307
|
+
const responses = _convertMessagesToOpenAIResponsesParams([rawOnlyMessage]);
|
|
308
|
+
const chatArguments = (
|
|
309
|
+
chat[0] as {
|
|
310
|
+
tool_calls: Array<{ function: { arguments: string } }>;
|
|
311
|
+
}
|
|
312
|
+
).tool_calls[0].function.arguments;
|
|
313
|
+
const responseArguments = (
|
|
314
|
+
responses.find((item) => item.type === 'function_call') as {
|
|
315
|
+
arguments: string;
|
|
316
|
+
}
|
|
317
|
+
).arguments;
|
|
318
|
+
|
|
319
|
+
expect(chatArguments.length).toBeLessThanOrEqual(maxInputChars);
|
|
320
|
+
expect(responseArguments.length).toBeLessThanOrEqual(maxInputChars);
|
|
321
|
+
expect(() => JSON.parse(chatArguments)).not.toThrow();
|
|
322
|
+
expect(() => JSON.parse(responseArguments)).not.toThrow();
|
|
323
|
+
expect(
|
|
324
|
+
(
|
|
325
|
+
rawOnlyMessage.additional_kwargs.tool_calls as Array<{
|
|
326
|
+
function: { arguments: string };
|
|
327
|
+
}>
|
|
328
|
+
)[0].function.arguments
|
|
329
|
+
).toBe(rawArguments);
|
|
330
|
+
expect(
|
|
331
|
+
(
|
|
332
|
+
rawOnlyMessage.response_metadata.output as Array<{
|
|
333
|
+
arguments: string;
|
|
334
|
+
}>
|
|
335
|
+
)[0].arguments
|
|
336
|
+
).toBe(rawArguments);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('normalizes legacy function calls before wire serialization', () => {
|
|
340
|
+
let toJSONCalls = 0;
|
|
341
|
+
const functionCall = {
|
|
342
|
+
name: 'legacy_lookup',
|
|
343
|
+
arguments: '{}',
|
|
344
|
+
};
|
|
345
|
+
const message = new AIMessage({
|
|
346
|
+
content: '',
|
|
347
|
+
additional_kwargs: {
|
|
348
|
+
function_call: functionCall,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
Object.defineProperty(functionCall, 'toJSON', {
|
|
352
|
+
enumerable: true,
|
|
353
|
+
value: () => {
|
|
354
|
+
toJSONCalls++;
|
|
355
|
+
return {
|
|
356
|
+
name: 'legacy_lookup',
|
|
357
|
+
arguments: 'x'.repeat(500_000),
|
|
358
|
+
};
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const [chat] = _convertMessagesToOpenAIParams([message]);
|
|
363
|
+
const serialized = JSON.stringify(chat);
|
|
364
|
+
|
|
365
|
+
expect(chat).toMatchObject({
|
|
366
|
+
role: 'assistant',
|
|
367
|
+
function_call: {
|
|
368
|
+
name: 'legacy_lookup',
|
|
369
|
+
arguments: '{}',
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
expect(serialized.length).toBeLessThanOrEqual(300);
|
|
373
|
+
expect(toJSONCalls).toBe(0);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('makes Responses raw reuse match the projected LangChain tool-call args', () => {
|
|
377
|
+
const message = new AIMessage({
|
|
378
|
+
content: '',
|
|
379
|
+
tool_calls: [
|
|
380
|
+
{
|
|
381
|
+
id: 'call_synced',
|
|
382
|
+
name: 'lookup',
|
|
383
|
+
args: { query: 'canonical', payload: 'x'.repeat(500_000) },
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
response_metadata: {
|
|
387
|
+
output: [
|
|
388
|
+
{
|
|
389
|
+
type: 'function_call',
|
|
390
|
+
id: 'fc_synced',
|
|
391
|
+
call_id: 'call_synced',
|
|
392
|
+
name: 'lookup',
|
|
393
|
+
arguments: '{"query":"stale"}',
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const chat = _convertMessagesToOpenAIParams([message]);
|
|
400
|
+
const responses = _convertMessagesToOpenAIResponsesParams([message]);
|
|
401
|
+
const chatArguments = (
|
|
402
|
+
chat[0] as {
|
|
403
|
+
tool_calls: Array<{ function: { arguments: string } }>;
|
|
404
|
+
}
|
|
405
|
+
).tool_calls[0].function.arguments;
|
|
406
|
+
const responseArguments = (
|
|
407
|
+
responses.find((item) => item.type === 'function_call') as {
|
|
408
|
+
arguments: string;
|
|
409
|
+
}
|
|
410
|
+
).arguments;
|
|
411
|
+
|
|
412
|
+
expect(responseArguments).toBe(chatArguments);
|
|
413
|
+
expect(() => JSON.parse(responseArguments)).not.toThrow();
|
|
414
|
+
expect(
|
|
415
|
+
(
|
|
416
|
+
message.response_metadata.output as Array<{
|
|
417
|
+
arguments: string;
|
|
418
|
+
}>
|
|
419
|
+
)[0].arguments
|
|
420
|
+
).toBe('{"query":"stale"}');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('normalizes native Responses computer-call output screenshots', () => {
|
|
424
|
+
const screenshot = `data:image/png;base64,${'A'.repeat(2_000)}`;
|
|
425
|
+
const messages = [
|
|
426
|
+
new ToolMessage({
|
|
427
|
+
content: screenshot,
|
|
428
|
+
tool_call_id: 'call_computer_string',
|
|
429
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
430
|
+
}),
|
|
431
|
+
new ToolMessage({
|
|
432
|
+
content: [{ type: 'computer_screenshot', image_url: screenshot }],
|
|
433
|
+
tool_call_id: 'call_computer_structured',
|
|
434
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
435
|
+
}),
|
|
436
|
+
new ToolMessage({
|
|
437
|
+
content: [{ type: 'input_image', file_id: 'file_screenshot123' }],
|
|
438
|
+
tool_call_id: 'call_computer_file',
|
|
439
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
440
|
+
}),
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
expect(_convertMessagesToOpenAIResponsesParams(messages)).toEqual([
|
|
444
|
+
{
|
|
445
|
+
type: 'computer_call_output',
|
|
446
|
+
call_id: 'call_computer_string',
|
|
447
|
+
output: { type: 'computer_screenshot', image_url: screenshot },
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
type: 'computer_call_output',
|
|
451
|
+
call_id: 'call_computer_structured',
|
|
452
|
+
output: { type: 'computer_screenshot', image_url: screenshot },
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
type: 'computer_call_output',
|
|
456
|
+
call_id: 'call_computer_file',
|
|
457
|
+
output: {
|
|
458
|
+
type: 'computer_screenshot',
|
|
459
|
+
file_id: 'file_screenshot123',
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
]);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('rejects malformed native Responses computer-call output', () => {
|
|
466
|
+
const message = new ToolMessage({
|
|
467
|
+
content: 'not a screenshot URL',
|
|
468
|
+
tool_call_id: 'call_computer_invalid',
|
|
469
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
expect(() => _convertMessagesToOpenAIResponsesParams([message])).toThrow(
|
|
473
|
+
'Invalid computer call output'
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('rejects a truncated data URI marked as a computer screenshot', () => {
|
|
478
|
+
const message = new ToolMessage({
|
|
479
|
+
content: 'data:image/png;base64,AAAA\n\n… [truncated: 1000 chars] …',
|
|
480
|
+
tool_call_id: 'call_computer_truncated',
|
|
481
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
expect(() => _convertMessagesToOpenAIResponsesParams([message])).toThrow(
|
|
485
|
+
'Invalid computer call output'
|
|
486
|
+
);
|
|
487
|
+
});
|
|
159
488
|
});
|
|
@@ -32,6 +32,7 @@ export interface ChatOpenRouterCallOptions
|
|
|
32
32
|
include_reasoning?: boolean;
|
|
33
33
|
reasoning?: OpenRouterReasoning;
|
|
34
34
|
modelKwargs?: OpenAIChatInput['modelKwargs'];
|
|
35
|
+
useResponsesApi?: boolean;
|
|
35
36
|
promptCache?: boolean;
|
|
36
37
|
/**
|
|
37
38
|
* Prompt-cache breakpoint TTL. Defaults to `'1h'` (extended cache) when
|
|
@@ -52,6 +53,7 @@ export type OpenRouterInvocationParams = Omit<
|
|
|
52
53
|
'messages'
|
|
53
54
|
> & {
|
|
54
55
|
reasoning?: OpenRouterReasoning;
|
|
56
|
+
cache_control?: { type: 'ephemeral'; ttl?: '1h' };
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
type InvocationParamsExtra = {
|
|
@@ -150,6 +152,9 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|
|
150
152
|
modelKwargs: parentModelKwargs,
|
|
151
153
|
includeReasoningDetails: true,
|
|
152
154
|
convertReasoningDetailsToContent: true,
|
|
155
|
+
preserveToolCacheControl: true,
|
|
156
|
+
responsesPromptCache: _fields.promptCache,
|
|
157
|
+
responsesPromptCacheTtl: _fields.promptCacheTtl,
|
|
153
158
|
});
|
|
154
159
|
|
|
155
160
|
// Merge reasoning config: modelKwargs.reasoning < constructor reasoning
|
|
@@ -171,11 +176,16 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|
|
171
176
|
type MutableParams = Omit<
|
|
172
177
|
OpenAIClient.Chat.ChatCompletionCreateParams,
|
|
173
178
|
'messages'
|
|
174
|
-
> & {
|
|
179
|
+
> & {
|
|
180
|
+
reasoning_effort?: string;
|
|
181
|
+
reasoning?: OpenRouterReasoning;
|
|
182
|
+
cache_control?: { type: 'ephemeral'; ttl?: '1h' };
|
|
183
|
+
};
|
|
175
184
|
|
|
176
185
|
const optionsWithDefaults = this._combineCallOptions(options);
|
|
186
|
+
const useResponsesApi = this._useResponsesApi(options);
|
|
177
187
|
const params = (
|
|
178
|
-
|
|
188
|
+
useResponsesApi
|
|
179
189
|
? this.responses.invocationParams(optionsWithDefaults)
|
|
180
190
|
: this.completions.invocationParams(optionsWithDefaults, extra)
|
|
181
191
|
) as MutableParams;
|