@librechat/agents 3.1.78-dev.0 → 3.1.79
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/graphs/Graph.cjs +7 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +44 -55
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +33 -21
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -4
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/messages/anthropicToolCache.cjs +48 -15
- package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +97 -14
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +14 -16
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +30 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +7 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +43 -54
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -21
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -4
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/messages/anthropicToolCache.mjs +48 -15
- package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +97 -14
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +14 -16
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +30 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/llm/anthropic/index.d.ts +1 -9
- package/dist/types/messages/anthropicToolCache.d.ts +5 -5
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +29 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +9 -0
- package/src/llm/anthropic/index.ts +55 -64
- package/src/llm/anthropic/llm.spec.ts +585 -0
- package/src/llm/anthropic/utils/message_inputs.ts +36 -21
- package/src/llm/anthropic/utils/message_outputs.ts +0 -4
- package/src/llm/anthropic/utils/server-tool-inputs.test.ts +95 -13
- package/src/messages/__tests__/anthropicToolCache.test.ts +46 -0
- package/src/messages/anthropicToolCache.ts +70 -25
- package/src/messages/format.ts +117 -18
- package/src/messages/formatAgentMessages.test.ts +202 -1
- package/src/scripts/subagent-configurable-inheritance.ts +252 -0
- package/src/specs/summarization.test.ts +3 -3
- package/src/tools/__tests__/LocalExecutionRoots.test.ts +8 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +148 -0
- package/src/tools/local/LocalExecutionEngine.ts +55 -54
- package/src/tools/subagent/SubagentExecutor.ts +60 -0
- package/src/types/diff.d.ts +15 -0
|
@@ -47,6 +47,8 @@ type GoogleFunctionCallBlock = MessageContentComplex & {
|
|
|
47
47
|
};
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
const ANTHROPIC_EMPTY_TEXT_PLACEHOLDER = '_';
|
|
51
|
+
|
|
50
52
|
function _formatImage(imageUrl: string) {
|
|
51
53
|
const parsed = parseBase64DataUrl({ dataUrl: imageUrl });
|
|
52
54
|
if (parsed) {
|
|
@@ -126,13 +128,15 @@ function _ensureMessageContents(
|
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
} else {
|
|
131
|
+
const toolMessageContent = (
|
|
132
|
+
message as { content?: BaseMessage['content'] | null }
|
|
133
|
+
).content;
|
|
129
134
|
updatedMsgs.push(
|
|
130
135
|
new HumanMessage({
|
|
131
136
|
content: [
|
|
132
137
|
{
|
|
133
138
|
type: 'tool_result',
|
|
134
|
-
|
|
135
|
-
...(message.content != null
|
|
139
|
+
...(toolMessageContent != null
|
|
136
140
|
? { content: _formatContent(message) }
|
|
137
141
|
: {}),
|
|
138
142
|
tool_use_id: (message as ToolMessage).tool_call_id,
|
|
@@ -486,7 +490,7 @@ function _formatContent(message: BaseMessage) {
|
|
|
486
490
|
return {
|
|
487
491
|
type: 'image' as const, // Explicitly setting the type as "image"
|
|
488
492
|
source,
|
|
489
|
-
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
493
|
+
...(cacheControl != null ? { cache_control: cacheControl } : {}),
|
|
490
494
|
};
|
|
491
495
|
} else if (isAnthropicImageBlockParam(contentPart)) {
|
|
492
496
|
return contentPart;
|
|
@@ -494,7 +498,7 @@ function _formatContent(message: BaseMessage) {
|
|
|
494
498
|
// PDF
|
|
495
499
|
return {
|
|
496
500
|
...contentPart,
|
|
497
|
-
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
501
|
+
...(cacheControl != null ? { cache_control: cacheControl } : {}),
|
|
498
502
|
};
|
|
499
503
|
} else if (contentPart.type === 'thinking') {
|
|
500
504
|
const thinkingPart = contentPart as AnthropicThinkingBlockParam;
|
|
@@ -502,7 +506,7 @@ function _formatContent(message: BaseMessage) {
|
|
|
502
506
|
type: 'thinking' as const, // Explicitly setting the type as "thinking"
|
|
503
507
|
thinking: thinkingPart.thinking,
|
|
504
508
|
signature: thinkingPart.signature,
|
|
505
|
-
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
509
|
+
...(cacheControl != null ? { cache_control: cacheControl } : {}),
|
|
506
510
|
};
|
|
507
511
|
return block;
|
|
508
512
|
} else if (contentPart.type === 'redacted_thinking') {
|
|
@@ -510,7 +514,7 @@ function _formatContent(message: BaseMessage) {
|
|
|
510
514
|
const block: AnthropicRedactedThinkingBlockParam = {
|
|
511
515
|
type: 'redacted_thinking' as const, // Explicitly setting the type as "redacted_thinking"
|
|
512
516
|
data: redactedPart.data,
|
|
513
|
-
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
517
|
+
...(cacheControl != null ? { cache_control: cacheControl } : {}),
|
|
514
518
|
};
|
|
515
519
|
return block;
|
|
516
520
|
} else if (contentPart.type === 'search_result') {
|
|
@@ -519,10 +523,11 @@ function _formatContent(message: BaseMessage) {
|
|
|
519
523
|
type: 'search_result' as const,
|
|
520
524
|
title: searchResultPart.title,
|
|
521
525
|
source: searchResultPart.source,
|
|
522
|
-
...('cache_control' in contentPart &&
|
|
526
|
+
...('cache_control' in contentPart &&
|
|
527
|
+
contentPart.cache_control != null
|
|
523
528
|
? { cache_control: contentPart.cache_control }
|
|
524
529
|
: {}),
|
|
525
|
-
...('citations' in contentPart && contentPart.citations
|
|
530
|
+
...('citations' in contentPart && contentPart.citations != null
|
|
526
531
|
? { citations: contentPart.citations }
|
|
527
532
|
: {}),
|
|
528
533
|
content: searchResultPart.content,
|
|
@@ -533,23 +538,23 @@ function _formatContent(message: BaseMessage) {
|
|
|
533
538
|
const block: AnthropicCompactionBlockParam = {
|
|
534
539
|
type: 'compaction' as const,
|
|
535
540
|
content: compactionPart.content,
|
|
536
|
-
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
541
|
+
...(cacheControl != null ? { cache_control: cacheControl } : {}),
|
|
537
542
|
};
|
|
538
543
|
return block;
|
|
539
544
|
} else if (
|
|
540
|
-
textTypes.
|
|
545
|
+
textTypes.some((t) => t === contentPart.type) &&
|
|
541
546
|
'text' in contentPart
|
|
542
547
|
) {
|
|
543
548
|
// Assuming contentPart is of type MessageContentText here
|
|
544
549
|
return {
|
|
545
550
|
type: 'text' as const, // Explicitly setting the type as "text"
|
|
546
551
|
text: contentPart.text,
|
|
547
|
-
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
548
|
-
...('citations' in contentPart && contentPart.citations
|
|
552
|
+
...(cacheControl != null ? { cache_control: cacheControl } : {}),
|
|
553
|
+
...('citations' in contentPart && contentPart.citations != null
|
|
549
554
|
? { citations: contentPart.citations }
|
|
550
555
|
: {}),
|
|
551
556
|
};
|
|
552
|
-
} else if (toolTypes.
|
|
557
|
+
} else if (toolTypes.some((t) => t === contentPart.type)) {
|
|
553
558
|
const contentPartCopy = { ...contentPart };
|
|
554
559
|
if ('index' in contentPartCopy) {
|
|
555
560
|
// Anthropic does not support passing the index field here, so we remove it.
|
|
@@ -593,12 +598,12 @@ function _formatContent(message: BaseMessage) {
|
|
|
593
598
|
// TODO: Fix when SDK types are fixed
|
|
594
599
|
return {
|
|
595
600
|
...contentPartCopy,
|
|
596
|
-
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
601
|
+
...(cacheControl != null ? { cache_control: cacheControl } : {}),
|
|
597
602
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
598
603
|
} as any;
|
|
599
604
|
} else if (
|
|
600
605
|
'functionCall' in contentPart &&
|
|
601
|
-
contentPart.functionCall &&
|
|
606
|
+
contentPart.functionCall != null &&
|
|
602
607
|
typeof contentPart.functionCall === 'object' &&
|
|
603
608
|
isAIMessage(message)
|
|
604
609
|
) {
|
|
@@ -626,7 +631,7 @@ function _formatContent(message: BaseMessage) {
|
|
|
626
631
|
throw new Error('Unsupported message content format');
|
|
627
632
|
}
|
|
628
633
|
});
|
|
629
|
-
|
|
634
|
+
const filteredContentBlocks = contentBlocks.filter(
|
|
630
635
|
(block) =>
|
|
631
636
|
block !== null &&
|
|
632
637
|
!(
|
|
@@ -636,6 +641,9 @@ function _formatContent(message: BaseMessage) {
|
|
|
636
641
|
block.text.trim() === ''
|
|
637
642
|
)
|
|
638
643
|
);
|
|
644
|
+
return filteredContentBlocks.length > 0
|
|
645
|
+
? filteredContentBlocks
|
|
646
|
+
: [{ type: 'text' as const, text: ANTHROPIC_EMPTY_TEXT_PLACEHOLDER }];
|
|
639
647
|
}
|
|
640
648
|
}
|
|
641
649
|
|
|
@@ -670,9 +678,11 @@ export function _convertMessagesToAnthropicPayload(
|
|
|
670
678
|
} else {
|
|
671
679
|
throw new Error(`Message type "${message._getType()}" is not supported.`);
|
|
672
680
|
}
|
|
673
|
-
|
|
681
|
+
const isAI = isAIMessage(message);
|
|
682
|
+
const toolCalls = isAI ? (message.tool_calls ?? []) : [];
|
|
683
|
+
if (isAI && toolCalls.length > 0) {
|
|
674
684
|
if (typeof message.content === 'string') {
|
|
675
|
-
const clientToolCalls =
|
|
685
|
+
const clientToolCalls = toolCalls.filter(
|
|
676
686
|
(tc) =>
|
|
677
687
|
!(
|
|
678
688
|
tc.id?.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX) ?? false
|
|
@@ -684,7 +694,12 @@ export function _convertMessagesToAnthropicPayload(
|
|
|
684
694
|
content:
|
|
685
695
|
clientToolCalls.length > 0
|
|
686
696
|
? clientToolCalls.map(_convertLangChainToolCallToAnthropic)
|
|
687
|
-
: [
|
|
697
|
+
: [
|
|
698
|
+
{
|
|
699
|
+
type: 'text' as const,
|
|
700
|
+
text: ANTHROPIC_EMPTY_TEXT_PLACEHOLDER,
|
|
701
|
+
},
|
|
702
|
+
],
|
|
688
703
|
};
|
|
689
704
|
} else {
|
|
690
705
|
return {
|
|
@@ -697,7 +712,7 @@ export function _convertMessagesToAnthropicPayload(
|
|
|
697
712
|
}
|
|
698
713
|
} else {
|
|
699
714
|
const { content } = message;
|
|
700
|
-
const hasMismatchedToolCalls = !
|
|
715
|
+
const hasMismatchedToolCalls = !toolCalls.every(
|
|
701
716
|
(toolCall) =>
|
|
702
717
|
!!content.find(
|
|
703
718
|
(contentPart) =>
|
|
@@ -731,7 +746,7 @@ export function _convertMessagesToAnthropicPayload(
|
|
|
731
746
|
}
|
|
732
747
|
|
|
733
748
|
function mergeMessages(messages: AnthropicMessageCreateParams['messages']) {
|
|
734
|
-
if (
|
|
749
|
+
if (messages.length <= 1) {
|
|
735
750
|
return messages;
|
|
736
751
|
}
|
|
737
752
|
|
|
@@ -98,10 +98,6 @@ export function _makeMessageChunkFromAnthropicEvent(
|
|
|
98
98
|
input_tokens: 0,
|
|
99
99
|
output_tokens: data.usage.output_tokens,
|
|
100
100
|
total_tokens: data.usage.output_tokens,
|
|
101
|
-
input_token_details: {
|
|
102
|
-
cache_creation: data.usage.cache_creation_input_tokens ?? undefined,
|
|
103
|
-
cache_read: data.usage.cache_read_input_tokens ?? undefined,
|
|
104
|
-
},
|
|
105
101
|
};
|
|
106
102
|
return {
|
|
107
103
|
chunk: new AIMessageChunk({
|
|
@@ -3,6 +3,14 @@ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
|
3
3
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
4
4
|
import { _convertMessagesToAnthropicPayload } from './message_inputs';
|
|
5
5
|
|
|
6
|
+
type AnthropicTestBlock = {
|
|
7
|
+
id?: unknown;
|
|
8
|
+
type?: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const isServerToolId = (id: unknown): id is string =>
|
|
12
|
+
typeof id === 'string' && id.startsWith('srvtoolu_');
|
|
13
|
+
|
|
6
14
|
describe('_convertMessagesToAnthropicPayload — server tool use (web search) multi-turn', () => {
|
|
7
15
|
it('corrects tool_use blocks with srvtoolu_ IDs to server_tool_use', () => {
|
|
8
16
|
const messageHistory: BaseMessage[] = [
|
|
@@ -78,9 +86,10 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
|
|
|
78
86
|
const searchResultBlocks = assistantContent.filter(
|
|
79
87
|
(b: any) => b.type === 'web_search_tool_result'
|
|
80
88
|
);
|
|
81
|
-
const regularToolUseBlocks = assistantContent.filter(
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
const regularToolUseBlocks = assistantContent.filter((block: unknown) => {
|
|
90
|
+
const b = block as AnthropicTestBlock;
|
|
91
|
+
return b.type === 'tool_use' && isServerToolId(b.id);
|
|
92
|
+
});
|
|
84
93
|
|
|
85
94
|
expect(serverToolBlocks).toHaveLength(2);
|
|
86
95
|
expect(searchResultBlocks).toHaveLength(2);
|
|
@@ -190,9 +199,10 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
|
|
|
190
199
|
expect(toolUseBlocks).toHaveLength(1);
|
|
191
200
|
expect(toolUseBlocks[0].id).toBe('toolu_regular');
|
|
192
201
|
|
|
193
|
-
const serverToolBlocks = assistantContent.filter((
|
|
194
|
-
b
|
|
195
|
-
|
|
202
|
+
const serverToolBlocks = assistantContent.filter((block: unknown) => {
|
|
203
|
+
const b = block as AnthropicTestBlock;
|
|
204
|
+
return isServerToolId(b.id);
|
|
205
|
+
});
|
|
196
206
|
expect(serverToolBlocks).toHaveLength(0);
|
|
197
207
|
});
|
|
198
208
|
|
|
@@ -217,7 +227,80 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
|
|
|
217
227
|
const assistantContent = messages[1].content as any[];
|
|
218
228
|
expect(assistantContent).toHaveLength(1);
|
|
219
229
|
expect(assistantContent[0].type).toBe('text');
|
|
220
|
-
expect(assistantContent[0].text).toBe('
|
|
230
|
+
expect(assistantContent[0].text).toBe('_');
|
|
231
|
+
expect(assistantContent[0].text.trim()).toBe('_');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('uses non-whitespace fallback content after filtering empty array text', () => {
|
|
235
|
+
const messageHistory: BaseMessage[] = [
|
|
236
|
+
new HumanMessage({
|
|
237
|
+
content: [
|
|
238
|
+
{ type: 'text', text: ' ' },
|
|
239
|
+
{ type: 'text', text: '\n' },
|
|
240
|
+
],
|
|
241
|
+
}),
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
|
|
245
|
+
expect(messages[0].content).toEqual([{ type: 'text', text: '_' }]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('uses non-whitespace fallback content for empty server tool result artifacts', () => {
|
|
249
|
+
const messageHistory: BaseMessage[] = [
|
|
250
|
+
new HumanMessage('search for X'),
|
|
251
|
+
new AIMessage({
|
|
252
|
+
content: '',
|
|
253
|
+
tool_calls: [
|
|
254
|
+
{
|
|
255
|
+
id: 'srvtoolu_1',
|
|
256
|
+
name: 'web_search',
|
|
257
|
+
args: { query: 'X' },
|
|
258
|
+
type: 'tool_call',
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
}),
|
|
262
|
+
new ToolMessage({
|
|
263
|
+
content: '',
|
|
264
|
+
tool_call_id: 'srvtoolu_1',
|
|
265
|
+
}),
|
|
266
|
+
new HumanMessage('follow up'),
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
|
|
270
|
+
|
|
271
|
+
expect(messages[1].content).toEqual([{ type: 'text', text: '_' }]);
|
|
272
|
+
expect(messages[2].content).toEqual([{ type: 'text', text: '_' }]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('does not throw when a non-string ToolMessage has undefined content', () => {
|
|
276
|
+
const undefinedContentToolMessage = {
|
|
277
|
+
_getType: (): 'tool' => 'tool',
|
|
278
|
+
content: undefined,
|
|
279
|
+
tool_call_id: 'toolu_calc',
|
|
280
|
+
} as unknown as ToolMessage;
|
|
281
|
+
const messageHistory: BaseMessage[] = [
|
|
282
|
+
new HumanMessage('call the calculator'),
|
|
283
|
+
new AIMessage({
|
|
284
|
+
content: '',
|
|
285
|
+
tool_calls: [
|
|
286
|
+
{
|
|
287
|
+
id: 'toolu_calc',
|
|
288
|
+
name: 'calculator',
|
|
289
|
+
args: { expr: '2+2' },
|
|
290
|
+
type: 'tool_call',
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
}),
|
|
294
|
+
undefinedContentToolMessage,
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
|
|
298
|
+
expect(messages[2].content).toEqual([
|
|
299
|
+
{
|
|
300
|
+
type: 'tool_result',
|
|
301
|
+
tool_use_id: 'toolu_calc',
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
221
304
|
});
|
|
222
305
|
|
|
223
306
|
it('preserves regular tool_use blocks alongside corrected server tool blocks', () => {
|
|
@@ -283,9 +366,10 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
|
|
|
283
366
|
const webSearchResult = assistantContent.filter(
|
|
284
367
|
(b: any) => b.type === 'web_search_tool_result'
|
|
285
368
|
);
|
|
286
|
-
const regularToolUse = assistantContent.filter(
|
|
287
|
-
|
|
288
|
-
|
|
369
|
+
const regularToolUse = assistantContent.filter((block: unknown) => {
|
|
370
|
+
const b = block as AnthropicTestBlock;
|
|
371
|
+
return b.type === 'tool_use' && !isServerToolId(b.id);
|
|
372
|
+
});
|
|
289
373
|
|
|
290
374
|
expect(serverToolUse).toHaveLength(1);
|
|
291
375
|
expect(serverToolUse[0].id).toBe('srvtoolu_1');
|
|
@@ -416,9 +500,7 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
|
|
|
416
500
|
);
|
|
417
501
|
expect(whitespaceTextBlocks).toHaveLength(0);
|
|
418
502
|
|
|
419
|
-
const textBlocks = assistantContent.filter(
|
|
420
|
-
(b: any) => b.type === 'text'
|
|
421
|
-
);
|
|
503
|
+
const textBlocks = assistantContent.filter((b: any) => b.type === 'text');
|
|
422
504
|
expect(textBlocks).toHaveLength(1);
|
|
423
505
|
expect(textBlocks[0].text).toBe('Here are the results.');
|
|
424
506
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
makeIsDeferred,
|
|
6
6
|
partitionAndMarkAnthropicToolCache,
|
|
7
7
|
} from '../anthropicToolCache';
|
|
8
|
+
import { CustomAnthropic } from '@/llm/anthropic';
|
|
8
9
|
|
|
9
10
|
function fakeTool(name: string): unknown {
|
|
10
11
|
return tool(async () => 'ok', {
|
|
@@ -86,6 +87,51 @@ describe('partitionAndMarkAnthropicToolCache', () => {
|
|
|
86
87
|
expect(out[0].extras?.cache_control).toEqual({ type: 'ephemeral' });
|
|
87
88
|
});
|
|
88
89
|
|
|
90
|
+
it('stamps Anthropic built-in tools with direct cache_control', () => {
|
|
91
|
+
const webSearch = {
|
|
92
|
+
type: 'web_search_20250305',
|
|
93
|
+
name: 'web_search',
|
|
94
|
+
max_uses: 3,
|
|
95
|
+
};
|
|
96
|
+
const out = partitionAndMarkAnthropicToolCache(
|
|
97
|
+
[webSearch] as never,
|
|
98
|
+
() => false
|
|
99
|
+
) as Array<{
|
|
100
|
+
cache_control?: { type: string };
|
|
101
|
+
extras?: { cache_control?: { type: string } };
|
|
102
|
+
}>;
|
|
103
|
+
|
|
104
|
+
expect(out[0]).not.toBe(webSearch);
|
|
105
|
+
expect(out[0].cache_control).toEqual({ type: 'ephemeral' });
|
|
106
|
+
expect(out[0].extras).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('does not serialize extras on Anthropic built-in tools', () => {
|
|
110
|
+
const model = new CustomAnthropic({
|
|
111
|
+
model: 'claude-haiku-4-5',
|
|
112
|
+
apiKey: 'testing',
|
|
113
|
+
});
|
|
114
|
+
const webSearch = {
|
|
115
|
+
type: 'web_search_20250305',
|
|
116
|
+
name: 'web_search',
|
|
117
|
+
max_uses: 3,
|
|
118
|
+
};
|
|
119
|
+
const tools = partitionAndMarkAnthropicToolCache(
|
|
120
|
+
[webSearch] as never,
|
|
121
|
+
() => false
|
|
122
|
+
);
|
|
123
|
+
const formattedTools = model.formatStructuredToolToAnthropic(tools);
|
|
124
|
+
const formatted = formattedTools?.[0];
|
|
125
|
+
|
|
126
|
+
expect(formatted).toEqual({
|
|
127
|
+
type: 'web_search_20250305',
|
|
128
|
+
name: 'web_search',
|
|
129
|
+
max_uses: 3,
|
|
130
|
+
cache_control: { type: 'ephemeral' },
|
|
131
|
+
});
|
|
132
|
+
expect(formatted).not.toHaveProperty('extras');
|
|
133
|
+
});
|
|
134
|
+
|
|
89
135
|
it('is idempotent when re-marking a tool that already has the marker', () => {
|
|
90
136
|
const a = fakeTool('a-static') as { extras?: Record<string, unknown> };
|
|
91
137
|
a.extras = { cache_control: { type: 'ephemeral' } };
|
|
@@ -19,14 +19,74 @@
|
|
|
19
19
|
* the breakpoint.
|
|
20
20
|
*
|
|
21
21
|
* LangChain's Anthropic adapter passes the marker through via
|
|
22
|
-
* `tool.extras.cache_control`
|
|
23
|
-
*
|
|
24
|
-
* mutating the original tool instance, since callers may share them
|
|
22
|
+
* `tool.extras.cache_control` for custom tools, while Anthropic built-ins
|
|
23
|
+
* require direct `cache_control`. Either way, we stamp a fresh wrapper —
|
|
24
|
+
* never mutating the original tool instance, since callers may share them
|
|
25
25
|
* across runs.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import type { GraphTools } from '@/types';
|
|
29
29
|
|
|
30
|
+
const ANTHROPIC_BUILT_IN_TOOL_PREFIXES = [
|
|
31
|
+
'text_editor_',
|
|
32
|
+
'computer_',
|
|
33
|
+
'bash_',
|
|
34
|
+
'web_search_',
|
|
35
|
+
'web_fetch_',
|
|
36
|
+
'str_replace_editor_',
|
|
37
|
+
'str_replace_based_edit_tool_',
|
|
38
|
+
'code_execution_',
|
|
39
|
+
'memory_',
|
|
40
|
+
'tool_search_',
|
|
41
|
+
'mcp_toolset',
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
const CACHE_CONTROL = { type: 'ephemeral' as const };
|
|
45
|
+
|
|
46
|
+
type AnthropicToolCacheCandidate = {
|
|
47
|
+
name?: unknown;
|
|
48
|
+
type?: unknown;
|
|
49
|
+
extras?: Record<string, unknown>;
|
|
50
|
+
cache_control?: unknown;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function isAnthropicBuiltInTool(
|
|
54
|
+
tool: AnthropicToolCacheCandidate
|
|
55
|
+
): tool is AnthropicToolCacheCandidate & { type: string } {
|
|
56
|
+
const { type } = tool;
|
|
57
|
+
return (
|
|
58
|
+
typeof type === 'string' &&
|
|
59
|
+
ANTHROPIC_BUILT_IN_TOOL_PREFIXES.some((prefix) => type.startsWith(prefix))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasCacheControl(tool: AnthropicToolCacheCandidate): boolean {
|
|
64
|
+
if (isAnthropicBuiltInTool(tool)) {
|
|
65
|
+
return tool.cache_control != null;
|
|
66
|
+
}
|
|
67
|
+
return tool.extras?.cache_control != null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function markCacheControl(
|
|
71
|
+
tool: AnthropicToolCacheCandidate
|
|
72
|
+
): AnthropicToolCacheCandidate {
|
|
73
|
+
const prototype = Object.getPrototypeOf(tool) ?? Object.prototype;
|
|
74
|
+
if (isAnthropicBuiltInTool(tool)) {
|
|
75
|
+
const wrapped = { ...tool };
|
|
76
|
+
delete wrapped.extras;
|
|
77
|
+
return Object.assign(Object.create(prototype), wrapped, {
|
|
78
|
+
cache_control: CACHE_CONTROL,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return Object.assign(Object.create(prototype), tool, {
|
|
83
|
+
extras: {
|
|
84
|
+
...(tool.extras ?? {}),
|
|
85
|
+
cache_control: CACHE_CONTROL,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
30
90
|
/**
|
|
31
91
|
* Returns a callable that reports whether a given tool *name* is deferred
|
|
32
92
|
* according to the supplied registry of tool definitions. Tools without
|
|
@@ -59,8 +119,8 @@ export function makeIsDeferred(
|
|
|
59
119
|
*
|
|
60
120
|
* The original tool instances are never mutated. The marked entry is a
|
|
61
121
|
* shallow wrapper that preserves the prototype chain so downstream
|
|
62
|
-
* `instanceof` checks still pass. `extras` is merged
|
|
63
|
-
* `providerToolDefinition` / other extras
|
|
122
|
+
* `instanceof` checks still pass. For custom tools, `extras` is merged
|
|
123
|
+
* so any existing `providerToolDefinition` / other extras are kept.
|
|
64
124
|
*/
|
|
65
125
|
export function partitionAndMarkAnthropicToolCache(
|
|
66
126
|
tools: GraphTools | undefined,
|
|
@@ -87,30 +147,15 @@ export function partitionAndMarkAnthropicToolCache(
|
|
|
87
147
|
return tools;
|
|
88
148
|
}
|
|
89
149
|
|
|
90
|
-
const last = staticTools[
|
|
91
|
-
|
|
92
|
-
|
|
150
|
+
const last = staticTools[
|
|
151
|
+
staticTools.length - 1
|
|
152
|
+
] as AnthropicToolCacheCandidate;
|
|
93
153
|
// Already marked? Don't double-clone.
|
|
94
|
-
if (
|
|
95
|
-
last.extras != null &&
|
|
96
|
-
'cache_control' in last.extras &&
|
|
97
|
-
(last.extras as { cache_control?: unknown }).cache_control != null
|
|
98
|
-
) {
|
|
154
|
+
if (hasCacheControl(last)) {
|
|
99
155
|
if (deferredTools.length === 0) return tools;
|
|
100
156
|
return [...staticTools, ...deferredTools] as GraphTools;
|
|
101
157
|
}
|
|
102
158
|
|
|
103
|
-
|
|
104
|
-
Object.create(Object.getPrototypeOf(last) ?? Object.prototype),
|
|
105
|
-
last,
|
|
106
|
-
{
|
|
107
|
-
extras: {
|
|
108
|
-
...((last.extras as Record<string, unknown> | undefined) ?? {}),
|
|
109
|
-
cache_control: { type: 'ephemeral' as const },
|
|
110
|
-
},
|
|
111
|
-
}
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
staticTools[staticTools.length - 1] = wrapped;
|
|
159
|
+
staticTools[staticTools.length - 1] = markCacheControl(last);
|
|
115
160
|
return [...staticTools, ...deferredTools] as GraphTools;
|
|
116
161
|
}
|