fix(claude): preserve native subagent and environment headers (#4982) (#5084)

This commit is contained in:
sususu98
2026-08-20 00:30:41 +08:00
committed by GitHub
parent 8aa6868d0d
commit 85d2faddd1
3 changed files with 119 additions and 1 deletions

View File

@@ -624,7 +624,10 @@ func copyClaudeCallerFingerprintHeaders(dst, src http.Header) {
lowerName != "x-app" && lowerName != "x-client-request-id" &&
!strings.HasPrefix(lowerName, "anthropic-") &&
!strings.HasPrefix(lowerName, "x-stainless-") &&
!strings.HasPrefix(lowerName, "x-claude-code-") {
!strings.HasPrefix(lowerName, "x-claude-code-") &&
!strings.HasPrefix(lowerName, "x-claude-remote-") &&
lowerName != "x-client-app" &&
lowerName != "x-anthropic-additional-protection" {
continue
}
dst.Del(name)
@@ -880,6 +883,19 @@ func applyClaudeHeadersWithNativeProfile(
}
identityHeader("X-Claude-Code-Session-Id", sessionID)
}
// Preserve native Claude Code subagent and environment headers when present in the incoming request.
for _, hdr := range []string{
"X-Claude-Code-Agent-Id",
"X-Claude-Code-Parent-Agent-Id",
"X-Claude-Remote-Container-Id",
"X-Claude-Remote-Session-Id",
"X-Client-App",
"X-Anthropic-Additional-Protection",
} {
if val := helps.HeaderValueCaseInsensitive(incomingHeaders, hdr); val != "" {
r.Header.Set(hdr, val)
}
}
// Per-request UUID, matches Claude Code's x-client-request-id for first-party API.
// identityHeader prefers the incoming value for a confirmed client, so a confirmed
// helper keeps its own native request ID and this fresh UUID only covers a caller

View File

@@ -6319,3 +6319,100 @@ func TestClaudeExecutor_CacheTTLIsPairedWithExtendedCacheTTLBeta(t *testing.T) {
})
}
}
func TestClaudeExecutor_PreservesNativeAgentAndEnvironmentHeaders(t *testing.T) {
tests := []struct {
name string
incomingHeaders http.Header
wantHeaders map[string]string
wantAbsent []string
}{
{
name: "preserves canonical agent and parent agent headers",
incomingHeaders: http.Header{
"X-Claude-Code-Agent-Id": {"subagent-001"},
"X-Claude-Code-Parent-Agent-Id": {"parent-agent-root"},
},
wantHeaders: map[string]string{
"X-Claude-Code-Agent-Id": "subagent-001",
"X-Claude-Code-Parent-Agent-Id": "parent-agent-root",
},
},
{
name: "preserves lowercased agent and environment headers",
incomingHeaders: http.Header{
"x-claude-code-agent-id": {"agent-xyz"},
"x-claude-remote-container-id": {"container-123"},
"x-claude-remote-session-id": {"remote-sess-456"},
"x-client-app": {"custom-sdk"},
"x-anthropic-additional-protection": {"true"},
},
wantHeaders: map[string]string{
"X-Claude-Code-Agent-Id": "agent-xyz",
"X-Claude-Remote-Container-Id": "container-123",
"X-Claude-Remote-Session-Id": "remote-sess-456",
"X-Client-App": "custom-sdk",
"X-Anthropic-Additional-Protection": "true",
},
},
{
name: "does not fabricate agent header when absent",
incomingHeaders: http.Header{
"User-Agent": {"test-client"},
},
wantAbsent: []string{
"X-Claude-Code-Agent-Id",
"X-Claude-Code-Parent-Agent-Id",
"X-Claude-Remote-Container-Id",
"X-Claude-Remote-Session-Id",
"X-Client-App",
"X-Anthropic-Additional-Protection",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_agent","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "agent-header-test",
Attributes: map[string]string{
"api_key": "sk-ant-test-key",
"base_url": server.URL,
"cloak_mode": "always",
},
Metadata: claudeOAuthTestMetadata(),
}
_, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: []byte(`{"model":"claude-opus-4-6","messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`),
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
Headers: tt.incomingHeaders,
})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
for wantKey, wantVal := range tt.wantHeaders {
if got := seenHeaders.Get(wantKey); got != wantVal {
t.Errorf("header %s = %q, want %q", wantKey, got, wantVal)
}
}
for _, absentKey := range tt.wantAbsent {
if got := seenHeaders.Get(absentKey); got != "" {
t.Errorf("header %s = %q, want absent", absentKey, got)
}
}
})
}
}

View File

@@ -56,6 +56,11 @@ func claudeCodeHeader(ctx context.Context, headers http.Header, name string) str
return ""
}
// HeaderValueCaseInsensitive returns the first non-empty header value matching name case-insensitively.
func HeaderValueCaseInsensitive(headers http.Header, name string) string {
return headerValueCaseInsensitive(headers, name)
}
func headerValueCaseInsensitive(headers http.Header, name string) string {
if headers == nil {
return ""