diff --git a/README.md b/README.md index 81b9bf5..fe69759 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ cp config.example.yaml config.yaml Edit `config.yaml`: - `api_keys` — key(s) your clients use to authenticate with the proxy - `claude_credentials` — path to your Claude credentials file -- `auth_dir` — optional, directory with additional OAuth credential JSON files - `claude_binary` — path to `claude` binary (used on startup to capture request fingerprint) ## Build and run diff --git a/internal/auth/selector.go b/internal/auth/selector.go index 9e78d96..19c99a4 100644 --- a/internal/auth/selector.go +++ b/internal/auth/selector.go @@ -48,9 +48,7 @@ func (p *Pool) MarkFailure(cred *Credential, statusCode int) { } func (p *Pool) MarkSuccess(cred *Credential) { - cred.mu.Lock() - defer cred.mu.Unlock() - cred.CooldownUntil = time.Time{} + cred.ClearCooldown() } func (p *Pool) RefreshExpiring(ctx context.Context) { diff --git a/internal/auth/types.go b/internal/auth/types.go index 1b857d4..adf12a4 100644 --- a/internal/auth/types.go +++ b/internal/auth/types.go @@ -7,22 +7,15 @@ import ( // Credential represents an Anthropic API credential loaded from a JSON file. type Credential struct { - ID string - Email string - AccessToken string - RefreshToken string - ExpiresAt time.Time - FilePath string - CooldownUntil time.Time - nextRefreshAfter time.Time - mu sync.Mutex -} - -// IsExpired returns true if the credential's access token has expired. -func (c *Credential) IsExpired() bool { - c.mu.Lock() - defer c.mu.Unlock() - return time.Now().After(c.ExpiresAt) + ID string + Email string + AccessToken string + RefreshToken string + ExpiresAt time.Time + FilePath string + CooldownUntil time.Time + nextRefreshAfter time.Time + mu sync.Mutex } // IsOnCooldown returns true if the credential is currently on cooldown. @@ -39,6 +32,13 @@ func (c *Credential) SetCooldown(duration time.Duration) { c.CooldownUntil = time.Now().Add(duration) } +// ClearCooldown removes any active cooldown on the credential. +func (c *Credential) ClearCooldown() { + c.mu.Lock() + defer c.mu.Unlock() + c.CooldownUntil = time.Time{} +} + // Token returns the current access token. func (c *Credential) Token() string { c.mu.Lock() diff --git a/internal/proxy/sniff.go b/internal/proxy/sniff.go index c29b304..20d0a8f 100644 --- a/internal/proxy/sniff.go +++ b/internal/proxy/sniff.go @@ -10,8 +10,6 @@ import ( "strings" "sync" "time" - - "github.com/tidwall/gjson" ) // SniffedProfile holds everything captured from a real Claude Code request. @@ -124,18 +122,15 @@ func SniffClaudeCode(claudeBinary string) (*SniffedProfile, error) { } func extractProfile(r *http.Request, body []byte) *SniffedProfile { - // Capture raw headers preserving original casing and order. + // Capture raw headers preserving original casing. var headers [][2]string - for i := 0; i < len(r.Header); i++ { - for name, vals := range r.Header { - if skipHeaders[strings.ToLower(name)] { - continue - } - for _, v := range vals { - headers = append(headers, [2]string{name, v}) - } + for name, vals := range r.Header { + if skipHeaders[strings.ToLower(name)] { + continue + } + for _, v := range vals { + headers = append(headers, [2]string{name, v}) } - break } // Deduplicate and strip subscription-specific betas. @@ -170,21 +165,6 @@ func extractProfile(r *http.Request, body []byte) *SniffedProfile { } } - // Extract the system prompt template from the body (everything except the billing header block). - // The billing header is the first system block starting with "x-anthropic-billing-header:". - systemBlocks := gjson.GetBytes(body, "system") - var templateSystem []string - if systemBlocks.IsArray() { - for _, block := range systemBlocks.Array() { - text := block.Get("text").String() - if strings.HasPrefix(text, "x-anthropic-billing-header:") { - continue - } - templateSystem = append(templateSystem, block.Raw) - } - } - _ = templateSystem // stored in body for now - return &SniffedProfile{ Headers: deduped, Body: body, diff --git a/internal/server/server.go b/internal/server/server.go index ebfd08b..a4ee0f2 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,7 +7,6 @@ import ( "net/http" "strings" "sync/atomic" - "time" "github.com/gin-gonic/gin" @@ -47,7 +46,6 @@ func New(cfg *config.Config, pool *auth.Pool, profile *proxy.SniffedProfile) *Se engine.POST("/reload", s.handleReload()) engine.POST("/debug/refresh", handleDebugRefresh(pool)) - engine.POST("/debug/shutdown", handleDebugShutdown(s)) engine.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) @@ -100,19 +98,6 @@ func (s *Server) handleReload() gin.HandlerFunc { } } -func handleDebugShutdown(s *Server) gin.HandlerFunc { - return func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "shutting down"}) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := s.Shutdown(ctx); err != nil { - log.Printf("shutdown error: %v", err) - } - }() - } -} - func handleDebugRefresh(pool *auth.Pool) gin.HandlerFunc { return func(c *gin.Context) { results := pool.RefreshAll(c.Request.Context()) @@ -146,7 +131,7 @@ func corsMiddleware() gin.HandlerFunc { func (s *Server) authMiddleware() gin.HandlerFunc { return func(c *gin.Context) { path := c.Request.URL.Path - if path == "/healthz" || path == "/reload" || strings.HasPrefix(path, "/debug/") { + if path == "/healthz" || path == "/reload" { c.Next() return }