package config import ( "fmt" "strings" "time" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) type ResolveInput struct { PipelineID string Only []string Catalog pipeline.ModuleCatalog LLMProfileOverride string ReferenceOverrides []pipeline.ReferenceBinding ReferenceUnbinds []pipeline.ReferenceUnbind } type EffectiveConfig struct { Config Config PipelineID string Only []string ReferenceOverrides []pipeline.ReferenceBinding ReferenceUnbinds []pipeline.ReferenceUnbind ResolvedPipeline pipeline.ResolvedPipeline } func (c Config) Resolve(input ResolveInput) (EffectiveConfig, error) { if err := c.Validate(); err != nil { return EffectiveConfig{}, err } pipelineID := strings.TrimSpace(input.PipelineID) if pipelineID == "" { return EffectiveConfig{}, fmt.Errorf("pipeline id must not be empty") } profile, ok := lookupPipelineProfile(c.Pipelines, pipelineID) if !ok { return EffectiveConfig{}, fmt.Errorf("pipeline %q is not configured", pipelineID) } profile = clonePipelineProfile(profile) profile.ID = pipelineID if override := strings.TrimSpace(input.LLMProfileOverride); override != "" { if !hasLLMProfile(c.LLMProfiles, override) { return EffectiveConfig{}, fmt.Errorf("LLM profile override %q is not configured", override) } applyLLMProfileOverride(&profile, override) } resolved, err := pipeline.ResolvePipeline(profile, pipeline.ResolveOptions{ Only: input.Only, ReferenceOverrides: append([]pipeline.ReferenceBinding(nil), input.ReferenceOverrides...), ReferenceUnbinds: append([]pipeline.ReferenceUnbind(nil), input.ReferenceUnbinds...), }, input.Catalog) if err != nil { return EffectiveConfig{}, fmt.Errorf("resolve pipeline %q: %w", pipelineID, err) } return EffectiveConfig{ Config: cloneConfig(c), PipelineID: pipelineID, Only: append([]string(nil), input.Only...), ReferenceOverrides: append([]pipeline.ReferenceBinding(nil), input.ReferenceOverrides...), ReferenceUnbinds: append([]pipeline.ReferenceUnbind(nil), input.ReferenceUnbinds...), ResolvedPipeline: resolved, }, nil } func applyLLMProfileOverride(profile *pipeline.PipelineProfile, profileID string) { profile.Input.LLMProfile = profileID profile.Chunk.LLMProfile = profileID profile.Output.LLMProfile = profileID for laneID, lane := range profile.Artifacts { lane.Extract.LLMProfile = profileID lane.Merge.LLMProfile = profileID lane.Normalize.LLMProfile = profileID for i := range lane.Validators { lane.Validators[i].LLMProfile = profileID } profile.Artifacts[laneID] = lane } } func lookupPipelineProfile(profiles map[string]pipeline.PipelineProfile, pipelineID string) (pipeline.PipelineProfile, bool) { pipelineID = strings.TrimSpace(pipelineID) for rawID, profile := range profiles { if strings.TrimSpace(rawID) == pipelineID { return profile, true } } return pipeline.PipelineProfile{}, false } func (c Config) OpenAICompatibleClientConfig(profileID string) (llm.OpenAICompatibleClientConfig, error) { trimmedID := strings.TrimSpace(profileID) profile, ok := c.LLMProfile(trimmedID) if !ok { return llm.OpenAICompatibleClientConfig{}, fmt.Errorf("LLM profile %q is not configured", trimmedID) } provider := strings.TrimSpace(profile.Provider) if provider == "" { provider = providerOpenAICompatible } if provider != providerOpenAICompatible { return llm.OpenAICompatibleClientConfig{}, fmt.Errorf("LLM profile %q provider %q is not supported", trimmedID, provider) } baseURL := strings.TrimSpace(profile.BaseURL) if baseURL == "" { return llm.OpenAICompatibleClientConfig{}, fmt.Errorf("LLM profile %q base URL must not be empty", trimmedID) } model := strings.TrimSpace(profile.Model) if model == "" { return llm.OpenAICompatibleClientConfig{}, fmt.Errorf("LLM profile %q model must not be empty", trimmedID) } return llm.OpenAICompatibleClientConfig{ BaseURL: baseURL, Model: model, APIKey: profile.APIKey, MaxRetries: profile.MaxRetries, RequestTimeout: time.Duration(profile.TimeoutSeconds) * time.Second, }, nil }