From 7cc725496e3f198b1a3fd3fdf0c14033fdaf33e2 Mon Sep 17 00:00:00 2001 From: Kirill Turanskiy Date: Tue, 17 Feb 2026 21:42:32 +0300 Subject: [PATCH] fix: skip proxy_ prefix for built-in tools in message history The proxy_ prefix logic correctly skips built-in tools (those with a non-empty "type" field) in tools[] definitions but does not skip them in messages[].content[] tool_use blocks or tool_choice. This causes web_search in conversation history to become proxy_web_search, which Anthropic does not recognize. Fix: collect built-in tool names from tools[] into a set and also maintain a hardcoded fallback set (web_search, code_execution, text_editor, computer) for cases where the built-in tool appears in history but not in the current request's tools[] array. Skip prefixing in messages and tool_choice when name matches a built-in. --- internal/runtime/executor/claude_executor.go | 14 ++- .../runtime/executor/claude_executor_test.go | 91 +++++++++++++++++-- 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 89a366ee..717bb335 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -753,11 +753,21 @@ func applyClaudeToolPrefix(body []byte, prefix string) []byte { return body } + // Collect built-in tool names (those with a non-empty "type" field) so we can + // skip them consistently in both tools and message history. + builtinTools := map[string]bool{} + for _, name := range []string{"web_search", "code_execution", "text_editor", "computer"} { + builtinTools[name] = true + } + if tools := gjson.GetBytes(body, "tools"); tools.Exists() && tools.IsArray() { tools.ForEach(func(index, tool gjson.Result) bool { // Skip built-in tools (web_search, code_execution, etc.) which have // a "type" field and require their name to remain unchanged. if tool.Get("type").Exists() && tool.Get("type").String() != "" { + if n := tool.Get("name").String(); n != "" { + builtinTools[n] = true + } return true } name := tool.Get("name").String() @@ -772,7 +782,7 @@ func applyClaudeToolPrefix(body []byte, prefix string) []byte { if gjson.GetBytes(body, "tool_choice.type").String() == "tool" { name := gjson.GetBytes(body, "tool_choice.name").String() - if name != "" && !strings.HasPrefix(name, prefix) { + if name != "" && !strings.HasPrefix(name, prefix) && !builtinTools[name] { body, _ = sjson.SetBytes(body, "tool_choice.name", prefix+name) } } @@ -788,7 +798,7 @@ func applyClaudeToolPrefix(body []byte, prefix string) []byte { return true } name := part.Get("name").String() - if name == "" || strings.HasPrefix(name, prefix) { + if name == "" || strings.HasPrefix(name, prefix) || builtinTools[name] { return true } path := fmt.Sprintf("messages.%d.content.%d.name", msgIndex.Int(), contentIndex.Int()) diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index 36fb7ad4..ac359bb8 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -25,15 +25,94 @@ func TestApplyClaudeToolPrefix(t *testing.T) { } } -func TestApplyClaudeToolPrefix_SkipsBuiltinTools(t *testing.T) { - input := []byte(`{"tools":[{"type":"web_search_20250305","name":"web_search"},{"name":"my_custom_tool","input_schema":{"type":"object"}}]}`) - out := applyClaudeToolPrefix(input, "proxy_") +func TestApplyClaudeToolPrefix_BuiltinToolSkipped(t *testing.T) { + body := []byte(`{ + "tools": [ + {"type": "web_search_20250305", "name": "web_search", "max_uses": 5}, + {"name": "Read"} + ], + "messages": [ + {"role": "user", "content": [ + {"type": "tool_use", "name": "web_search", "id": "ws1", "input": {}}, + {"type": "tool_use", "name": "Read", "id": "r1", "input": {}} + ]} + ] + }`) + out := applyClaudeToolPrefix(body, "proxy_") if got := gjson.GetBytes(out, "tools.0.name").String(); got != "web_search" { - t.Fatalf("built-in tool name should not be prefixed: tools.0.name = %q, want %q", got, "web_search") + t.Fatalf("tools.0.name = %q, want %q", got, "web_search") } - if got := gjson.GetBytes(out, "tools.1.name").String(); got != "proxy_my_custom_tool" { - t.Fatalf("custom tool should be prefixed: tools.1.name = %q, want %q", got, "proxy_my_custom_tool") + if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != "web_search" { + t.Fatalf("messages.0.content.0.name = %q, want %q", got, "web_search") + } + if got := gjson.GetBytes(out, "tools.1.name").String(); got != "proxy_Read" { + t.Fatalf("tools.1.name = %q, want %q", got, "proxy_Read") + } + if got := gjson.GetBytes(out, "messages.0.content.1.name").String(); got != "proxy_Read" { + t.Fatalf("messages.0.content.1.name = %q, want %q", got, "proxy_Read") + } +} + +func TestApplyClaudeToolPrefix_KnownBuiltinInHistoryOnly(t *testing.T) { + body := []byte(`{ + "tools": [ + {"name": "Read"} + ], + "messages": [ + {"role": "user", "content": [ + {"type": "tool_use", "name": "web_search", "id": "ws1", "input": {}} + ]} + ] + }`) + out := applyClaudeToolPrefix(body, "proxy_") + + if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != "web_search" { + t.Fatalf("messages.0.content.0.name = %q, want %q", got, "web_search") + } + if got := gjson.GetBytes(out, "tools.0.name").String(); got != "proxy_Read" { + t.Fatalf("tools.0.name = %q, want %q", got, "proxy_Read") + } +} + +func TestApplyClaudeToolPrefix_CustomToolsPrefixed(t *testing.T) { + body := []byte(`{ + "tools": [{"name": "Read"}, {"name": "Write"}], + "messages": [ + {"role": "user", "content": [ + {"type": "tool_use", "name": "Read", "id": "r1", "input": {}}, + {"type": "tool_use", "name": "Write", "id": "w1", "input": {}} + ]} + ] + }`) + out := applyClaudeToolPrefix(body, "proxy_") + + if got := gjson.GetBytes(out, "tools.0.name").String(); got != "proxy_Read" { + t.Fatalf("tools.0.name = %q, want %q", got, "proxy_Read") + } + if got := gjson.GetBytes(out, "tools.1.name").String(); got != "proxy_Write" { + t.Fatalf("tools.1.name = %q, want %q", got, "proxy_Write") + } + if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != "proxy_Read" { + t.Fatalf("messages.0.content.0.name = %q, want %q", got, "proxy_Read") + } + if got := gjson.GetBytes(out, "messages.0.content.1.name").String(); got != "proxy_Write" { + t.Fatalf("messages.0.content.1.name = %q, want %q", got, "proxy_Write") + } +} + +func TestApplyClaudeToolPrefix_ToolChoiceBuiltin(t *testing.T) { + body := []byte(`{ + "tools": [ + {"type": "web_search_20250305", "name": "web_search"}, + {"name": "Read"} + ], + "tool_choice": {"type": "tool", "name": "web_search"} + }`) + out := applyClaudeToolPrefix(body, "proxy_") + + if got := gjson.GetBytes(out, "tool_choice.name").String(); got != "web_search" { + t.Fatalf("tool_choice.name = %q, want %q", got, "web_search") } }