package config import ( "fmt" "strings" "gitea.maximumdirect.net/eric/notarius/internal/core/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) const providerOpenAICompatible = "openai-compatible" func (c Config) Validate() error { if err := validateLLMProfiles(c.LLMProfiles); err != nil { return err } if err := validateDiagnostics(c.Diagnostics); err != nil { return err } if c.Concurrency.TotalLLM <= 0 { return fmt.Errorf("total LLM concurrency must be greater than zero") } return validatePipelineProfiles(c.Pipelines, c.LLMProfiles) } func (c Config) LLMProfile(id string) (LLMProfile, bool) { trimmedID := strings.TrimSpace(id) for rawID, profile := range c.LLMProfiles { if strings.TrimSpace(rawID) == trimmedID { return profile, true } } return LLMProfile{}, false } func validateLLMProfiles(profiles map[string]LLMProfile) error { seen := make(map[string]struct{}, len(profiles)) for rawID, profile := range profiles { id := strings.TrimSpace(rawID) if id == "" { return fmt.Errorf("LLM profile id must not be empty") } if _, ok := seen[id]; ok { return fmt.Errorf("LLM profile id %q is duplicated after trimming", id) } seen[id] = struct{}{} provider := strings.TrimSpace(profile.Provider) if provider != "" && provider != providerOpenAICompatible { return fmt.Errorf("LLM profile %q provider %q is not supported", id, provider) } if profile.TimeoutSeconds < 0 { return fmt.Errorf("LLM profile %q timeout seconds must not be negative", id) } if profile.MaxRetries < 0 { return fmt.Errorf("LLM profile %q max retries must not be negative", id) } if profile.MaxConcurrency < 0 { return fmt.Errorf("LLM profile %q max concurrency must not be negative", id) } } return nil } func validateDiagnostics(cfg DiagnosticsConfig) error { if strings.TrimSpace(cfg.WorkDir) == "" { return fmt.Errorf("diagnostics work dir must not be empty") } switch cfg.Retention { case "", diagnostics.RetentionAuto, diagnostics.RetentionAlways, diagnostics.RetentionNever: return nil default: return fmt.Errorf("diagnostics retention %q is not supported", cfg.Retention) } } func validatePipelineProfiles(profiles map[string]pipeline.PipelineProfile, llmProfiles map[string]LLMProfile) error { seen := make(map[string]struct{}, len(profiles)) for rawID, profile := range profiles { id := strings.TrimSpace(rawID) if id == "" { return fmt.Errorf("pipeline id must not be empty") } if _, ok := seen[id]; ok { return fmt.Errorf("pipeline id %q is duplicated after trimming", id) } seen[id] = struct{}{} if profile.ID != "" && strings.TrimSpace(profile.ID) != id { return fmt.Errorf("pipeline %q profile id %q does not match map key", id, profile.ID) } if err := validateBindingLLMProfile(id, "", "input", profile.Input, llmProfiles); err != nil { return err } if err := validateBindingLLMProfile(id, "", "chunk", profile.Chunk, llmProfiles); err != nil { return err } if err := validateBindingLLMProfile(id, "", "output", profile.Output, llmProfiles); err != nil { return err } for rawLaneID, lane := range profile.Artifacts { laneID := strings.TrimSpace(rawLaneID) if laneID == "" { return fmt.Errorf("pipeline %q artifact lane id must not be empty", id) } if err := validateBindingLLMProfile(id, laneID, "extract", lane.Extract, llmProfiles); err != nil { return err } if err := validateBindingLLMProfile(id, laneID, "merge", lane.Merge, llmProfiles); err != nil { return err } if err := validateBindingLLMProfile(id, laneID, "normalize", lane.Normalize, llmProfiles); err != nil { return err } for i, validator := range lane.Validators { if err := validateBindingLLMProfile(id, laneID, fmt.Sprintf("validator[%d]", i), validator, llmProfiles); err != nil { return err } } } } return nil } func validateBindingLLMProfile( pipelineID string, laneID string, slot string, binding pipeline.ModuleBinding, profiles map[string]LLMProfile, ) error { profileID := strings.TrimSpace(binding.LLMProfile) if profileID == "" { profileID = pipeline.DefaultLLMProfile } if hasLLMProfile(profiles, profileID) { return nil } if laneID != "" { return fmt.Errorf("pipeline %q lane %q %s references unknown LLM profile %q", pipelineID, laneID, slot, profileID) } return fmt.Errorf("pipeline %q %s references unknown LLM profile %q", pipelineID, slot, profileID) } func hasLLMProfile(profiles map[string]LLMProfile, profileID string) bool { profileID = strings.TrimSpace(profileID) for rawID := range profiles { if strings.TrimSpace(rawID) == profileID { return true } } return false }