diff options
| author | Paul Buetow <paul@buetow.org> | 2025-08-19 23:12:56 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-08-19 23:12:56 +0300 |
| commit | fd8e2fe8177305c9271d12c90cc6ad2ed73a1673 (patch) | |
| tree | 362e20bb2ddc20821b1666c207cf073b1ff13f34 /internal/appconfig | |
| parent | 7abb7c9177d34f3b2a1773624f0da7daa8c8e2de (diff) | |
config: add HEXAI_* env overrides with precedence; prefer HEXAI_OPENAI_API_KEY over OPENAI_API_KEY; update docs
Diffstat (limited to 'internal/appconfig')
| -rw-r--r-- | internal/appconfig/config.go | 119 |
1 files changed, 103 insertions, 16 deletions
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go index 3067dd1..58bcc3a 100644 --- a/internal/appconfig/config.go +++ b/internal/appconfig/config.go @@ -2,13 +2,14 @@ package appconfig import ( - "encoding/json" - "fmt" - "log" - "os" - "path/filepath" - "slices" - "strings" + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "slices" + "strconv" + "strings" ) // App holds user-configurable settings read from ~/.config/hexai/config.json. @@ -60,10 +61,10 @@ func newDefaultConfig() App { // Load reads configuration from a file and merges with defaults. // It respects the XDG Base Directory Specification. func Load(logger *log.Logger) App { - cfg := newDefaultConfig() - if logger == nil { - return cfg // Return defaults if no logger is provided (e.g. in tests) - } + cfg := newDefaultConfig() + if logger == nil { + return cfg // Return defaults if no logger is provided (e.g. in tests) + } configPath, err := getConfigPath() if err != nil { @@ -76,8 +77,12 @@ func Load(logger *log.Logger) App { return cfg } - cfg.mergeWith(fileCfg) - return cfg + cfg.mergeWith(fileCfg) + // Environment overrides (take precedence over file) + if envCfg := loadFromEnv(logger); envCfg != nil { + cfg.mergeWith(envCfg) + } + return cfg } // Private helpers @@ -103,8 +108,8 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) { } func (a *App) mergeWith(other *App) { - a.mergeBasics(other) - a.mergeProviderFields(other) + a.mergeBasics(other) + a.mergeProviderFields(other) } // mergeBasics merges general (non-provider) fields. @@ -177,5 +182,87 @@ func getConfigPath() (string, error) { } configPath = filepath.Join(home, ".config", "hexai", "config.json") } - return configPath, nil + return configPath, nil +} + +// --- Environment overrides --- + +// loadFromEnv constructs an App containing only fields set via HEXAI_* env vars. +// These values should take precedence over file config when merged. +func loadFromEnv(logger *log.Logger) *App { + var out App + var any bool + + // helpers + getenv := func(k string) string { return strings.TrimSpace(os.Getenv(k)) } + parseInt := func(k string) (int, bool) { + v := getenv(k) + if v == "" { return 0, false } + n, err := strconv.Atoi(v) + if err != nil { + if logger != nil { logger.Printf("invalid %s: %v", k, err) } + return 0, false + } + return n, true + } + parseFloatPtr := func(k string) (*float64, bool) { + v := getenv(k) + if v == "" { return nil, false } + f, err := strconv.ParseFloat(v, 64) + if err != nil { + if logger != nil { logger.Printf("invalid %s: %v", k, err) } + return nil, false + } + return &f, true + } + + if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok { + out.MaxTokens = n; any = true + } + if s := getenv("HEXAI_CONTEXT_MODE"); s != "" { + out.ContextMode = s; any = true + } + if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok { + out.ContextWindowLines = n; any = true + } + if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok { + out.MaxContextTokens = n; any = true + } + if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok { + out.LogPreviewLimit = n; any = true + } + if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok { + out.CodingTemperature = f; any = true + } + if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" { + parts := strings.Split(s, ",") + out.TriggerCharacters = nil + for _, p := range parts { + if t := strings.TrimSpace(p); t != "" { + out.TriggerCharacters = append(out.TriggerCharacters, t) + } + } + any = true + } + if s := getenv("HEXAI_PROVIDER"); s != "" { + out.Provider = s; any = true + } + + // Provider-specific + if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" { out.OpenAIBaseURL = s; any = true } + if s := getenv("HEXAI_OPENAI_MODEL"); s != "" { out.OpenAIModel = s; any = true } + if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok { out.OpenAITemperature = f; any = true } + + if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" { out.OllamaBaseURL = s; any = true } + if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" { out.OllamaModel = s; any = true } + if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok { out.OllamaTemperature = f; any = true } + + if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" { out.CopilotBaseURL = s; any = true } + if s := getenv("HEXAI_COPILOT_MODEL"); s != "" { out.CopilotModel = s; any = true } + if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok { out.CopilotTemperature = f; any = true } + + if !any { + return nil + } + return &out } |
