package config import ( "encoding/json" "fmt" "os" "time" "github.com/fujin/anthropic-proxy/internal/auth" "gopkg.in/yaml.v3" ) type Config struct { Port int `yaml:"port"` APIKeys []string `yaml:"api_keys"` ClaudeBinary string `yaml:"claude_binary"` Sanitize SanitizeConfig `yaml:"sanitize"` Logging LoggingConfig `yaml:"logging"` Telemetry TelemetryConfig `yaml:"telemetry"` } 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 TelemetryConfig struct { Endpoint string `yaml:"endpoint"` Insecure bool `yaml:"insecure"` ServiceName string `yaml:"service_name"` Headers map[string]string `yaml:"headers"` } func (t TelemetryConfig) ExportEnabled() bool { return t.Endpoint != "" } type LoggingConfig struct { Level string `yaml:"level"` File string `yaml:"file"` MaxSizeMB int `yaml:"max_size_mb"` MaxBackups int `yaml:"max_backups"` MaxAgeDays int `yaml:"max_age_days"` Compress bool `yaml:"compress"` } 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) } if cfg.Logging.Level == "" { cfg.Logging.Level = "info" } if cfg.Logging.MaxSizeMB == 0 { cfg.Logging.MaxSizeMB = 100 } if cfg.Logging.MaxBackups == 0 { cfg.Logging.MaxBackups = 5 } if cfg.Logging.MaxAgeDays == 0 { cfg.Logging.MaxAgeDays = 30 } if cfg.Telemetry.ServiceName == "" { cfg.Telemetry.ServiceName = "anthropic-proxy" } // Check for deprecated claude_credentials field var rawCfg map[string]interface{} if err := yaml.Unmarshal(data, &rawCfg); err == nil { if _, exists := rawCfg["claude_credentials"]; exists { if val, ok := rawCfg["claude_credentials"].(string); ok && val != "" { return nil, fmt.Errorf("claude_credentials is no longer supported, remove it from config.yaml — the proxy now manages credentials at ~/.claude/.credentials.json") } } } return cfg, nil } func DefaultCredentialPath() string { home, err := os.UserHomeDir() if err != nil { return "" } return home + "/.claude/.credentials.json" } func LoadDefaultCredentials() ([]*auth.Credential, error) { path := DefaultCredentialPath() if path == "" { return nil, nil } data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, 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) } cred := &auth.Credential{ ID: "claude-native", Email: oauth.SubscriptionType, AccessToken: oauth.AccessToken, RefreshToken: oauth.RefreshToken, ExpiresAt: time.UnixMilli(oauth.ExpiresAt), FilePath: path, } return []*auth.Credential{cred}, nil }