diff --git a/internal/runtime/executor/claude_device_profile.go b/internal/runtime/executor/claude_device_profile.go index 9de3689f..44d7069d 100644 --- a/internal/runtime/executor/claude_device_profile.go +++ b/internal/runtime/executor/claude_device_profile.go @@ -158,6 +158,19 @@ func parseClaudeCLIVersion(userAgent string) (claudeCLIVersion, bool) { return claudeCLIVersion{major: major, minor: minor, patch: patch}, true } +func shouldUpgradeClaudeDeviceProfile(candidate, current claudeDeviceProfile) bool { + if candidate.UserAgent == "" { + return false + } + if current.UserAgent == "" { + return true + } + if current.Version == (claudeCLIVersion{}) { + return false + } + return candidate.Version.Compare(current.Version) > 0 +} + func extractClaudeDeviceProfile(headers http.Header, cfg *config.Config) (claudeDeviceProfile, bool) { if headers == nil { return claudeDeviceProfile{}, false @@ -235,13 +248,16 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers now := time.Now() baseline := defaultClaudeDeviceProfile(cfg) candidate, hasCandidate := extractClaudeDeviceProfile(headers, cfg) + if hasCandidate && !shouldUpgradeClaudeDeviceProfile(candidate, baseline) { + hasCandidate = false + } claudeDeviceProfileCacheMu.RLock() entry, hasCached := claudeDeviceProfileCache[cacheKey] cachedValid := hasCached && entry.expire.After(now) && entry.profile.UserAgent != "" claudeDeviceProfileCacheMu.RUnlock() - if hasCandidate && (!cachedValid || candidate.Version.Compare(entry.profile.Version) > 0) { + if hasCandidate && (!cachedValid || shouldUpgradeClaudeDeviceProfile(candidate, entry.profile)) { newEntry := claudeDeviceProfileCacheEntry{ profile: candidate, expire: now.Add(claudeDeviceProfileTTL), diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index 3ff8fd7b..e73b1c06 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -164,6 +164,48 @@ func TestApplyClaudeHeaders_TracksHighestClaudeCLIFingerprint(t *testing.T) { assertClaudeFingerprint(t, lowerReq.Header, "claude-cli/2.1.63 (external, cli)", "0.75.0", "v24.4.0", "MacOS", "arm64") } +func TestApplyClaudeHeaders_DoesNotDowngradeConfiguredBaselineOnFirstClaudeClient(t *testing.T) { + resetClaudeDeviceProfileCache() + stabilize := true + + cfg := &config.Config{ + ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{ + UserAgent: "claude-cli/2.1.70 (external, cli)", + PackageVersion: "0.80.0", + RuntimeVersion: "v24.5.0", + OS: "MacOS", + Arch: "arm64", + StabilizeDeviceProfile: &stabilize, + }, + } + auth := &cliproxyauth.Auth{ + ID: "auth-baseline-floor", + Attributes: map[string]string{ + "api_key": "key-baseline-floor", + }, + } + + olderClaudeReq := 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(olderClaudeReq, auth, "key-baseline-floor", false, nil, cfg) + assertClaudeFingerprint(t, olderClaudeReq.Header, "claude-cli/2.1.70 (external, cli)", "0.80.0", "v24.5.0", "MacOS", "arm64") + + newerClaudeReq := newClaudeHeaderTestRequest(t, http.Header{ + "User-Agent": []string{"claude-cli/2.1.71 (external, cli)"}, + "X-Stainless-Package-Version": []string{"0.81.0"}, + "X-Stainless-Runtime-Version": []string{"v24.6.0"}, + "X-Stainless-Os": []string{"Linux"}, + "X-Stainless-Arch": []string{"x64"}, + }) + applyClaudeHeaders(newerClaudeReq, auth, "key-baseline-floor", false, nil, cfg) + assertClaudeFingerprint(t, newerClaudeReq.Header, "claude-cli/2.1.71 (external, cli)", "0.81.0", "v24.6.0", "Linux", "x64") +} + func TestApplyClaudeHeaders_DisableDeviceProfileStabilization(t *testing.T) { resetClaudeDeviceProfileCache()