From 9261b0c20b4e4cae8f8ecffe5bbe52f8898cf6f6 Mon Sep 17 00:00:00 2001 From: Kirill Turanskiy Date: Tue, 17 Feb 2026 21:48:19 +0300 Subject: [PATCH] feat: add per-auth tool_prefix_disabled option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow disabling the proxy_ tool name prefix on a per-account basis. Users who route their own Anthropic account through CPA can set "tool_prefix_disabled": true in their OAuth auth JSON to send tool names unchanged to Anthropic. Default behavior is fully preserved — prefix is applied unless explicitly disabled. Changes: - Add ToolPrefixDisabled() accessor to Auth (reads metadata key "tool_prefix_disabled" or "tool-prefix-disabled") - Gate all 6 prefix apply/strip points with the new flag - Add unit tests for the accessor --- internal/runtime/executor/claude_executor.go | 12 +++---- sdk/cliproxy/auth/types.go | 17 ++++++++++ sdk/cliproxy/auth/types_test.go | 35 ++++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 sdk/cliproxy/auth/types_test.go diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 89a366ee..d7a894b9 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -134,7 +134,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r extraBetas, body = extractAndRemoveBetas(body) bodyForTranslation := body bodyForUpstream := body - if isClaudeOAuthToken(apiKey) { + if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { bodyForUpstream = applyClaudeToolPrefix(body, claudeToolPrefix) } @@ -208,7 +208,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r } else { reporter.publish(ctx, parseClaudeUsage(data)) } - if isClaudeOAuthToken(apiKey) { + if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { data = stripClaudeToolPrefixFromResponse(data, claudeToolPrefix) } var param any @@ -275,7 +275,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A extraBetas, body = extractAndRemoveBetas(body) bodyForTranslation := body bodyForUpstream := body - if isClaudeOAuthToken(apiKey) { + if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { bodyForUpstream = applyClaudeToolPrefix(body, claudeToolPrefix) } @@ -348,7 +348,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A if detail, ok := parseClaudeStreamUsage(line); ok { reporter.publish(ctx, detail) } - if isClaudeOAuthToken(apiKey) { + if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix) } // Forward the line as-is to preserve SSE format @@ -375,7 +375,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A if detail, ok := parseClaudeStreamUsage(line); ok { reporter.publish(ctx, detail) } - if isClaudeOAuthToken(apiKey) { + if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix) } chunks := sdktranslator.TranslateStream( @@ -423,7 +423,7 @@ 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) { + if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { body = applyClaudeToolPrefix(body, claudeToolPrefix) } diff --git a/sdk/cliproxy/auth/types.go b/sdk/cliproxy/auth/types.go index b2bbe0a2..96534bbe 100644 --- a/sdk/cliproxy/auth/types.go +++ b/sdk/cliproxy/auth/types.go @@ -213,6 +213,23 @@ func (a *Auth) DisableCoolingOverride() (bool, bool) { return false, false } +// ToolPrefixDisabled returns whether the proxy_ tool name prefix should be +// skipped for this auth. When true, tool names are sent to Anthropic unchanged. +// The value is read from metadata key "tool_prefix_disabled" (or "tool-prefix-disabled"). +func (a *Auth) ToolPrefixDisabled() bool { + if a == nil || a.Metadata == nil { + return false + } + for _, key := range []string{"tool_prefix_disabled", "tool-prefix-disabled"} { + if val, ok := a.Metadata[key]; ok { + if parsed, okParse := parseBoolAny(val); okParse { + return parsed + } + } + } + return false +} + // RequestRetryOverride returns the auth-file scoped request_retry override when present. // The value is read from metadata key "request_retry" (or legacy "request-retry"). func (a *Auth) RequestRetryOverride() (int, bool) { diff --git a/sdk/cliproxy/auth/types_test.go b/sdk/cliproxy/auth/types_test.go new file mode 100644 index 00000000..8249b063 --- /dev/null +++ b/sdk/cliproxy/auth/types_test.go @@ -0,0 +1,35 @@ +package auth + +import "testing" + +func TestToolPrefixDisabled(t *testing.T) { + var a *Auth + if a.ToolPrefixDisabled() { + t.Error("nil auth should return false") + } + + a = &Auth{} + if a.ToolPrefixDisabled() { + t.Error("empty auth should return false") + } + + a = &Auth{Metadata: map[string]any{"tool_prefix_disabled": true}} + if !a.ToolPrefixDisabled() { + t.Error("should return true when set to true") + } + + a = &Auth{Metadata: map[string]any{"tool_prefix_disabled": "true"}} + if !a.ToolPrefixDisabled() { + t.Error("should return true when set to string 'true'") + } + + a = &Auth{Metadata: map[string]any{"tool-prefix-disabled": true}} + if !a.ToolPrefixDisabled() { + t.Error("should return true with kebab-case key") + } + + a = &Auth{Metadata: map[string]any{"tool_prefix_disabled": false}} + if a.ToolPrefixDisabled() { + t.Error("should return false when set to false") + } +}