Unified · Split
| @@ -179,6 +179,19 @@ func pinClaudeDeviceProfilePlatform(profile, baseline claudeDeviceProfile) claud · 178 unmodified lines | |||
| 179 | return profile | 179 | return profile |
| 180 | } | 180 | } |
| 181 | 181 | ||
| 182 | // normalizeClaudeDeviceProfile keeps stabilized profiles pinned to the current | ||
| 183 | // baseline platform and enforces the baseline software fingerprint as a floor. | ||
| 184 | func normalizeClaudeDeviceProfile(profile, baseline claudeDeviceProfile) claudeDeviceProfile { | ||
| 185 | profile = pinClaudeDeviceProfilePlatform(profile, baseline) | ||
| 186 | if profile.UserAgent == "" || profile.Version == (claudeCLIVersion{}) || shouldUpgradeClaudeDeviceProfile(baseline, profile) { | ||
| 187 | profile.UserAgent = baseline.UserAgent | ||
| 188 | profile.PackageVersion = baseline.PackageVersion | ||
| 189 | profile.RuntimeVersion = baseline.RuntimeVersion | ||
| 190 | profile.Version = baseline.Version | ||
| 191 | } | ||
| 192 | return profile | ||
| 193 | } | ||
| 194 | |||
| 182 | func extractClaudeDeviceProfile(headers http.Header, cfg *config.Config) (claudeDeviceProfile, bool) { | 195 | func extractClaudeDeviceProfile(headers http.Header, cfg *config.Config) (claudeDeviceProfile, bool) { |
| 183 | if headers == nil { | 196 | if headers == nil { |
| 184 | return claudeDeviceProfile{}, false | 197 | return claudeDeviceProfile{}, false |
| @@ -277,7 +290,7 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers · 92 unmodified lines | |||
| 277 | entry, hasCached = claudeDeviceProfileCache[cacheKey] | 290 | entry, hasCached = claudeDeviceProfileCache[cacheKey] |
| 278 | cachedValid = hasCached && entry.expire.After(now) && entry.profile.UserAgent != "" | 291 | cachedValid = hasCached && entry.expire.After(now) && entry.profile.UserAgent != "" |
| 279 | if cachedValid { | 292 | if cachedValid { |
| 280 | entry.profile = pinClaudeDeviceProfilePlatform(entry.profile, baseline) | 293 | entry.profile = normalizeClaudeDeviceProfile(entry.profile, baseline) |
| 281 | } | 294 | } |
| 282 | if cachedValid && !shouldUpgradeClaudeDeviceProfile(candidate, entry.profile) { | 295 | if cachedValid && !shouldUpgradeClaudeDeviceProfile(candidate, entry.profile) { |
| 283 | entry.expire = now.Add(claudeDeviceProfileTTL) | 296 | entry.expire = now.Add(claudeDeviceProfileTTL) |
| @@ -298,7 +311,7 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers · 14 unmodified lines | |||
| 298 | claudeDeviceProfileCacheMu.Lock() | 311 | claudeDeviceProfileCacheMu.Lock() |
| 299 | entry = claudeDeviceProfileCache[cacheKey] | 312 | entry = claudeDeviceProfileCache[cacheKey] |
| 300 | if entry.expire.After(now) && entry.profile.UserAgent != "" { | 313 | if entry.expire.After(now) && entry.profile.UserAgent != "" { |
| 301 | entry.profile = pinClaudeDeviceProfilePlatform(entry.profile, baseline) | 314 | entry.profile = normalizeClaudeDeviceProfile(entry.profile, baseline) |
| 302 | entry.expire = now.Add(claudeDeviceProfileTTL) | 315 | entry.expire = now.Add(claudeDeviceProfileTTL) |
| 303 | claudeDeviceProfileCache[cacheKey] = entry | 316 | claudeDeviceProfileCache[cacheKey] = entry |
| 304 | claudeDeviceProfileCacheMu.Unlock() | 317 | claudeDeviceProfileCacheMu.Unlock() |
| @@ -208,6 +208,58 @@ func TestApplyClaudeHeaders_DoesNotDowngradeConfiguredBaselineOnFirstClaudeClien · 207 unmodified lines | |||
| 208 | assertClaudeFingerprint(t, newerClaudeReq.Header, "claude-cli/2.1.71 (external, cli)", "0.81.0", "v24.6.0", "MacOS", "arm64") | 208 | assertClaudeFingerprint(t, newerClaudeReq.Header, "claude-cli/2.1.71 (external, cli)", "0.81.0", "v24.6.0", "MacOS", "arm64") |
| 209 | } | 209 | } |
| 210 | 210 | ||
| 211 | func TestApplyClaudeHeaders_UpgradesCachedSoftwareFingerprintWhenBaselineAdvances(t *testing.T) { | ||
| 212 | resetClaudeDeviceProfileCache() | ||
| 213 | stabilize := true | ||
| 214 | |||
| 215 | oldCfg := &config.Config{ | ||
| 216 | ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{ | ||
| 217 | UserAgent: "claude-cli/2.1.70 (external, cli)", | ||
| 218 | PackageVersion: "0.80.0", | ||
| 219 | RuntimeVersion: "v24.5.0", | ||
| 220 | OS: "MacOS", | ||
| 221 | Arch: "arm64", | ||
| 222 | StabilizeDeviceProfile: &stabilize, | ||
| 223 | }, | ||
| 224 | } | ||
| 225 | newCfg := &config.Config{ | ||
| 226 | ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{ | ||
| 227 | UserAgent: "claude-cli/2.1.77 (external, cli)", | ||
| 228 | PackageVersion: "0.87.0", | ||
| 229 | RuntimeVersion: "v24.8.0", | ||
| 230 | OS: "MacOS", | ||
| 231 | Arch: "arm64", | ||
| 232 | StabilizeDeviceProfile: &stabilize, | ||
| 233 | }, | ||
| 234 | } | ||
| 235 | auth := &cliproxyauth.Auth{ | ||
| 236 | ID: "auth-baseline-reload", | ||
| 237 | Attributes: map[string]string{ | ||
| 238 | "api_key": "key-baseline-reload", | ||
| 239 | }, | ||
| 240 | } | ||
| 241 | |||
| 242 | officialReq := newClaudeHeaderTestRequest(t, http.Header{ | ||
| 243 | "User-Agent": []string{"claude-cli/2.1.71 (external, cli)"}, | ||
| 244 | "X-Stainless-Package-Version": []string{"0.81.0"}, | ||
| 245 | "X-Stainless-Runtime-Version": []string{"v24.6.0"}, | ||
| 246 | "X-Stainless-Os": []string{"Linux"}, | ||
| 247 | "X-Stainless-Arch": []string{"x64"}, | ||
| 248 | }) | ||
| 249 | applyClaudeHeaders(officialReq, auth, "key-baseline-reload", false, nil, oldCfg) | ||
| 250 | assertClaudeFingerprint(t, officialReq.Header, "claude-cli/2.1.71 (external, cli)", "0.81.0", "v24.6.0", "MacOS", "arm64") | ||
| 251 | |||
| 252 | thirdPartyReq := newClaudeHeaderTestRequest(t, http.Header{ | ||
| 253 | "User-Agent": []string{"curl/8.7.1"}, | ||
| 254 | "X-Stainless-Package-Version": []string{"0.10.0"}, | ||
| 255 | "X-Stainless-Runtime-Version": []string{"v18.0.0"}, | ||
| 256 | "X-Stainless-Os": []string{"Linux"}, | ||
| 257 | "X-Stainless-Arch": []string{"x64"}, | ||
| 258 | }) | ||
| 259 | applyClaudeHeaders(thirdPartyReq, auth, "key-baseline-reload", false, nil, newCfg) | ||
| 260 | assertClaudeFingerprint(t, thirdPartyReq.Header, "claude-cli/2.1.77 (external, cli)", "0.87.0", "v24.8.0", "MacOS", "arm64") | ||
| 261 | } | ||
| 262 | |||
| 211 | func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testing.T) { | 263 | func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testing.T) { |
| 212 | resetClaudeDeviceProfileCache() | 264 | resetClaudeDeviceProfileCache() |
| 213 | stabilize := true | 265 | stabilize := true |