Add Go configuration model
This commit is contained in:
@@ -5,6 +5,9 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const processNotImplementedMessage = "process command is not implemented yet"
|
const processNotImplementedMessage = "process command is not implemented yet"
|
||||||
@@ -36,18 +39,83 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg, err := config.LoadFromEnv()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(stderr, "audita process: invalid environment configuration: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
fs := flag.NewFlagSet("process", flag.ContinueOnError)
|
fs := flag.NewFlagSet("process", flag.ContinueOnError)
|
||||||
fs.SetOutput(stderr)
|
fs.SetOutput(stderr)
|
||||||
|
|
||||||
glossaryPath := fs.String("glossary", "", "Path to glossary YAML file")
|
glossaryPath := fs.String("glossary", "", "Path to glossary YAML file")
|
||||||
outputPath := fs.String("output", "", "Path to corrected transcript JSON output file")
|
outputPath := fs.String("output", "", "Path to corrected transcript JSON output file")
|
||||||
reportJSONPath := fs.String("report-json", "", "Path to machine-readable report JSON output file")
|
reportJSONPath := fs.String("report-json", "", "Path to machine-readable report JSON output file")
|
||||||
modules := fs.String("modules", "", "Comma-separated module sequence override")
|
modules := fs.String("modules", strings.Join(cfg.Modules, ","), "Comma-separated module sequence override")
|
||||||
|
llmAPIKey := fs.String("llm-api-key", cfg.PrimaryLLM.APIKey, "Primary LLM API key")
|
||||||
|
validationLLMAPIKey := fs.String("validation-llm-api-key", cfg.ValidationLLM.APIKey, "Validation LLM API key")
|
||||||
|
model := fs.String("model", cfg.PrimaryLLM.Model, "Primary LLM model name")
|
||||||
|
validationModel := fs.String("validation-model", cfg.ValidationLLM.Model, "Validation LLM model name")
|
||||||
|
baseURL := fs.String("base-url", cfg.PrimaryLLM.BaseURL, "Primary OpenAI-compatible base URL")
|
||||||
|
validationBaseURL := fs.String("validation-base-url", cfg.ValidationLLM.BaseURL, "Validation OpenAI-compatible base URL")
|
||||||
|
llmTimeoutSeconds := fs.Int("llm-timeout-seconds", cfg.PrimaryLLM.TimeoutSeconds, "Primary LLM timeout in seconds")
|
||||||
|
validationTimeoutSecondsDefault := cfg.PrimaryLLM.TimeoutSeconds
|
||||||
|
if cfg.ValidationLLM.TimeoutSeconds != nil {
|
||||||
|
validationTimeoutSecondsDefault = *cfg.ValidationLLM.TimeoutSeconds
|
||||||
|
}
|
||||||
|
validationLLMTimeoutSeconds := fs.Int("validation-llm-timeout-seconds", validationTimeoutSecondsDefault, "Validation LLM timeout in seconds")
|
||||||
|
maxRetries := fs.Int("max-retries", cfg.PrimaryLLM.MaxRetries, "Maximum structured-output retries")
|
||||||
|
validationMaxRetriesDefault := cfg.PrimaryLLM.MaxRetries
|
||||||
|
if cfg.ValidationLLM.MaxRetries != nil {
|
||||||
|
validationMaxRetriesDefault = *cfg.ValidationLLM.MaxRetries
|
||||||
|
}
|
||||||
|
validationMaxRetries := fs.Int("validation-max-retries", validationMaxRetriesDefault, "Validation structured-output retries")
|
||||||
|
validationMaxPromptTokens := fs.Int("validation-max-prompt-tokens", cfg.ValidationMaxPromptTokens, "Validation max prompt tokens")
|
||||||
|
maxSectionTokens := fs.Int("max-section-tokens", cfg.MaxSectionTokens, "Max section tokens")
|
||||||
|
minSectionTokens := fs.Int("min-section-tokens", cfg.MinSectionTokens, "Min section tokens")
|
||||||
|
targetSectionsDefault := 0
|
||||||
|
if cfg.TargetSections != nil {
|
||||||
|
targetSectionsDefault = *cfg.TargetSections
|
||||||
|
}
|
||||||
|
targetSections := fs.Int("target-sections", targetSectionsDefault, "Target number of transcript sections")
|
||||||
|
glossaryConfidenceThreshold := fs.Float64("glossary-confidence-threshold", cfg.Thresholds.Glossary, "Glossary confidence threshold")
|
||||||
|
grammarConfidenceThreshold := fs.Float64("grammar-confidence-threshold", cfg.Thresholds.Grammar, "Grammar confidence threshold")
|
||||||
|
homophonesConfidenceThreshold := fs.Float64("homophones-confidence-threshold", cfg.Thresholds.Homophones, "Homophones confidence threshold")
|
||||||
|
spokenWordConfidenceThreshold := fs.Float64("spoken-word-confidence-threshold", cfg.Thresholds.SpokenWord, "Spoken-word confidence threshold")
|
||||||
|
normalizeMaxSegmentGap := fs.Float64("normalize-max-segment-gap", cfg.Normalization.MaxSegmentGap, "Maximum same-speaker merge gap")
|
||||||
|
normalizeEllipsisGap := fs.Float64("normalize-ellipsis-gap", cfg.Normalization.EllipsisGap, "Gap threshold for ellipsis insertion")
|
||||||
|
normalizeMaxSegmentDuration := fs.Float64("normalize-max-segment-duration", cfg.Normalization.MaxSegmentDuration, "Maximum merged segment duration")
|
||||||
|
normalizeMaxSegmentTokens := fs.Int("normalize-max-segment-tokens", cfg.Normalization.MaxSegmentTokens, "Maximum merged segment token estimate")
|
||||||
|
workDir := fs.String("work-dir", cfg.WorkDir, "Per-run work directory")
|
||||||
|
workDirRetention := fs.String("work-dir-retention", string(cfg.WorkDirRetention), "Work-dir retention policy: auto|always|never")
|
||||||
|
|
||||||
_ = glossaryPath
|
_ = glossaryPath
|
||||||
_ = outputPath
|
_ = outputPath
|
||||||
_ = reportJSONPath
|
_ = reportJSONPath
|
||||||
_ = modules
|
_ = llmAPIKey
|
||||||
|
_ = validationLLMAPIKey
|
||||||
|
_ = model
|
||||||
|
_ = validationModel
|
||||||
|
_ = baseURL
|
||||||
|
_ = validationBaseURL
|
||||||
|
_ = llmTimeoutSeconds
|
||||||
|
_ = validationLLMTimeoutSeconds
|
||||||
|
_ = maxRetries
|
||||||
|
_ = validationMaxRetries
|
||||||
|
_ = validationMaxPromptTokens
|
||||||
|
_ = maxSectionTokens
|
||||||
|
_ = minSectionTokens
|
||||||
|
_ = targetSections
|
||||||
|
_ = glossaryConfidenceThreshold
|
||||||
|
_ = grammarConfidenceThreshold
|
||||||
|
_ = homophonesConfidenceThreshold
|
||||||
|
_ = spokenWordConfidenceThreshold
|
||||||
|
_ = normalizeMaxSegmentGap
|
||||||
|
_ = normalizeEllipsisGap
|
||||||
|
_ = normalizeMaxSegmentDuration
|
||||||
|
_ = normalizeMaxSegmentTokens
|
||||||
|
_ = workDir
|
||||||
|
_ = workDirRetention
|
||||||
|
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
if errors.Is(err, flag.ErrHelp) {
|
if errors.Is(err, flag.ErrHelp) {
|
||||||
@@ -57,6 +125,67 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
overrides := config.CLIOverrides{}
|
||||||
|
fs.Visit(func(f *flag.Flag) {
|
||||||
|
switch f.Name {
|
||||||
|
case "modules":
|
||||||
|
overrides.ModulesCSV = modules
|
||||||
|
case "llm-api-key":
|
||||||
|
overrides.PrimaryLLMAPIKey = llmAPIKey
|
||||||
|
case "validation-llm-api-key":
|
||||||
|
overrides.ValidationLLMAPIKey = validationLLMAPIKey
|
||||||
|
case "model":
|
||||||
|
overrides.PrimaryModel = model
|
||||||
|
case "validation-model":
|
||||||
|
overrides.ValidationModel = validationModel
|
||||||
|
case "base-url":
|
||||||
|
overrides.PrimaryBaseURL = baseURL
|
||||||
|
case "validation-base-url":
|
||||||
|
overrides.ValidationBaseURL = validationBaseURL
|
||||||
|
case "llm-timeout-seconds":
|
||||||
|
overrides.PrimaryLLMTimeoutSeconds = llmTimeoutSeconds
|
||||||
|
case "validation-llm-timeout-seconds":
|
||||||
|
overrides.ValidationLLMTimeoutSeconds = validationLLMTimeoutSeconds
|
||||||
|
case "max-retries":
|
||||||
|
overrides.MaxRetries = maxRetries
|
||||||
|
case "validation-max-retries":
|
||||||
|
overrides.ValidationMaxRetries = validationMaxRetries
|
||||||
|
case "validation-max-prompt-tokens":
|
||||||
|
overrides.ValidationMaxPromptTokens = validationMaxPromptTokens
|
||||||
|
case "max-section-tokens":
|
||||||
|
overrides.MaxSectionTokens = maxSectionTokens
|
||||||
|
case "min-section-tokens":
|
||||||
|
overrides.MinSectionTokens = minSectionTokens
|
||||||
|
case "target-sections":
|
||||||
|
overrides.TargetSections = targetSections
|
||||||
|
case "glossary-confidence-threshold":
|
||||||
|
overrides.GlossaryConfidenceThreshold = glossaryConfidenceThreshold
|
||||||
|
case "grammar-confidence-threshold":
|
||||||
|
overrides.GrammarConfidenceThreshold = grammarConfidenceThreshold
|
||||||
|
case "homophones-confidence-threshold":
|
||||||
|
overrides.HomophonesConfidenceThreshold = homophonesConfidenceThreshold
|
||||||
|
case "spoken-word-confidence-threshold":
|
||||||
|
overrides.SpokenWordConfidenceThreshold = spokenWordConfidenceThreshold
|
||||||
|
case "normalize-max-segment-gap":
|
||||||
|
overrides.NormalizeMaxSegmentGap = normalizeMaxSegmentGap
|
||||||
|
case "normalize-ellipsis-gap":
|
||||||
|
overrides.NormalizeEllipsisGap = normalizeEllipsisGap
|
||||||
|
case "normalize-max-segment-duration":
|
||||||
|
overrides.NormalizeMaxSegmentDuration = normalizeMaxSegmentDuration
|
||||||
|
case "normalize-max-segment-tokens":
|
||||||
|
overrides.NormalizeMaxSegmentTokens = normalizeMaxSegmentTokens
|
||||||
|
case "work-dir":
|
||||||
|
overrides.WorkDir = workDir
|
||||||
|
case "work-dir-retention":
|
||||||
|
overrides.WorkDirRetention = workDirRetention
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := cfg.ApplyCLIOverrides(overrides); err != nil {
|
||||||
|
fmt.Fprintf(stderr, "audita process: invalid CLI configuration: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
remaining := fs.Args()
|
remaining := fs.Args()
|
||||||
if len(remaining) != 1 {
|
if len(remaining) != 1 {
|
||||||
fmt.Fprintln(stderr, "audita process: expected exactly 1 transcript JSON path argument")
|
fmt.Fprintln(stderr, "audita process: expected exactly 1 transcript JSON path argument")
|
||||||
@@ -121,6 +250,11 @@ func writeProcessUsage(w io.Writer) {
|
|||||||
fmt.Fprintln(w, " --output <path> Path to corrected transcript JSON output file")
|
fmt.Fprintln(w, " --output <path> Path to corrected transcript JSON output file")
|
||||||
fmt.Fprintln(w, " --report-json <path> Path to machine-readable report JSON output file")
|
fmt.Fprintln(w, " --report-json <path> Path to machine-readable report JSON output file")
|
||||||
fmt.Fprintln(w, " --modules <list> Comma-separated module sequence override")
|
fmt.Fprintln(w, " --modules <list> Comma-separated module sequence override")
|
||||||
|
fmt.Fprintln(w, " --llm-api-key <key> Primary LLM API key")
|
||||||
|
fmt.Fprintln(w, " --model <name> Primary LLM model")
|
||||||
|
fmt.Fprintln(w, " --base-url <url> Primary OpenAI-compatible API base URL")
|
||||||
|
fmt.Fprintln(w, " --work-dir <path> Per-run work directory")
|
||||||
|
fmt.Fprintln(w, " --work-dir-retention <mode> auto|always|never")
|
||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
fmt.Fprintln(w, "Example:")
|
fmt.Fprintln(w, "Example:")
|
||||||
fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json")
|
fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json")
|
||||||
|
|||||||
147
internal/core/config/config.go
Normal file
147
internal/core/config/config.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorkDirRetention string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WorkDirRetentionAuto WorkDirRetention = "auto"
|
||||||
|
WorkDirRetentionAlways WorkDirRetention = "always"
|
||||||
|
WorkDirRetentionNever WorkDirRetention = "never"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar"
|
||||||
|
DefaultPrimaryModel = "openrouter/google/gemma-4-31b-it"
|
||||||
|
DefaultPrimaryBaseURL = "https://openrouter.ai/api/v1"
|
||||||
|
DefaultPrimaryLLMTimeoutSeconds = 600
|
||||||
|
DefaultMaxRetries = 3
|
||||||
|
DefaultValidationMaxPromptTokens = 2048
|
||||||
|
DefaultMaxSectionTokens = 8192
|
||||||
|
DefaultMinSectionTokens = 2048
|
||||||
|
DefaultConfidenceThreshold = 0.8
|
||||||
|
DefaultNormalizeMaxSegmentGap = 4.0
|
||||||
|
DefaultNormalizeEllipsisGap = 3.5
|
||||||
|
DefaultNormalizeMaxSegmentDuration = 60.0
|
||||||
|
DefaultNormalizeMaxSegmentTokens = 2048
|
||||||
|
DefaultWorkDir = "/tmp/audita"
|
||||||
|
DefaultWorkDirRetention WorkDirRetention = WorkDirRetentionAuto
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Modules []string
|
||||||
|
PrimaryLLM LLMConfig
|
||||||
|
ValidationLLM ValidationLLMConfig
|
||||||
|
ValidationMaxPromptTokens int
|
||||||
|
MaxSectionTokens int
|
||||||
|
MinSectionTokens int
|
||||||
|
TargetSections *int
|
||||||
|
Thresholds ConfidenceThresholds
|
||||||
|
Normalization NormalizationConfig
|
||||||
|
WorkDir string
|
||||||
|
WorkDirRetention WorkDirRetention
|
||||||
|
}
|
||||||
|
|
||||||
|
type LLMConfig struct {
|
||||||
|
APIKey string
|
||||||
|
Model string
|
||||||
|
BaseURL string
|
||||||
|
TimeoutSeconds int
|
||||||
|
MaxRetries int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationLLMConfig struct {
|
||||||
|
APIKey string
|
||||||
|
Model string
|
||||||
|
BaseURL string
|
||||||
|
TimeoutSeconds *int
|
||||||
|
MaxRetries *int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfidenceThresholds struct {
|
||||||
|
Glossary float64
|
||||||
|
Grammar float64
|
||||||
|
Homophones float64
|
||||||
|
SpokenWord float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type NormalizationConfig struct {
|
||||||
|
MaxSegmentGap float64
|
||||||
|
EllipsisGap float64
|
||||||
|
MaxSegmentDuration float64
|
||||||
|
MaxSegmentTokens int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Default() Config {
|
||||||
|
modules, _ := ParseModulesCSV(DefaultModulesCSV)
|
||||||
|
|
||||||
|
return Config{
|
||||||
|
Modules: modules,
|
||||||
|
PrimaryLLM: LLMConfig{
|
||||||
|
Model: DefaultPrimaryModel,
|
||||||
|
BaseURL: DefaultPrimaryBaseURL,
|
||||||
|
TimeoutSeconds: DefaultPrimaryLLMTimeoutSeconds,
|
||||||
|
MaxRetries: DefaultMaxRetries,
|
||||||
|
},
|
||||||
|
ValidationLLM: ValidationLLMConfig{},
|
||||||
|
ValidationMaxPromptTokens: DefaultValidationMaxPromptTokens,
|
||||||
|
MaxSectionTokens: DefaultMaxSectionTokens,
|
||||||
|
MinSectionTokens: DefaultMinSectionTokens,
|
||||||
|
TargetSections: nil,
|
||||||
|
Thresholds: ConfidenceThresholds{
|
||||||
|
Glossary: DefaultConfidenceThreshold,
|
||||||
|
Grammar: DefaultConfidenceThreshold,
|
||||||
|
Homophones: DefaultConfidenceThreshold,
|
||||||
|
SpokenWord: DefaultConfidenceThreshold,
|
||||||
|
},
|
||||||
|
Normalization: NormalizationConfig{
|
||||||
|
MaxSegmentGap: DefaultNormalizeMaxSegmentGap,
|
||||||
|
EllipsisGap: DefaultNormalizeEllipsisGap,
|
||||||
|
MaxSegmentDuration: DefaultNormalizeMaxSegmentDuration,
|
||||||
|
MaxSegmentTokens: DefaultNormalizeMaxSegmentTokens,
|
||||||
|
},
|
||||||
|
WorkDir: DefaultWorkDir,
|
||||||
|
WorkDirRetention: DefaultWorkDirRetention,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseModulesCSV(raw string) ([]string, error) {
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
modules := make([]string, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
trimmed := strings.TrimSpace(part)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil, fmt.Errorf("modules list contains an empty value")
|
||||||
|
}
|
||||||
|
modules = append(modules, trimmed)
|
||||||
|
}
|
||||||
|
if len(modules) == 0 {
|
||||||
|
return nil, fmt.Errorf("modules list must not be empty")
|
||||||
|
}
|
||||||
|
return modules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) EffectiveValidationLLMConfig() LLMConfig {
|
||||||
|
effective := c.PrimaryLLM
|
||||||
|
|
||||||
|
if c.ValidationLLM.APIKey != "" {
|
||||||
|
effective.APIKey = c.ValidationLLM.APIKey
|
||||||
|
}
|
||||||
|
if c.ValidationLLM.Model != "" {
|
||||||
|
effective.Model = c.ValidationLLM.Model
|
||||||
|
}
|
||||||
|
if c.ValidationLLM.BaseURL != "" {
|
||||||
|
effective.BaseURL = c.ValidationLLM.BaseURL
|
||||||
|
}
|
||||||
|
if c.ValidationLLM.TimeoutSeconds != nil {
|
||||||
|
effective.TimeoutSeconds = *c.ValidationLLM.TimeoutSeconds
|
||||||
|
}
|
||||||
|
if c.ValidationLLM.MaxRetries != nil {
|
||||||
|
effective.MaxRetries = *c.ValidationLLM.MaxRetries
|
||||||
|
}
|
||||||
|
|
||||||
|
return effective
|
||||||
|
}
|
||||||
215
internal/core/config/config_test.go
Normal file
215
internal/core/config/config_test.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultConfigValues(t *testing.T) {
|
||||||
|
cfg := Default()
|
||||||
|
|
||||||
|
if got, want := strings.Join(cfg.Modules, ","), DefaultModulesCSV; got != want {
|
||||||
|
t.Fatalf("modules mismatch: got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
if cfg.PrimaryLLM.Model != DefaultPrimaryModel {
|
||||||
|
t.Fatalf("unexpected default primary model: %q", cfg.PrimaryLLM.Model)
|
||||||
|
}
|
||||||
|
if cfg.PrimaryLLM.BaseURL != DefaultPrimaryBaseURL {
|
||||||
|
t.Fatalf("unexpected default primary base url: %q", cfg.PrimaryLLM.BaseURL)
|
||||||
|
}
|
||||||
|
if cfg.PrimaryLLM.TimeoutSeconds != DefaultPrimaryLLMTimeoutSeconds {
|
||||||
|
t.Fatalf("unexpected default timeout seconds: %d", cfg.PrimaryLLM.TimeoutSeconds)
|
||||||
|
}
|
||||||
|
if cfg.PrimaryLLM.MaxRetries != DefaultMaxRetries {
|
||||||
|
t.Fatalf("unexpected default max retries: %d", cfg.PrimaryLLM.MaxRetries)
|
||||||
|
}
|
||||||
|
if cfg.ValidationLLM.TimeoutSeconds != nil {
|
||||||
|
t.Fatalf("expected validation timeout to be unset by default")
|
||||||
|
}
|
||||||
|
if cfg.ValidationLLM.MaxRetries != nil {
|
||||||
|
t.Fatalf("expected validation max retries to be unset by default")
|
||||||
|
}
|
||||||
|
if cfg.TargetSections != nil {
|
||||||
|
t.Fatalf("expected target sections to be unset by default")
|
||||||
|
}
|
||||||
|
if cfg.WorkDir != DefaultWorkDir {
|
||||||
|
t.Fatalf("unexpected default work dir: %q", cfg.WorkDir)
|
||||||
|
}
|
||||||
|
if cfg.WorkDirRetention != DefaultWorkDirRetention {
|
||||||
|
t.Fatalf("unexpected default work dir retention: %q", cfg.WorkDirRetention)
|
||||||
|
}
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
t.Fatalf("default config should validate: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromEnvOverridesAndFallback(t *testing.T) {
|
||||||
|
env := map[string]string{
|
||||||
|
"AUDITA_MODEL": "openai/gpt-4.1-mini",
|
||||||
|
"AUDITA_BASE_URL": "https://api.openai.com/v1",
|
||||||
|
"AUDITA_LLM_TIMEOUT_SECONDS": "120",
|
||||||
|
"AUDITA_MAX_RETRIES": "7",
|
||||||
|
"AUDITA_VALIDATION_MAX_PROMPT_TOKENS": "4096",
|
||||||
|
"AUDITA_MAX_SECTION_TOKENS": "9000",
|
||||||
|
"AUDITA_MIN_SECTION_TOKENS": "3000",
|
||||||
|
"AUDITA_TARGET_SECTIONS": "5",
|
||||||
|
"AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD": "0.9",
|
||||||
|
"AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD": "0.7",
|
||||||
|
"AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD": "0.6",
|
||||||
|
"AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD": "0.5",
|
||||||
|
"AUDITA_NORMALIZE_MAX_SEGMENT_GAP": "2.5",
|
||||||
|
"AUDITA_NORMALIZE_ELLIPSIS_GAP": "2.0",
|
||||||
|
"AUDITA_NORMALIZE_MAX_SEGMENT_DURATION": "30.0",
|
||||||
|
"AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS": "1024",
|
||||||
|
"AUDITA_WORK_DIR": "/var/tmp/audita",
|
||||||
|
"AUDITA_WORK_DIR_RETENTION": "always",
|
||||||
|
"OPENROUTER_API_KEY": "fallback-key",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := loadFromLookup(mapLookup(env))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loadFromLookup returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.PrimaryLLM.APIKey != "fallback-key" {
|
||||||
|
t.Fatalf("expected OPENROUTER_API_KEY fallback, got %q", cfg.PrimaryLLM.APIKey)
|
||||||
|
}
|
||||||
|
if cfg.PrimaryLLM.Model != env["AUDITA_MODEL"] {
|
||||||
|
t.Fatalf("unexpected model: %q", cfg.PrimaryLLM.Model)
|
||||||
|
}
|
||||||
|
if cfg.PrimaryLLM.BaseURL != env["AUDITA_BASE_URL"] {
|
||||||
|
t.Fatalf("unexpected base url: %q", cfg.PrimaryLLM.BaseURL)
|
||||||
|
}
|
||||||
|
if cfg.TargetSections == nil || *cfg.TargetSections != 5 {
|
||||||
|
t.Fatalf("unexpected target sections: %#v", cfg.TargetSections)
|
||||||
|
}
|
||||||
|
if cfg.WorkDirRetention != WorkDirRetentionAlways {
|
||||||
|
t.Fatalf("unexpected work dir retention: %q", cfg.WorkDirRetention)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromEnvUsesAuditaLLMAPIKeyOverFallback(t *testing.T) {
|
||||||
|
env := map[string]string{
|
||||||
|
"AUDITA_LLM_API_KEY": "primary-key",
|
||||||
|
"OPENROUTER_API_KEY": "fallback-key",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := loadFromLookup(mapLookup(env))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loadFromLookup returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.PrimaryLLM.APIKey != "primary-key" {
|
||||||
|
t.Fatalf("expected AUDITA_LLM_API_KEY to win, got %q", cfg.PrimaryLLM.APIKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCLIOverridesPrecedence(t *testing.T) {
|
||||||
|
cfg := Default()
|
||||||
|
cfg.PrimaryLLM.Model = "env-model"
|
||||||
|
cfg.WorkDir = "/env/work"
|
||||||
|
|
||||||
|
model := "cli-model"
|
||||||
|
workDir := "/cli/work"
|
||||||
|
modules := "grammar"
|
||||||
|
overrides := CLIOverrides{
|
||||||
|
PrimaryModel: &model,
|
||||||
|
WorkDir: &workDir,
|
||||||
|
ModulesCSV: &modules,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.ApplyCLIOverrides(overrides); err != nil {
|
||||||
|
t.Fatalf("ApplyCLIOverrides failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.PrimaryLLM.Model != "cli-model" {
|
||||||
|
t.Fatalf("expected CLI model override, got %q", cfg.PrimaryLLM.Model)
|
||||||
|
}
|
||||||
|
if cfg.WorkDir != "/cli/work" {
|
||||||
|
t.Fatalf("expected CLI work dir override, got %q", cfg.WorkDir)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.Modules, []string{"grammar"}) {
|
||||||
|
t.Fatalf("unexpected modules: %#v", cfg.Modules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidationFailures(t *testing.T) {
|
||||||
|
cfg := Default()
|
||||||
|
cfg.PrimaryLLM.TimeoutSeconds = -1
|
||||||
|
cfg.ValidationMaxPromptTokens = 0
|
||||||
|
cfg.MaxSectionTokens = 100
|
||||||
|
cfg.MinSectionTokens = 200
|
||||||
|
cfg.Thresholds.Grammar = 1.5
|
||||||
|
cfg.WorkDirRetention = WorkDirRetention("sometimes")
|
||||||
|
|
||||||
|
err := cfg.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected validation error")
|
||||||
|
}
|
||||||
|
|
||||||
|
message := err.Error()
|
||||||
|
for _, expected := range []string{
|
||||||
|
"primary llm timeout seconds",
|
||||||
|
"validation max prompt tokens",
|
||||||
|
"min section tokens",
|
||||||
|
"grammar confidence threshold",
|
||||||
|
"work dir retention",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(message, expected) {
|
||||||
|
t.Fatalf("expected error to contain %q, got %q", expected, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveValidationLLMInheritance(t *testing.T) {
|
||||||
|
cfg := Default()
|
||||||
|
cfg.PrimaryLLM.APIKey = "primary-key"
|
||||||
|
cfg.PrimaryLLM.Model = "primary-model"
|
||||||
|
cfg.PrimaryLLM.BaseURL = "https://primary.example/v1"
|
||||||
|
cfg.PrimaryLLM.TimeoutSeconds = 111
|
||||||
|
cfg.PrimaryLLM.MaxRetries = 2
|
||||||
|
|
||||||
|
effective := cfg.EffectiveValidationLLMConfig()
|
||||||
|
if effective.APIKey != "primary-key" || effective.Model != "primary-model" || effective.BaseURL != "https://primary.example/v1" || effective.TimeoutSeconds != 111 || effective.MaxRetries != 2 {
|
||||||
|
t.Fatalf("unexpected inherited config: %#v", effective)
|
||||||
|
}
|
||||||
|
|
||||||
|
validationTimeout := 222
|
||||||
|
validationRetries := 9
|
||||||
|
cfg.ValidationLLM.APIKey = "validation-key"
|
||||||
|
cfg.ValidationLLM.Model = "validation-model"
|
||||||
|
cfg.ValidationLLM.BaseURL = "https://validation.example/v1"
|
||||||
|
cfg.ValidationLLM.TimeoutSeconds = &validationTimeout
|
||||||
|
cfg.ValidationLLM.MaxRetries = &validationRetries
|
||||||
|
|
||||||
|
effective = cfg.EffectiveValidationLLMConfig()
|
||||||
|
if effective.APIKey != "validation-key" || effective.Model != "validation-model" || effective.BaseURL != "https://validation.example/v1" || effective.TimeoutSeconds != 222 || effective.MaxRetries != 9 {
|
||||||
|
t.Fatalf("unexpected overridden validation config: %#v", effective)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedactedConfig(t *testing.T) {
|
||||||
|
cfg := Default()
|
||||||
|
cfg.PrimaryLLM.APIKey = "secret-primary"
|
||||||
|
cfg.ValidationLLM.APIKey = "secret-validation"
|
||||||
|
|
||||||
|
redacted := cfg.Redacted()
|
||||||
|
|
||||||
|
if redacted.PrimaryLLM.APIKey != redactedSecret {
|
||||||
|
t.Fatalf("expected primary api key to be redacted, got %q", redacted.PrimaryLLM.APIKey)
|
||||||
|
}
|
||||||
|
if redacted.ValidationLLM.APIKey != redactedSecret {
|
||||||
|
t.Fatalf("expected validation api key to be redacted, got %q", redacted.ValidationLLM.APIKey)
|
||||||
|
}
|
||||||
|
if cfg.PrimaryLLM.APIKey != "secret-primary" {
|
||||||
|
t.Fatalf("redaction should not mutate original config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapLookup(values map[string]string) func(string) (string, bool) {
|
||||||
|
return func(key string) (string, bool) {
|
||||||
|
value, ok := values[key]
|
||||||
|
return value, ok
|
||||||
|
}
|
||||||
|
}
|
||||||
198
internal/core/config/env.go
Normal file
198
internal/core/config/env.go
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadFromEnv() (Config, error) {
|
||||||
|
return loadFromLookup(os.LookupEnv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFromLookup(lookup func(string) (string, bool)) (Config, error) {
|
||||||
|
cfg := Default()
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_MODULES"); ok {
|
||||||
|
modules, err := ParseModulesCSV(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_MODULES: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Modules = modules
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_LLM_API_KEY"); ok {
|
||||||
|
cfg.PrimaryLLM.APIKey = raw
|
||||||
|
} else if raw, ok := lookup("OPENROUTER_API_KEY"); ok {
|
||||||
|
cfg.PrimaryLLM.APIKey = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_VALIDATION_LLM_API_KEY"); ok {
|
||||||
|
cfg.ValidationLLM.APIKey = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_MODEL"); ok {
|
||||||
|
cfg.PrimaryLLM.Model = raw
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("AUDITA_VALIDATION_MODEL"); ok {
|
||||||
|
cfg.ValidationLLM.Model = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_BASE_URL"); ok {
|
||||||
|
cfg.PrimaryLLM.BaseURL = raw
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("AUDITA_VALIDATION_BASE_URL"); ok {
|
||||||
|
cfg.ValidationLLM.BaseURL = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_LLM_TIMEOUT_SECONDS"); ok {
|
||||||
|
value, err := parseInt(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_LLM_TIMEOUT_SECONDS: %w", err)
|
||||||
|
}
|
||||||
|
cfg.PrimaryLLM.TimeoutSeconds = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS"); ok {
|
||||||
|
value, err := parseInt(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS: %w", err)
|
||||||
|
}
|
||||||
|
cfg.ValidationLLM.TimeoutSeconds = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_MAX_RETRIES"); ok {
|
||||||
|
value, err := parseInt(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_MAX_RETRIES: %w", err)
|
||||||
|
}
|
||||||
|
cfg.PrimaryLLM.MaxRetries = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_VALIDATION_MAX_RETRIES"); ok {
|
||||||
|
value, err := parseInt(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_VALIDATION_MAX_RETRIES: %w", err)
|
||||||
|
}
|
||||||
|
cfg.ValidationLLM.MaxRetries = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_VALIDATION_MAX_PROMPT_TOKENS"); ok {
|
||||||
|
value, err := parseInt(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_VALIDATION_MAX_PROMPT_TOKENS: %w", err)
|
||||||
|
}
|
||||||
|
cfg.ValidationMaxPromptTokens = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_MAX_SECTION_TOKENS"); ok {
|
||||||
|
value, err := parseInt(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_MAX_SECTION_TOKENS: %w", err)
|
||||||
|
}
|
||||||
|
cfg.MaxSectionTokens = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_MIN_SECTION_TOKENS"); ok {
|
||||||
|
value, err := parseInt(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_MIN_SECTION_TOKENS: %w", err)
|
||||||
|
}
|
||||||
|
cfg.MinSectionTokens = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_TARGET_SECTIONS"); ok {
|
||||||
|
value, err := parseInt(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_TARGET_SECTIONS: %w", err)
|
||||||
|
}
|
||||||
|
cfg.TargetSections = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD"); ok {
|
||||||
|
value, err := parseFloat(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Thresholds.Glossary = value
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD"); ok {
|
||||||
|
value, err := parseFloat(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Thresholds.Grammar = value
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD"); ok {
|
||||||
|
value, err := parseFloat(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Thresholds.Homophones = value
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD"); ok {
|
||||||
|
value, err := parseFloat(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Thresholds.SpokenWord = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_GAP"); ok {
|
||||||
|
value, err := parseFloat(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_GAP: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Normalization.MaxSegmentGap = value
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("AUDITA_NORMALIZE_ELLIPSIS_GAP"); ok {
|
||||||
|
value, err := parseFloat(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_NORMALIZE_ELLIPSIS_GAP: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Normalization.EllipsisGap = value
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_DURATION"); ok {
|
||||||
|
value, err := parseFloat(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_DURATION: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Normalization.MaxSegmentDuration = value
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS"); ok {
|
||||||
|
value, err := parseInt(raw)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Normalization.MaxSegmentTokens = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, ok := lookup("AUDITA_WORK_DIR"); ok {
|
||||||
|
cfg.WorkDir = raw
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("AUDITA_WORK_DIR_RETENTION"); ok {
|
||||||
|
cfg.WorkDirRetention = WorkDirRetention(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, 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
|
||||||
|
}
|
||||||
119
internal/core/config/flags.go
Normal file
119
internal/core/config/flags.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type CLIOverrides struct {
|
||||||
|
ModulesCSV *string
|
||||||
|
PrimaryLLMAPIKey *string
|
||||||
|
ValidationLLMAPIKey *string
|
||||||
|
PrimaryModel *string
|
||||||
|
ValidationModel *string
|
||||||
|
PrimaryBaseURL *string
|
||||||
|
ValidationBaseURL *string
|
||||||
|
PrimaryLLMTimeoutSeconds *int
|
||||||
|
ValidationLLMTimeoutSeconds *int
|
||||||
|
MaxRetries *int
|
||||||
|
ValidationMaxRetries *int
|
||||||
|
ValidationMaxPromptTokens *int
|
||||||
|
MaxSectionTokens *int
|
||||||
|
MinSectionTokens *int
|
||||||
|
TargetSections *int
|
||||||
|
GlossaryConfidenceThreshold *float64
|
||||||
|
GrammarConfidenceThreshold *float64
|
||||||
|
HomophonesConfidenceThreshold *float64
|
||||||
|
SpokenWordConfidenceThreshold *float64
|
||||||
|
NormalizeMaxSegmentGap *float64
|
||||||
|
NormalizeEllipsisGap *float64
|
||||||
|
NormalizeMaxSegmentDuration *float64
|
||||||
|
NormalizeMaxSegmentTokens *int
|
||||||
|
WorkDir *string
|
||||||
|
WorkDirRetention *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ApplyCLIOverrides(overrides CLIOverrides) error {
|
||||||
|
if overrides.ModulesCSV != nil {
|
||||||
|
modules, err := ParseModulesCSV(*overrides.ModulesCSV)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("--modules: %w", err)
|
||||||
|
}
|
||||||
|
c.Modules = modules
|
||||||
|
}
|
||||||
|
|
||||||
|
if overrides.PrimaryLLMAPIKey != nil {
|
||||||
|
c.PrimaryLLM.APIKey = *overrides.PrimaryLLMAPIKey
|
||||||
|
}
|
||||||
|
if overrides.ValidationLLMAPIKey != nil {
|
||||||
|
c.ValidationLLM.APIKey = *overrides.ValidationLLMAPIKey
|
||||||
|
}
|
||||||
|
if overrides.PrimaryModel != nil {
|
||||||
|
c.PrimaryLLM.Model = *overrides.PrimaryModel
|
||||||
|
}
|
||||||
|
if overrides.ValidationModel != nil {
|
||||||
|
c.ValidationLLM.Model = *overrides.ValidationModel
|
||||||
|
}
|
||||||
|
if overrides.PrimaryBaseURL != nil {
|
||||||
|
c.PrimaryLLM.BaseURL = *overrides.PrimaryBaseURL
|
||||||
|
}
|
||||||
|
if overrides.ValidationBaseURL != nil {
|
||||||
|
c.ValidationLLM.BaseURL = *overrides.ValidationBaseURL
|
||||||
|
}
|
||||||
|
if overrides.PrimaryLLMTimeoutSeconds != nil {
|
||||||
|
c.PrimaryLLM.TimeoutSeconds = *overrides.PrimaryLLMTimeoutSeconds
|
||||||
|
}
|
||||||
|
if overrides.ValidationLLMTimeoutSeconds != nil {
|
||||||
|
value := *overrides.ValidationLLMTimeoutSeconds
|
||||||
|
c.ValidationLLM.TimeoutSeconds = &value
|
||||||
|
}
|
||||||
|
if overrides.MaxRetries != nil {
|
||||||
|
c.PrimaryLLM.MaxRetries = *overrides.MaxRetries
|
||||||
|
}
|
||||||
|
if overrides.ValidationMaxRetries != nil {
|
||||||
|
value := *overrides.ValidationMaxRetries
|
||||||
|
c.ValidationLLM.MaxRetries = &value
|
||||||
|
}
|
||||||
|
if overrides.ValidationMaxPromptTokens != nil {
|
||||||
|
c.ValidationMaxPromptTokens = *overrides.ValidationMaxPromptTokens
|
||||||
|
}
|
||||||
|
if overrides.MaxSectionTokens != nil {
|
||||||
|
c.MaxSectionTokens = *overrides.MaxSectionTokens
|
||||||
|
}
|
||||||
|
if overrides.MinSectionTokens != nil {
|
||||||
|
c.MinSectionTokens = *overrides.MinSectionTokens
|
||||||
|
}
|
||||||
|
if overrides.TargetSections != nil {
|
||||||
|
value := *overrides.TargetSections
|
||||||
|
c.TargetSections = &value
|
||||||
|
}
|
||||||
|
if overrides.GlossaryConfidenceThreshold != nil {
|
||||||
|
c.Thresholds.Glossary = *overrides.GlossaryConfidenceThreshold
|
||||||
|
}
|
||||||
|
if overrides.GrammarConfidenceThreshold != nil {
|
||||||
|
c.Thresholds.Grammar = *overrides.GrammarConfidenceThreshold
|
||||||
|
}
|
||||||
|
if overrides.HomophonesConfidenceThreshold != nil {
|
||||||
|
c.Thresholds.Homophones = *overrides.HomophonesConfidenceThreshold
|
||||||
|
}
|
||||||
|
if overrides.SpokenWordConfidenceThreshold != nil {
|
||||||
|
c.Thresholds.SpokenWord = *overrides.SpokenWordConfidenceThreshold
|
||||||
|
}
|
||||||
|
if overrides.NormalizeMaxSegmentGap != nil {
|
||||||
|
c.Normalization.MaxSegmentGap = *overrides.NormalizeMaxSegmentGap
|
||||||
|
}
|
||||||
|
if overrides.NormalizeEllipsisGap != nil {
|
||||||
|
c.Normalization.EllipsisGap = *overrides.NormalizeEllipsisGap
|
||||||
|
}
|
||||||
|
if overrides.NormalizeMaxSegmentDuration != nil {
|
||||||
|
c.Normalization.MaxSegmentDuration = *overrides.NormalizeMaxSegmentDuration
|
||||||
|
}
|
||||||
|
if overrides.NormalizeMaxSegmentTokens != nil {
|
||||||
|
c.Normalization.MaxSegmentTokens = *overrides.NormalizeMaxSegmentTokens
|
||||||
|
}
|
||||||
|
if overrides.WorkDir != nil {
|
||||||
|
c.WorkDir = *overrides.WorkDir
|
||||||
|
}
|
||||||
|
if overrides.WorkDirRetention != nil {
|
||||||
|
c.WorkDirRetention = WorkDirRetention(*overrides.WorkDirRetention)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Validate()
|
||||||
|
}
|
||||||
17
internal/core/config/redaction.go
Normal file
17
internal/core/config/redaction.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
const redactedSecret = "[REDACTED]"
|
||||||
|
|
||||||
|
func (c Config) Redacted() Config {
|
||||||
|
redacted := c
|
||||||
|
redacted.PrimaryLLM.APIKey = redactSecret(redacted.PrimaryLLM.APIKey)
|
||||||
|
redacted.ValidationLLM.APIKey = redactSecret(redacted.ValidationLLM.APIKey)
|
||||||
|
return redacted
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactSecret(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return redactedSecret
|
||||||
|
}
|
||||||
106
internal/core/config/validation.go
Normal file
106
internal/core/config/validation.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c Config) Validate() error {
|
||||||
|
var issues []string
|
||||||
|
|
||||||
|
if len(c.Modules) == 0 {
|
||||||
|
issues = append(issues, "modules must not be empty")
|
||||||
|
}
|
||||||
|
for _, module := range c.Modules {
|
||||||
|
if strings.TrimSpace(module) == "" {
|
||||||
|
issues = append(issues, "modules must not contain empty values")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.PrimaryLLM.TimeoutSeconds <= 0 {
|
||||||
|
issues = append(issues, "primary llm timeout seconds must be greater than zero")
|
||||||
|
}
|
||||||
|
if c.PrimaryLLM.MaxRetries < 0 {
|
||||||
|
issues = append(issues, "max retries must be zero or greater")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ValidationLLM.TimeoutSeconds != nil && *c.ValidationLLM.TimeoutSeconds <= 0 {
|
||||||
|
issues = append(issues, "validation llm timeout seconds must be greater than zero")
|
||||||
|
}
|
||||||
|
if c.ValidationLLM.MaxRetries != nil && *c.ValidationLLM.MaxRetries < 0 {
|
||||||
|
issues = append(issues, "validation max retries must be zero or greater")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ValidationMaxPromptTokens <= 0 {
|
||||||
|
issues = append(issues, "validation max prompt tokens must be greater than zero")
|
||||||
|
}
|
||||||
|
if c.MaxSectionTokens <= 0 {
|
||||||
|
issues = append(issues, "max section tokens must be greater than zero")
|
||||||
|
}
|
||||||
|
if c.MinSectionTokens <= 0 {
|
||||||
|
issues = append(issues, "min section tokens must be greater than zero")
|
||||||
|
}
|
||||||
|
if c.MinSectionTokens > c.MaxSectionTokens {
|
||||||
|
issues = append(issues, "min section tokens must be less than or equal to max section tokens")
|
||||||
|
}
|
||||||
|
if c.TargetSections != nil && *c.TargetSections <= 0 {
|
||||||
|
issues = append(issues, "target sections must be greater than zero when set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateConfidence("glossary", c.Thresholds.Glossary); err != nil {
|
||||||
|
issues = append(issues, err.Error())
|
||||||
|
}
|
||||||
|
if err := validateConfidence("grammar", c.Thresholds.Grammar); err != nil {
|
||||||
|
issues = append(issues, err.Error())
|
||||||
|
}
|
||||||
|
if err := validateConfidence("homophones", c.Thresholds.Homophones); err != nil {
|
||||||
|
issues = append(issues, err.Error())
|
||||||
|
}
|
||||||
|
if err := validateConfidence("spoken-word", c.Thresholds.SpokenWord); err != nil {
|
||||||
|
issues = append(issues, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Normalization.MaxSegmentGap < 0 {
|
||||||
|
issues = append(issues, "normalize max segment gap must be zero or greater")
|
||||||
|
}
|
||||||
|
if c.Normalization.EllipsisGap < 0 {
|
||||||
|
issues = append(issues, "normalize ellipsis gap must be zero or greater")
|
||||||
|
}
|
||||||
|
if c.Normalization.MaxSegmentDuration <= 0 {
|
||||||
|
issues = append(issues, "normalize max segment duration must be greater than zero")
|
||||||
|
}
|
||||||
|
if c.Normalization.MaxSegmentTokens <= 0 {
|
||||||
|
issues = append(issues, "normalize max segment tokens must be greater than zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(c.WorkDir) == "" {
|
||||||
|
issues = append(issues, "work dir must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateRetention(c.WorkDirRetention); err != nil {
|
||||||
|
issues = append(issues, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(issues) > 0 {
|
||||||
|
return fmt.Errorf("invalid config: %s", strings.Join(issues, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfidence(name string, threshold float64) error {
|
||||||
|
if threshold < 0.0 || threshold > 1.0 {
|
||||||
|
return fmt.Errorf("%s confidence threshold must be between 0.0 and 1.0", name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRetention(retention WorkDirRetention) error {
|
||||||
|
switch retention {
|
||||||
|
case WorkDirRetentionAuto, WorkDirRetentionAlways, WorkDirRetentionNever:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("work dir retention must be one of: auto, always, never")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user