Skip to content

Commit 82d63b2

Browse files
committed
Anthropic Prompt Caching: Fix SYSTEM_ONLY cache strategy to not explicitly cache tools
This commit fixes a bug where the SYSTEM_ONLY cache strategy was incorrectly placing cache_control on tool definitions, making it functionally identical to SYSTEM_AND_TOOLS. Problem: - SYSTEM_ONLY and SYSTEM_AND_TOOLS both placed cache breakpoints on tools AND system messages, using 2 breakpoints - This violated the documented behavior and wasted breakpoints - The strategies were functionally identical Root Cause: - CacheEligibilityResolver.resolveToolCacheControl() checked if eligible message types included SYSTEM, which both strategies have - It should have checked the strategy directly instead Solution: - Changed resolveToolCacheControl() to use allowlist approach - Only SYSTEM_AND_TOOLS and CONVERSATION_HISTORY explicitly cache tools - SYSTEM_ONLY now places cache_control only on system message - Tools still get cached implicitly via Anthropic's cache hierarchy (tools → system → messages) Impact: - SYSTEM_ONLY: Uses 1 breakpoint (system only) instead of 2 - SYSTEM_AND_TOOLS: Uses 2 breakpoints (last tool + system) as before - Users can now optimize breakpoint usage more effectively Changes: - Fix CacheEligibilityResolver.resolveToolCacheControl() to check strategy directly instead of eligible message types - Enhance testSystemOnlyCacheStrategy() to include tools and verify they don't have cache_control - Update toolCacheControlRespectsStrategy() test to verify all strategies behave correctly Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
1 parent 60fb9ab commit 82d63b2

File tree

3 files changed

+44
-7
lines changed

3 files changed

+44
-7
lines changed

models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/utils/CacheEligibilityResolver.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,16 @@ public AnthropicApi.ChatCompletionRequest.CacheControl resolve(MessageType messa
108108
}
109109

110110
public AnthropicApi.ChatCompletionRequest.CacheControl resolveToolCacheControl() {
111-
// Tool definitions are only cache-eligible when caching is enabled and
112-
// the strategy includes SYSTEM messages (SYSTEM_ONLY, SYSTEM_AND_TOOLS, or
113-
// CONVERSATION_HISTORY). When NONE, tools must not be cached.
114-
if (!isCachingEnabled() || !this.cacheEligibleMessageTypes.contains(TOOL_DEFINITION_MESSAGE_TYPE)
115-
|| this.cacheBreakpointTracker.allBreakpointsAreUsed()) {
111+
// Tool definitions are only cache-eligible for SYSTEM_AND_TOOLS and
112+
// CONVERSATION_HISTORY strategies. SYSTEM_ONLY caches only system messages,
113+
// relying on Anthropic's cache hierarchy to implicitly cache tools.
114+
if (this.cacheStrategy != AnthropicCacheStrategy.SYSTEM_AND_TOOLS
115+
&& this.cacheStrategy != AnthropicCacheStrategy.CONVERSATION_HISTORY) {
116+
logger.debug("Caching not enabled for tool definition, cacheStrategy={}", this.cacheStrategy);
117+
return null;
118+
}
119+
120+
if (this.cacheBreakpointTracker.allBreakpointsAreUsed()) {
116121
logger.debug("Caching not enabled for tool definition, usedBreakpoints={}",
117122
this.cacheBreakpointTracker.getCount());
118123
return null;

models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicPromptCachingMockTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,17 @@ void testSystemOnlyCacheStrategy() throws Exception {
104104
this.mockWebServer
105105
.enqueue(new MockResponse().setBody(mockResponse).setHeader("Content-Type", "application/json"));
106106

107+
// Create tool callback to test that tools are NOT cached with SYSTEM_ONLY
108+
var toolMethod = ReflectionUtils.findMethod(TestTools.class, "getWeather", String.class);
109+
MethodToolCallback toolCallback = MethodToolCallback.builder()
110+
.toolDefinition(ToolDefinitions.builder(toolMethod).description("Get weather for a location").build())
111+
.toolMethod(toolMethod)
112+
.build();
113+
107114
// Test with SYSTEM_ONLY cache strategy
108115
AnthropicChatOptions options = AnthropicChatOptions.builder()
109116
.cacheOptions(AnthropicCacheOptions.builder().strategy(AnthropicCacheStrategy.SYSTEM_ONLY).build())
117+
.toolCallbacks(List.of(toolCallback))
110118
.build();
111119

112120
Prompt prompt = new Prompt(
@@ -130,6 +138,18 @@ void testSystemOnlyCacheStrategy() throws Exception {
130138
assertThat(lastSystemBlock.get("cache_control").get("type").asText()).isEqualTo("ephemeral");
131139
}
132140

141+
// Verify tools exist but DO NOT have cache_control (key difference from
142+
// SYSTEM_AND_TOOLS)
143+
if (requestBody.has("tools")) {
144+
JsonNode toolsArray = requestBody.get("tools");
145+
assertThat(toolsArray.isArray()).isTrue();
146+
// Verify NO tool has cache_control
147+
for (int i = 0; i < toolsArray.size(); i++) {
148+
JsonNode tool = toolsArray.get(i);
149+
assertThat(tool.has("cache_control")).isFalse();
150+
}
151+
}
152+
133153
// Verify response
134154
assertThat(response).isNotNull();
135155
assertThat(response.getResult().getOutput().getText()).contains("Hello!");

models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/utils/CacheEligibilityResolverTests.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,26 @@ void toolCacheControlRespectsStrategy() {
7878
.from(AnthropicCacheOptions.builder().strategy(AnthropicCacheStrategy.NONE).build());
7979
assertThat(none.resolveToolCacheControl()).isNull();
8080

81-
// SYSTEM_ONLY -> tool caching enabled (uses SYSTEM TTL)
81+
// SYSTEM_ONLY -> no explicit tool caching (tools cached implicitly via hierarchy)
8282
CacheEligibilityResolver sys = CacheEligibilityResolver.from(AnthropicCacheOptions.builder()
8383
.strategy(AnthropicCacheStrategy.SYSTEM_ONLY)
8484
.messageTypeTtl(MessageType.SYSTEM, AnthropicCacheTtl.ONE_HOUR)
8585
.build());
86-
var cc = sys.resolveToolCacheControl();
86+
assertThat(sys.resolveToolCacheControl()).isNull();
87+
88+
// SYSTEM_AND_TOOLS -> tool caching enabled (uses SYSTEM TTL)
89+
CacheEligibilityResolver sysAndTools = CacheEligibilityResolver.from(AnthropicCacheOptions.builder()
90+
.strategy(AnthropicCacheStrategy.SYSTEM_AND_TOOLS)
91+
.messageTypeTtl(MessageType.SYSTEM, AnthropicCacheTtl.ONE_HOUR)
92+
.build());
93+
var cc = sysAndTools.resolveToolCacheControl();
8794
assertThat(cc).isNotNull();
8895
assertThat(cc.ttl()).isEqualTo(AnthropicCacheTtl.ONE_HOUR.getValue());
96+
97+
// CONVERSATION_HISTORY -> tool caching enabled
98+
CacheEligibilityResolver history = CacheEligibilityResolver
99+
.from(AnthropicCacheOptions.builder().strategy(AnthropicCacheStrategy.CONVERSATION_HISTORY).build());
100+
assertThat(history.resolveToolCacheControl()).isNotNull();
89101
}
90102

91103
}

0 commit comments

Comments
 (0)