From 98f0a3e3bda66f25554b8bd11558ac0f4f6167fc Mon Sep 17 00:00:00 2001 From: Kirill Turanskiy <7106373+thebtf@users.noreply.github.com> Date: Mon, 16 Feb 2026 03:35:38 +0300 Subject: [PATCH] fix: add proxy_ prefix handling for tool_reference content blocks (#1) applyClaudeToolPrefix, stripClaudeToolPrefixFromResponse, and stripClaudeToolPrefixFromStreamLine now handle "tool_reference" blocks (field "tool_name") in addition to "tool_use" blocks (field "name"). Without this fix, tool_reference blocks in conversation history retain their original unprefixed names while tool definitions carry the proxy_ prefix, causing Anthropic API 400 errors: "Tool reference 'X' not found in available tools." Co-authored-by: Kirill Turanskiy --- internal/runtime/executor/claude_executor.go | 78 +++++++++++++------ .../runtime/executor/claude_executor_test.go | 37 +++++++++ 2 files changed, 92 insertions(+), 23 deletions(-) diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 89a366ee..217d22ae 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -784,15 +784,22 @@ func applyClaudeToolPrefix(body []byte, prefix string) []byte { return true } content.ForEach(func(contentIndex, part gjson.Result) bool { - if part.Get("type").String() != "tool_use" { - return true + partType := part.Get("type").String() + if partType == "tool_use" { + name := part.Get("name").String() + if name == "" || strings.HasPrefix(name, prefix) { + return true + } + path := fmt.Sprintf("messages.%d.content.%d.name", msgIndex.Int(), contentIndex.Int()) + body, _ = sjson.SetBytes(body, path, prefix+name) + } else if partType == "tool_reference" { + toolName := part.Get("tool_name").String() + if toolName == "" || strings.HasPrefix(toolName, prefix) { + return true + } + path := fmt.Sprintf("messages.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int()) + body, _ = sjson.SetBytes(body, path, prefix+toolName) } - name := part.Get("name").String() - if name == "" || strings.HasPrefix(name, prefix) { - return true - } - path := fmt.Sprintf("messages.%d.content.%d.name", msgIndex.Int(), contentIndex.Int()) - body, _ = sjson.SetBytes(body, path, prefix+name) return true }) return true @@ -811,15 +818,22 @@ func stripClaudeToolPrefixFromResponse(body []byte, prefix string) []byte { return body } content.ForEach(func(index, part gjson.Result) bool { - if part.Get("type").String() != "tool_use" { - return true + partType := part.Get("type").String() + if partType == "tool_use" { + name := part.Get("name").String() + if !strings.HasPrefix(name, prefix) { + return true + } + path := fmt.Sprintf("content.%d.name", index.Int()) + body, _ = sjson.SetBytes(body, path, strings.TrimPrefix(name, prefix)) + } else if partType == "tool_reference" { + toolName := part.Get("tool_name").String() + if !strings.HasPrefix(toolName, prefix) { + return true + } + path := fmt.Sprintf("content.%d.tool_name", index.Int()) + body, _ = sjson.SetBytes(body, path, strings.TrimPrefix(toolName, prefix)) } - name := part.Get("name").String() - if !strings.HasPrefix(name, prefix) { - return true - } - path := fmt.Sprintf("content.%d.name", index.Int()) - body, _ = sjson.SetBytes(body, path, strings.TrimPrefix(name, prefix)) return true }) return body @@ -834,15 +848,33 @@ func stripClaudeToolPrefixFromStreamLine(line []byte, prefix string) []byte { return line } contentBlock := gjson.GetBytes(payload, "content_block") - if !contentBlock.Exists() || contentBlock.Get("type").String() != "tool_use" { + if !contentBlock.Exists() { return line } - name := contentBlock.Get("name").String() - if !strings.HasPrefix(name, prefix) { - return line - } - updated, err := sjson.SetBytes(payload, "content_block.name", strings.TrimPrefix(name, prefix)) - if err != nil { + + blockType := contentBlock.Get("type").String() + var updated []byte + var err error + + if blockType == "tool_use" { + name := contentBlock.Get("name").String() + if !strings.HasPrefix(name, prefix) { + return line + } + updated, err = sjson.SetBytes(payload, "content_block.name", strings.TrimPrefix(name, prefix)) + if err != nil { + return line + } + } else if blockType == "tool_reference" { + toolName := contentBlock.Get("tool_name").String() + if !strings.HasPrefix(toolName, prefix) { + return line + } + updated, err = sjson.SetBytes(payload, "content_block.tool_name", strings.TrimPrefix(toolName, prefix)) + if err != nil { + return line + } + } else { return line } diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index 36fb7ad4..cec9a3cd 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -25,6 +25,18 @@ func TestApplyClaudeToolPrefix(t *testing.T) { } } +func TestApplyClaudeToolPrefix_WithToolReference(t *testing.T) { + input := []byte(`{"tools":[{"name":"alpha"}],"messages":[{"role":"user","content":[{"type":"tool_reference","tool_name":"beta"},{"type":"tool_reference","tool_name":"proxy_gamma"}]}]}`) + out := applyClaudeToolPrefix(input, "proxy_") + + if got := gjson.GetBytes(out, "messages.0.content.0.tool_name").String(); got != "proxy_beta" { + t.Fatalf("messages.0.content.0.tool_name = %q, want %q", got, "proxy_beta") + } + if got := gjson.GetBytes(out, "messages.0.content.1.tool_name").String(); got != "proxy_gamma" { + t.Fatalf("messages.0.content.1.tool_name = %q, want %q", got, "proxy_gamma") + } +} + 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_") @@ -49,6 +61,18 @@ func TestStripClaudeToolPrefixFromResponse(t *testing.T) { } } +func TestStripClaudeToolPrefixFromResponse_WithToolReference(t *testing.T) { + input := []byte(`{"content":[{"type":"tool_reference","tool_name":"proxy_alpha"},{"type":"tool_reference","tool_name":"bravo"}]}`) + out := stripClaudeToolPrefixFromResponse(input, "proxy_") + + if got := gjson.GetBytes(out, "content.0.tool_name").String(); got != "alpha" { + t.Fatalf("content.0.tool_name = %q, want %q", got, "alpha") + } + if got := gjson.GetBytes(out, "content.1.tool_name").String(); got != "bravo" { + t.Fatalf("content.1.tool_name = %q, want %q", got, "bravo") + } +} + func TestStripClaudeToolPrefixFromStreamLine(t *testing.T) { line := []byte(`data: {"type":"content_block_start","content_block":{"type":"tool_use","name":"proxy_alpha","id":"t1"},"index":0}`) out := stripClaudeToolPrefixFromStreamLine(line, "proxy_") @@ -61,3 +85,16 @@ func TestStripClaudeToolPrefixFromStreamLine(t *testing.T) { t.Fatalf("content_block.name = %q, want %q", got, "alpha") } } + +func TestStripClaudeToolPrefixFromStreamLine_WithToolReference(t *testing.T) { + line := []byte(`data: {"type":"content_block_start","content_block":{"type":"tool_reference","tool_name":"proxy_beta"},"index":0}`) + out := stripClaudeToolPrefixFromStreamLine(line, "proxy_") + + payload := bytes.TrimSpace(out) + if bytes.HasPrefix(payload, []byte("data:")) { + payload = bytes.TrimSpace(payload[len("data:"):]) + } + if got := gjson.GetBytes(payload, "content_block.tool_name").String(); got != "beta" { + t.Fatalf("content_block.tool_name = %q, want %q", got, "beta") + } +}