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,79 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Pool struct {
|
||||
creds []*Credential
|
||||
cursor int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewPool(creds []*Credential) *Pool {
|
||||
return &Pool{creds: creds}
|
||||
}
|
||||
|
||||
func (p *Pool) Pick() (*Credential, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
n := len(p.creds)
|
||||
if n == 0 {
|
||||
return nil, fmt.Errorf("no credentials available")
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
idx := (p.cursor + i) % n
|
||||
cred := p.creds[idx]
|
||||
if !cred.IsOnCooldown() {
|
||||
p.cursor = (idx + 1) % n
|
||||
return cred, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all %d credentials are on cooldown", n)
|
||||
}
|
||||
|
||||
func (p *Pool) MarkFailure(cred *Credential, statusCode int) {
|
||||
switch {
|
||||
case statusCode == 429:
|
||||
cred.SetCooldown(30 * time.Second)
|
||||
case statusCode >= 500:
|
||||
cred.SetCooldown(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) MarkSuccess(cred *Credential) {
|
||||
cred.mu.Lock()
|
||||
defer cred.mu.Unlock()
|
||||
cred.CooldownUntil = time.Time{}
|
||||
}
|
||||
|
||||
func (p *Pool) RefreshExpiring(ctx context.Context) {
|
||||
p.mu.Lock()
|
||||
creds := make([]*Credential, len(p.creds))
|
||||
copy(creds, p.creds)
|
||||
p.mu.Unlock()
|
||||
|
||||
threshold := time.Now().Add(5 * time.Minute)
|
||||
for _, cred := range creds {
|
||||
cred.mu.Lock()
|
||||
needsRefresh := cred.ExpiresAt.Before(threshold)
|
||||
email := cred.Email
|
||||
cred.mu.Unlock()
|
||||
|
||||
if needsRefresh {
|
||||
log.Printf("refreshing token for %s (expires %s)", email, cred.ExpiresAt.Format(time.RFC3339))
|
||||
if err := RefreshToken(ctx, cred); err != nil {
|
||||
log.Printf("failed to refresh token for %s: %v", email, err)
|
||||
} else {
|
||||
log.Printf("refreshed token for %s, new expiry %s", email, cred.ExpiresAt.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user