// Package configuration adapts ConfigKit's effective configuration to // Promptkit's definition and execution boundaries. package configuration import ( "fmt" "net/http" "regexp" "sort" "strings" "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/promptkit/internal/backend" "gitea.maximumdirect.net/eric/promptkit/internal/domain" "gitea.maximumdirect.net/eric/promptkit/internal/jsonvalue" "gitea.maximumdirect.net/eric/promptkit/internal/llm" ) type document struct { Backends map[string]backendConfig `yaml:"backends"` Profiles map[string]profileConfig `yaml:"profiles"` Prompts map[string]promptConfig `yaml:"prompts"` } type backendConfig struct { Endpoint *string `yaml:"endpoint"` APIKeyEnv *string `yaml:"api_key_env"` ConcurrencyLimit *int `yaml:"concurrency_limit"` QueueCapacity *int `yaml:"queue_capacity"` ExtraParams map[string]any `yaml:"extra_params"` SmokeTests *backendSmokeTestConfig `yaml:"smoke_tests"` } type backendSmokeTestConfig struct { DefaultProfile *string `yaml:"default_profile"` Reachability *reachabilityProbeConfig `yaml:"reachability"` Authentication *authenticationProbeConfig `yaml:"authentication"` } type reachabilityProbeConfig struct { Endpoint *string `yaml:"endpoint"` } type authenticationProbeConfig struct { Kind *string `yaml:"kind"` Endpoint *string `yaml:"endpoint"` } type profileConfig struct { Backend *string `yaml:"backend"` Endpoint *string `yaml:"endpoint"` Model *string `yaml:"model"` Temperature *float64 `yaml:"temperature"` TopP *float64 `yaml:"top_p"` MaxTokens *int `yaml:"max_tokens"` TimeoutSeconds *int `yaml:"timeout_seconds"` ServiceTier *string `yaml:"service_tier"` ReasoningEffort *string `yaml:"reasoning_effort"` APIKeyEnv *string `yaml:"api_key_env"` APIKeyRequired *bool `yaml:"api_key_required"` ExtraParams map[string]any `yaml:"extra_params"` } type promptConfig struct { Version *string `yaml:"version"` DefaultProfile *string `yaml:"default_profile"` Description *string `yaml:"description"` SessionID *string `yaml:"session_id"` Inputs []promptInputConfig `yaml:"inputs"` Messages []promptMessageConfig `yaml:"messages"` Output *promptOutputConfig `yaml:"output"` } type promptInputConfig struct { Name string `yaml:"name"` Required bool `yaml:"required"` ContentType *string `yaml:"content_type"` Description *string `yaml:"description"` } type promptMessageConfig struct { Role *string `yaml:"role"` Content *string `yaml:"content"` ContentFile *string `yaml:"content_file"` CacheControl *cacheControlConfig `yaml:"cache_control"` } type cacheControlConfig struct { Type *string `yaml:"type"` TTL *string `yaml:"ttl"` } type promptOutputConfig struct { Format *string `yaml:"format"` ValidationMode *string `yaml:"validation_mode"` SchemaPath *string `yaml:"schema_path"` RepairAttempts int `yaml:"repair_attempts"` } // fieldState distinguishes omission, an explicit value, and an explicit null. // Inherited values are explicit because ConfigKit has already expanded them. type fieldState uint8 const ( fieldAbsent fieldState = iota fieldValue fieldNull ) type loaded struct { result *configkit.Result[document] backends map[string]map[string]fieldState profiles map[string]map[string]fieldState promptMetadata map[string][]promptInputPresence prompts map[string]*promptEntry registry *backend.Registry indexedProfiles map[string]domain.ExecutionProfile profileIDs []string promptIDs []domain.PromptIdentity } type resourceBases struct { backends configkit.Path profiles configkit.Path prompts configkit.Path } func rootResourceBases() resourceBases { return resourceBases{ backends: configkit.Keys("backends"), profiles: configkit.Keys("profiles"), prompts: configkit.Keys("prompts"), } } var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) var backendFields = []string{"endpoint", "api_key_env", "concurrency_limit", "queue_capacity", "extra_params", "smoke_tests"} var profileFields = []string{"backend", "endpoint", "model", "temperature", "top_p", "max_tokens", "timeout_seconds", "service_tier", "reasoning_effort", "api_key_env", "api_key_required", "extra_params"} // Load captures one immutable effective configuration for internal consumers. func Load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) { return load(main, underlays) } // load performs one ConfigKit lifecycle over the selected source tiers. func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) { bases := rootResourceBases() state := &loaded{ backends: make(map[string]map[string]fieldState), profiles: make(map[string]map[string]fieldState), promptMetadata: make(map[string][]promptInputPresence), } result, err := configkit.LoadLayered(main, configkit.LayeredOptions[document]{ Underlays: underlays, Definition: configkit.Definition[document]{ Secret: secretProviderPath, Resolve: func(config *document, resolution *configkit.Resolution) error { captureFields(config.Backends, bases.backends, backendFields, resolution, state.backends) captureFields(config.Profiles, bases.profiles, profileFields, resolution, state.profiles) capturePromptMetadata(config.Prompts, bases.prompts, resolution, state.promptMetadata) resolveBackends(config.Backends, bases.backends, resolution) resolveProfiles(config.Profiles, bases.profiles, resolution) resolvePrompts(config.Prompts, bases.prompts, resolution) return nil }, Validate: func(config *document, validation *configkit.Validation) { validateEffective(config, bases, validation) }, }, }) if err != nil { return nil, fmt.Errorf("load configuration: %w", err) } state.result = result if err := state.materializePrompts(result.Config.Prompts, bases.prompts); err != nil { return nil, fmt.Errorf("load configuration: %w", err) } if err := state.buildExecutionProfiles(); err != nil { return nil, fmt.Errorf("load configuration: %w", err) } state.profileIDs = sortedKeys(state.indexedProfiles) for _, id := range sortedKeys(state.prompts) { state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id, Version: stringPointerValue(state.prompts[id].definition.Version)}) } return state, nil } type promptInputPresence struct { contentType fieldState description fieldState } func capturePromptMetadata(prompts map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string][]promptInputPresence) { for _, id := range sortedKeys(prompts) { inputs := prompts[id].Inputs states := make([]promptInputPresence, len(inputs)) base := basePath.Key(id).Key("inputs") for index := range inputs { path := base.Index(index) states[index] = promptInputPresence{ contentType: captureFieldState(path.Key("content_type"), resolution), description: captureFieldState(path.Key("description"), resolution), } } destination[id] = states } } func captureFieldState(path configkit.Path, resolution *configkit.Resolution) fieldState { info, exists := resolution.InputInfo(path) switch { case !exists || !info.Explicit: return fieldAbsent case info.Null: return fieldNull default: return fieldValue } } func captureFields[T any](entries map[string]T, basePath configkit.Path, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) { for id := range entries { states := make(map[string]fieldState, len(fields)) for _, field := range fields { info, exists := resolution.InputInfo(basePath.Key(id).Key(field)) switch { case !exists || !info.Explicit: states[field] = fieldAbsent case info.Null: states[field] = fieldNull default: states[field] = fieldValue } } destination[id] = states } } func sortedKeys[T any](entries map[string]T) []string { keys := make([]string, 0, len(entries)) for key := range entries { keys = append(keys, key) } sort.Strings(keys) return keys } func normalizeString(value **string, path configkit.Path, resolution *configkit.Resolution, normalized string) bool { if value != nil && *value != nil && **value != normalized { configkit.Normalize(resolution, path, value, &normalized, "normalize configuration value") return true } return false } func resolveBackends(backends map[string]backendConfig, basePath configkit.Path, resolution *configkit.Resolution) { for _, id := range sortedKeys(backends) { backend := backends[id] base := basePath.Key(id) if backend.Endpoint != nil { endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*backend.Endpoint) if err != nil { resolution.Error(base.Key("endpoint"), err.Error()) } else { normalizeString(&backend.Endpoint, base.Key("endpoint"), resolution, endpoint) } } if backend.APIKeyEnv != nil { normalizeString(&backend.APIKeyEnv, base.Key("api_key_env"), resolution, strings.TrimSpace(*backend.APIKeyEnv)) } if smokeTests := backend.SmokeTests; smokeTests != nil { smokePath := base.Key("smoke_tests") if smokeTests.Reachability != nil { normalizeDiagnosticEndpoint(&smokeTests.Reachability.Endpoint, smokePath.Key("reachability").Key("endpoint"), resolution) } if smokeTests.Authentication != nil { if smokeTests.Authentication.Kind != nil { normalizeString(&smokeTests.Authentication.Kind, smokePath.Key("authentication").Key("kind"), resolution, strings.ToLower(strings.TrimSpace(*smokeTests.Authentication.Kind))) } normalizeDiagnosticEndpoint(&smokeTests.Authentication.Endpoint, smokePath.Key("authentication").Key("endpoint"), resolution) } } if backend.ConcurrencyLimit != nil && *backend.ConcurrencyLimit > 0 && backend.QueueCapacity == nil { capacity := 1024 if resolution.IsExplicit(base.Key("queue_capacity")) { configkit.Normalize(resolution, base.Key("queue_capacity"), &backend.QueueCapacity, &capacity, "select default queue capacity") } else { configkit.Default(resolution, base.Key("queue_capacity"), &backend.QueueCapacity, &capacity, "select default queue capacity") } } backends[id] = backend } } func normalizeDiagnosticEndpoint(value **string, path configkit.Path, resolution *configkit.Resolution) { if value == nil || *value == nil { return } normalized := strings.TrimSpace(**value) request, err := http.NewRequest(http.MethodGet, normalized, nil) if normalized == "" || err != nil || request.URL == nil || (strings.ToLower(request.URL.Scheme) != "http" && strings.ToLower(request.URL.Scheme) != "https") || request.URL.Hostname() == "" { resolution.Error(path, "diagnostic endpoint must be an absolute HTTP(S) URL with a host") return } normalizeString(value, path, resolution, normalized) } func resolveProfiles(profiles map[string]profileConfig, basePath configkit.Path, resolution *configkit.Resolution) { for _, id := range sortedKeys(profiles) { profile := profiles[id] base := basePath.Key(id) if profile.Endpoint != nil && *profile.Endpoint != "" { endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint) if err != nil { resolution.Error(base.Key("endpoint"), err.Error()) } else { normalizeString(&profile.Endpoint, base.Key("endpoint"), resolution, endpoint) } } for _, field := range []struct { name string value **string }{ {"service_tier", &profile.ServiceTier}, {"reasoning_effort", &profile.ReasoningEffort}, {"api_key_env", &profile.APIKeyEnv}, } { if *field.value != nil { normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value)) } } profiles[id] = profile } } func validateEffective(config *document, bases resourceBases, validation *configkit.Validation) { for _, id := range sortedKeys(config.Backends) { backend := config.Backends[id] base := bases.backends.Key(id) if strings.TrimSpace(id) == "" { validation.Error(base, "backend ID must not be blank") } if backend.Endpoint == nil || *backend.Endpoint == "" { validation.Error(base.Key("endpoint"), "endpoint is required") } if backend.APIKeyEnv != nil && *backend.APIKeyEnv != "" && !envName.MatchString(*backend.APIKeyEnv) { validation.Error(base.Key("api_key_env"), "environment variable name is invalid") } if backend.ConcurrencyLimit != nil && *backend.ConcurrencyLimit < 0 { validation.Error(base.Key("concurrency_limit"), "concurrency limit must not be negative") } if backend.QueueCapacity != nil { if *backend.QueueCapacity < 0 { validation.Error(base.Key("queue_capacity"), "queue capacity must not be negative") } if backend.ConcurrencyLimit == nil || *backend.ConcurrencyLimit <= 0 { validation.Error(base.Key("queue_capacity"), "queue capacity requires a positive concurrency limit") } else if *backend.QueueCapacity > int(^uint(0)>>1)-*backend.ConcurrencyLimit { validation.Error(base.Key("queue_capacity"), "total capacity overflows int") } } validateExtraParams(backend.ExtraParams, base.Key("extra_params"), true, validation) if smokeTests := backend.SmokeTests; smokeTests != nil { smokePath := base.Key("smoke_tests") if smokeTests.DefaultProfile != nil && strings.TrimSpace(*smokeTests.DefaultProfile) == "" { validation.Error(smokePath.Key("default_profile"), "default profile must not be blank") } if authentication := smokeTests.Authentication; authentication != nil { kindPath := smokePath.Key("authentication").Key("kind") if authentication.Kind == nil { validation.Error(kindPath, "authentication kind is required") } else if *authentication.Kind != "none" && *authentication.Kind != "openrouter_key" { validation.Error(kindPath, "authentication kind must be none or openrouter_key") } } } } for _, id := range sortedKeys(config.Profiles) { profile := config.Profiles[id] base := bases.profiles.Key(id) if strings.TrimSpace(id) == "" { validation.Error(base, "profile ID must not be blank") } if profile.Backend != nil && *profile.Backend != "" { if _, exists := config.Backends[*profile.Backend]; !exists { validation.Error(base.Key("backend"), "backend is not defined") } } if profile.Model != nil && *profile.Model != "" && strings.TrimSpace(*profile.Model) == "" { validation.Error(base.Key("model"), "model must not be blank") } if profile.Temperature != nil { if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{Temperature: *profile.Temperature}); err != nil { validation.Error(base.Key("temperature"), err.Error()) } } if profile.TopP != nil { if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{TopP: *profile.TopP}); err != nil { validation.Error(base.Key("top_p"), err.Error()) } } if profile.MaxTokens != nil { if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{MaxTokens: *profile.MaxTokens}); err != nil { validation.Error(base.Key("max_tokens"), err.Error()) } } if profile.TimeoutSeconds != nil { if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{TimeoutSeconds: *profile.TimeoutSeconds}); err != nil { validation.Error(base.Key("timeout_seconds"), err.Error()) } } validateExtraParams(profile.ExtraParams, base.Key("extra_params"), false, validation) } } func validateExtraParams(values map[string]any, path configkit.Path, backend bool, validation *configkit.Validation) { if values == nil { return } if _, err := jsonvalue.CopyMap(values); err != nil { validation.Error(path, "extra parameters must be a JSON-compatible map with nonempty keys") return } if backend { for key := range values { if llm.IsReservedOpenAIChatRequestField(key) { validation.Error(path.Key(key), "extra parameter conflicts with a request field") } } } }