summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-16 03:58:02 +0200
committerPaul Buetow <paul@buetow.org>2026-03-16 03:58:02 +0200
commit52938e05c1ab250cae1c19c29eaa050351559b3b (patch)
tree85c7eab51fc4622838003e32afac1c744c1e1dcf /internal
parent9e8ca4696f4fcbc1657eb7802aa52f8684fab202 (diff)
Split config_load.go into config_load.go and config_env.go
Move environment variable handling functions into config_env.go (269L), keeping config_load.go at 697L. Both well under the 1000-line limit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/appconfig/config_env.go269
-rw-r--r--internal/appconfig/config_load.go261
2 files changed, 269 insertions, 261 deletions
diff --git a/internal/appconfig/config_env.go b/internal/appconfig/config_env.go
new file mode 100644
index 0000000..5f576e0
--- /dev/null
+++ b/internal/appconfig/config_env.go
@@ -0,0 +1,269 @@
+package appconfig
+
+import (
+ "log"
+ "os"
+ "strconv"
+ "strings"
+)
+
+// --- 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
+ any := applyCoreEnv(&out, logger)
+ any = applyProviderEnv(&out, logger) || any
+ any = applySurfaceEnv(&out, logger) || any
+ any = applyIgnoreEnv(&out) || any
+ any = applyMCPEnv(&out) || any
+ if !any {
+ return nil
+ }
+ return &out
+}
+
+func applyCoreEnv(out *App, logger *log.Logger) bool {
+ any := false
+ any = applyEnvInt(&out.MaxTokens, "HEXAI_MAX_TOKENS", logger) || any
+ any = applyEnvString(&out.ContextMode, "HEXAI_CONTEXT_MODE") || any
+ any = applyEnvInt(&out.ContextWindowLines, "HEXAI_CONTEXT_WINDOW_LINES", logger) || any
+ any = applyEnvInt(&out.MaxContextTokens, "HEXAI_MAX_CONTEXT_TOKENS", logger) || any
+ any = applyEnvInt(&out.LogPreviewLimit, "HEXAI_LOG_PREVIEW_LIMIT", logger) || any
+ any = applyEnvInt(&out.RequestTimeout, "HEXAI_REQUEST_TIMEOUT", logger) || any
+ any = applyEnvInt(&out.ManualInvokeMinPrefix, "HEXAI_MANUAL_INVOKE_MIN_PREFIX", logger) || any
+ any = applyEnvInt(&out.CompletionDebounceMs, "HEXAI_COMPLETION_DEBOUNCE_MS", logger) || any
+ any = applyEnvInt(&out.CompletionThrottleMs, "HEXAI_COMPLETION_THROTTLE_MS", logger) || any
+ any = applyEnvFloat(&out.CodingTemperature, "HEXAI_CODING_TEMPERATURE", logger) || any
+ any = applyEnvCSV(&out.TriggerCharacters, "HEXAI_TRIGGER_CHARACTERS") || any
+ any = applyEnvString(&out.InlineOpen, "HEXAI_INLINE_OPEN") || any
+ any = applyEnvString(&out.InlineClose, "HEXAI_INLINE_CLOSE") || any
+ any = applyEnvString(&out.ChatSuffix, "HEXAI_CHAT_SUFFIX") || any
+ any = applyEnvCSV(&out.ChatPrefixes, "HEXAI_CHAT_PREFIXES") || any
+ any = applyEnvString(&out.Provider, "HEXAI_PROVIDER") || any
+ return any
+}
+
+func applyProviderEnv(out *App, logger *log.Logger) bool {
+ picker := newModelPicker(out.Provider)
+ any := false
+ any = applyEnvString(&out.OpenAIBaseURL, "HEXAI_OPENAI_BASE_URL") || any
+ if model, ok := picker.pick("openai", getenvTrim("HEXAI_OPENAI_MODEL")); ok {
+ out.OpenAIModel = model
+ any = true
+ }
+ any = applyEnvFloat(&out.OpenAITemperature, "HEXAI_OPENAI_TEMPERATURE", logger) || any
+
+ any = applyEnvString(&out.OpenRouterBaseURL, "HEXAI_OPENROUTER_BASE_URL") || any
+ if model, ok := picker.pick("openrouter", getenvTrim("HEXAI_OPENROUTER_MODEL")); ok {
+ out.OpenRouterModel = model
+ any = true
+ }
+ any = applyEnvFloat(&out.OpenRouterTemperature, "HEXAI_OPENROUTER_TEMPERATURE", logger) || any
+
+ any = applyEnvString(&out.OllamaBaseURL, "HEXAI_OLLAMA_BASE_URL") || any
+ if model, ok := picker.pick("ollama", getenvTrim("HEXAI_OLLAMA_MODEL")); ok {
+ out.OllamaModel = model
+ any = true
+ }
+ any = applyEnvFloat(&out.OllamaTemperature, "HEXAI_OLLAMA_TEMPERATURE", logger) || any
+
+ any = applyEnvString(&out.AnthropicBaseURL, "HEXAI_ANTHROPIC_BASE_URL") || any
+ if model, ok := picker.pick("anthropic", getenvTrim("HEXAI_ANTHROPIC_MODEL")); ok {
+ out.AnthropicModel = model
+ any = true
+ }
+ any = applyEnvFloat(&out.AnthropicTemperature, "HEXAI_ANTHROPIC_TEMPERATURE", logger) || any
+ return any
+}
+
+func applySurfaceEnv(out *App, logger *log.Logger) bool {
+ any := false
+ if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_COMPLETION", "HEXAI_TEMPERATURE_COMPLETION", "HEXAI_PROVIDER_COMPLETION", logger); ok {
+ out.CompletionConfigs = entries
+ any = true
+ }
+ if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CODE_ACTION", "HEXAI_TEMPERATURE_CODE_ACTION", "HEXAI_PROVIDER_CODE_ACTION", logger); ok {
+ out.CodeActionConfigs = entries
+ any = true
+ }
+ if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CHAT", "HEXAI_TEMPERATURE_CHAT", "HEXAI_PROVIDER_CHAT", logger); ok {
+ out.ChatConfigs = entries
+ any = true
+ }
+ if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CLI", "HEXAI_TEMPERATURE_CLI", "HEXAI_PROVIDER_CLI", logger); ok {
+ out.CLIConfigs = entries
+ any = true
+ }
+ return any
+}
+
+func applyIgnoreEnv(out *App) bool {
+ any := false
+ any = applyEnvBoolPtr(&out.IgnoreGitignore, "HEXAI_IGNORE_GITIGNORE") || any
+ any = applyEnvCSV(&out.IgnoreExtraPatterns, "HEXAI_IGNORE_EXTRA_PATTERNS") || any
+ any = applyEnvBoolPtr(&out.IgnoreLSPNotify, "HEXAI_IGNORE_LSP_NOTIFY") || any
+ return any
+}
+
+func applyMCPEnv(out *App) bool {
+ any := false
+ any = applyEnvString(&out.MCPPromptsDir, "HEXAI_MCP_PROMPTS_DIR") || any
+ any = applyEnvBool(&out.MCPSlashCommandSync, "HEXAI_MCP_SLASHCOMMAND_SYNC") || any
+ any = applyEnvString(&out.MCPSlashCommandDir, "HEXAI_MCP_SLASHCOMMAND_DIR") || any
+ return any
+}
+
+func buildSurfaceEntryFromEnv(modelKey, tempKey, providerKey string, logger *log.Logger) ([]SurfaceConfig, bool) {
+ model := getenvTrim(modelKey)
+ tempPtr, tempSet := parseEnvFloatPtr(tempKey, logger)
+ provider := getenvTrim(providerKey)
+ if model == "" && provider == "" && !tempSet {
+ return nil, false
+ }
+ entry := SurfaceConfig{Provider: provider, Model: model}
+ if tempSet {
+ entry.Temperature = tempPtr
+ }
+ return []SurfaceConfig{entry}, true
+}
+
+func applyEnvString(target *string, key string) bool {
+ value := getenvTrim(key)
+ if value == "" {
+ return false
+ }
+ *target = value
+ return true
+}
+
+func applyEnvInt(target *int, key string, logger *log.Logger) bool {
+ value, ok := parseEnvInt(key, logger)
+ if !ok {
+ return false
+ }
+ *target = value
+ return true
+}
+
+func applyEnvFloat(target **float64, key string, logger *log.Logger) bool {
+ value, ok := parseEnvFloatPtr(key, logger)
+ if !ok {
+ return false
+ }
+ *target = value
+ return true
+}
+
+func applyEnvCSV(target *[]string, key string) bool {
+ value := getenvTrim(key)
+ if value == "" {
+ return false
+ }
+ parts := strings.Split(value, ",")
+ *target = nil
+ for _, p := range parts {
+ if t := strings.TrimSpace(p); t != "" {
+ *target = append(*target, t)
+ }
+ }
+ return true
+}
+
+func applyEnvBool(target *bool, key string) bool {
+ value := getenvTrim(key)
+ if value == "" {
+ return false
+ }
+ *target = value == "true" || value == "1"
+ return true
+}
+
+func applyEnvBoolPtr(target **bool, key string) bool {
+ value := getenvTrim(key)
+ if value == "" {
+ return false
+ }
+ parsed := value == "true" || value == "1"
+ *target = &parsed
+ return true
+}
+
+func getenvTrim(key string) string {
+ return strings.TrimSpace(os.Getenv(key))
+}
+
+func parseEnvInt(key string, logger *log.Logger) (int, bool) {
+ value := getenvTrim(key)
+ if value == "" {
+ return 0, false
+ }
+ n, err := strconv.Atoi(value)
+ if err != nil {
+ if logger != nil {
+ logger.Printf("invalid %s: %v", key, err)
+ }
+ return 0, false
+ }
+ return n, true
+}
+
+func parseEnvFloatPtr(key string, logger *log.Logger) (*float64, bool) {
+ value := getenvTrim(key)
+ if value == "" {
+ return nil, false
+ }
+ f, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ if logger != nil {
+ logger.Printf("invalid %s: %v", key, err)
+ }
+ return nil, false
+ }
+ return &f, true
+}
+
+type modelPicker struct {
+ providerLower string
+ modelForce string
+ modelGeneric string
+ forceUsed bool
+ genericUsed bool
+}
+
+func newModelPicker(provider string) *modelPicker {
+ return &modelPicker{
+ providerLower: strings.ToLower(strings.TrimSpace(provider)),
+ modelForce: getenvTrim("HEXAI_MODEL_FORCE"),
+ modelGeneric: getenvTrim("HEXAI_MODEL"),
+ }
+}
+
+func (p *modelPicker) pick(providerName, specific string) (string, bool) {
+ specific = strings.TrimSpace(specific)
+ nameLower := strings.ToLower(strings.TrimSpace(providerName))
+ if p.modelForce != "" {
+ if p.providerLower == nameLower {
+ p.forceUsed = true
+ return p.modelForce, true
+ }
+ if p.providerLower == "" && !p.forceUsed {
+ p.forceUsed = true
+ return p.modelForce, true
+ }
+ }
+ if specific != "" {
+ return specific, true
+ }
+ if p.modelGeneric != "" {
+ if p.providerLower == nameLower {
+ return p.modelGeneric, true
+ }
+ if p.providerLower == "" && !p.genericUsed {
+ p.genericUsed = true
+ return p.modelGeneric, true
+ }
+ }
+ return "", false
+}
diff --git a/internal/appconfig/config_load.go b/internal/appconfig/config_load.go
index b098b48..99461ca 100644
--- a/internal/appconfig/config_load.go
+++ b/internal/appconfig/config_load.go
@@ -695,264 +695,3 @@ func floatPtr(v float64) *float64 {
f := v
return &f
}
-
-// --- 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
- any := applyCoreEnv(&out, logger)
- any = applyProviderEnv(&out, logger) || any
- any = applySurfaceEnv(&out, logger) || any
- any = applyIgnoreEnv(&out) || any
- any = applyMCPEnv(&out) || any
- if !any {
- return nil
- }
- return &out
-}
-
-func applyCoreEnv(out *App, logger *log.Logger) bool {
- any := false
- any = applyEnvInt(&out.MaxTokens, "HEXAI_MAX_TOKENS", logger) || any
- any = applyEnvString(&out.ContextMode, "HEXAI_CONTEXT_MODE") || any
- any = applyEnvInt(&out.ContextWindowLines, "HEXAI_CONTEXT_WINDOW_LINES", logger) || any
- any = applyEnvInt(&out.MaxContextTokens, "HEXAI_MAX_CONTEXT_TOKENS", logger) || any
- any = applyEnvInt(&out.LogPreviewLimit, "HEXAI_LOG_PREVIEW_LIMIT", logger) || any
- any = applyEnvInt(&out.RequestTimeout, "HEXAI_REQUEST_TIMEOUT", logger) || any
- any = applyEnvInt(&out.ManualInvokeMinPrefix, "HEXAI_MANUAL_INVOKE_MIN_PREFIX", logger) || any
- any = applyEnvInt(&out.CompletionDebounceMs, "HEXAI_COMPLETION_DEBOUNCE_MS", logger) || any
- any = applyEnvInt(&out.CompletionThrottleMs, "HEXAI_COMPLETION_THROTTLE_MS", logger) || any
- any = applyEnvFloat(&out.CodingTemperature, "HEXAI_CODING_TEMPERATURE", logger) || any
- any = applyEnvCSV(&out.TriggerCharacters, "HEXAI_TRIGGER_CHARACTERS") || any
- any = applyEnvString(&out.InlineOpen, "HEXAI_INLINE_OPEN") || any
- any = applyEnvString(&out.InlineClose, "HEXAI_INLINE_CLOSE") || any
- any = applyEnvString(&out.ChatSuffix, "HEXAI_CHAT_SUFFIX") || any
- any = applyEnvCSV(&out.ChatPrefixes, "HEXAI_CHAT_PREFIXES") || any
- any = applyEnvString(&out.Provider, "HEXAI_PROVIDER") || any
- return any
-}
-
-func applyProviderEnv(out *App, logger *log.Logger) bool {
- picker := newModelPicker(out.Provider)
- any := false
- any = applyEnvString(&out.OpenAIBaseURL, "HEXAI_OPENAI_BASE_URL") || any
- if model, ok := picker.pick("openai", getenvTrim("HEXAI_OPENAI_MODEL")); ok {
- out.OpenAIModel = model
- any = true
- }
- any = applyEnvFloat(&out.OpenAITemperature, "HEXAI_OPENAI_TEMPERATURE", logger) || any
-
- any = applyEnvString(&out.OpenRouterBaseURL, "HEXAI_OPENROUTER_BASE_URL") || any
- if model, ok := picker.pick("openrouter", getenvTrim("HEXAI_OPENROUTER_MODEL")); ok {
- out.OpenRouterModel = model
- any = true
- }
- any = applyEnvFloat(&out.OpenRouterTemperature, "HEXAI_OPENROUTER_TEMPERATURE", logger) || any
-
- any = applyEnvString(&out.OllamaBaseURL, "HEXAI_OLLAMA_BASE_URL") || any
- if model, ok := picker.pick("ollama", getenvTrim("HEXAI_OLLAMA_MODEL")); ok {
- out.OllamaModel = model
- any = true
- }
- any = applyEnvFloat(&out.OllamaTemperature, "HEXAI_OLLAMA_TEMPERATURE", logger) || any
-
- any = applyEnvString(&out.AnthropicBaseURL, "HEXAI_ANTHROPIC_BASE_URL") || any
- if model, ok := picker.pick("anthropic", getenvTrim("HEXAI_ANTHROPIC_MODEL")); ok {
- out.AnthropicModel = model
- any = true
- }
- any = applyEnvFloat(&out.AnthropicTemperature, "HEXAI_ANTHROPIC_TEMPERATURE", logger) || any
- return any
-}
-
-func applySurfaceEnv(out *App, logger *log.Logger) bool {
- any := false
- if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_COMPLETION", "HEXAI_TEMPERATURE_COMPLETION", "HEXAI_PROVIDER_COMPLETION", logger); ok {
- out.CompletionConfigs = entries
- any = true
- }
- if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CODE_ACTION", "HEXAI_TEMPERATURE_CODE_ACTION", "HEXAI_PROVIDER_CODE_ACTION", logger); ok {
- out.CodeActionConfigs = entries
- any = true
- }
- if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CHAT", "HEXAI_TEMPERATURE_CHAT", "HEXAI_PROVIDER_CHAT", logger); ok {
- out.ChatConfigs = entries
- any = true
- }
- if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CLI", "HEXAI_TEMPERATURE_CLI", "HEXAI_PROVIDER_CLI", logger); ok {
- out.CLIConfigs = entries
- any = true
- }
- return any
-}
-
-func applyIgnoreEnv(out *App) bool {
- any := false
- any = applyEnvBoolPtr(&out.IgnoreGitignore, "HEXAI_IGNORE_GITIGNORE") || any
- any = applyEnvCSV(&out.IgnoreExtraPatterns, "HEXAI_IGNORE_EXTRA_PATTERNS") || any
- any = applyEnvBoolPtr(&out.IgnoreLSPNotify, "HEXAI_IGNORE_LSP_NOTIFY") || any
- return any
-}
-
-func applyMCPEnv(out *App) bool {
- any := false
- any = applyEnvString(&out.MCPPromptsDir, "HEXAI_MCP_PROMPTS_DIR") || any
- any = applyEnvBool(&out.MCPSlashCommandSync, "HEXAI_MCP_SLASHCOMMAND_SYNC") || any
- any = applyEnvString(&out.MCPSlashCommandDir, "HEXAI_MCP_SLASHCOMMAND_DIR") || any
- return any
-}
-
-func buildSurfaceEntryFromEnv(modelKey, tempKey, providerKey string, logger *log.Logger) ([]SurfaceConfig, bool) {
- model := getenvTrim(modelKey)
- tempPtr, tempSet := parseEnvFloatPtr(tempKey, logger)
- provider := getenvTrim(providerKey)
- if model == "" && provider == "" && !tempSet {
- return nil, false
- }
- entry := SurfaceConfig{Provider: provider, Model: model}
- if tempSet {
- entry.Temperature = tempPtr
- }
- return []SurfaceConfig{entry}, true
-}
-
-func applyEnvString(target *string, key string) bool {
- value := getenvTrim(key)
- if value == "" {
- return false
- }
- *target = value
- return true
-}
-
-func applyEnvInt(target *int, key string, logger *log.Logger) bool {
- value, ok := parseEnvInt(key, logger)
- if !ok {
- return false
- }
- *target = value
- return true
-}
-
-func applyEnvFloat(target **float64, key string, logger *log.Logger) bool {
- value, ok := parseEnvFloatPtr(key, logger)
- if !ok {
- return false
- }
- *target = value
- return true
-}
-
-func applyEnvCSV(target *[]string, key string) bool {
- value := getenvTrim(key)
- if value == "" {
- return false
- }
- parts := strings.Split(value, ",")
- *target = nil
- for _, p := range parts {
- if t := strings.TrimSpace(p); t != "" {
- *target = append(*target, t)
- }
- }
- return true
-}
-
-func applyEnvBool(target *bool, key string) bool {
- value := getenvTrim(key)
- if value == "" {
- return false
- }
- *target = value == "true" || value == "1"
- return true
-}
-
-func applyEnvBoolPtr(target **bool, key string) bool {
- value := getenvTrim(key)
- if value == "" {
- return false
- }
- parsed := value == "true" || value == "1"
- *target = &parsed
- return true
-}
-
-func getenvTrim(key string) string {
- return strings.TrimSpace(os.Getenv(key))
-}
-
-func parseEnvInt(key string, logger *log.Logger) (int, bool) {
- value := getenvTrim(key)
- if value == "" {
- return 0, false
- }
- n, err := strconv.Atoi(value)
- if err != nil {
- if logger != nil {
- logger.Printf("invalid %s: %v", key, err)
- }
- return 0, false
- }
- return n, true
-}
-
-func parseEnvFloatPtr(key string, logger *log.Logger) (*float64, bool) {
- value := getenvTrim(key)
- if value == "" {
- return nil, false
- }
- f, err := strconv.ParseFloat(value, 64)
- if err != nil {
- if logger != nil {
- logger.Printf("invalid %s: %v", key, err)
- }
- return nil, false
- }
- return &f, true
-}
-
-type modelPicker struct {
- providerLower string
- modelForce string
- modelGeneric string
- forceUsed bool
- genericUsed bool
-}
-
-func newModelPicker(provider string) *modelPicker {
- return &modelPicker{
- providerLower: strings.ToLower(strings.TrimSpace(provider)),
- modelForce: getenvTrim("HEXAI_MODEL_FORCE"),
- modelGeneric: getenvTrim("HEXAI_MODEL"),
- }
-}
-
-func (p *modelPicker) pick(providerName, specific string) (string, bool) {
- specific = strings.TrimSpace(specific)
- nameLower := strings.ToLower(strings.TrimSpace(providerName))
- if p.modelForce != "" {
- if p.providerLower == nameLower {
- p.forceUsed = true
- return p.modelForce, true
- }
- if p.providerLower == "" && !p.forceUsed {
- p.forceUsed = true
- return p.modelForce, true
- }
- }
- if specific != "" {
- return specific, true
- }
- if p.modelGeneric != "" {
- if p.providerLower == nameLower {
- return p.modelGeneric, true
- }
- if p.providerLower == "" && !p.genericUsed {
- p.genericUsed = true
- return p.modelGeneric, true
- }
- }
- return "", false
-}