Skip to content

Commit c169f83

Browse files
committed
fix: prevent tool result cache accumulation across multiple tool calls
Apply tool_result_cache_type only to the last tool result message across all messages instead of applying it to each tool result during creation. This prevents cache accumulation when there are multiple tool call rounds.
1 parent a1a0cc8 commit c169f83

File tree

4 files changed

+186
-12
lines changed

4 files changed

+186
-12
lines changed

src/Providers/Anthropic/Handlers/Stream.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -617,12 +617,6 @@ protected function addMessagesToRequest(Request $request, array $toolResults, ?a
617617

618618
$message = new ToolResultMessage($toolResults);
619619

620-
// Apply tool result caching if configured
621-
$tool_result_cache_type = $request->providerOptions('tool_result_cache_type');
622-
if ($tool_result_cache_type) {
623-
$message->withProviderOptions(['cacheType' => $tool_result_cache_type]);
624-
}
625-
626620
$request->addMessage($message);
627621
}
628622

src/Providers/Anthropic/Handlers/Text.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,6 @@ protected function handleToolCalls(): Response
104104
$toolResults = $this->callTools($this->request->tools(), $this->tempResponse->toolCalls);
105105
$message = new ToolResultMessage($toolResults);
106106

107-
// Apply tool result caching if configured
108-
if ($tool_result_cache_type = $this->request->providerOptions('tool_result_cache_type')) {
109-
$message->withProviderOptions(['cacheType' => $tool_result_cache_type]);
110-
}
111-
112107
$this->request->addMessage($message);
113108

114109
$this->addStep($toolResults);

src/Providers/Anthropic/Maps/MessageMap.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,32 @@ public static function map(array $messages, array $requestProviderOptions = []):
3030
throw new PrismException('Anthropic does not support SystemMessages in the messages array. Use withSystemPrompt or withSystemPrompts instead.');
3131
}
3232

33-
return array_map(
33+
$mappedMessages = array_map(
3434
fn (Message $message): array => self::mapMessage($message, $requestProviderOptions),
3535
$messages
3636
);
37+
38+
if (isset($requestProviderOptions['tool_result_cache_type'])) {
39+
$lastToolResultIndex = null;
40+
41+
for ($i = count($mappedMessages) - 1; $i >= 0; $i--) {
42+
if ($mappedMessages[$i]['role'] === 'user' &&
43+
isset($mappedMessages[$i]['content'][0]['type']) &&
44+
$mappedMessages[$i]['content'][0]['type'] === 'tool_result') {
45+
$lastToolResultIndex = $i;
46+
break;
47+
}
48+
}
49+
50+
if ($lastToolResultIndex !== null) {
51+
$lastContent = &$mappedMessages[$lastToolResultIndex]['content'];
52+
$lastContent[count($lastContent) - 1]['cache_control'] = [
53+
'type' => $requestProviderOptions['tool_result_cache_type'],
54+
];
55+
}
56+
}
57+
58+
return $mappedMessages;
3759
}
3860

3961
/**
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Providers\Anthropic;
6+
7+
use Prism\Prism\Providers\Anthropic\Maps\MessageMap;
8+
use Prism\Prism\ValueObjects\Messages\AssistantMessage;
9+
use Prism\Prism\ValueObjects\Messages\ToolResultMessage;
10+
use Prism\Prism\ValueObjects\Messages\UserMessage;
11+
use Prism\Prism\ValueObjects\ToolCall;
12+
use Prism\Prism\ValueObjects\ToolResult;
13+
14+
beforeEach(function (): void {
15+
config()->set('prism.providers.anthropic.api_key', env('ANTHROPIC_API_KEY', 'sk-1234'));
16+
});
17+
18+
it('applies tool_result_cache_type only to the last tool result message across all messages', function (): void {
19+
// Create test messages simulating multiple tool call rounds
20+
$messages = [
21+
new UserMessage('What time is the tigers game today and should I wear a coat?'),
22+
new AssistantMessage('', toolCalls: [
23+
new ToolCall(
24+
id: 'call_1',
25+
name: 'search',
26+
arguments: ['query' => 'Detroit Tigers baseball game time today']
27+
),
28+
]),
29+
new ToolResultMessage([
30+
new ToolResult(
31+
toolCallId: 'call_1',
32+
toolName: 'search',
33+
args: ['query' => 'Detroit Tigers baseball game time today'],
34+
result: 'The tigers game is at 3pm in detroit'
35+
),
36+
]),
37+
new AssistantMessage('', toolCalls: [
38+
new ToolCall(
39+
id: 'call_2',
40+
name: 'weather',
41+
arguments: ['city' => 'Detroit']
42+
),
43+
]),
44+
new ToolResultMessage([
45+
new ToolResult(
46+
toolCallId: 'call_2',
47+
toolName: 'weather',
48+
args: ['city' => 'Detroit'],
49+
result: 'The weather will be 75° and sunny'
50+
),
51+
]),
52+
new AssistantMessage('The Tigers game is at 3pm today. The weather will be 75° and sunny, so you won\'t need a coat!'),
53+
];
54+
55+
// Map the messages with provider options
56+
$mappedMessages = MessageMap::map(
57+
$messages,
58+
['tool_result_cache_type' => 'ephemeral']
59+
);
60+
61+
// Verify that only the last tool result message has cache_control
62+
$toolResultMessages = array_filter($mappedMessages, fn ($message): bool => $message['role'] === 'user' &&
63+
isset($message['content'][0]['type']) &&
64+
$message['content'][0]['type'] === 'tool_result');
65+
66+
expect(count($toolResultMessages))->toBe(2);
67+
68+
// Get the tool result messages by their indices
69+
$toolResultIndices = array_keys($toolResultMessages);
70+
$firstToolResultIndex = $toolResultIndices[0];
71+
$lastToolResultIndex = $toolResultIndices[1];
72+
73+
// First tool result should NOT have cache_control
74+
$firstToolResult = $mappedMessages[$firstToolResultIndex];
75+
expect($firstToolResult['content'][0])->not->toHaveKey('cache_control');
76+
77+
// Last tool result SHOULD have cache_control
78+
$lastToolResult = $mappedMessages[$lastToolResultIndex];
79+
expect($lastToolResult['content'][0])->toHaveKey('cache_control');
80+
expect($lastToolResult['content'][0]['cache_control'])->toBe(['type' => 'ephemeral']);
81+
});
82+
83+
it('handles single tool result message with cache_control', function (): void {
84+
$messages = [
85+
new UserMessage('What is the weather?'),
86+
new AssistantMessage('', toolCalls: [
87+
new ToolCall(
88+
id: 'call_1',
89+
name: 'weather',
90+
arguments: ['city' => 'Detroit']
91+
),
92+
]),
93+
new ToolResultMessage([
94+
new ToolResult(
95+
toolCallId: 'call_1',
96+
toolName: 'weather',
97+
args: ['city' => 'Detroit'],
98+
result: 'The weather will be 75° and sunny'
99+
),
100+
]),
101+
];
102+
103+
// Map the messages with provider options
104+
$mappedMessages = MessageMap::map(
105+
$messages,
106+
['tool_result_cache_type' => 'ephemeral']
107+
);
108+
109+
// Find the tool result message
110+
$toolResultMessage = null;
111+
foreach ($mappedMessages as $message) {
112+
if ($message['role'] === 'user' &&
113+
isset($message['content'][0]['type']) &&
114+
$message['content'][0]['type'] === 'tool_result') {
115+
$toolResultMessage = $message;
116+
break;
117+
}
118+
}
119+
120+
// The single tool result should have cache_control
121+
expect($toolResultMessage)->not->toBeNull();
122+
expect($toolResultMessage['content'][0])->toHaveKey('cache_control');
123+
expect($toolResultMessage['content'][0]['cache_control'])->toBe(['type' => 'ephemeral']);
124+
});
125+
126+
it('does not apply cache_control when tool_result_cache_type is not set', function (): void {
127+
$messages = [
128+
new UserMessage('What is the weather?'),
129+
new AssistantMessage('', toolCalls: [
130+
new ToolCall(
131+
id: 'call_1',
132+
name: 'weather',
133+
arguments: ['city' => 'Detroit']
134+
),
135+
]),
136+
new ToolResultMessage([
137+
new ToolResult(
138+
toolCallId: 'call_1',
139+
toolName: 'weather',
140+
args: ['city' => 'Detroit'],
141+
result: 'The weather will be 75° and sunny'
142+
),
143+
]),
144+
];
145+
146+
// Map the messages without provider options
147+
$mappedMessages = MessageMap::map($messages);
148+
149+
// Find the tool result message
150+
$toolResultMessage = null;
151+
foreach ($mappedMessages as $message) {
152+
if ($message['role'] === 'user' &&
153+
isset($message['content'][0]['type']) &&
154+
$message['content'][0]['type'] === 'tool_result') {
155+
$toolResultMessage = $message;
156+
break;
157+
}
158+
}
159+
160+
// Should not have cache_control
161+
expect($toolResultMessage)->not->toBeNull();
162+
expect($toolResultMessage['content'][0])->not->toHaveKey('cache_control');
163+
});

0 commit comments

Comments
 (0)