Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ nonstream-keepalive-interval: 0
# Default headers for Claude API requests. Update when Claude Code releases new versions.
# These are used as fallbacks when the client does not send its own headers.
# claude-header-defaults:
# user-agent: "claude-cli/2.1.44 (external, sdk-cli)"
# version: "2.1.63"
# user-agent: "claude-cli/2.1.63 (external, cli)"
# package-version: "0.74.0"
# runtime-version: "v24.3.0"
# timeout: "600"
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ type Config struct {
// ClaudeHeaderDefaults configures default header values injected into Claude API requests
// when the client does not send them. Update these when Claude Code releases a new version.
type ClaudeHeaderDefaults struct {
Version string `yaml:"version" json:"version"`
UserAgent string `yaml:"user-agent" json:"user-agent"`
PackageVersion string `yaml:"package-version" json:"package-version"`
RuntimeVersion string `yaml:"runtime-version" json:"runtime-version"`
Expand Down
36 changes: 24 additions & 12 deletions internal/runtime/executor/claude_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type ClaudeExecutor struct {
// claudeToolPrefix is empty to match real Claude Code behavior (no tool name prefix).
// Previously "proxy_" was used but this is a detectable fingerprint difference.
const claudeToolPrefix = ""
const defaultClaudeCodeVersion = "2.1.63"

func NewClaudeExecutor(cfg *config.Config) *ClaudeExecutor { return &ClaudeExecutor{cfg: cfg} }

Expand Down Expand Up @@ -471,7 +472,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
body, _ = sjson.SetBytes(body, "model", baseModel)

if !strings.HasPrefix(baseModel, "claude-3-5-haiku") {
body = checkSystemInstructions(body)
body = checkSystemInstructionsWithMode(body, false, e.cfg)
}

// Keep count_tokens requests compatible with Anthropic cache-control constraints too.
Expand Down Expand Up @@ -865,7 +866,7 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string,
misc.EnsureHeader(r.Header, ginHeaders, "Anthropic-Version", "2023-06-01")
misc.EnsureHeader(r.Header, ginHeaders, "Anthropic-Dangerous-Direct-Browser-Access", "true")
misc.EnsureHeader(r.Header, ginHeaders, "X-App", "cli")
// Values below match Claude Code 2.1.63 / @anthropic-ai/sdk 0.74.0 (updated 2026-02-28).
// Fallback values below match Claude Code 2.1.63 / @anthropic-ai/sdk 0.74.0 (updated 2026-02-28).
misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Retry-Count", "0")
misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Runtime-Version", hdrDefault(hd.RuntimeVersion, "v24.3.0"))
misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Package-Version", hdrDefault(hd.PackageVersion, "0.74.0"))
Expand All @@ -884,7 +885,7 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string,
if isClaudeCodeClient(clientUA) {
r.Header.Set("User-Agent", clientUA)
} else {
r.Header.Set("User-Agent", hdrDefault(hd.UserAgent, "claude-cli/2.1.63 (external, cli)"))
r.Header.Set("User-Agent", hdrDefault(hd.UserAgent, defaultClaudeUserAgent(hd)))
}
r.Header.Set("Connection", "keep-alive")
if stream {
Expand Down Expand Up @@ -928,10 +929,6 @@ func claudeCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
return
}

func checkSystemInstructions(payload []byte) []byte {
return checkSystemInstructionsWithMode(payload, false)
}

func isClaudeOAuthToken(apiKey string) bool {
return strings.Contains(apiKey, "sk-ant-oat")
}
Expand Down Expand Up @@ -1203,10 +1200,21 @@ func injectFakeUserID(payload []byte, apiKey string, useCache bool) []byte {
return payload
}

func claudeCodeVersion(hd config.ClaudeHeaderDefaults) string {
if version := strings.TrimSpace(hd.Version); version != "" {
return version
}
return defaultClaudeCodeVersion
}

func defaultClaudeUserAgent(hd config.ClaudeHeaderDefaults) string {
return fmt.Sprintf("claude-cli/%s (external, cli)", claudeCodeVersion(hd))
}

// generateBillingHeader creates the x-anthropic-billing-header text block that
// real Claude Code prepends to every system prompt array.
// Format: x-anthropic-billing-header: cc_version=<ver>.<build>; cc_entrypoint=cli; cch=<hash>;
func generateBillingHeader(payload []byte) string {
func generateBillingHeader(payload []byte, hd config.ClaudeHeaderDefaults) string {
// Generate a deterministic cch hash from the payload content (system + messages + tools).
// Real Claude Code uses a 5-char hex hash that varies per request.
h := sha256.Sum256(payload)
Expand All @@ -1217,18 +1225,22 @@ func generateBillingHeader(payload []byte) string {
_, _ = rand.Read(buildBytes)
buildHash := hex.EncodeToString(buildBytes)[:3]

return fmt.Sprintf("x-anthropic-billing-header: cc_version=2.1.63.%s; cc_entrypoint=cli; cch=%s;", buildHash, cch)
return fmt.Sprintf("x-anthropic-billing-header: cc_version=%s.%s; cc_entrypoint=cli; cch=%s;", claudeCodeVersion(hd), buildHash, cch)
}

// checkSystemInstructionsWithMode injects Claude Code-style system blocks:
//
// system[0]: billing header (no cache_control)
// system[1]: agent identifier (no cache_control)
// system[2..]: user system messages (cache_control added when missing)
func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte {
func checkSystemInstructionsWithMode(payload []byte, strictMode bool, cfg *config.Config) []byte {
system := gjson.GetBytes(payload, "system")

billingText := generateBillingHeader(payload)
var hd config.ClaudeHeaderDefaults
if cfg != nil {
hd = cfg.ClaudeHeaderDefaults
}
billingText := generateBillingHeader(payload, hd)
billingBlock := fmt.Sprintf(`{"type":"text","text":"%s"}`, billingText)
// No cache_control on the agent block. It is a cloaking artifact with zero cache
// value (the last system block is what actually triggers caching of all system content).
Expand Down Expand Up @@ -1325,7 +1337,7 @@ func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.A

// Skip system instructions for claude-3-5-haiku models
if !strings.HasPrefix(model, "claude-3-5-haiku") {
payload = checkSystemInstructionsWithMode(payload, strictMode)
payload = checkSystemInstructionsWithMode(payload, strictMode, cfg)
}

// Inject fake user ID
Expand Down
97 changes: 92 additions & 5 deletions internal/runtime/executor/claude_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,43 @@ func TestClaudeExecutor_CountTokens_AppliesCacheControlGuards(t *testing.T) {
}
}

func TestClaudeExecutor_CountTokens_UsesConfiguredClaudeVersion(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"input_tokens":42}`))
}))
defer server.Close()

executor := NewClaudeExecutor(&config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
Version: "2.2.0",
},
})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)

_, err := executor.CountTokens(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("CountTokens error: %v", err)
}

if len(seenBody) == 0 {
t.Fatal("expected count_tokens request body to be captured")
}
if got := gjson.GetBytes(seenBody, "system.0.text").String(); !strings.Contains(got, "cc_version=2.2.0.") {
t.Fatalf("count_tokens billing header should use configured Claude version, got %q", got)
}
}

func hasTTLOrderingViolation(payload []byte) bool {
seen5m := false
violates := false
Expand Down Expand Up @@ -985,7 +1022,7 @@ func TestClaudeExecutor_ExecuteStream_GzipErrorBodyNoContentEncodingHeader(t *te
func TestCheckSystemInstructionsWithMode_StringSystemPreserved(t *testing.T) {
payload := []byte(`{"system":"You are a helpful assistant.","messages":[{"role":"user","content":"hi"}]}`)

out := checkSystemInstructionsWithMode(payload, false)
out := checkSystemInstructionsWithMode(payload, false, nil)

system := gjson.GetBytes(out, "system")
if !system.IsArray() {
Expand All @@ -1000,6 +1037,9 @@ func TestCheckSystemInstructionsWithMode_StringSystemPreserved(t *testing.T) {
if !strings.HasPrefix(blocks[0].Get("text").String(), "x-anthropic-billing-header:") {
t.Fatalf("blocks[0] should be billing header, got %q", blocks[0].Get("text").String())
}
if !strings.Contains(blocks[0].Get("text").String(), "cc_version=2.1.63.") {
t.Fatalf("blocks[0] should use default Claude version, got %q", blocks[0].Get("text").String())
}
if blocks[1].Get("text").String() != "You are a Claude agent, built on Anthropic's Claude Agent SDK." {
t.Fatalf("blocks[1] should be agent block, got %q", blocks[1].Get("text").String())
}
Expand All @@ -1015,7 +1055,7 @@ func TestCheckSystemInstructionsWithMode_StringSystemPreserved(t *testing.T) {
func TestCheckSystemInstructionsWithMode_StringSystemStrict(t *testing.T) {
payload := []byte(`{"system":"You are a helpful assistant.","messages":[{"role":"user","content":"hi"}]}`)

out := checkSystemInstructionsWithMode(payload, true)
out := checkSystemInstructionsWithMode(payload, true, nil)

blocks := gjson.GetBytes(out, "system").Array()
if len(blocks) != 2 {
Expand All @@ -1027,7 +1067,7 @@ func TestCheckSystemInstructionsWithMode_StringSystemStrict(t *testing.T) {
func TestCheckSystemInstructionsWithMode_EmptyStringSystemIgnored(t *testing.T) {
payload := []byte(`{"system":"","messages":[{"role":"user","content":"hi"}]}`)

out := checkSystemInstructionsWithMode(payload, false)
out := checkSystemInstructionsWithMode(payload, false, nil)

blocks := gjson.GetBytes(out, "system").Array()
if len(blocks) != 2 {
Expand All @@ -1039,7 +1079,7 @@ func TestCheckSystemInstructionsWithMode_EmptyStringSystemIgnored(t *testing.T)
func TestCheckSystemInstructionsWithMode_ArraySystemStillWorks(t *testing.T) {
payload := []byte(`{"system":[{"type":"text","text":"Be concise."}],"messages":[{"role":"user","content":"hi"}]}`)

out := checkSystemInstructionsWithMode(payload, false)
out := checkSystemInstructionsWithMode(payload, false, nil)

blocks := gjson.GetBytes(out, "system").Array()
if len(blocks) != 3 {
Expand All @@ -1054,7 +1094,7 @@ func TestCheckSystemInstructionsWithMode_ArraySystemStillWorks(t *testing.T) {
func TestCheckSystemInstructionsWithMode_StringWithSpecialChars(t *testing.T) {
payload := []byte(`{"system":"Use <xml> tags & \"quotes\" in output.","messages":[{"role":"user","content":"hi"}]}`)

out := checkSystemInstructionsWithMode(payload, false)
out := checkSystemInstructionsWithMode(payload, false, nil)

blocks := gjson.GetBytes(out, "system").Array()
if len(blocks) != 3 {
Expand All @@ -1064,3 +1104,50 @@ func TestCheckSystemInstructionsWithMode_StringWithSpecialChars(t *testing.T) {
t.Fatalf("blocks[2] text mangled, got %q", blocks[2].Get("text").String())
}
}

func TestCheckSystemInstructionsWithMode_UsesConfiguredClaudeVersion(t *testing.T) {
payload := []byte(`{"system":"You are a helpful assistant.","messages":[{"role":"user","content":"hi"}]}`)
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
Version: "2.2.0",
},
}

out := checkSystemInstructionsWithMode(payload, false, cfg)

billingHeader := gjson.GetBytes(out, "system.0.text").String()
if !strings.Contains(billingHeader, "cc_version=2.2.0.") {
t.Fatalf("billing header should use configured Claude version, got %q", billingHeader)
}
}

func TestApplyClaudeHeaders_UsesConfiguredClaudeVersionForFallbackUserAgent(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://example.com/v1/messages", nil)
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
Version: "2.2.0",
},
}

applyClaudeHeaders(req, nil, "key-123", false, nil, cfg)

if got := req.Header.Get("User-Agent"); got != "claude-cli/2.2.0 (external, cli)" {
t.Fatalf("User-Agent = %q, want %q", got, "claude-cli/2.2.0 (external, cli)")
}
}

func TestApplyClaudeHeaders_UsesConfiguredUserAgent(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "https://example.com/v1/messages", nil)
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
Version: "2.3.4",
UserAgent: "claude-cli/custom-build (external, cli)",
},
}

applyClaudeHeaders(req, nil, "key-123", false, nil, cfg)

if got := req.Header.Get("User-Agent"); got != "claude-cli/custom-build (external, cli)" {
t.Fatalf("User-Agent = %q, want %q", got, "claude-cli/custom-build (external, cli)")
}
}
Loading