mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-02 21:13:44 +08:00
fix(claude): centralize oauth tool-name transform flow
This commit is contained in:
@@ -191,14 +191,8 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
||||
bodyForUpstream := body
|
||||
oauthToken := isClaudeOAuthToken(apiKey)
|
||||
var oauthToolNamesReverseMap map[string]string
|
||||
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.
|
||||
if oauthToken {
|
||||
bodyForUpstream, oauthToolNamesReverseMap = 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.
|
||||
@@ -292,13 +286,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) && len(oauthToolNamesReverseMap) > 0 {
|
||||
data = reverseRemapOAuthToolNames(data, oauthToolNamesReverseMap)
|
||||
}
|
||||
data = restoreClaudeOAuthToolNamesFromResponse(data, claudeToolPrefix, auth.ToolPrefixDisabled(), oauthToolNamesReverseMap)
|
||||
var param any
|
||||
out := sdktranslator.TranslateNonStream(
|
||||
ctx,
|
||||
@@ -373,14 +361,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
||||
bodyForUpstream := body
|
||||
oauthToken := isClaudeOAuthToken(apiKey)
|
||||
var oauthToolNamesReverseMap map[string]string
|
||||
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.
|
||||
if oauthToken {
|
||||
bodyForUpstream, oauthToolNamesReverseMap = 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) {
|
||||
@@ -471,12 +453,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) && len(oauthToolNamesReverseMap) > 0 {
|
||||
line = reverseRemapOAuthToolNamesFromStreamLine(line, oauthToolNamesReverseMap)
|
||||
}
|
||||
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)
|
||||
@@ -501,12 +478,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) && len(oauthToolNamesReverseMap) > 0 {
|
||||
line = reverseRemapOAuthToolNamesFromStreamLine(line, oauthToolNamesReverseMap)
|
||||
}
|
||||
line = restoreClaudeOAuthToolNamesFromStreamLine(line, claudeToolPrefix, auth.ToolPrefixDisabled(), oauthToolNamesReverseMap)
|
||||
chunks := sdktranslator.TranslateStream(
|
||||
ctx,
|
||||
to,
|
||||
@@ -556,12 +528,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)
|
||||
@@ -1001,6 +969,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.
|
||||
|
||||
@@ -2090,3 +2090,67 @@ func TestReverseRemapOAuthToolNamesFromStreamLine_HonorsPerRequestMap(t *testing
|
||||
t.Fatalf("Glob should be restored to glob, got: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareClaudeOAuthToolNamesForUpstream_MixedCaseWithPrefix(t *testing.T) {
|
||||
body := []byte(`{"tools":[` +
|
||||
`{"name":"Bash","input_schema":{"type":"object","properties":{"cmd":{"type":"string"}}}},` +
|
||||
`{"name":"glob","input_schema":{"type":"object","properties":{"filePattern":{"type":"string"}}}}` +
|
||||
`],"messages":[{"role":"assistant","content":[` +
|
||||
`{"type":"tool_use","id":"toolu_01","name":"Bash","input":{}},` +
|
||||
`{"type":"tool_use","id":"toolu_02","name":"glob","input":{}}` +
|
||||
`]}]}`)
|
||||
|
||||
out, reverseMap := prepareClaudeOAuthToolNamesForUpstream(body, "proxy_", false)
|
||||
|
||||
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "proxy_Bash" {
|
||||
t.Fatalf("tools.0.name = %q, want %q", got, "proxy_Bash")
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.1.name").String(); got != "proxy_Glob" {
|
||||
t.Fatalf("tools.1.name = %q, want %q", got, "proxy_Glob")
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != "proxy_Bash" {
|
||||
t.Fatalf("messages.0.content.0.name = %q, want %q", got, "proxy_Bash")
|
||||
}
|
||||
if got := gjson.GetBytes(out, "messages.0.content.1.name").String(); got != "proxy_Glob" {
|
||||
t.Fatalf("messages.0.content.1.name = %q, want %q", got, "proxy_Glob")
|
||||
}
|
||||
if len(reverseMap) != 1 || reverseMap["Glob"] != "glob" {
|
||||
t.Fatalf("reverseMap = %v, want {Glob:glob}", reverseMap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreClaudeOAuthToolNamesFromResponse_MixedCaseWithPrefix(t *testing.T) {
|
||||
reverseMap := map[string]string{"Glob": "glob"}
|
||||
resp := []byte(`{"content":[` +
|
||||
`{"type":"tool_use","id":"toolu_01","name":"proxy_Bash","input":{}},` +
|
||||
`{"type":"tool_use","id":"toolu_02","name":"proxy_Glob","input":{}}` +
|
||||
`]}`)
|
||||
|
||||
out := restoreClaudeOAuthToolNamesFromResponse(resp, "proxy_", false, reverseMap)
|
||||
|
||||
if got := gjson.GetBytes(out, "content.0.name").String(); got != "Bash" {
|
||||
t.Fatalf("content.0.name = %q, want %q", got, "Bash")
|
||||
}
|
||||
if got := gjson.GetBytes(out, "content.1.name").String(); got != "glob" {
|
||||
t.Fatalf("content.1.name = %q, want %q", got, "glob")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreClaudeOAuthToolNamesFromStreamLine_MixedCaseWithPrefix(t *testing.T) {
|
||||
reverseMap := map[string]string{"Glob": "glob"}
|
||||
|
||||
bashLine := []byte(`data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_01","name":"proxy_Bash","input":{}}}`)
|
||||
out := restoreClaudeOAuthToolNamesFromStreamLine(bashLine, "proxy_", false, reverseMap)
|
||||
if !bytes.Contains(out, []byte(`"name":"Bash"`)) {
|
||||
t.Fatalf("Bash should be preserved, got: %s", string(out))
|
||||
}
|
||||
if bytes.Contains(out, []byte(`"name":"bash"`)) {
|
||||
t.Fatalf("Bash must not be lowercased, got: %s", string(out))
|
||||
}
|
||||
|
||||
globLine := []byte(`data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_02","name":"proxy_Glob","input":{}}}`)
|
||||
out = restoreClaudeOAuthToolNamesFromStreamLine(globLine, "proxy_", false, reverseMap)
|
||||
if !bytes.Contains(out, []byte(`"name":"glob"`)) {
|
||||
t.Fatalf("Glob should be restored to glob, got: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user