mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-03 14:35:47 +08:00
Merge pull request #2896 from edlsh/fix/oauth-tool-rename-per-request-map
fix(amp): smart-mode tool name fixes + deep-mode response repair
This commit is contained in:
@@ -65,14 +65,13 @@ var oauthToolRenameMap = map[string]string{
|
||||
"notebookedit": "NotebookEdit",
|
||||
}
|
||||
|
||||
// oauthToolRenameReverseMap is the inverse of oauthToolRenameMap for response decoding.
|
||||
var oauthToolRenameReverseMap = func() map[string]string {
|
||||
m := make(map[string]string, len(oauthToolRenameMap))
|
||||
for k, v := range oauthToolRenameMap {
|
||||
m[v] = k
|
||||
}
|
||||
return m
|
||||
}()
|
||||
// The reverse map is now computed per-request in remapOAuthToolNames so that
|
||||
// only names the client actually caused us to rewrite are restored on the
|
||||
// response. A global reverse map — as used previously — corrupted responses
|
||||
// for clients that sent mixed casing (e.g. Amp CLI sends `Bash` TitleCase
|
||||
// alongside `glob` lowercase; the request flagged renames via `glob→Glob`,
|
||||
// then the global reverse map incorrectly rewrote every `Bash` in the
|
||||
// response to `bash`, causing Amp to reject the tool_use as unknown).
|
||||
|
||||
// oauthToolsToRemove lists tool names that must be stripped from OAuth requests
|
||||
// even after remapping. Currently empty — all tools are mapped instead of removed.
|
||||
@@ -192,15 +191,9 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
||||
bodyForTranslation := body
|
||||
bodyForUpstream := body
|
||||
oauthToken := isClaudeOAuthToken(apiKey)
|
||||
oauthToolNamesRemapped := false
|
||||
if oauthToken && !auth.ToolPrefixDisabled() {
|
||||
bodyForUpstream = applyClaudeToolPrefix(body, claudeToolPrefix)
|
||||
}
|
||||
// Remap third-party tool names to Claude Code equivalents and remove
|
||||
// tools without official counterparts. This prevents Anthropic from
|
||||
// fingerprinting the request as third-party via tool naming patterns.
|
||||
var oauthToolNamesReverseMap map[string]string
|
||||
if oauthToken {
|
||||
bodyForUpstream, oauthToolNamesRemapped = remapOAuthToolNames(bodyForUpstream)
|
||||
bodyForUpstream, oauthToolNamesReverseMap = prepareClaudeOAuthToolNamesForUpstream(bodyForUpstream, claudeToolPrefix, auth.ToolPrefixDisabled())
|
||||
}
|
||||
// Enable cch signing by default for OAuth tokens (not just experimental flag).
|
||||
// Claude Code always computes cch; missing or invalid cch is a detectable fingerprint.
|
||||
@@ -298,13 +291,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
||||
} else {
|
||||
reporter.Publish(ctx, helps.ParseClaudeUsage(data))
|
||||
}
|
||||
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
||||
data = stripClaudeToolPrefixFromResponse(data, claudeToolPrefix)
|
||||
}
|
||||
// Reverse the OAuth tool name remap so the downstream client sees original names.
|
||||
if isClaudeOAuthToken(apiKey) && oauthToolNamesRemapped {
|
||||
data = reverseRemapOAuthToolNames(data)
|
||||
}
|
||||
data = restoreClaudeOAuthToolNamesFromResponse(data, claudeToolPrefix, auth.ToolPrefixDisabled(), oauthToolNamesReverseMap)
|
||||
var param any
|
||||
out := sdktranslator.TranslateNonStream(
|
||||
ctx,
|
||||
@@ -379,15 +366,9 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
||||
bodyForTranslation := body
|
||||
bodyForUpstream := body
|
||||
oauthToken := isClaudeOAuthToken(apiKey)
|
||||
oauthToolNamesRemapped := false
|
||||
if oauthToken && !auth.ToolPrefixDisabled() {
|
||||
bodyForUpstream = applyClaudeToolPrefix(body, claudeToolPrefix)
|
||||
}
|
||||
// Remap third-party tool names to Claude Code equivalents and remove
|
||||
// tools without official counterparts. This prevents Anthropic from
|
||||
// fingerprinting the request as third-party via tool naming patterns.
|
||||
var oauthToolNamesReverseMap map[string]string
|
||||
if oauthToken {
|
||||
bodyForUpstream, oauthToolNamesRemapped = remapOAuthToolNames(bodyForUpstream)
|
||||
bodyForUpstream, oauthToolNamesReverseMap = prepareClaudeOAuthToolNamesForUpstream(bodyForUpstream, claudeToolPrefix, auth.ToolPrefixDisabled())
|
||||
}
|
||||
// Enable cch signing by default for OAuth tokens (not just experimental flag).
|
||||
if oauthToken || experimentalCCHSigningEnabled(e.cfg, auth) {
|
||||
@@ -478,12 +459,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
||||
if detail, ok := helps.ParseClaudeStreamUsage(line); ok {
|
||||
reporter.Publish(ctx, detail)
|
||||
}
|
||||
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
||||
line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix)
|
||||
}
|
||||
if isClaudeOAuthToken(apiKey) && oauthToolNamesRemapped {
|
||||
line = reverseRemapOAuthToolNamesFromStreamLine(line)
|
||||
}
|
||||
line = restoreClaudeOAuthToolNamesFromStreamLine(line, claudeToolPrefix, auth.ToolPrefixDisabled(), oauthToolNamesReverseMap)
|
||||
// Forward the line as-is to preserve SSE format
|
||||
cloned := make([]byte, len(line)+1)
|
||||
copy(cloned, line)
|
||||
@@ -515,12 +491,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
||||
if detail, ok := helps.ParseClaudeStreamUsage(line); ok {
|
||||
reporter.Publish(ctx, detail)
|
||||
}
|
||||
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
||||
line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix)
|
||||
}
|
||||
if isClaudeOAuthToken(apiKey) && oauthToolNamesRemapped {
|
||||
line = reverseRemapOAuthToolNamesFromStreamLine(line)
|
||||
}
|
||||
line = restoreClaudeOAuthToolNamesFromStreamLine(line, claudeToolPrefix, auth.ToolPrefixDisabled(), oauthToolNamesReverseMap)
|
||||
chunks := sdktranslator.TranslateStream(
|
||||
ctx,
|
||||
to,
|
||||
@@ -635,12 +606,8 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
||||
// Extract betas from body and convert to header (for count_tokens too)
|
||||
var extraBetas []string
|
||||
extraBetas, body = extractAndRemoveBetas(body)
|
||||
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
||||
body = applyClaudeToolPrefix(body, claudeToolPrefix)
|
||||
}
|
||||
// Remap tool names for OAuth token requests to avoid third-party fingerprinting.
|
||||
if isClaudeOAuthToken(apiKey) {
|
||||
body, _ = remapOAuthToolNames(body)
|
||||
body, _ = prepareClaudeOAuthToolNamesForUpstream(body, claudeToolPrefix, auth.ToolPrefixDisabled())
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/messages/count_tokens?beta=true", baseURL)
|
||||
@@ -1080,6 +1047,36 @@ func isClaudeOAuthToken(apiKey string) bool {
|
||||
return strings.Contains(apiKey, "sk-ant-oat")
|
||||
}
|
||||
|
||||
// prepareClaudeOAuthToolNamesForUpstream applies the Claude OAuth tool-name
|
||||
// transforms in the same order across request paths. Remap runs before prefixing
|
||||
// so any future non-empty prefix still composes correctly with the per-request
|
||||
// reverse map.
|
||||
func prepareClaudeOAuthToolNamesForUpstream(body []byte, prefix string, prefixDisabled bool) ([]byte, map[string]string) {
|
||||
body, reverseMap := remapOAuthToolNames(body)
|
||||
if !prefixDisabled {
|
||||
body = applyClaudeToolPrefix(body, prefix)
|
||||
}
|
||||
return body, reverseMap
|
||||
}
|
||||
|
||||
// restoreClaudeOAuthToolNamesFromResponse undoes the Claude OAuth tool-name
|
||||
// transforms for non-stream responses in reverse order.
|
||||
func restoreClaudeOAuthToolNamesFromResponse(body []byte, prefix string, prefixDisabled bool, reverseMap map[string]string) []byte {
|
||||
if !prefixDisabled {
|
||||
body = stripClaudeToolPrefixFromResponse(body, prefix)
|
||||
}
|
||||
return reverseRemapOAuthToolNames(body, reverseMap)
|
||||
}
|
||||
|
||||
// restoreClaudeOAuthToolNamesFromStreamLine undoes the Claude OAuth tool-name
|
||||
// transforms for SSE lines in reverse order.
|
||||
func restoreClaudeOAuthToolNamesFromStreamLine(line []byte, prefix string, prefixDisabled bool, reverseMap map[string]string) []byte {
|
||||
if !prefixDisabled {
|
||||
line = stripClaudeToolPrefixFromStreamLine(line, prefix)
|
||||
}
|
||||
return reverseRemapOAuthToolNamesFromStreamLine(line, reverseMap)
|
||||
}
|
||||
|
||||
// remapOAuthToolNames renames third-party tool names to Claude Code equivalents
|
||||
// and removes tools without an official counterpart. This prevents Anthropic from
|
||||
// fingerprinting the request as a third-party client via tool naming patterns.
|
||||
@@ -1087,8 +1084,25 @@ func isClaudeOAuthToken(apiKey string) bool {
|
||||
// It operates on: tools[].name, tool_choice.name, and all tool_use/tool_reference
|
||||
// references in messages. Removed tools' corresponding tool_result blocks are preserved
|
||||
// (they just become orphaned, which is safe for Claude).
|
||||
func remapOAuthToolNames(body []byte) ([]byte, bool) {
|
||||
renamed := false
|
||||
//
|
||||
// The returned map is keyed on the upstream (TitleCase) name and maps to the
|
||||
// client-supplied original name. Callers MUST pass this map to the reverse
|
||||
// functions so only names the client actually caused us to rewrite are restored
|
||||
// on the response. A global reverse map (the previous implementation) incorrectly
|
||||
// rewrote names the client originally sent in TitleCase (e.g. Amp CLI's `Bash`)
|
||||
// when any OTHER tool in the same request triggered a forward rename (e.g.
|
||||
// Amp's `glob`→`Glob`), because the global reverse map contained `Bash`→`bash`
|
||||
// regardless of what the client originally sent.
|
||||
func remapOAuthToolNames(body []byte) ([]byte, map[string]string) {
|
||||
reverseMap := make(map[string]string, len(oauthToolRenameMap))
|
||||
recordRename := func(original, renamed string) {
|
||||
// Preserve the first-seen original name if the same upstream name is
|
||||
// produced from multiple call sites; they all map back identically.
|
||||
if _, exists := reverseMap[renamed]; !exists {
|
||||
reverseMap[renamed] = original
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Rewrite tools array in a single pass (if present).
|
||||
// IMPORTANT: do not mutate names first and then rebuild from an older gjson
|
||||
// snapshot. gjson results are snapshots of the original bytes; rebuilding from a
|
||||
@@ -1121,7 +1135,7 @@ func remapOAuthToolNames(body []byte) ([]byte, bool) {
|
||||
updatedTool, err := sjson.Set(toolJSON, "name", newName)
|
||||
if err == nil {
|
||||
toolJSON = updatedTool
|
||||
renamed = true
|
||||
recordRename(name, newName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1146,7 +1160,7 @@ func remapOAuthToolNames(body []byte) ([]byte, bool) {
|
||||
body, _ = sjson.DeleteBytes(body, "tool_choice")
|
||||
} else if newName, ok := oauthToolRenameMap[tcName]; ok && newName != tcName {
|
||||
body, _ = sjson.SetBytes(body, "tool_choice.name", newName)
|
||||
renamed = true
|
||||
recordRename(tcName, newName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1166,14 +1180,14 @@ func remapOAuthToolNames(body []byte) ([]byte, bool) {
|
||||
if newName, ok := oauthToolRenameMap[name]; ok && newName != name {
|
||||
path := fmt.Sprintf("messages.%d.content.%d.name", msgIndex.Int(), contentIndex.Int())
|
||||
body, _ = sjson.SetBytes(body, path, newName)
|
||||
renamed = true
|
||||
recordRename(name, newName)
|
||||
}
|
||||
case "tool_reference":
|
||||
toolName := part.Get("tool_name").String()
|
||||
if newName, ok := oauthToolRenameMap[toolName]; ok && newName != toolName {
|
||||
path := fmt.Sprintf("messages.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int())
|
||||
body, _ = sjson.SetBytes(body, path, newName)
|
||||
renamed = true
|
||||
recordRename(toolName, newName)
|
||||
}
|
||||
case "tool_result":
|
||||
// Handle nested tool_reference blocks inside tool_result.content[]
|
||||
@@ -1187,7 +1201,7 @@ func remapOAuthToolNames(body []byte) ([]byte, bool) {
|
||||
if newName, ok := oauthToolRenameMap[nestedToolName]; ok && newName != nestedToolName {
|
||||
nestedPath := fmt.Sprintf("messages.%d.content.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int(), nestedIndex.Int())
|
||||
body, _ = sjson.SetBytes(body, nestedPath, newName)
|
||||
renamed = true
|
||||
recordRename(nestedToolName, newName)
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -1200,13 +1214,16 @@ func remapOAuthToolNames(body []byte) ([]byte, bool) {
|
||||
})
|
||||
}
|
||||
|
||||
return body, renamed
|
||||
return body, reverseMap
|
||||
}
|
||||
|
||||
// reverseRemapOAuthToolNames reverses the tool name mapping for non-stream responses.
|
||||
// It maps Claude Code TitleCase names back to the original lowercase names so the
|
||||
// downstream client receives tool names it recognizes.
|
||||
func reverseRemapOAuthToolNames(body []byte) []byte {
|
||||
// reverseRemapOAuthToolNames reverses the tool name mapping for non-stream responses
|
||||
// using the per-request map produced by remapOAuthToolNames. Names the client sent
|
||||
// that were NOT forward-renamed are passed through unchanged.
|
||||
func reverseRemapOAuthToolNames(body []byte, reverseMap map[string]string) []byte {
|
||||
if len(reverseMap) == 0 {
|
||||
return body
|
||||
}
|
||||
content := gjson.GetBytes(body, "content")
|
||||
if !content.Exists() || !content.IsArray() {
|
||||
return body
|
||||
@@ -1216,13 +1233,13 @@ func reverseRemapOAuthToolNames(body []byte) []byte {
|
||||
switch partType {
|
||||
case "tool_use":
|
||||
name := part.Get("name").String()
|
||||
if origName, ok := oauthToolRenameReverseMap[name]; ok {
|
||||
if origName, ok := reverseMap[name]; ok {
|
||||
path := fmt.Sprintf("content.%d.name", index.Int())
|
||||
body, _ = sjson.SetBytes(body, path, origName)
|
||||
}
|
||||
case "tool_reference":
|
||||
toolName := part.Get("tool_name").String()
|
||||
if origName, ok := oauthToolRenameReverseMap[toolName]; ok {
|
||||
if origName, ok := reverseMap[toolName]; ok {
|
||||
path := fmt.Sprintf("content.%d.tool_name", index.Int())
|
||||
body, _ = sjson.SetBytes(body, path, origName)
|
||||
}
|
||||
@@ -1232,8 +1249,12 @@ func reverseRemapOAuthToolNames(body []byte) []byte {
|
||||
return body
|
||||
}
|
||||
|
||||
// reverseRemapOAuthToolNamesFromStreamLine reverses the tool name mapping for SSE stream lines.
|
||||
func reverseRemapOAuthToolNamesFromStreamLine(line []byte) []byte {
|
||||
// reverseRemapOAuthToolNamesFromStreamLine reverses the tool name mapping for SSE
|
||||
// stream lines, using the per-request reverseMap produced by remapOAuthToolNames.
|
||||
func reverseRemapOAuthToolNamesFromStreamLine(line []byte, reverseMap map[string]string) []byte {
|
||||
if len(reverseMap) == 0 {
|
||||
return line
|
||||
}
|
||||
payload := helps.JSONPayload(line)
|
||||
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
||||
return line
|
||||
@@ -1251,7 +1272,7 @@ func reverseRemapOAuthToolNamesFromStreamLine(line []byte) []byte {
|
||||
switch blockType {
|
||||
case "tool_use":
|
||||
name := contentBlock.Get("name").String()
|
||||
if origName, ok := oauthToolRenameReverseMap[name]; ok {
|
||||
if origName, ok := reverseMap[name]; ok {
|
||||
updated, err = sjson.SetBytes(payload, "content_block.name", origName)
|
||||
if err != nil {
|
||||
return line
|
||||
@@ -1261,7 +1282,7 @@ func reverseRemapOAuthToolNamesFromStreamLine(line []byte) []byte {
|
||||
}
|
||||
case "tool_reference":
|
||||
toolName := contentBlock.Get("tool_name").String()
|
||||
if origName, ok := oauthToolRenameReverseMap[toolName]; ok {
|
||||
if origName, ok := reverseMap[toolName]; ok {
|
||||
updated, err = sjson.SetBytes(payload, "content_block.tool_name", origName)
|
||||
if err != nil {
|
||||
return line
|
||||
|
||||
Reference in New Issue
Block a user