Files
anthropic-proxy/internal/config/config.go
T

175 lines
4.3 KiB
Go

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 {
Export ExportConfig `yaml:"export"`
Embedded EmbeddedConfig `yaml:"embedded"`
ServiceName string `yaml:"service_name"`
}
type ExportConfig struct {
Endpoint string `yaml:"endpoint"`
Insecure bool `yaml:"insecure"`
Headers map[string]string `yaml:"headers"`
}
func (e ExportConfig) Enabled() bool { return e.Endpoint != "" }
type EmbeddedConfig struct {
Enabled bool `yaml:"enabled"`
Port int `yaml:"port"`
PersesBinary string `yaml:"perses_binary"`
VMBinary string `yaml:"vm_binary"`
VMPort int `yaml:"vm_port"`
BinDir string `yaml:"bin_dir"`
}
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"
}
if cfg.Telemetry.Embedded.Port == 0 {
cfg.Telemetry.Embedded.Port = 8080
}
if cfg.Telemetry.Embedded.PersesBinary == "" {
cfg.Telemetry.Embedded.PersesBinary = "perses"
}
if cfg.Telemetry.Embedded.VMBinary == "" {
cfg.Telemetry.Embedded.VMBinary = "victoria-metrics"
}
if cfg.Telemetry.Embedded.VMPort == 0 {
cfg.Telemetry.Embedded.VMPort = 8428
}
// 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
}