anthropic: fix KV cache reuse degraded by tool call argument reordering

Use typed structs for tool call arguments instead of map[string]any to
preserve JSON key order, which Go maps do not guarantee.
This commit is contained in:
Jesse Gross
2026-03-09 16:24:57 -07:00
parent e7ccc129ea
commit ac83ac20c4
5 changed files with 275 additions and 308 deletions

View File

@@ -283,7 +283,7 @@ func (w *WebSearchAnthropicWriter) runWebSearchLoop(ctx context.Context, initial
Type: "server_tool_use",
ID: toolUseID,
Name: "web_search",
Input: map[string]any{"query": query},
Input: queryArgs(query),
},
anthropic.ContentBlock{
Type: "web_search_tool_result",
@@ -348,7 +348,7 @@ func (w *WebSearchAnthropicWriter) runWebSearchLoop(ctx context.Context, initial
Type: "server_tool_use",
ID: maxLoopToolUseID,
Name: "web_search",
Input: map[string]any{"query": maxLoopQuery},
Input: queryArgs(maxLoopQuery),
},
anthropic.ContentBlock{
Type: "web_search_tool_result",
@@ -786,7 +786,7 @@ func (w *WebSearchAnthropicWriter) webSearchErrorResponse(errorCode, query strin
Type: "server_tool_use",
ID: toolUseID,
Name: "web_search",
Input: map[string]any{"query": query},
Input: queryArgs(query),
},
{
Type: "web_search_tool_result",
@@ -942,6 +942,13 @@ func writeSSE(w http.ResponseWriter, eventType string, data any) error {
return nil
}
// queryArgs creates a ToolCallFunctionArguments with a single "query" key.
func queryArgs(query string) api.ToolCallFunctionArguments {
args := api.NewToolCallFunctionArguments()
args.Set("query", query)
return args
}
// serverToolUseID derives a server tool use ID from a message ID
func serverToolUseID(messageID string) string {
return "srvtoolu_" + strings.TrimPrefix(messageID, "msg_")

View File

@@ -1208,7 +1208,7 @@ func TestWebSearchStreamResponse(t *testing.T) {
Type: "server_tool_use",
ID: "srvtoolu_test123",
Name: "web_search",
Input: map[string]any{"query": "test query"},
Input: queryArgs("test query"),
},
{
Type: "web_search_tool_result",
@@ -1413,12 +1413,8 @@ func TestWebSearchSendError_NonStreaming(t *testing.T) {
t.Errorf("expected name 'web_search', got %q", result.Content[0].Name)
}
// Verify input contains the query
inputMap, ok := result.Content[0].Input.(map[string]any)
if !ok {
t.Fatalf("expected Input to be map, got %T", result.Content[0].Input)
}
if inputMap["query"] != "test query" {
t.Errorf("expected query 'test query', got %v", inputMap["query"])
if q, ok := result.Content[0].Input.Get("query"); !ok || q != "test query" {
t.Errorf("expected query 'test query', got %v", q)
}
// Block 1: web_search_tool_result with error
@@ -1561,12 +1557,8 @@ func TestWebSearchSendError_EmptyQuery(t *testing.T) {
}
// Verify the input has empty query
inputMap, ok := result.Content[0].Input.(map[string]any)
if !ok {
t.Fatalf("expected Input to be map, got %T", result.Content[0].Input)
}
if inputMap["query"] != "" {
t.Errorf("expected empty query, got %v", inputMap["query"])
if q, ok := result.Content[0].Input.Get("query"); !ok || q != "" {
t.Errorf("expected empty query, got %v", q)
}
}