Implemented the config source setter cleanup identified during the code audit

This commit is contained in:
2026-05-23 18:49:44 -05:00
parent 0630d36734
commit 0b01c3a83d
6 changed files with 933 additions and 231 deletions

View File

@@ -0,0 +1,171 @@
package config
import "strings"
type llmTargetPatch struct {
apiKey *string
model *string
baseURL *string
timeoutSeconds *int
maxRetries *int
}
type concurrencyPatch struct {
totalLLM *int
legacyTotalLLM *int
proposalLLM *int
validationLLM *int
inheritProposal bool
allowLegacyAlias bool
}
type chunkingPatch struct {
targetSections *int
maxSectionTokens *int
minSectionTokens *int
}
type thresholdsPatch struct {
glossary *float64
grammar *float64
homophones *float64
spokenWord *float64
}
type normalizationPatch struct {
maxSegmentGap *float64
ellipsisGap *float64
maxSegmentDuration *float64
maxSegmentTokens *int
}
type contextPatch struct {
transcriptDescription *string
}
type diagnosticsPatch struct {
workDir *string
workDirRetention *string
}
func (c *Config) applyPrimaryLLMTargetPatch(patch llmTargetPatch) {
if patch.apiKey != nil {
c.PrimaryLLM.APIKey = *patch.apiKey
}
if patch.model != nil {
c.PrimaryLLM.Model = *patch.model
}
if patch.baseURL != nil {
c.PrimaryLLM.BaseURL = *patch.baseURL
}
if patch.timeoutSeconds != nil {
c.PrimaryLLM.TimeoutSeconds = *patch.timeoutSeconds
}
if patch.maxRetries != nil {
c.PrimaryLLM.MaxRetries = *patch.maxRetries
}
}
func (c *Config) applyValidationLLMTargetPatch(patch llmTargetPatch) {
if patch.apiKey != nil {
c.ValidationLLM.APIKey = *patch.apiKey
}
if patch.model != nil {
c.ValidationLLM.Model = *patch.model
}
if patch.baseURL != nil {
c.ValidationLLM.BaseURL = *patch.baseURL
}
if patch.timeoutSeconds != nil {
value := *patch.timeoutSeconds
c.ValidationLLM.TimeoutSeconds = &value
}
if patch.maxRetries != nil {
value := *patch.maxRetries
c.ValidationLLM.MaxRetries = &value
}
}
func (c *Config) applyConcurrencyPatch(patch concurrencyPatch) {
totalSet := false
if patch.totalLLM != nil {
c.TotalLLMConcurrency = *patch.totalLLM
totalSet = true
}
if patch.allowLegacyAlias && patch.legacyTotalLLM != nil && !totalSet {
c.TotalLLMConcurrency = *patch.legacyTotalLLM
totalSet = true
}
proposalSet := false
if patch.proposalLLM != nil {
c.ProposalLLMConcurrency = *patch.proposalLLM
proposalSet = true
}
if patch.inheritProposal && totalSet && !proposalSet {
c.ProposalLLMConcurrency = c.TotalLLMConcurrency
}
if patch.validationLLM != nil {
value := *patch.validationLLM
c.ValidationLLMConcurrency = &value
}
}
func (c *Config) applyChunkingPatch(patch chunkingPatch) {
if patch.targetSections != nil {
value := *patch.targetSections
c.TargetSections = &value
}
if patch.maxSectionTokens != nil {
c.MaxSectionTokens = *patch.maxSectionTokens
}
if patch.minSectionTokens != nil {
c.MinSectionTokens = *patch.minSectionTokens
}
}
func (c *Config) applyThresholdsPatch(patch thresholdsPatch) {
if patch.glossary != nil {
c.Thresholds.Glossary = *patch.glossary
}
if patch.grammar != nil {
c.Thresholds.Grammar = *patch.grammar
}
if patch.homophones != nil {
c.Thresholds.Homophones = *patch.homophones
}
if patch.spokenWord != nil {
c.Thresholds.SpokenWord = *patch.spokenWord
}
}
func (c *Config) applyNormalizationPatch(patch normalizationPatch) {
if patch.maxSegmentGap != nil {
c.Normalization.MaxSegmentGap = *patch.maxSegmentGap
}
if patch.ellipsisGap != nil {
c.Normalization.EllipsisGap = *patch.ellipsisGap
}
if patch.maxSegmentDuration != nil {
c.Normalization.MaxSegmentDuration = *patch.maxSegmentDuration
}
if patch.maxSegmentTokens != nil {
c.Normalization.MaxSegmentTokens = *patch.maxSegmentTokens
}
}
func (c *Config) applyContextPatch(patch contextPatch) {
if patch.transcriptDescription != nil {
c.TranscriptDescription = strings.TrimSpace(*patch.transcriptDescription)
}
}
func (c *Config) applyDiagnosticsPatch(patch diagnosticsPatch) {
if patch.workDir != nil {
c.WorkDir = *patch.workDir
}
if patch.workDirRetention != nil {
c.WorkDirRetention = WorkDirRetention(*patch.workDirRetention)
}
}

View File

@@ -239,6 +239,186 @@ func TestApplyCLIOverridesTrimsTranscriptDescription(t *testing.T) {
}
}
func TestConfigSourcesApplySharedEffectiveFieldsConsistently(t *testing.T) {
fileCfg := mustParseFileConfigYAML(t, `
version: 1
output:
schema: " audita-v1 "
llm:
proposal:
base_url: https://proposal.example.test/v1
model: provider/proposal
timeout: 101
max_retries: 5
validation:
base_url: https://validation.example.test/v1
model: provider/validation
timeout: 202
max_retries: 6
chunking:
target_sections: 7
max_section_tokens: 9000
min_section_tokens: 1000
thresholds:
glossary: 0.91
grammar: 0.92
homophones: 0.93
spoken_word: 0.94
normalization:
max_segment_gap: 1.2
ellipsis_gap: 2.3
max_segment_duration: 45.6
max_segment_tokens: 321
context:
description: " shared context "
diagnostics:
work_dir: /tmp/audita-shared
retention: always
`)
tests := []struct {
name string
apply func(*Config) error
}{
{
name: "file",
apply: func(cfg *Config) error {
return cfg.applyFileConfigWithLookup(fileCfg, mapLookup(map[string]string{}))
},
},
{
name: "env",
apply: func(cfg *Config) error {
return cfg.applyEnvOverrides(mapLookup(map[string]string{
"AUDITA_MODEL": "provider/proposal",
"AUDITA_BASE_URL": "https://proposal.example.test/v1",
"AUDITA_LLM_TIMEOUT_SECONDS": "101",
"AUDITA_MAX_RETRIES": "5",
"AUDITA_VALIDATION_MODEL": "provider/validation",
"AUDITA_VALIDATION_BASE_URL": "https://validation.example.test/v1",
"AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS": "202",
"AUDITA_VALIDATION_MAX_RETRIES": "6",
"AUDITA_TARGET_SECTIONS": "7",
"AUDITA_MAX_SECTION_TOKENS": "9000",
"AUDITA_MIN_SECTION_TOKENS": "1000",
"AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD": "0.91",
"AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD": "0.92",
"AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD": "0.93",
"AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD": "0.94",
"AUDITA_NORMALIZE_MAX_SEGMENT_GAP": "1.2",
"AUDITA_NORMALIZE_ELLIPSIS_GAP": "2.3",
"AUDITA_NORMALIZE_MAX_SEGMENT_DURATION": "45.6",
"AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS": "321",
"AUDITA_WORK_DIR": "/tmp/audita-shared",
"AUDITA_WORK_DIR_RETENTION": "always",
}))
},
},
{
name: "cli",
apply: func(cfg *Config) error {
outputSchema := " audita-v1 "
proposalModel := "provider/proposal"
proposalBaseURL := "https://proposal.example.test/v1"
proposalTimeout := 101
proposalMaxRetries := 5
validationModel := "provider/validation"
validationBaseURL := "https://validation.example.test/v1"
validationTimeout := 202
validationMaxRetries := 6
targetSections := 7
maxSectionTokens := 9000
minSectionTokens := 1000
glossaryThreshold := 0.91
grammarThreshold := 0.92
homophonesThreshold := 0.93
spokenWordThreshold := 0.94
normalizeMaxSegmentGap := 1.2
normalizeEllipsisGap := 2.3
normalizeMaxSegmentDuration := 45.6
normalizeMaxSegmentTokens := 321
description := " shared context "
workDir := "/tmp/audita-shared"
workDirRetention := "always"
return cfg.ApplyCLIOverrides(CLIOverrides{
OutputSchema: &outputSchema,
PrimaryModel: &proposalModel,
PrimaryBaseURL: &proposalBaseURL,
PrimaryLLMTimeoutSeconds: &proposalTimeout,
MaxRetries: &proposalMaxRetries,
ValidationModel: &validationModel,
ValidationBaseURL: &validationBaseURL,
ValidationLLMTimeoutSeconds: &validationTimeout,
ValidationMaxRetries: &validationMaxRetries,
TargetSections: &targetSections,
MaxSectionTokens: &maxSectionTokens,
MinSectionTokens: &minSectionTokens,
GlossaryConfidenceThreshold: &glossaryThreshold,
GrammarConfidenceThreshold: &grammarThreshold,
HomophonesConfidenceThreshold: &homophonesThreshold,
SpokenWordConfidenceThreshold: &spokenWordThreshold,
NormalizeMaxSegmentGap: &normalizeMaxSegmentGap,
NormalizeEllipsisGap: &normalizeEllipsisGap,
NormalizeMaxSegmentDuration: &normalizeMaxSegmentDuration,
NormalizeMaxSegmentTokens: &normalizeMaxSegmentTokens,
TranscriptDescription: &description,
WorkDir: &workDir,
WorkDirRetention: &workDirRetention,
})
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cfg := Default()
if err := tc.apply(&cfg); err != nil {
t.Fatalf("apply config source: %v", err)
}
assertSharedEffectiveFields(t, cfg, sharedEffectiveFieldOptions{
wantOutputSchemaOverride: tc.name != "env",
wantTranscriptDescriptionPatch: tc.name != "env",
})
})
}
}
func TestCLIAPIKeyOverrideIsDirectValue(t *testing.T) {
cfg := Default()
apiKey := "NOT_AN_ENV_VAR_NAME"
validationAPIKey := "also direct"
if err := cfg.ApplyCLIOverrides(CLIOverrides{PrimaryLLMAPIKey: &apiKey, ValidationLLMAPIKey: &validationAPIKey}); err != nil {
t.Fatalf("ApplyCLIOverrides failed: %v", err)
}
if cfg.PrimaryLLM.APIKey != apiKey {
t.Fatalf("expected direct primary api key, got %q", cfg.PrimaryLLM.APIKey)
}
if cfg.ValidationLLM.APIKey != validationAPIKey {
t.Fatalf("expected direct validation api key, got %q", cfg.ValidationLLM.APIKey)
}
}
func TestApplyFileConfigTotalConcurrencyDoesNotChangeProposalWhenProposalUnset(t *testing.T) {
fileCfg := mustParseFileConfigYAML(t, `
version: 1
concurrency:
total_llm: 4
`)
cfg := Default()
if err := cfg.applyFileConfigWithLookup(fileCfg, mapLookup(map[string]string{})); err != nil {
t.Fatalf("applyFileConfigWithLookup failed: %v", err)
}
if cfg.TotalLLMConcurrency != 4 {
t.Fatalf("expected file total concurrency 4, got %d", cfg.TotalLLMConcurrency)
}
if cfg.ProposalLLMConcurrency != DefaultLLMConcurrency {
t.Fatalf("expected file config to preserve proposal concurrency when unset, got %d", cfg.ProposalLLMConcurrency)
}
}
func TestValidationRejectsOverlyLongTranscriptDescription(t *testing.T) {
cfg := Default()
cfg.TranscriptDescription = strings.Repeat("a", DefaultTranscriptDescriptionMaxChars+1)
@@ -456,3 +636,70 @@ func mapLookup(values map[string]string) func(string) (string, bool) {
return value, ok
}
}
func mustParseFileConfigYAML(t *testing.T, raw string) FileConfig {
t.Helper()
fileCfg, err := ParseFileConfigYAML([]byte(raw))
if err != nil {
t.Fatalf("ParseFileConfigYAML failed: %v", err)
}
return fileCfg
}
type sharedEffectiveFieldOptions struct {
wantOutputSchemaOverride bool
wantTranscriptDescriptionPatch bool
}
func assertSharedEffectiveFields(t *testing.T, cfg Config, opts sharedEffectiveFieldOptions) {
t.Helper()
wantOutputSchema := DefaultOutputSchema
if opts.wantOutputSchemaOverride {
wantOutputSchema = "audita-v1"
}
if cfg.OutputSchema != wantOutputSchema {
t.Fatalf("unexpected output schema: %q", cfg.OutputSchema)
}
if cfg.PrimaryLLM.Model != "provider/proposal" ||
cfg.PrimaryLLM.BaseURL != "https://proposal.example.test/v1" ||
cfg.PrimaryLLM.TimeoutSeconds != 101 ||
cfg.PrimaryLLM.MaxRetries != 5 {
t.Fatalf("unexpected primary llm config: %+v", cfg.PrimaryLLM)
}
if cfg.ValidationLLM.Model != "provider/validation" ||
cfg.ValidationLLM.BaseURL != "https://validation.example.test/v1" ||
cfg.ValidationLLM.TimeoutSeconds == nil ||
*cfg.ValidationLLM.TimeoutSeconds != 202 ||
cfg.ValidationLLM.MaxRetries == nil ||
*cfg.ValidationLLM.MaxRetries != 6 {
t.Fatalf("unexpected validation llm config: %+v", cfg.ValidationLLM)
}
if cfg.TargetSections == nil || *cfg.TargetSections != 7 ||
cfg.MaxSectionTokens != 9000 ||
cfg.MinSectionTokens != 1000 {
t.Fatalf("unexpected chunking config: target=%v max=%d min=%d", cfg.TargetSections, cfg.MaxSectionTokens, cfg.MinSectionTokens)
}
if cfg.Thresholds.Glossary != 0.91 ||
cfg.Thresholds.Grammar != 0.92 ||
cfg.Thresholds.Homophones != 0.93 ||
cfg.Thresholds.SpokenWord != 0.94 {
t.Fatalf("unexpected thresholds: %+v", cfg.Thresholds)
}
if cfg.Normalization.MaxSegmentGap != 1.2 ||
cfg.Normalization.EllipsisGap != 2.3 ||
cfg.Normalization.MaxSegmentDuration != 45.6 ||
cfg.Normalization.MaxSegmentTokens != 321 {
t.Fatalf("unexpected normalization: %+v", cfg.Normalization)
}
wantDescription := ""
if opts.wantTranscriptDescriptionPatch {
wantDescription = "shared context"
}
if cfg.TranscriptDescription != wantDescription {
t.Fatalf("unexpected transcript description: %q", cfg.TranscriptDescription)
}
if cfg.WorkDir != "/tmp/audita-shared" ||
cfg.WorkDirRetention != WorkDirRetentionAlways {
t.Fatalf("unexpected diagnostics config: work_dir=%q retention=%q", cfg.WorkDir, cfg.WorkDirRetention)
}
}

View File

@@ -7,8 +7,8 @@ import (
)
const (
DefaultConfigPath = "/etc/audita/config.yml"
DefaultConfigPathUsrLocal = "/usr/local/etc/audita/config.yml"
DefaultConfigPath = "/etc/audita/config.yml"
DefaultConfigPathUsrLocal = "/usr/local/etc/audita/config.yml"
)
var DefaultConfigSearchPaths = []string{
@@ -50,100 +50,93 @@ func (c *Config) applyEnvOverrides(lookup func(string) (string, bool)) error {
cfg.Modules = modules
}
primaryLLM := llmTargetPatch{}
if raw, ok := lookup("AUDITA_LLM_API_KEY"); ok {
cfg.PrimaryLLM.APIKey = raw
primaryLLM.apiKey = &raw
} else if raw, ok := lookup("OPENROUTER_API_KEY"); ok {
cfg.PrimaryLLM.APIKey = raw
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
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
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 fmt.Errorf("AUDITA_LLM_TIMEOUT_SECONDS: %w", err)
}
cfg.PrimaryLLM.TimeoutSeconds = value
primaryLLM.timeoutSeconds = &value
}
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)
}
cfg.ValidationLLM.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)
}
cfg.PrimaryLLM.MaxRetries = value
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,
}
totalConcurrencySet := false
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)
}
cfg.TotalLLMConcurrency = value
totalConcurrencySet = true
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)
}
if !totalConcurrencySet {
cfg.TotalLLMConcurrency = value
totalConcurrencySet = true
}
concurrency.legacyTotalLLM = &value
}
proposalConcurrencySet := false
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)
}
cfg.ProposalLLMConcurrency = value
proposalConcurrencySet = true
}
if totalConcurrencySet && !proposalConcurrencySet {
cfg.ProposalLLMConcurrency = cfg.TotalLLMConcurrency
}
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)
}
cfg.ValidationLLM.MaxRetries = &value
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)
}
cfg.ValidationLLMConcurrency = &value
concurrency.validationLLM = &value
}
cfg.applyConcurrencyPatch(concurrency)
if raw, ok := lookup("AUDITA_VALIDATION_MAX_PROMPT_TOKENS"); ok {
value, err := parseInt(raw)
@@ -153,12 +146,13 @@ func (c *Config) applyEnvOverrides(lookup func(string) (string, bool)) error {
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)
}
cfg.MaxSectionTokens = value
chunking.maxSectionTokens = &value
}
if raw, ok := lookup("AUDITA_MIN_SECTION_TOKENS"); ok {
@@ -166,7 +160,7 @@ func (c *Config) applyEnvOverrides(lookup func(string) (string, bool)) error {
if err != nil {
return fmt.Errorf("AUDITA_MIN_SECTION_TOKENS: %w", err)
}
cfg.MinSectionTokens = value
chunking.minSectionTokens = &value
}
if raw, ok := lookup("AUDITA_TARGET_SECTIONS"); ok {
@@ -174,73 +168,80 @@ func (c *Config) applyEnvOverrides(lookup func(string) (string, bool)) error {
if err != nil {
return fmt.Errorf("AUDITA_TARGET_SECTIONS: %w", err)
}
cfg.TargetSections = &value
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)
}
cfg.Thresholds.Glossary = value
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)
}
cfg.Thresholds.Grammar = value
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)
}
cfg.Thresholds.Homophones = value
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)
}
cfg.Thresholds.SpokenWord = value
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)
}
cfg.Normalization.MaxSegmentGap = value
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)
}
cfg.Normalization.EllipsisGap = value
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)
}
cfg.Normalization.MaxSegmentDuration = value
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)
}
cfg.Normalization.MaxSegmentTokens = value
normalization.maxSegmentTokens = &value
}
cfg.applyNormalizationPatch(normalization)
diagnostics := diagnosticsPatch{}
if raw, ok := lookup("AUDITA_WORK_DIR"); ok {
cfg.WorkDir = raw
diagnostics.workDir = &raw
}
if raw, ok := lookup("AUDITA_WORK_DIR_RETENTION"); ok {
cfg.WorkDirRetention = WorkDirRetention(raw)
diagnostics.workDirRetention = &raw
}
cfg.applyDiagnosticsPatch(diagnostics)
cfg.syncLegacyConcurrencyAliases()

View File

@@ -205,118 +205,98 @@ func (c *Config) applyFileConfigWithLookup(fileCfg FileConfig, lookup func(strin
if fileCfg.LLM != nil {
if fileCfg.LLM.Proposal != nil {
if fileCfg.LLM.Proposal.BaseURL != nil {
c.PrimaryLLM.BaseURL = *fileCfg.LLM.Proposal.BaseURL
}
if fileCfg.LLM.Proposal.Model != nil {
c.PrimaryLLM.Model = *fileCfg.LLM.Proposal.Model
patch := llmTargetPatch{
model: fileCfg.LLM.Proposal.Model,
baseURL: fileCfg.LLM.Proposal.BaseURL,
maxRetries: fileCfg.LLM.Proposal.MaxRetries,
}
if fileCfg.LLM.Proposal.Timeout != nil {
c.PrimaryLLM.TimeoutSeconds = fileCfg.LLM.Proposal.Timeout.Seconds()
}
if fileCfg.LLM.Proposal.MaxRetries != nil {
c.PrimaryLLM.MaxRetries = *fileCfg.LLM.Proposal.MaxRetries
timeoutSeconds := fileCfg.LLM.Proposal.Timeout.Seconds()
patch.timeoutSeconds = &timeoutSeconds
}
if fileCfg.LLM.Proposal.APIKeyEnv != nil {
apiKey, err := resolveAPIKeyEnv(*fileCfg.LLM.Proposal.APIKeyEnv, lookup)
if err != nil {
return fmt.Errorf("llm.proposal.api_key_env: %w", err)
}
c.PrimaryLLM.APIKey = apiKey
patch.apiKey = &apiKey
}
c.applyPrimaryLLMTargetPatch(patch)
}
if fileCfg.LLM.Validation != nil {
if fileCfg.LLM.Validation.BaseURL != nil {
c.ValidationLLM.BaseURL = *fileCfg.LLM.Validation.BaseURL
}
if fileCfg.LLM.Validation.Model != nil {
c.ValidationLLM.Model = *fileCfg.LLM.Validation.Model
patch := llmTargetPatch{
model: fileCfg.LLM.Validation.Model,
baseURL: fileCfg.LLM.Validation.BaseURL,
maxRetries: fileCfg.LLM.Validation.MaxRetries,
}
if fileCfg.LLM.Validation.Timeout != nil {
v := fileCfg.LLM.Validation.Timeout.Seconds()
c.ValidationLLM.TimeoutSeconds = &v
}
if fileCfg.LLM.Validation.MaxRetries != nil {
v := *fileCfg.LLM.Validation.MaxRetries
c.ValidationLLM.MaxRetries = &v
timeoutSeconds := fileCfg.LLM.Validation.Timeout.Seconds()
patch.timeoutSeconds = &timeoutSeconds
}
if fileCfg.LLM.Validation.APIKeyEnv != nil {
apiKey, err := resolveAPIKeyEnv(*fileCfg.LLM.Validation.APIKeyEnv, lookup)
if err != nil {
return fmt.Errorf("llm.validation.api_key_env: %w", err)
}
c.ValidationLLM.APIKey = apiKey
patch.apiKey = &apiKey
}
c.applyValidationLLMTargetPatch(patch)
}
}
if fileCfg.Concurrency != nil {
if fileCfg.Concurrency.TotalLLM != nil {
c.TotalLLMConcurrency = *fileCfg.Concurrency.TotalLLM
}
if fileCfg.Concurrency.ProposalLLM != nil {
c.ProposalLLMConcurrency = *fileCfg.Concurrency.ProposalLLM
}
if fileCfg.Concurrency.ValidationLLM != nil {
v := *fileCfg.Concurrency.ValidationLLM
c.ValidationLLMConcurrency = &v
}
c.applyConcurrencyPatch(concurrencyPatch{
totalLLM: fileCfg.Concurrency.TotalLLM,
proposalLLM: fileCfg.Concurrency.ProposalLLM,
validationLLM: fileCfg.Concurrency.ValidationLLM,
})
}
if fileCfg.Chunking != nil {
if fileCfg.Chunking.TargetSections != nil {
v := *fileCfg.Chunking.TargetSections
c.TargetSections = &v
}
if fileCfg.Chunking.MaxSectionTokens != nil {
c.MaxSectionTokens = *fileCfg.Chunking.MaxSectionTokens
}
if fileCfg.Chunking.MinSectionTokens != nil {
c.MinSectionTokens = *fileCfg.Chunking.MinSectionTokens
}
c.applyChunkingPatch(chunkingPatch{
targetSections: fileCfg.Chunking.TargetSections,
maxSectionTokens: fileCfg.Chunking.MaxSectionTokens,
minSectionTokens: fileCfg.Chunking.MinSectionTokens,
})
}
if fileCfg.Normalization != nil {
patch := normalizationPatch{
maxSegmentTokens: fileCfg.Normalization.MaxSegmentTokens,
}
if fileCfg.Normalization.MaxSegmentGap != nil {
c.Normalization.MaxSegmentGap = fileCfg.Normalization.MaxSegmentGap.Seconds()
maxSegmentGap := fileCfg.Normalization.MaxSegmentGap.Seconds()
patch.maxSegmentGap = &maxSegmentGap
}
if fileCfg.Normalization.EllipsisGap != nil {
c.Normalization.EllipsisGap = fileCfg.Normalization.EllipsisGap.Seconds()
ellipsisGap := fileCfg.Normalization.EllipsisGap.Seconds()
patch.ellipsisGap = &ellipsisGap
}
if fileCfg.Normalization.MaxSegmentDuration != nil {
c.Normalization.MaxSegmentDuration = fileCfg.Normalization.MaxSegmentDuration.Seconds()
}
if fileCfg.Normalization.MaxSegmentTokens != nil {
c.Normalization.MaxSegmentTokens = *fileCfg.Normalization.MaxSegmentTokens
maxSegmentDuration := fileCfg.Normalization.MaxSegmentDuration.Seconds()
patch.maxSegmentDuration = &maxSegmentDuration
}
c.applyNormalizationPatch(patch)
}
if fileCfg.Thresholds != nil {
if fileCfg.Thresholds.Glossary != nil {
c.Thresholds.Glossary = *fileCfg.Thresholds.Glossary
}
if fileCfg.Thresholds.Homophones != nil {
c.Thresholds.Homophones = *fileCfg.Thresholds.Homophones
}
if fileCfg.Thresholds.SpokenWord != nil {
c.Thresholds.SpokenWord = *fileCfg.Thresholds.SpokenWord
}
if fileCfg.Thresholds.Grammar != nil {
c.Thresholds.Grammar = *fileCfg.Thresholds.Grammar
}
c.applyThresholdsPatch(thresholdsPatch{
glossary: fileCfg.Thresholds.Glossary,
grammar: fileCfg.Thresholds.Grammar,
homophones: fileCfg.Thresholds.Homophones,
spokenWord: fileCfg.Thresholds.SpokenWord,
})
}
if fileCfg.Context != nil && fileCfg.Context.Description != nil {
c.TranscriptDescription = strings.TrimSpace(*fileCfg.Context.Description)
c.applyContextPatch(contextPatch{transcriptDescription: fileCfg.Context.Description})
}
if fileCfg.Diagnostics != nil {
if fileCfg.Diagnostics.WorkDir != nil {
c.WorkDir = *fileCfg.Diagnostics.WorkDir
}
if fileCfg.Diagnostics.Retention != nil {
c.WorkDirRetention = WorkDirRetention(*fileCfg.Diagnostics.Retention)
}
c.applyDiagnosticsPatch(diagnosticsPatch{
workDir: fileCfg.Diagnostics.WorkDir,
workDirRetention: fileCfg.Diagnostics.Retention,
})
}
c.syncLegacyConcurrencyAliases()

View File

@@ -51,107 +51,54 @@ func (c *Config) ApplyCLIOverrides(overrides CLIOverrides) error {
c.OutputSchema = strings.TrimSpace(*overrides.OutputSchema)
}
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
}
totalConcurrencySet := false
if overrides.TotalLLMConcurrency != nil {
c.TotalLLMConcurrency = *overrides.TotalLLMConcurrency
totalConcurrencySet = true
}
// Backward-compatible alias: --llm-concurrency maps to total concurrency
// only when --total-llm-concurrency is not set in the same CLI invocation.
if overrides.PrimaryLLMConcurrency != nil && !totalConcurrencySet {
c.TotalLLMConcurrency = *overrides.PrimaryLLMConcurrency
totalConcurrencySet = true
}
proposalConcurrencySet := false
if overrides.ProposalLLMConcurrency != nil {
c.ProposalLLMConcurrency = *overrides.ProposalLLMConcurrency
proposalConcurrencySet = true
}
if totalConcurrencySet && !proposalConcurrencySet {
c.ProposalLLMConcurrency = c.TotalLLMConcurrency
}
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.ValidationLLMConcurrency != nil {
value := *overrides.ValidationLLMConcurrency
c.ValidationLLMConcurrency = &value
}
c.applyPrimaryLLMTargetPatch(llmTargetPatch{
apiKey: overrides.PrimaryLLMAPIKey,
model: overrides.PrimaryModel,
baseURL: overrides.PrimaryBaseURL,
timeoutSeconds: overrides.PrimaryLLMTimeoutSeconds,
maxRetries: overrides.MaxRetries,
})
c.applyValidationLLMTargetPatch(llmTargetPatch{
apiKey: overrides.ValidationLLMAPIKey,
model: overrides.ValidationModel,
baseURL: overrides.ValidationBaseURL,
timeoutSeconds: overrides.ValidationLLMTimeoutSeconds,
maxRetries: overrides.ValidationMaxRetries,
})
c.applyConcurrencyPatch(concurrencyPatch{
totalLLM: overrides.TotalLLMConcurrency,
legacyTotalLLM: overrides.PrimaryLLMConcurrency,
proposalLLM: overrides.ProposalLLMConcurrency,
validationLLM: overrides.ValidationLLMConcurrency,
inheritProposal: true,
allowLegacyAlias: true,
})
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.TranscriptDescription != nil {
c.TranscriptDescription = strings.TrimSpace(*overrides.TranscriptDescription)
}
if overrides.WorkDir != nil {
c.WorkDir = *overrides.WorkDir
}
if overrides.WorkDirRetention != nil {
c.WorkDirRetention = WorkDirRetention(*overrides.WorkDirRetention)
}
c.applyChunkingPatch(chunkingPatch{
targetSections: overrides.TargetSections,
maxSectionTokens: overrides.MaxSectionTokens,
minSectionTokens: overrides.MinSectionTokens,
})
c.applyThresholdsPatch(thresholdsPatch{
glossary: overrides.GlossaryConfidenceThreshold,
grammar: overrides.GrammarConfidenceThreshold,
homophones: overrides.HomophonesConfidenceThreshold,
spokenWord: overrides.SpokenWordConfidenceThreshold,
})
c.applyNormalizationPatch(normalizationPatch{
maxSegmentGap: overrides.NormalizeMaxSegmentGap,
ellipsisGap: overrides.NormalizeEllipsisGap,
maxSegmentDuration: overrides.NormalizeMaxSegmentDuration,
maxSegmentTokens: overrides.NormalizeMaxSegmentTokens,
})
c.applyContextPatch(contextPatch{transcriptDescription: overrides.TranscriptDescription})
c.applyDiagnosticsPatch(diagnosticsPatch{
workDir: overrides.WorkDir,
workDirRetention: overrides.WorkDirRetention,
})
c.syncLegacyConcurrencyAliases()