feat(claude): add switch for device profile stabilization

This commit is contained in:
tpob
2026-03-18 19:31:59 +08:00
parent d52839fced
commit e0e337aeb9
6 changed files with 142 additions and 19 deletions
+1
View File
@@ -178,6 +178,7 @@ nonstream-keepalive-interval: 0
# os: "MacOS" # os: "MacOS"
# arch: "arm64" # arch: "arm64"
# timeout: "600" # timeout: "600"
# stabilize-device-profile: false # optional, default false; set true to enable per-auth/API-key fingerprint pinning
# Default headers for Codex OAuth model requests. # Default headers for Codex OAuth model requests.
# These are used only for file-backed/OAuth Codex requests when the client # These are used only for file-backed/OAuth Codex requests when the client
@@ -17,6 +17,7 @@ claude-header-defaults:
os: " MacOS " os: " MacOS "
arch: " arm64 " arch: " arm64 "
timeout: " 900 " timeout: " 900 "
stabilize-device-profile: false
`) `)
if err := os.WriteFile(configPath, configYAML, 0o600); err != nil { if err := os.WriteFile(configPath, configYAML, 0o600); err != nil {
t.Fatalf("failed to write config: %v", err) t.Fatalf("failed to write config: %v", err)
@@ -45,4 +46,10 @@ claude-header-defaults:
if got := cfg.ClaudeHeaderDefaults.Timeout; got != "900" { if got := cfg.ClaudeHeaderDefaults.Timeout; got != "900" {
t.Fatalf("Timeout = %q, want %q", got, "900") t.Fatalf("Timeout = %q, want %q", got, "900")
} }
if cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile == nil {
t.Fatal("StabilizeDeviceProfile = nil, want non-nil")
}
if got := *cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile; got {
t.Fatalf("StabilizeDeviceProfile = %v, want false", got)
}
} }
+1
View File
@@ -137,6 +137,7 @@ type ClaudeHeaderDefaults struct {
OS string `yaml:"os" json:"os"` OS string `yaml:"os" json:"os"`
Arch string `yaml:"arch" json:"arch"` Arch string `yaml:"arch" json:"arch"`
Timeout string `yaml:"timeout" json:"timeout"` Timeout string `yaml:"timeout" json:"timeout"`
StabilizeDeviceProfile *bool `yaml:"stabilize-device-profile,omitempty" json:"stabilize-device-profile,omitempty"`
} }
// CodexHeaderDefaults configures fallback header values injected into Codex // CodexHeaderDefaults configures fallback header values injected into Codex
@@ -74,6 +74,13 @@ type claudeDeviceProfileCacheEntry struct {
expire time.Time expire time.Time
} }
func claudeDeviceProfileStabilizationEnabled(cfg *config.Config) bool {
if cfg == nil || cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile == nil {
return false
}
return *cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile
}
func defaultClaudeDeviceProfile(cfg *config.Config) claudeDeviceProfile { func defaultClaudeDeviceProfile(cfg *config.Config) claudeDeviceProfile {
hdrDefault := func(cfgVal, fallback string) string { hdrDefault := func(cfgVal, fallback string) string {
if strings.TrimSpace(cfgVal) != "" { if strings.TrimSpace(cfgVal) != "" {
@@ -248,3 +255,37 @@ func applyClaudeDeviceProfileHeaders(r *http.Request, profile claudeDeviceProfil
r.Header.Set("X-Stainless-Os", profile.OS) r.Header.Set("X-Stainless-Os", profile.OS)
r.Header.Set("X-Stainless-Arch", profile.Arch) r.Header.Set("X-Stainless-Arch", profile.Arch)
} }
func applyClaudeLegacyDeviceHeaders(r *http.Request, ginHeaders http.Header, cfg *config.Config) {
if r == nil {
return
}
profile := defaultClaudeDeviceProfile(cfg)
miscEnsure := func(name, fallback string) {
if strings.TrimSpace(r.Header.Get(name)) != "" {
return
}
if strings.TrimSpace(ginHeaders.Get(name)) != "" {
r.Header.Set(name, strings.TrimSpace(ginHeaders.Get(name)))
return
}
r.Header.Set(name, fallback)
}
miscEnsure("X-Stainless-Runtime-Version", profile.RuntimeVersion)
miscEnsure("X-Stainless-Package-Version", profile.PackageVersion)
miscEnsure("X-Stainless-Os", profile.OS)
miscEnsure("X-Stainless-Arch", profile.Arch)
clientUA := ""
if ginHeaders != nil {
clientUA = 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)
}
}
+9 -1
View File
@@ -793,7 +793,11 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string,
if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
ginHeaders = ginCtx.Request.Header ginHeaders = ginCtx.Request.Header
} }
deviceProfile := resolveClaudeDeviceProfile(auth, apiKey, ginHeaders, cfg) stabilizeDeviceProfile := claudeDeviceProfileStabilizationEnabled(cfg)
var deviceProfile claudeDeviceProfile
if stabilizeDeviceProfile {
deviceProfile = resolveClaudeDeviceProfile(auth, apiKey, ginHeaders, cfg)
}
baseBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05" baseBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05"
if val := strings.TrimSpace(ginHeaders.Get("Anthropic-Beta")); val != "" { if val := strings.TrimSpace(ginHeaders.Get("Anthropic-Beta")); val != "" {
@@ -858,7 +862,11 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string,
attrs = auth.Attributes attrs = auth.Attributes
} }
util.ApplyCustomHeadersFromAttrs(r, attrs) util.ApplyCustomHeadersFromAttrs(r, attrs)
if stabilizeDeviceProfile {
applyClaudeDeviceProfileHeaders(r, deviceProfile) applyClaudeDeviceProfileHeaders(r, deviceProfile)
} else {
applyClaudeLegacyDeviceHeaders(r, ginHeaders, cfg)
}
// Re-enforce Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which // Re-enforce Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which
// may override it with a user-configured value. Compressed SSE breaks the line // may override it with a user-configured value. Compressed SSE breaks the line
// scanner regardless of user preference, so this is non-negotiable for streams. // scanner regardless of user preference, so this is non-negotiable for streams.
@@ -62,6 +62,7 @@ func assertClaudeFingerprint(t *testing.T, headers http.Header, userAgent, pkgVe
func TestApplyClaudeHeaders_UsesConfiguredBaselineFingerprint(t *testing.T) { func TestApplyClaudeHeaders_UsesConfiguredBaselineFingerprint(t *testing.T) {
resetClaudeDeviceProfileCache() resetClaudeDeviceProfileCache()
stabilize := true
cfg := &config.Config{ cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{ ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
@@ -71,6 +72,7 @@ func TestApplyClaudeHeaders_UsesConfiguredBaselineFingerprint(t *testing.T) {
OS: "MacOS", OS: "MacOS",
Arch: "arm64", Arch: "arm64",
Timeout: "900", Timeout: "900",
StabilizeDeviceProfile: &stabilize,
}, },
} }
auth := &cliproxyauth.Auth{ auth := &cliproxyauth.Auth{
@@ -102,6 +104,7 @@ func TestApplyClaudeHeaders_UsesConfiguredBaselineFingerprint(t *testing.T) {
func TestApplyClaudeHeaders_TracksHighestClaudeCLIFingerprint(t *testing.T) { func TestApplyClaudeHeaders_TracksHighestClaudeCLIFingerprint(t *testing.T) {
resetClaudeDeviceProfileCache() resetClaudeDeviceProfileCache()
stabilize := true
cfg := &config.Config{ cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{ ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
@@ -110,6 +113,7 @@ func TestApplyClaudeHeaders_TracksHighestClaudeCLIFingerprint(t *testing.T) {
RuntimeVersion: "v22.0.0", RuntimeVersion: "v22.0.0",
OS: "MacOS", OS: "MacOS",
Arch: "arm64", Arch: "arm64",
StabilizeDeviceProfile: &stabilize,
}, },
} }
auth := &cliproxyauth.Auth{ auth := &cliproxyauth.Auth{
@@ -160,6 +164,67 @@ 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") assertClaudeFingerprint(t, lowerReq.Header, "claude-cli/2.1.63 (external, cli)", "0.75.0", "v24.4.0", "MacOS", "arm64")
} }
func TestApplyClaudeHeaders_DisableDeviceProfileStabilization(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",
OS: "MacOS",
Arch: "arm64",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-disable-stability",
Attributes: map[string]string{
"api_key": "key-disable-stability",
},
}
firstReq := 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(firstReq, auth, "key-disable-stability", false, nil, cfg)
assertClaudeFingerprint(t, firstReq.Header, "claude-cli/2.1.62 (external, cli)", "0.74.0", "v24.3.0", "Linux", "x64")
thirdPartyReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"lobe-chat/1.0"},
"X-Stainless-Package-Version": []string{"0.10.0"},
"X-Stainless-Runtime-Version": []string{"v18.0.0"},
"X-Stainless-Os": []string{"Windows"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(thirdPartyReq, auth, "key-disable-stability", false, nil, cfg)
assertClaudeFingerprint(t, thirdPartyReq.Header, "claude-cli/2.1.60 (external, cli)", "0.10.0", "v18.0.0", "Windows", "x64")
lowerReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.61 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.73.0"},
"X-Stainless-Runtime-Version": []string{"v24.2.0"},
"X-Stainless-Os": []string{"Windows"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(lowerReq, auth, "key-disable-stability", false, nil, cfg)
assertClaudeFingerprint(t, lowerReq.Header, "claude-cli/2.1.61 (external, cli)", "0.73.0", "v24.2.0", "Windows", "x64")
}
func TestClaudeDeviceProfileStabilizationEnabled_DefaultFalse(t *testing.T) {
if claudeDeviceProfileStabilizationEnabled(nil) {
t.Fatal("expected nil config to default to disabled stabilization")
}
if claudeDeviceProfileStabilizationEnabled(&config.Config{}) {
t.Fatal("expected unset stabilize-device-profile to default to disabled stabilization")
}
}
func TestApplyClaudeToolPrefix(t *testing.T) { func TestApplyClaudeToolPrefix(t *testing.T) {
input := []byte(`{"tools":[{"name":"alpha"},{"name":"proxy_bravo"}],"tool_choice":{"type":"tool","name":"charlie"},"messages":[{"role":"assistant","content":[{"type":"tool_use","name":"delta","id":"t1","input":{}}]}]}`) input := []byte(`{"tools":[{"name":"alpha"},{"name":"proxy_bravo"}],"tool_choice":{"type":"tool","name":"charlie"},"messages":[{"role":"assistant","content":[{"type":"tool_use","name":"delta","id":"t1","input":{}}]}]}`)
out := applyClaudeToolPrefix(input, "proxy_") out := applyClaudeToolPrefix(input, "proxy_")