909c8b1894
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.
165 lines
3.7 KiB
Go
165 lines
3.7 KiB
Go
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"`
|
|
Sanitize SanitizeConfig `yaml:"sanitize"`
|
|
}
|
|
|
|
type SanitizeConfig struct {
|
|
Tools []RenameRule `yaml:"tools"`
|
|
System []ReplaceRule `yaml:"system"`
|
|
Body []ReplaceRule `yaml:"body"`
|
|
}
|
|
|
|
type RenameRule struct {
|
|
From string `yaml:"from"`
|
|
To string `yaml:"to"`
|
|
}
|
|
|
|
type ReplaceRule struct {
|
|
Match string `yaml:"match"`
|
|
Replace string `yaml:"replace"`
|
|
}
|
|
|
|
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
|
|
}
|