Add request sanitizer, background token refresh, and OpenCode support

Sanitizer renames tool names and replaces system prompt patterns
that Anthropic fingerprints to detect non-Claude-Code clients.
Lowercase tool names (bash, read, glob, etc.) combined together
trigger rejection — renaming to PascalCase bypasses this.
Configurable via YAML sanitize rules for tools, system, and body.

Background OAuth token refresh every 30s with 5-minute pre-expiry
lead. Uses Chrome TLS fingerprint for refresh endpoint too.

Adds /messages route (without /v1 prefix) for OpenCode compat.
This commit is contained in:
Alexander
2026-04-09 22:52:43 +02:00
parent c4c1d4daa4
commit 909c8b1894
11 changed files with 428 additions and 89 deletions
+15 -8
View File
@@ -10,10 +10,12 @@ import (
"github.com/tidwall/gjson"
"github.com/fujin/anthropic-proxy/internal/auth"
"github.com/fujin/anthropic-proxy/internal/config"
)
func HandleMessages(pool *auth.Pool, profile *SniffedProfile) gin.HandlerFunc {
func HandleMessages(pool *auth.Pool, profile *SniffedProfile, sanitizeCfg config.SanitizeConfig) gin.HandlerFunc {
upstream := NewUpstreamClient(profile)
san := NewSanitizer(sanitizeCfg)
return func(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
@@ -22,6 +24,10 @@ func HandleMessages(pool *auth.Pool, profile *SniffedProfile) gin.HandlerFunc {
return
}
log.Printf("incoming: %s %s (%d bytes) model=%s", c.Request.Method, c.Request.URL.Path, len(body), gjson.GetBytes(body, "model").String())
body = san.SanitizeRequest(body)
cred, err := pool.Pick()
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()})
@@ -31,14 +37,14 @@ func HandleMessages(pool *auth.Pool, profile *SniffedProfile) gin.HandlerFunc {
isStream := gjson.GetBytes(body, "stream").Bool()
if isStream {
handleStream(c, upstream, pool, cred, body)
handleStream(c, upstream, san, pool, cred, body)
} else {
handleNonStream(c, upstream, pool, cred, body)
handleNonStream(c, upstream, san, pool, cred, body)
}
}
}
func handleNonStream(c *gin.Context, upstream *UpstreamClient, pool *auth.Pool, cred *auth.Credential, body []byte) {
func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte) {
respBody, headers, statusCode, err := upstream.Execute(c.Request.Context(), cred, body)
if err != nil {
log.Printf("upstream error for %s: %v", cred.Email, err)
@@ -48,9 +54,10 @@ func handleNonStream(c *gin.Context, upstream *UpstreamClient, pool *auth.Pool,
if statusCode >= 400 {
pool.MarkFailure(cred, statusCode)
log.Printf("upstream %d for %s", statusCode, cred.Email)
log.Printf("upstream %d for %s: %s", statusCode, cred.Email, string(respBody))
} else {
pool.MarkSuccess(cred)
respBody = san.DesanitizeResponse(respBody)
}
for _, h := range []string{"Content-Type", "X-Request-Id"} {
@@ -62,7 +69,7 @@ func handleNonStream(c *gin.Context, upstream *UpstreamClient, pool *auth.Pool,
c.Data(statusCode, headers.Get("Content-Type"), respBody)
}
func handleStream(c *gin.Context, upstream *UpstreamClient, pool *auth.Pool, cred *auth.Credential, body []byte) {
func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte) {
resp, err := upstream.ExecuteStream(c.Request.Context(), cred, body)
if err != nil {
log.Printf("upstream stream error for %s: %v", cred.Email, err)
@@ -73,8 +80,8 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, pool *auth.Pool, cre
if resp.StatusCode >= 400 {
pool.MarkFailure(cred, resp.StatusCode)
log.Printf("upstream stream %d for %s", resp.StatusCode, cred.Email)
respBody, _ := io.ReadAll(resp.Body)
log.Printf("upstream stream %d for %s: %s", resp.StatusCode, cred.Email, string(respBody))
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody)
return
}
@@ -96,7 +103,7 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, pool *auth.Pool, cre
scanner := bufio.NewScanner(resp.Body)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
line := san.DesanitizeStreamEvent(scanner.Text())
c.Writer.WriteString(line + "\n")
flusher.Flush()
}