@@ -66,7 +66,7 @@ type ChatLLM struct {
6666 // ollama, anthropic, openai, openrouter, deepseek, gemini, groq,
6767 // mistral, llamacpp, llamafile. Auto-detected from base_url and
6868 // api_key when empty.
69- Provider string `toml:"provider"`
69+ Provider string `toml:"provider" validate:"provider" `
7070
7171 // BaseURL is the base URL for the provider's API.
7272 // No /v1 suffix needed -- the provider handles path construction.
@@ -81,11 +81,11 @@ type ChatLLM struct {
8181
8282 // Timeout is the inference timeout for LLM responses (including
8383 // streaming). Go duration string, e.g. "5m", "10m". Default: "5m".
84- Timeout string `toml:"timeout" default:"5m"`
84+ Timeout string `toml:"timeout" default:"5m" validate:"omitempty,positive_duration" `
8585
8686 // Thinking controls the model's reasoning effort level.
8787 // Supported: none, low, medium, high, auto. Empty = server default.
88- Thinking string `toml:"thinking,omitempty"`
88+ Thinking string `toml:"thinking,omitempty" validate:"omitempty,oneof=none low medium high auto" `
8989
9090 // ExtraContext is custom text appended to chat system prompts.
9191 // Useful for domain-specific details: house style, location, etc.
@@ -102,7 +102,7 @@ func (l ChatLLM) TimeoutDuration() time.Duration {
102102type Extraction struct {
103103 // MaxPages is the maximum number of pages for async extraction of
104104 // scanned documents. 0 means no limit. Default: 0.
105- MaxPages int `toml:"max_pages"`
105+ MaxPages int `toml:"max_pages" validate:"min=0" `
106106
107107 // LLM holds the LLM connection settings for the extraction pipeline.
108108 LLM ExtractionLLM `toml:"llm" doc:"LLM connection settings for extraction."`
@@ -121,7 +121,7 @@ type ExtractionLLM struct {
121121
122122 // Provider selects which LLM provider to use. See ChatLLM.Provider
123123 // for supported values. Auto-detected when empty.
124- Provider string `toml:"provider"`
124+ Provider string `toml:"provider" validate:"provider" `
125125
126126 // BaseURL is the base URL for the provider's API.
127127 BaseURL string `toml:"base_url" default:"http://localhost:11434"`
@@ -134,11 +134,11 @@ type ExtractionLLM struct {
134134 APIKey string `toml:"api_key"` //nolint:gosec // config field, not a hardcoded credential
135135
136136 // Timeout is the inference timeout for extraction LLM responses.
137- Timeout string `toml:"timeout" default:"5m"`
137+ Timeout string `toml:"timeout" default:"5m" validate:"omitempty,positive_duration" `
138138
139139 // Thinking controls the model's reasoning effort level.
140140 // Supported: none, low, medium, high, auto. Empty = server default.
141- Thinking string `toml:"thinking,omitempty"`
141+ Thinking string `toml:"thinking,omitempty" validate:"omitempty,oneof=none low medium high auto" `
142142}
143143
144144// IsEnabled returns whether LLM extraction is enabled. Defaults to true.
@@ -186,7 +186,7 @@ type OCRTSV struct {
186186 // OCR confidence annotations are included in spatial layout output.
187187 // Lines with min confidence >= this value omit the score to save
188188 // tokens. Set to 0 to never show confidence. Default: 70.
189- ConfidenceThreshold * int `toml:"confidence_threshold,omitempty"`
189+ ConfidenceThreshold * int `toml:"confidence_threshold,omitempty" validate:"omitempty,min=0,max=100" `
190190}
191191
192192// IsEnabled returns whether TSV spatial annotations are enabled.
@@ -230,15 +230,12 @@ type Documents struct {
230230 // MaxFileSize is the largest file that can be imported as a document
231231 // attachment. Accepts unitized strings ("50 MiB") or bare integers
232232 // (bytes). Default: 50 MiB.
233- MaxFileSize ByteSize `toml:"max_file_size" default:"52428800"`
233+ MaxFileSize ByteSize `toml:"max_file_size" default:"52428800" validate:"required" `
234234
235- // CacheTTL is the preferred cache lifetime setting. Accepts unitized
236- // strings ("30d", "720h") or bare integers (seconds). Default: 30d.
237- CacheTTL * Duration `toml:"cache_ttl,omitempty"`
238-
239- // CacheTTLDays is deprecated; use CacheTTL instead. Kept for backward
240- // compatibility. Bare integer interpreted as days.
241- CacheTTLDays * int `toml:"cache_ttl_days,omitempty"`
235+ // CacheTTL is the cache lifetime for extracted documents. Accepts
236+ // unitized strings ("30d", "720h") or bare integers (seconds).
237+ // Set to "0s" to disable eviction. Default: 30d.
238+ CacheTTL * Duration `toml:"cache_ttl,omitempty" validate:"omitempty,nonneg_duration"`
242239
243240 // FilePickerDir is the starting directory for the document file picker.
244241 // Default: the system Downloads folder (e.g. ~/Downloads).
@@ -266,14 +263,11 @@ func (d Documents) ResolvedFilePickerDir() string {
266263}
267264
268265// CacheTTLDuration returns the resolved cache TTL as a time.Duration.
269- // CacheTTL takes precedence over CacheTTLDays. Returns 0 to disable.
266+ // Returns 0 to disable eviction .
270267func (d Documents ) CacheTTLDuration () time.Duration {
271268 if d .CacheTTL != nil {
272269 return d .CacheTTL .Duration
273270 }
274- if d .CacheTTLDays != nil {
275- return time .Duration (* d .CacheTTLDays ) * 24 * time .Hour
276- }
277271 return DefaultCacheTTL
278272}
279273
@@ -332,107 +326,7 @@ func LoadFromPath(path string) (Config, error) {
332326 )
333327 }
334328
335- // Validate providers.
336- if ! validProvider (cfg .Chat .LLM .Provider ) {
337- return cfg , fmt .Errorf (
338- "chat.llm.provider: unknown provider %q -- supported: %s" ,
339- cfg .Chat .LLM .Provider , strings .Join (providerNames (), ", " ),
340- )
341- }
342- if ! validProvider (cfg .Extraction .LLM .Provider ) {
343- return cfg , fmt .Errorf (
344- "extraction.llm.provider: unknown provider %q -- supported: %s" ,
345- cfg .Extraction .LLM .Provider , strings .Join (providerNames (), ", " ),
346- )
347- }
348-
349- // Validate thinking levels.
350- if cfg .Chat .LLM .Thinking != "" && ! validThinkingLevel (cfg .Chat .LLM .Thinking ) {
351- return cfg , fmt .Errorf (
352- "chat.llm.thinking: invalid level %q -- supported: none, low, medium, high, auto" ,
353- cfg .Chat .LLM .Thinking ,
354- )
355- }
356- if cfg .Extraction .LLM .Thinking != "" && ! validThinkingLevel (cfg .Extraction .LLM .Thinking ) {
357- return cfg , fmt .Errorf (
358- "extraction.llm.thinking: invalid level %q -- supported: none, low, medium, high, auto" ,
359- cfg .Extraction .LLM .Thinking ,
360- )
361- }
362-
363- // Validate timeouts.
364- if err := validateTimeout (cfg .Chat .LLM .Timeout , "chat.llm" ); err != nil {
365- return cfg , err
366- }
367- if err := validateTimeout (cfg .Extraction .LLM .Timeout , "extraction.llm" ); err != nil {
368- return cfg , err
369- }
370-
371- if cfg .Documents .MaxFileSize == 0 {
372- return cfg , fmt .Errorf ("documents.max_file_size must be positive" )
373- }
374-
375- if cfg .Documents .CacheTTL != nil && cfg .Documents .CacheTTLDays != nil {
376- return cfg , fmt .Errorf (
377- "documents.cache_ttl and documents.cache_ttl_days cannot both be set -- " +
378- "remove cache_ttl_days (deprecated) and use cache_ttl instead" ,
379- )
380- }
381-
382- if cfg .Documents .CacheTTLDays != nil {
383- cfg .Warnings = append (
384- cfg .Warnings ,
385- "documents.cache_ttl_days is deprecated -- use documents.cache_ttl (e.g. \" 30d\" ) instead" ,
386- )
387- if * cfg .Documents .CacheTTLDays < 0 {
388- return cfg , fmt .Errorf (
389- "documents.cache_ttl_days must be non-negative, got %d" ,
390- * cfg .Documents .CacheTTLDays ,
391- )
392- }
393- }
394-
395- if cfg .Documents .CacheTTL != nil && cfg .Documents .CacheTTL .Duration < 0 {
396- return cfg , fmt .Errorf (
397- "documents.cache_ttl must be non-negative, got %s" ,
398- cfg .Documents .CacheTTL .Duration ,
399- )
400- }
401-
402- if cfg .Extraction .MaxPages < 0 {
403- return cfg , fmt .Errorf (
404- "extraction.max_pages must be non-negative, got %d" ,
405- cfg .Extraction .MaxPages ,
406- )
407- }
408-
409- if t := cfg .Extraction .OCR .TSV .Threshold (); t < 0 || t > 100 {
410- return cfg , fmt .Errorf (
411- "extraction.ocr.tsv.confidence_threshold must be 0-100, got %d" , t ,
412- )
413- }
414-
415- checkFilePermissions (& cfg , path )
416-
417- return cfg , nil
418- }
419-
420- // validateTimeout validates a pipeline timeout string.
421- func validateTimeout (timeout , prefix string ) error {
422- if timeout == "" {
423- return nil
424- }
425- d , err := time .ParseDuration (timeout )
426- if err != nil {
427- return fmt .Errorf (
428- "%s.timeout: invalid duration %q -- use Go syntax like \" 5m\" or \" 10m\" " ,
429- prefix , timeout ,
430- )
431- }
432- if d <= 0 {
433- return fmt .Errorf ("%s.timeout must be positive, got %s" , prefix , timeout )
434- }
435- return nil
329+ return cfg , cfg .validate (path )
436330}
437331
438332// applyEnvOverrides walks the Config struct and applies environment variable
@@ -794,14 +688,6 @@ func validProvider(name string) bool {
794688 return false
795689}
796690
797- var thinkingLevels = map [string ]bool {
798- "none" : true , "low" : true , "medium" : true , "high" : true , "auto" : true ,
799- }
800-
801- func validThinkingLevel (level string ) bool {
802- return thinkingLevels [level ]
803- }
804-
805691// detectProvider infers the provider from the base URL and API key.
806692func detectProvider (baseURL , apiKey string ) string {
807693 if apiKey != "" {
0 commit comments