312 lines
8.2 KiB
Go
312 lines
8.2 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
Server ServerConfig `yaml:"server"`
|
|
Providers ProvidersConfig `yaml:"providers"`
|
|
Storage StorageConfig `yaml:"storage"`
|
|
Subagents SubagentsConfig `yaml:"subagents"`
|
|
CORS CORSConfig `yaml:"cors"`
|
|
Anthropic AnthropicConfig
|
|
}
|
|
|
|
type CORSConfig struct {
|
|
AllowedOrigins []string `yaml:"allowed_origins"`
|
|
AllowedMethods []string `yaml:"allowed_methods"`
|
|
AllowedHeaders []string `yaml:"allowed_headers"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Port string `yaml:"port"`
|
|
Timeouts TimeoutsConfig `yaml:"timeouts"`
|
|
// Legacy fields
|
|
ReadTimeout time.Duration
|
|
WriteTimeout time.Duration
|
|
IdleTimeout time.Duration
|
|
}
|
|
|
|
type TimeoutsConfig struct {
|
|
Read string `yaml:"read"`
|
|
Write string `yaml:"write"`
|
|
Idle string `yaml:"idle"`
|
|
}
|
|
|
|
type ProvidersConfig struct {
|
|
Anthropic AnthropicProviderConfig `yaml:"anthropic"`
|
|
OpenAI OpenAIProviderConfig `yaml:"openai"`
|
|
}
|
|
|
|
type AnthropicProviderConfig struct {
|
|
BaseURL string `yaml:"base_url"`
|
|
Version string `yaml:"version"`
|
|
MaxRetries int `yaml:"max_retries"`
|
|
}
|
|
|
|
type OpenAIProviderConfig struct {
|
|
BaseURL string `yaml:"base_url"`
|
|
APIKey string `yaml:"api_key"`
|
|
AllowClientAPIKey bool `yaml:"allow_client_api_key"` // Allow clients to provide their own API key
|
|
ClientAPIKeyHeader string `yaml:"client_api_key_header"` // Header name for client API key (default: x-openai-api-key)
|
|
}
|
|
|
|
type AnthropicConfig struct {
|
|
BaseURL string
|
|
Version string
|
|
MaxRetries int
|
|
}
|
|
|
|
type StorageConfig struct {
|
|
RequestsDir string `yaml:"requests_dir"`
|
|
DBPath string `yaml:"db_path"`
|
|
}
|
|
|
|
type SubagentsConfig struct {
|
|
Enable bool `yaml:"enable"`
|
|
Mappings map[string]string `yaml:"mappings"`
|
|
}
|
|
|
|
func Load() (*Config, error) {
|
|
// Load .env file if it exists
|
|
// Look for .env file in the project root (one level up from proxy/)
|
|
envPath := filepath.Join("..", ".env")
|
|
if err := godotenv.Load(envPath); err != nil {
|
|
// If .env doesn't exist in parent directory, try current directory
|
|
if err := godotenv.Load(".env"); err != nil {
|
|
// .env file is optional, so we just log and continue
|
|
// This allows the app to work with system environment variables only
|
|
}
|
|
}
|
|
|
|
// Start with default configuration
|
|
cfg := &Config{
|
|
Server: ServerConfig{
|
|
Port: "3001",
|
|
ReadTimeout: 600 * time.Second,
|
|
WriteTimeout: 600 * time.Second,
|
|
IdleTimeout: 600 * time.Second,
|
|
},
|
|
Providers: ProvidersConfig{
|
|
Anthropic: AnthropicProviderConfig{
|
|
BaseURL: "https://api.anthropic.com",
|
|
Version: "2023-06-01",
|
|
MaxRetries: 3,
|
|
},
|
|
OpenAI: OpenAIProviderConfig{
|
|
BaseURL: "https://api.openai.com",
|
|
APIKey: "",
|
|
AllowClientAPIKey: false,
|
|
ClientAPIKeyHeader: "x-openai-api-key",
|
|
},
|
|
},
|
|
Storage: StorageConfig{
|
|
DBPath: "requests.db",
|
|
},
|
|
Subagents: SubagentsConfig{
|
|
Enable: false,
|
|
Mappings: make(map[string]string),
|
|
},
|
|
CORS: CORSConfig{
|
|
AllowedOrigins: []string{"*"},
|
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
AllowedHeaders: []string{"*"},
|
|
},
|
|
}
|
|
|
|
if err := loadFirstAvailableConfig(cfg, candidateConfigPaths()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Apply environment variable overrides AFTER loading from file
|
|
if envPort := os.Getenv("PORT"); envPort != "" {
|
|
cfg.Server.Port = envPort
|
|
}
|
|
if envTimeout := os.Getenv("READ_TIMEOUT"); envTimeout != "" {
|
|
cfg.Server.ReadTimeout = getDuration("READ_TIMEOUT", cfg.Server.ReadTimeout)
|
|
}
|
|
if envTimeout := os.Getenv("WRITE_TIMEOUT"); envTimeout != "" {
|
|
cfg.Server.WriteTimeout = getDuration("WRITE_TIMEOUT", cfg.Server.WriteTimeout)
|
|
}
|
|
if envTimeout := os.Getenv("IDLE_TIMEOUT"); envTimeout != "" {
|
|
cfg.Server.IdleTimeout = getDuration("IDLE_TIMEOUT", cfg.Server.IdleTimeout)
|
|
}
|
|
|
|
// Override Anthropic settings
|
|
if envURL := os.Getenv("ANTHROPIC_FORWARD_URL"); envURL != "" {
|
|
cfg.Providers.Anthropic.BaseURL = envURL
|
|
}
|
|
if envVersion := os.Getenv("ANTHROPIC_VERSION"); envVersion != "" {
|
|
cfg.Providers.Anthropic.Version = envVersion
|
|
}
|
|
if envRetries := os.Getenv("ANTHROPIC_MAX_RETRIES"); envRetries != "" {
|
|
cfg.Providers.Anthropic.MaxRetries = getInt("ANTHROPIC_MAX_RETRIES", cfg.Providers.Anthropic.MaxRetries)
|
|
}
|
|
|
|
// Override OpenAI settings
|
|
if envURL := os.Getenv("OPENAI_BASE_URL"); envURL != "" {
|
|
cfg.Providers.OpenAI.BaseURL = envURL
|
|
}
|
|
if envKey := os.Getenv("OPENAI_API_KEY"); envKey != "" {
|
|
cfg.Providers.OpenAI.APIKey = envKey
|
|
}
|
|
if envAllow := os.Getenv("OPENAI_ALLOW_CLIENT_API_KEY"); envAllow != "" {
|
|
cfg.Providers.OpenAI.AllowClientAPIKey = envAllow == "true" || envAllow == "1"
|
|
}
|
|
if envHeader := os.Getenv("OPENAI_CLIENT_API_KEY_HEADER"); envHeader != "" {
|
|
cfg.Providers.OpenAI.ClientAPIKeyHeader = envHeader
|
|
}
|
|
|
|
// Override storage settings
|
|
if envPath := os.Getenv("DB_PATH"); envPath != "" {
|
|
cfg.Storage.DBPath = envPath
|
|
}
|
|
|
|
// Override CORS settings (comma-separated values)
|
|
if envOrigins := os.Getenv("CORS_ALLOWED_ORIGINS"); envOrigins != "" {
|
|
cfg.CORS.AllowedOrigins = splitAndTrim(envOrigins)
|
|
}
|
|
if envMethods := os.Getenv("CORS_ALLOWED_METHODS"); envMethods != "" {
|
|
cfg.CORS.AllowedMethods = splitAndTrim(envMethods)
|
|
}
|
|
if envHeaders := os.Getenv("CORS_ALLOWED_HEADERS"); envHeaders != "" {
|
|
cfg.CORS.AllowedHeaders = splitAndTrim(envHeaders)
|
|
}
|
|
|
|
// Sync legacy Anthropic config
|
|
cfg.Anthropic = AnthropicConfig{
|
|
BaseURL: cfg.Providers.Anthropic.BaseURL,
|
|
Version: cfg.Providers.Anthropic.Version,
|
|
MaxRetries: cfg.Providers.Anthropic.MaxRetries,
|
|
}
|
|
|
|
// After loading from file, apply any timeout conversions if needed
|
|
if cfg.Server.Timeouts.Read != "" {
|
|
if duration, err := time.ParseDuration(cfg.Server.Timeouts.Read); err == nil {
|
|
cfg.Server.ReadTimeout = duration
|
|
}
|
|
}
|
|
if cfg.Server.Timeouts.Write != "" {
|
|
if duration, err := time.ParseDuration(cfg.Server.Timeouts.Write); err == nil {
|
|
cfg.Server.WriteTimeout = duration
|
|
}
|
|
}
|
|
if cfg.Server.Timeouts.Idle != "" {
|
|
if duration, err := time.ParseDuration(cfg.Server.Timeouts.Idle); err == nil {
|
|
cfg.Server.IdleTimeout = duration
|
|
}
|
|
}
|
|
|
|
// Sync legacy Anthropic config with new structure
|
|
cfg.Anthropic = AnthropicConfig{
|
|
BaseURL: cfg.Providers.Anthropic.BaseURL,
|
|
Version: cfg.Providers.Anthropic.Version,
|
|
MaxRetries: cfg.Providers.Anthropic.MaxRetries,
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func (c *Config) loadFromFile(path string) error {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return yaml.Unmarshal(data, c)
|
|
}
|
|
|
|
func candidateConfigPaths() []string {
|
|
paths := []string{
|
|
filepath.Join(filepath.Dir(os.Args[0]), "..", "config.yaml"),
|
|
"config.yaml",
|
|
"../config.yaml",
|
|
"../../config.yaml",
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(paths))
|
|
unique := make([]string, 0, len(paths))
|
|
for _, path := range paths {
|
|
if _, ok := seen[path]; ok {
|
|
continue
|
|
}
|
|
seen[path] = struct{}{}
|
|
unique = append(unique, path)
|
|
}
|
|
|
|
return unique
|
|
}
|
|
|
|
func loadFirstAvailableConfig(cfg *Config, paths []string) error {
|
|
for _, path := range paths {
|
|
if _, err := os.Stat(path); err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return fmt.Errorf("failed to stat config file %q: %w", path, err)
|
|
}
|
|
|
|
if err := cfg.loadFromFile(path); err != nil {
|
|
return fmt.Errorf("failed to load config file %q: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getEnv(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func getDuration(key string, defaultValue time.Duration) time.Duration {
|
|
value := os.Getenv(key)
|
|
if value == "" {
|
|
return defaultValue
|
|
}
|
|
|
|
duration, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
|
|
return duration
|
|
}
|
|
|
|
func getInt(key string, defaultValue int) int {
|
|
value := os.Getenv(key)
|
|
if value == "" {
|
|
return defaultValue
|
|
}
|
|
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
|
|
return intValue
|
|
}
|
|
|
|
func splitAndTrim(s string) []string {
|
|
parts := strings.Split(s, ",")
|
|
result := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
trimmed := strings.TrimSpace(part)
|
|
if trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
return result
|
|
}
|