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,105 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/fujin/anthropic-proxy/internal/auth"
|
||||
)
|
||||
|
||||
const messagesURL = "https://api.anthropic.com/v1/messages?beta=true"
|
||||
|
||||
type UpstreamClient struct {
|
||||
client http.Client
|
||||
sessionID string
|
||||
profile *SniffedProfile
|
||||
}
|
||||
|
||||
func NewUpstreamClient(profile *SniffedProfile) *UpstreamClient {
|
||||
return &UpstreamClient{
|
||||
client: http.Client{
|
||||
Timeout: 0,
|
||||
Transport: newUtlsRoundTripper(),
|
||||
},
|
||||
sessionID: uuid.New().String(),
|
||||
profile: profile,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UpstreamClient) version() string {
|
||||
if u.profile != nil && u.profile.Version != "" {
|
||||
return u.profile.Version
|
||||
}
|
||||
return "2.1.92"
|
||||
}
|
||||
|
||||
// applyHeaders replays sniffed headers, substituting auth + per-request IDs + accept.
|
||||
func (u *UpstreamClient) applyHeaders(req *http.Request, token string, streaming bool) {
|
||||
if u.profile != nil {
|
||||
for _, h := range u.profile.Headers {
|
||||
req.Header.Set(h[0], h[1])
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Del("x-api-key")
|
||||
if strings.HasPrefix(token, "sk-ant-oat") {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
} else {
|
||||
req.Header.Set("x-api-key", token)
|
||||
}
|
||||
|
||||
req.Header.Set("X-Claude-Code-Session-Id", u.sessionID)
|
||||
req.Header.Set("x-client-request-id", uuid.New().String())
|
||||
|
||||
if streaming {
|
||||
req.Header.Set("Accept", "text/event-stream")
|
||||
} else {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
req.Header.Set("Accept-Encoding", "identity")
|
||||
}
|
||||
|
||||
func (u *UpstreamClient) Execute(ctx context.Context, cred *auth.Credential, body []byte) ([]byte, http.Header, int, error) {
|
||||
body = injectBillingHeader(body, u.version())
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, messagesURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, nil, 0, fmt.Errorf("build upstream request: %w", err)
|
||||
}
|
||||
u.applyHeaders(req, cred.Token(), false)
|
||||
|
||||
resp, err := u.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, 0, fmt.Errorf("upstream request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, resp.StatusCode, fmt.Errorf("read upstream response: %w", err)
|
||||
}
|
||||
return respBody, resp.Header, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (u *UpstreamClient) ExecuteStream(ctx context.Context, cred *auth.Credential, body []byte) (*http.Response, error) {
|
||||
body = injectBillingHeader(body, u.version())
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, messagesURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build upstream stream request: %w", err)
|
||||
}
|
||||
u.applyHeaders(req, cred.Token(), true)
|
||||
|
||||
resp, err := u.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upstream stream request: %w", err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
Reference in New Issue
Block a user