package config import ( "fmt" "os" "strconv" ) const ( DefaultConfigPath = "/etc/audita/config.yml" DefaultConfigPathUsrLocal = "/usr/local/etc/audita/config.yml" ) var DefaultConfigSearchPaths = []string{ DefaultConfigPathUsrLocal, DefaultConfigPath, } func LoadFromEnv() (Config, error) { cfg := Default() if err := cfg.applyEnvOverrides(os.LookupEnv); err != nil { return Config{}, err } return cfg, nil } func loadFromLookup(lookup func(string) (string, bool)) (Config, error) { cfg := Default() if err := cfg.applyEnvOverrides(lookup); err != nil { return Config{}, err } return cfg, nil } func (c *Config) ApplyEnvOverrides() error { return c.applyEnvOverrides(os.LookupEnv) } func (c *Config) applyEnvOverrides(lookup func(string) (string, bool)) error { if c == nil { return fmt.Errorf("config must not be nil") } cfg := c if raw, ok := lookup("AUDITA_MODULES"); ok { modules, err := ParseModulesCSV(raw) if err != nil { return fmt.Errorf("AUDITA_MODULES: %w", err) } cfg.Modules = modules } primaryLLM := llmTargetPatch{} if raw, ok := lookup("AUDITA_LLM_API_KEY"); ok { primaryLLM.apiKey = &raw } else if raw, ok := lookup("OPENROUTER_API_KEY"); ok { primaryLLM.apiKey = &raw } if raw, ok := lookup("AUDITA_MODEL"); ok { primaryLLM.model = &raw } if raw, ok := lookup("AUDITA_BASE_URL"); ok { primaryLLM.baseURL = &raw } if raw, ok := lookup("AUDITA_LLM_TIMEOUT_SECONDS"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_LLM_TIMEOUT_SECONDS: %w", err) } primaryLLM.timeoutSeconds = &value } if raw, ok := lookup("AUDITA_MAX_RETRIES"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_MAX_RETRIES: %w", err) } primaryLLM.maxRetries = &value } cfg.applyPrimaryLLMTargetPatch(primaryLLM) validationLLM := llmTargetPatch{} if raw, ok := lookup("AUDITA_VALIDATION_LLM_API_KEY"); ok { validationLLM.apiKey = &raw } if raw, ok := lookup("AUDITA_VALIDATION_MODEL"); ok { validationLLM.model = &raw } if raw, ok := lookup("AUDITA_VALIDATION_BASE_URL"); ok { validationLLM.baseURL = &raw } if raw, ok := lookup("AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS: %w", err) } validationLLM.timeoutSeconds = &value } if raw, ok := lookup("AUDITA_VALIDATION_MAX_RETRIES"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_VALIDATION_MAX_RETRIES: %w", err) } validationLLM.maxRetries = &value } cfg.applyValidationLLMTargetPatch(validationLLM) concurrency := concurrencyPatch{ inheritProposal: true, allowLegacyAlias: true, } if raw, ok := lookup("AUDITA_TOTAL_LLM_CONCURRENCY"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_TOTAL_LLM_CONCURRENCY: %w", err) } concurrency.totalLLM = &value } if raw, ok := lookup("AUDITA_LLM_CONCURRENCY"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_LLM_CONCURRENCY: %w", err) } concurrency.legacyTotalLLM = &value } if raw, ok := lookup("AUDITA_PROPOSAL_LLM_CONCURRENCY"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_PROPOSAL_LLM_CONCURRENCY: %w", err) } concurrency.proposalLLM = &value } if raw, ok := lookup("AUDITA_VALIDATION_LLM_CONCURRENCY"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_VALIDATION_LLM_CONCURRENCY: %w", err) } concurrency.validationLLM = &value } cfg.applyConcurrencyPatch(concurrency) if raw, ok := lookup("AUDITA_VALIDATION_MAX_PROMPT_TOKENS"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_VALIDATION_MAX_PROMPT_TOKENS: %w", err) } cfg.ValidationMaxPromptTokens = value } chunking := chunkingPatch{} if raw, ok := lookup("AUDITA_MAX_SECTION_TOKENS"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_MAX_SECTION_TOKENS: %w", err) } chunking.maxSectionTokens = &value } if raw, ok := lookup("AUDITA_MIN_SECTION_TOKENS"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_MIN_SECTION_TOKENS: %w", err) } chunking.minSectionTokens = &value } if raw, ok := lookup("AUDITA_TARGET_SECTIONS"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_TARGET_SECTIONS: %w", err) } chunking.targetSections = &value } cfg.applyChunkingPatch(chunking) thresholds := thresholdsPatch{} if raw, ok := lookup("AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD: %w", err) } thresholds.glossary = &value } if raw, ok := lookup("AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD: %w", err) } thresholds.grammar = &value } if raw, ok := lookup("AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD: %w", err) } thresholds.homophones = &value } if raw, ok := lookup("AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD: %w", err) } thresholds.spokenWord = &value } cfg.applyThresholdsPatch(thresholds) normalization := normalizationPatch{} if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_GAP"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_GAP: %w", err) } normalization.maxSegmentGap = &value } if raw, ok := lookup("AUDITA_NORMALIZE_ELLIPSIS_GAP"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_NORMALIZE_ELLIPSIS_GAP: %w", err) } normalization.ellipsisGap = &value } if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_DURATION"); ok { value, err := parseFloat(raw) if err != nil { return fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_DURATION: %w", err) } normalization.maxSegmentDuration = &value } if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS"); ok { value, err := parseInt(raw) if err != nil { return fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS: %w", err) } normalization.maxSegmentTokens = &value } cfg.applyNormalizationPatch(normalization) diagnostics := diagnosticsPatch{} if raw, ok := lookup("AUDITA_WORK_DIR"); ok { diagnostics.workDir = &raw } if raw, ok := lookup("AUDITA_WORK_DIR_RETENTION"); ok { diagnostics.workDirRetention = &raw } cfg.applyDiagnosticsPatch(diagnostics) cfg.syncLegacyConcurrencyAliases() if err := cfg.Validate(); err != nil { return err } return nil } func parseInt(raw string) (int, error) { value, err := strconv.Atoi(raw) if err != nil { return 0, fmt.Errorf("must be an integer") } return value, nil } func parseFloat(raw string) (float64, error) { value, err := strconv.ParseFloat(raw, 64) if err != nil { return 0, fmt.Errorf("must be a number") } return value, nil }