Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -663,9 +663,10 @@ nonstream-keepalive-interval: 0
#
# Default headers for Claude API requests. Update only after measuring a new Claude Code release.
# Unconfirmed clients use this CLI baseline. Verified native Claude Code CLI, sdk-cli,
# and VSCode requests preserve their measured entrypoint and software shape only when the
# Claude Code version, package version, and runtime version exactly match this configured
# baseline; unmeasured versions fall back to it. In legacy mode, timeout is a fallback and
# and VSCode requests preserve their measured entrypoint when the Claude Code version is a
# patch release in the configured major/minor line at or above this baseline. Older versions
# and newer major/minor lines fall back to the baseline until measured. In legacy mode,
# timeout is a fallback and
# verified native OS/arch values remain client-supplied. When stabilize-device-profile is
# enabled, OS/arch are pinned to the values below and cached profiles remain constrained to
# the same exact software baseline rather than learning newer client versions.
Expand Down
18 changes: 9 additions & 9 deletions internal/runtime/executor/claude_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ func TestApplyClaudeHeaders_DisableDeviceProfileStabilization(t *testing.T) {
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(firstReq, auth, "key-disable-stability", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, firstReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "MacOS", "arm64")
assertClaudeFingerprint(t, firstReq.Header, "claude-cli/2.1.62 (external, cli)", "0.70.0", "v22.0.0", "Linux", "x64")

thirdPartyReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"lobe-chat/1.0"},
Expand All @@ -619,15 +619,15 @@ func TestApplyClaudeHeaders_DisableDeviceProfileStabilization(t *testing.T) {
applyClaudeHeaders(thirdPartyReq, auth, "key-disable-stability", false, nil, nil, cfg, nil, false)
assertClaudeFingerprint(t, thirdPartyReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "MacOS", "arm64")

lowerReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.61 (external, cli)"},
olderReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.59 (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, nil, cfg, nil, true)
assertClaudeFingerprint(t, lowerReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "MacOS", "arm64")
applyClaudeHeaders(olderReq, auth, "key-disable-stability", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, olderReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "MacOS", "arm64")
}

func TestApplyClaudeHeaders_LegacyModePreservesConfiguredUserAgentOverrideForClaudeClients(t *testing.T) {
Expand Down Expand Up @@ -659,7 +659,7 @@ func TestApplyClaudeHeaders_LegacyModePreservesConfiguredUserAgentOverrideForCla
})
applyClaudeHeaders(req, auth, "key-legacy-ua-override", false, nil, nil, cfg, nil, true)

assertClaudeFingerprint(t, req.Header, "config-ua/1.0", "0.70.0", "v22.0.0", "MacOS", "arm64")
assertClaudeFingerprint(t, req.Header, "config-ua/1.0", "0.70.0", "v22.0.0", "Linux", "x64")
}

func TestApplyClaudeHeaders_LegacyThirdPartyUsesStableConfiguredOSArch(t *testing.T) {
Expand Down Expand Up @@ -862,7 +862,7 @@ func TestClaudeExecutor_NonClaudeRequestUsesClaudeCode220CLIFingerprint(t *testi
}
}

func TestClaudeExecutor_ConfirmedClaudeCodeRequestPreservesInteractiveIdentity(t *testing.T) {
func TestClaudeExecutor_ConfirmedNewerPatchClaudeCodeRequestPreservesInteractiveIdentity(t *testing.T) {
var seenBody []byte
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -877,7 +877,7 @@ func TestClaudeExecutor_ConfirmedClaudeCodeRequestPreservesInteractiveIdentity(t
const userID = `{"device_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","account_uuid":"","session_id":"11111111-2222-4333-8444-555555555555"}`
payload := []byte(`{"model":"claude-opus-4-6","system":[{"type":"text","text":"interactive-system","cache_control":{"type":"ephemeral"}}],"messages":[{"role":"user","content":"x"}],"metadata":{"user_id":` + fmt.Sprintf("%q", userID) + `}}`)
incoming := http.Header{
"User-Agent": {"claude-cli/2.1.258 (external, cli)"},
"User-Agent": {"claude-cli/2.1.263 (external, cli)"},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24"},
"X-Claude-Code-Session-Id": {sessionID},
Expand All @@ -904,7 +904,7 @@ func TestClaudeExecutor_ConfirmedClaudeCodeRequestPreservesInteractiveIdentity(t
t.Fatalf("Execute() error = %v", errExecute)
}

assertClaudeFingerprint(t, seenHeaders, "claude-cli/2.1.258 (external, cli)", "0.112.1", "v26.3.0", "MacOS", "arm64")
assertClaudeFingerprint(t, seenHeaders, "claude-cli/2.1.263 (external, cli)", "0.112.1", "v26.3.0", "MacOS", "arm64")
if got := gjson.GetBytes(seenBody, "system.0.text").String(); got != "interactive-system" {
t.Fatalf("system.0.text = %q, want confirmed client system preserved", got)
}
Expand Down
2 changes: 2 additions & 0 deletions internal/runtime/executor/helps/claude_client_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@ func plausibleClaudeCodeUserAgent(userAgent string, cfg *config.Config) bool {
}
candidate, okCandidate := parseClaudeCLIVersion(userAgent)
baseline, okBaseline := parseClaudeCLIVersion(defaultClaudeDeviceProfile(cfg).UserAgent)
// Patch releases within the measured major/minor line keep the same native
// client contract. Major or minor changes still require a measured baseline.
return okCandidate && okBaseline && plausibleClaudeCLIVersion(candidate, baseline)
}

Expand Down
12 changes: 12 additions & 0 deletions internal/runtime/executor/helps/claude_client_detection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ func TestDetectClaudeCodeRequestRequiresAllFourMessageSignals(t *testing.T) {
}
}

func TestDetectClaudeCodeRequestAcceptsNewerPatchInMeasuredReleaseLine(t *testing.T) {
headers := confirmedClaudeCodeHeaders()
headers.Set("User-Agent", "claude-cli/2.1.263 (external, cli)")
payload := claudeCodeDetectionPayload(validClaudeCodeMetadataUserID)

detection := DetectClaudeCodeRequest(headers, payload, false)
if !detection.Confirmed || !detection.StrongSignals || !detection.NativeClient {
t.Fatalf("detection = %#v, want newer 2.1.x native CLI confirmed", detection)
}
}

func TestDetectClaudeCodeRequestAcceptsConfiguredMeasuredBaseline(t *testing.T) {
headers := confirmedClaudeCodeHeaders()
headers.Set("User-Agent", "claude-cli/2.2.0 (external, cli)")
Expand Down Expand Up @@ -340,6 +351,7 @@ func TestDetectClaudeCodeRequestRejectsMalformedNativeSignals(t *testing.T) {
{name: "uppercase device", headers: confirmedClaudeCodeHeaders(), userID: `{"device_id":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","account_uuid":"","session_id":"11111111-2222-4333-8444-555555555555"}`},
{name: "invalid session", headers: confirmedClaudeCodeHeaders(), userID: `{"device_id":"0000000000000000000000000000000000000000000000000000000000000000","account_uuid":"","session_id":"session"}`},
{name: "malformed user agent", headers: http.Header{"User-Agent": {"claude-cli/not-a-version (external, cli)"}, "X-App": {"cli"}, "Anthropic-Beta": {"claude-code-20250219"}}, userID: validClaudeCodeMetadataUserID},
{name: "older patch user agent", headers: http.Header{"User-Agent": {"claude-cli/2.1.257 (external, cli)"}, "X-App": {"cli"}, "Anthropic-Beta": {"claude-code-20250219"}}, userID: validClaudeCodeMetadataUserID},
{name: "unmeasured next-minor user agent", headers: http.Header{"User-Agent": {"claude-cli/2.2.0 (external, cli)"}, "X-App": {"cli"}, "Anthropic-Beta": {"claude-code-20250219"}}, userID: validClaudeCodeMetadataUserID},
{name: "implausible future user agent", headers: http.Header{"User-Agent": {"claude-cli/999.0.0 (external, cli)"}, "X-App": {"cli"}, "Anthropic-Beta": {"claude-code-20250219"}}, userID: validClaudeCodeMetadataUserID},
{name: "unrelated beta", headers: http.Header{"User-Agent": {"claude-cli/2.1.258 (external, cli)"}, "X-App": {"cli"}, "Anthropic-Beta": {"anything"}}, userID: validClaudeCodeMetadataUserID},
Expand Down
6 changes: 4 additions & 2 deletions internal/runtime/executor/helps/claude_device_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,9 @@ func shouldUpgradeClaudeDeviceProfile(candidate, current ClaudeDeviceProfile) bo
}

func plausibleClaudeCLIVersion(candidate, baseline claudeCLIVersion) bool {
return candidate.Compare(baseline) == 0
return candidate.major == baseline.major &&
candidate.minor == baseline.minor &&
candidate.patch >= baseline.patch
}

func meetsClaudeDeviceProfileBaseline(candidate, baseline ClaudeDeviceProfile) bool {
Expand All @@ -223,7 +225,7 @@ func meetsClaudeDeviceProfileBaseline(candidate, baseline ClaudeDeviceProfile) b
if baseline.UserAgent == "" || !baseline.hasVersion {
return false
}
return plausibleClaudeCLIVersion(candidate.version, baseline.version) &&
return candidate.version.Compare(baseline.version) == 0 &&
candidate.PackageVersion == baseline.PackageVersion &&
candidate.RuntimeVersion == baseline.RuntimeVersion
}
Expand Down
12 changes: 12 additions & 0 deletions internal/runtime/executor/helps/claude_device_profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ func TestResolveClaudeDeviceProfileLocalUsesBaselineForInvalidSignals(t *testing
}
}

func TestResolveClaudeDeviceProfileLocalKeepsExactMeasuredSoftwareBaseline(t *testing.T) {
ResetClaudeDeviceProfileCache()
auth := &cliproxyauth.Auth{ID: "auth-newer-patch-signals"}
headers := claudeDeviceHeaders("claude-cli/2.1.263 (external, cli)")

profile := resolveClaudeDeviceProfileLocal(auth, "api-key", headers, nil)
baseline := defaultClaudeDeviceProfile(nil)
if profile.UserAgent != baseline.UserAgent || profile.PackageVersion != baseline.PackageVersion || profile.RuntimeVersion != baseline.RuntimeVersion {
t.Fatalf("unmeasured profile = %#v, want exact local baseline %#v", profile, baseline)
}
}

func TestApplyClaudeLegacyDeviceHeadersReplacesInvalidNativeSoftwareSignals(t *testing.T) {
request, errRequest := http.NewRequest(http.MethodPost, "https://api.anthropic.com/v1/messages", nil)
if errRequest != nil {
Expand Down
Loading