Anthropic API proxy with OAuth credential rotation and Claude Code fingerprinting
Sniffs a real Claude Code request on startup to capture exact HTTP headers, then replays them for all proxied requests. Injects the billing header with per-request SHA256 fingerprint into the system prompt. Uses utls with Chrome TLS fingerprint to pass Cloudflare's bot detection on api.anthropic.com. Supports both streaming (SSE) and non-streaming modes, round-robin credential selection with automatic failover, and loading OAuth tokens from both cli-proxy-api auth files and native ~/.claude/.credentials.json.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const fingerprintSalt = "59cf53e54c78"
|
||||
|
||||
func computeFingerprint(firstUserMessage string, version string) string {
|
||||
indices := []int{4, 7, 20}
|
||||
runes := utf16.Encode([]rune(firstUserMessage))
|
||||
var chars string
|
||||
for _, i := range indices {
|
||||
if i < len(runes) {
|
||||
chars += string(rune(runes[i]))
|
||||
} else {
|
||||
chars += "0"
|
||||
}
|
||||
}
|
||||
input := fingerprintSalt + chars + version
|
||||
hash := sha256.Sum256([]byte(input))
|
||||
return hex.EncodeToString(hash[:])[:3]
|
||||
}
|
||||
|
||||
func extractFirstUserMessage(body []byte) string {
|
||||
messages := gjson.GetBytes(body, "messages")
|
||||
if !messages.Exists() || !messages.IsArray() {
|
||||
return ""
|
||||
}
|
||||
for _, msg := range messages.Array() {
|
||||
if msg.Get("role").String() != "user" {
|
||||
continue
|
||||
}
|
||||
content := msg.Get("content")
|
||||
if content.Type == gjson.String {
|
||||
return content.String()
|
||||
}
|
||||
if content.IsArray() {
|
||||
for _, block := range content.Array() {
|
||||
if block.Get("type").String() == "text" {
|
||||
return block.Get("text").String()
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func buildBillingHeader(body []byte, version string) string {
|
||||
userMsg := extractFirstUserMessage(body)
|
||||
fp := computeFingerprint(userMsg, version)
|
||||
return fmt.Sprintf("x-anthropic-billing-header: cc_version=%s.%s; cc_entrypoint=cli; cch=00000;", version, fp)
|
||||
}
|
||||
|
||||
func injectBillingHeader(body []byte, version string) []byte {
|
||||
header := buildBillingHeader(body, version)
|
||||
billingBlock := map[string]interface{}{"type": "text", "text": header}
|
||||
billingJSON, _ := json.Marshal(billingBlock)
|
||||
|
||||
existing := gjson.GetBytes(body, "system")
|
||||
if !existing.Exists() {
|
||||
body, _ = sjson.SetRawBytes(body, "system", []byte("["+string(billingJSON)+"]"))
|
||||
return body
|
||||
}
|
||||
|
||||
if existing.IsArray() {
|
||||
items := make([]json.RawMessage, 0, len(existing.Array())+1)
|
||||
items = append(items, billingJSON)
|
||||
for _, item := range existing.Array() {
|
||||
items = append(items, json.RawMessage(item.Raw))
|
||||
}
|
||||
systemJSON, _ := json.Marshal(items)
|
||||
body, _ = sjson.SetRawBytes(body, "system", systemJSON)
|
||||
return body
|
||||
}
|
||||
|
||||
origText := existing.String()
|
||||
origBlock := map[string]string{"type": "text", "text": origText}
|
||||
origJSON, _ := json.Marshal(origBlock)
|
||||
body, _ = sjson.SetRawBytes(body, "system", []byte("["+string(billingJSON)+","+string(origJSON)+"]"))
|
||||
return body
|
||||
}
|
||||
Reference in New Issue
Block a user