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:
Alexander
2026-04-09 21:05:32 +02:00
commit c4c1d4daa4
17 changed files with 1417 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/fujin/anthropic-proxy/internal/auth"
"gopkg.in/yaml.v3"
)
type Config struct {
Port int `yaml:"port"`
APIKeys []string `yaml:"api_keys"`
AuthDir string `yaml:"auth_dir"`
ClaudeCredentials string `yaml:"claude_credentials"`
ClaudeBinary string `yaml:"claude_binary"`
}
type authFileJSON struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
Email string `json:"email"`
Expired string `json:"expired"`
Type string `json:"type"`
}
type claudeCredentialsJSON struct {
ClaudeAiOauth struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresAt int64 `json:"expiresAt"`
SubscriptionType string `json:"subscriptionType"`
} `json:"claudeAiOauth"`
}
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config %s: %w", path, err)
}
cfg := &Config{Port: 8080}
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
return cfg, nil
}
func LoadCredentials(cfg *Config) ([]*auth.Credential, error) {
var creds []*auth.Credential
if cfg.ClaudeCredentials != "" {
cred, err := loadClaudeCredentials(cfg.ClaudeCredentials)
if err != nil {
return nil, fmt.Errorf("load claude credentials: %w", err)
}
creds = append(creds, cred)
}
if cfg.AuthDir != "" {
dirCreds, err := loadAuthDir(cfg.AuthDir)
if err != nil {
return nil, fmt.Errorf("load auth dir: %w", err)
}
creds = append(creds, dirCreds...)
}
return creds, nil
}
func loadClaudeCredentials(path string) (*auth.Credential, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cf claudeCredentialsJSON
if err := json.Unmarshal(data, &cf); err != nil {
return nil, err
}
oauth := cf.ClaudeAiOauth
if oauth.AccessToken == "" {
return nil, fmt.Errorf("no access token in %s", path)
}
return &auth.Credential{
ID: "claude-native",
Email: oauth.SubscriptionType,
AccessToken: oauth.AccessToken,
RefreshToken: oauth.RefreshToken,
ExpiresAt: time.UnixMilli(oauth.ExpiresAt),
FilePath: path,
}, nil
}
func loadAuthDir(authDir string) ([]*auth.Credential, error) {
pattern := filepath.Join(authDir, "*.json")
files, err := filepath.Glob(pattern)
if err != nil {
return nil, fmt.Errorf("glob auth files: %w", err)
}
var creds []*auth.Credential
for _, f := range files {
cred, err := loadAuthFile(f)
if err != nil {
return nil, fmt.Errorf("load auth file %s: %w", f, err)
}
creds = append(creds, cred)
}
return creds, nil
}
func loadAuthFile(path string) (*auth.Credential, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var af authFileJSON
if err := json.Unmarshal(data, &af); err != nil {
return nil, err
}
expiresAt, err := time.Parse(time.RFC3339, af.Expired)
if err != nil {
expiresAt, err = time.Parse("2006-01-02T15:04:05", af.Expired)
if err != nil {
expiresAt = time.Now()
}
}
return &auth.Credential{
ID: filepath.Base(path),
Email: af.Email,
AccessToken: af.AccessToken,
RefreshToken: af.RefreshToken,
ExpiresAt: expiresAt,
FilePath: path,
}, nil
}