Skip to content

Commit 23956e3

Browse files
committed
chore: prepend injected tools
Signed-off-by: Danny Kopping <danny@coder.com>
1 parent ffbd1a5 commit 23956e3

3 files changed

Lines changed: 25 additions & 50 deletions

File tree

intercept_anthropic_message_internal_test.go

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestInjectTools_CacheBreakpoints(t *testing.T) {
4343
require.Equal(t, constant.ValueOf[constant.Ephemeral](), i.req.Tools[0].OfTool.CacheControl.Type)
4444
})
4545

46-
t.Run("cache control breakpoint is preserved and moved to final tool", func(t *testing.T) {
46+
t.Run("cache control breakpoint is preserved by prepending injected tools", func(t *testing.T) {
4747
t.Parallel()
4848

4949
// Request has existing tool with cache control.
@@ -72,16 +72,16 @@ func TestInjectTools_CacheBreakpoints(t *testing.T) {
7272
i.injectTools()
7373

7474
require.Len(t, i.req.Tools, 2)
75-
// Original tool's cache control should be cleared.
76-
require.Equal(t, "existing_tool", i.req.Tools[0].OfTool.Name)
75+
// Injected tools are prepended.
76+
require.Equal(t, "injected_tool", i.req.Tools[0].OfTool.Name)
7777
require.Zero(t, i.req.Tools[0].OfTool.CacheControl)
78-
// Cache control breakpoint should be moved to the final tool.
79-
require.Equal(t, "injected_tool", i.req.Tools[1].OfTool.Name)
78+
// Original tool's cache control should be preserved at the end.
79+
require.Equal(t, "existing_tool", i.req.Tools[1].OfTool.Name)
8080
require.Equal(t, constant.ValueOf[constant.Ephemeral](), i.req.Tools[1].OfTool.CacheControl.Type)
8181
})
8282

83-
// Multiple breakpoints should not be set, but if they are we should only move the first one to the end.
84-
t.Run("only first cache control breakpoint is moved when multiple exist", func(t *testing.T) {
83+
// The cache breakpoint SHOULD be on the final tool, but may not be; we must preserve that intention.
84+
t.Run("cache control breakpoint in non-standard location is preserved", func(t *testing.T) {
8585
t.Parallel()
8686

8787
// Request has multiple tools with cache control breakpoints.
@@ -100,9 +100,6 @@ func TestInjectTools_CacheBreakpoints(t *testing.T) {
100100
{
101101
OfTool: &anthropic.ToolParam{
102102
Name: "tool_with_cache_2",
103-
CacheControl: anthropic.CacheControlEphemeralParam{
104-
Type: constant.ValueOf[constant.Ephemeral](),
105-
},
106103
},
107104
},
108105
},
@@ -118,15 +115,14 @@ func TestInjectTools_CacheBreakpoints(t *testing.T) {
118115
i.injectTools()
119116

120117
require.Len(t, i.req.Tools, 3)
121-
// First tool's cache control should be cleared (it was captured).
122-
require.Equal(t, "tool_with_cache_1", i.req.Tools[0].OfTool.Name)
118+
// Injected tool is prepended without cache control.
119+
require.Equal(t, "injected_tool", i.req.Tools[0].OfTool.Name)
123120
require.Zero(t, i.req.Tools[0].OfTool.CacheControl)
124-
// Second tool's cache control should remain (loop breaks after first match).
125-
require.Equal(t, "tool_with_cache_2", i.req.Tools[1].OfTool.Name)
121+
// Both original tools' cache controls should remain.
122+
require.Equal(t, "tool_with_cache_1", i.req.Tools[1].OfTool.Name)
126123
require.Equal(t, constant.ValueOf[constant.Ephemeral](), i.req.Tools[1].OfTool.CacheControl.Type)
127-
// Only the first breakpoint is moved to the final tool.
128-
require.Equal(t, "injected_tool", i.req.Tools[2].OfTool.Name)
129-
require.Equal(t, constant.ValueOf[constant.Ephemeral](), i.req.Tools[2].OfTool.CacheControl.Type)
124+
require.Equal(t, "tool_with_cache_2", i.req.Tools[2].OfTool.Name)
125+
require.Zero(t, i.req.Tools[2].OfTool.CacheControl)
130126
})
131127

132128
t.Run("no cache control added when none originally set", func(t *testing.T) {
@@ -155,10 +151,11 @@ func TestInjectTools_CacheBreakpoints(t *testing.T) {
155151
i.injectTools()
156152

157153
require.Len(t, i.req.Tools, 2)
158-
// Neither tool should have cache control.
159-
require.Equal(t, "existing_tool_no_cache", i.req.Tools[0].OfTool.Name)
154+
// Injected tool is prepended without cache control.
155+
require.Equal(t, "injected_tool", i.req.Tools[0].OfTool.Name)
160156
require.Zero(t, i.req.Tools[0].OfTool.CacheControl)
161-
require.Equal(t, "injected_tool", i.req.Tools[1].OfTool.Name)
157+
// Original tool remains at the end without cache control.
158+
require.Equal(t, "existing_tool_no_cache", i.req.Tools[1].OfTool.Name)
162159
require.Zero(t, i.req.Tools[1].OfTool.CacheControl)
163160
})
164161
}

intercept_anthropic_messages_base.go

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -83,32 +83,14 @@ func (i *AnthropicMessagesInterceptionBase) injectTools() {
8383

8484
tools := i.mcpProxy.ListTools()
8585
if len(tools) == 0 {
86-
// No injected tools: no need to affect cache breakpoints or influence parallel tool calling.
86+
// No injected tools: no need to influence parallel tool calling.
8787
return
8888
}
8989

90-
// Capture existing cache control breakpoint, if present.
91-
var cache *anthropic.CacheControlEphemeralParam
92-
for _, t := range i.req.Tools {
93-
if t.OfTool == nil {
94-
continue
95-
}
96-
97-
if t.OfTool.CacheControl.Type != "" {
98-
// Capture existing cache control breakpoint (copy values since we'll be clearing it in the next step).
99-
cache = &anthropic.CacheControlEphemeralParam{
100-
TTL: t.OfTool.CacheControl.TTL,
101-
Type: t.OfTool.CacheControl.Type,
102-
}
103-
// Reset it; we'll move this breakpoint to the final tool definition.
104-
t.OfTool.CacheControl = anthropic.CacheControlEphemeralParam{}
105-
break
106-
}
107-
}
108-
10990
// Inject tools.
91+
var injectedTools []anthropic.ToolUnionParam
11092
for _, tool := range tools {
111-
i.req.Tools = append(i.req.Tools, anthropic.ToolUnionParam{
93+
injectedTools = append(injectedTools, anthropic.ToolUnionParam{
11294
OfTool: &anthropic.ToolParam{
11395
InputSchema: anthropic.ToolInputSchemaParam{
11496
Properties: tool.Params,
@@ -121,14 +103,10 @@ func (i *AnthropicMessagesInterceptionBase) injectTools() {
121103
})
122104
}
123105

124-
// If there was a given cache control breakpoint, set it on the final tool as per the docs:
125-
// https://platform.claude.com/docs/en/build-with-claude/prompt-caching#prompt-caching-examples (see "Caching tool definitions").
126-
count := len(i.req.Tools)
127-
if cache != nil && count > 0 {
128-
if i.req.Tools[count-1].OfTool != nil {
129-
i.req.Tools[count-1].OfTool.CacheControl = *cache
130-
}
131-
}
106+
// Prepend the injected tools in order to maintain any configured cache breakpoints.
107+
// The order of injected tools is expected to be stable, and therefore will not cause
108+
// any cache invalidation when prepended.
109+
i.req.Tools = append(injectedTools, i.req.Tools...)
132110

133111
// Note: Parallel tool calls are disabled to avoid tool_use/tool_result block mismatches.
134112
// https://github.com/coder/aibridge/issues/2

mcp/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type ServerProxier interface {
1515
// See https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#session-management.
1616
Shutdown(ctx context.Context) error
1717

18-
// ListTools lists all known tools.
18+
// ListTools lists all known tools. These MUST be sorted in a stable order.
1919
ListTools() []*Tool
2020
// GetTool returns a given tool, if known, or returns nil.
2121
GetTool(id string) *Tool

0 commit comments

Comments
 (0)