fix(mimic): rewrite tool_use names in messages to match renamed tools
The Claude Code mimic path rewrites tool names in tools[] (and
tool_choice) but left tool_use blocks in messages[] with their
original names. Anthropic validates that every tool referenced by
a tool_use block is declared in tools[], so the mismatch produces:
messages.N.content.M: Input tag 'original_name' not found in tools
(surfaced as HTTP 400 directly, or wrapped as 424 by upstream proxies
such as Bedrock gateways.)
The previous code comment asserted 'this matches Parrot; response-side
bytes.Replace will restore the names'. Parrot's behavior is fine for
Claude Code's own tool set, but breaks once the upstream client sends
additional tools (e.g. web_search) that are not part of Claude Code
and therefore get renamed here.
Fix: apply the same ToolNameRewrite to messages[].content[] blocks
where type == 'tool_use', keeping tools[], tool_choice and messages
self-consistent before the request reaches Anthropic. tool_result
blocks reference tools via tool_use_id, not name, so no change is
needed there.
A new unit test covers the full rewrite flow and guards against
server tools (type != '') being affected.
This commit is contained in:
@@ -168,13 +168,13 @@ func buildToolNameRewriteFromBody(body []byte) *ToolNameRewrite {
|
||||
}
|
||||
|
||||
// applyToolNameRewriteToBody 把已构造的 ToolNameRewrite 应用到 body 上:
|
||||
// - 改写 $.tools[*].name(仅对 shouldMimicToolName 通过的 tool)
|
||||
// - 在 $.tools[last].cache_control 上打 ephemeral 缓存断点(Parrot 行为对齐,
|
||||
// ttl 客户端已有则透传,否则默认 claude.DefaultCacheControlTTL)
|
||||
// - 改写 $.tool_choice.name(仅当 $.tool_choice.type == "tool")
|
||||
//
|
||||
// 历史 $.messages[*].content[*].name(tool_use)不在请求侧改写——这与 Parrot 一致;
|
||||
// 响应侧 bytes.Replace 会连带还原它们。
|
||||
// - 改写 $.tools[*].name(仅对 shouldMimicToolName 通过的 tool)
|
||||
// - 改写 $.tool_choice.name(仅当 $.tool_choice.type == "tool")
|
||||
// - 改写 $.messages[*].content[*].name(仅当 type == "tool_use")
|
||||
// - 在 $.tools[last].cache_control 上打 ephemeral 缓存断点
|
||||
//
|
||||
// 响应侧 bytes.Replace 会连带还原假名 → 真名。
|
||||
func applyToolNameRewriteToBody(body []byte, rw *ToolNameRewrite) []byte {
|
||||
if rw == nil || len(rw.Forward) == 0 {
|
||||
body = applyToolsLastCacheBreakpoint(body)
|
||||
@@ -213,6 +213,38 @@ func applyToolNameRewriteToBody(body []byte, rw *ToolNameRewrite) []byte {
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite tool_use names in messages to match the renamed tools.
|
||||
// Without this, Anthropic rejects requests where messages reference tools
|
||||
// by their original name but tools[] declares the renamed (fake) name.
|
||||
messages := gjson.GetBytes(body, "messages")
|
||||
if messages.IsArray() {
|
||||
messages.ForEach(func(msgKey, msg gjson.Result) bool {
|
||||
msgIdx := int(msgKey.Num)
|
||||
content := msg.Get("content")
|
||||
if !content.IsArray() {
|
||||
return true
|
||||
}
|
||||
content.ForEach(func(blkKey, blk gjson.Result) bool {
|
||||
blkIdx := int(blkKey.Num)
|
||||
if blk.Get("type").String() != "tool_use" {
|
||||
return true
|
||||
}
|
||||
name := blk.Get("name").String()
|
||||
if name == "" {
|
||||
return true
|
||||
}
|
||||
if fake, ok := rw.Forward[name]; ok {
|
||||
path := fmt.Sprintf("messages.%d.content.%d.name", msgIdx, blkIdx)
|
||||
if next, err := sjson.SetBytes(body, path, fake); err == nil {
|
||||
body = next
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
body = applyToolsLastCacheBreakpoint(body)
|
||||
return body
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user