From dd64adbeeba9e7c19acf57bd0604d29400d2cc1d Mon Sep 17 00:00:00 2001 From: tpob Date: Thu, 19 Mar 2026 00:03:09 +0800 Subject: [PATCH] fix(claude): preserve legacy user agent overrides --- .../runtime/executor/claude_device_profile.go | 12 ++++--- .../runtime/executor/claude_executor_test.go | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/internal/runtime/executor/claude_device_profile.go b/internal/runtime/executor/claude_device_profile.go index a0e8a6ed..9de3689f 100644 --- a/internal/runtime/executor/claude_device_profile.go +++ b/internal/runtime/executor/claude_device_profile.go @@ -308,15 +308,19 @@ func applyClaudeLegacyDeviceHeaders(r *http.Request, ginHeaders http.Header, cfg miscEnsure("X-Stainless-Os", mapStainlessOS()) miscEnsure("X-Stainless-Arch", mapStainlessArch()) + // Legacy mode preserves per-auth custom header overrides. By the time we get + // here, ApplyCustomHeadersFromAttrs has already populated r.Header. + if strings.TrimSpace(r.Header.Get("User-Agent")) != "" { + return + } + clientUA := "" if ginHeaders != nil { - clientUA = ginHeaders.Get("User-Agent") + clientUA = strings.TrimSpace(ginHeaders.Get("User-Agent")) } if isClaudeCodeClient(clientUA) { r.Header.Set("User-Agent", clientUA) return } - if strings.TrimSpace(r.Header.Get("User-Agent")) == "" { - r.Header.Set("User-Agent", profile.UserAgent) - } + r.Header.Set("User-Agent", profile.UserAgent) } diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index 6b1d6400..3ff8fd7b 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -216,6 +216,38 @@ func TestApplyClaudeHeaders_DisableDeviceProfileStabilization(t *testing.T) { assertClaudeFingerprint(t, lowerReq.Header, "claude-cli/2.1.61 (external, cli)", "0.73.0", "v24.2.0", "Windows", "x64") } +func TestApplyClaudeHeaders_LegacyModePreservesConfiguredUserAgentOverrideForClaudeClients(t *testing.T) { + resetClaudeDeviceProfileCache() + + stabilize := false + cfg := &config.Config{ + ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{ + UserAgent: "claude-cli/2.1.60 (external, cli)", + PackageVersion: "0.70.0", + RuntimeVersion: "v22.0.0", + StabilizeDeviceProfile: &stabilize, + }, + } + auth := &cliproxyauth.Auth{ + ID: "auth-legacy-ua-override", + Attributes: map[string]string{ + "api_key": "key-legacy-ua-override", + "header:User-Agent": "config-ua/1.0", + }, + } + + req := newClaudeHeaderTestRequest(t, http.Header{ + "User-Agent": []string{"claude-cli/2.1.62 (external, cli)"}, + "X-Stainless-Package-Version": []string{"0.74.0"}, + "X-Stainless-Runtime-Version": []string{"v24.3.0"}, + "X-Stainless-Os": []string{"Linux"}, + "X-Stainless-Arch": []string{"x64"}, + }) + applyClaudeHeaders(req, auth, "key-legacy-ua-override", false, nil, cfg) + + assertClaudeFingerprint(t, req.Header, "config-ua/1.0", "0.74.0", "v24.3.0", "Linux", "x64") +} + func TestApplyClaudeHeaders_LegacyModeFallsBackToRuntimeOSArchWhenMissing(t *testing.T) { resetClaudeDeviceProfileCache()