// 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 { Version *string `yaml:"version"` Backends map[string]backendConfig `yaml:"backends"` Profiles map[string]profileConfig `yaml:"profiles"` Prompts map[string]promptConfig `yaml:"prompts"` Collections map[string]collectionConfig `yaml:"collections"` } type collectionConfig struct { ReleaseVersion *string `yaml:"release_version"` 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 backendIDs []string profileIDs []string promptIDs []domain.PromptIdentity namespaces map[string][]string collections []CollectionIdentity } type resourceBases struct { backends configkit.Path profiles configkit.Path prompts configkit.Path } type resourceSet struct { isCollection bool namespace string bases resourceBases backends map[string]backendConfig profiles map[string]profileConfig prompts map[string]promptConfig } func rootResourceBases() resourceBases { return resourceBases{ backends: configkit.Keys("backends"), profiles: configkit.Keys("profiles"), prompts: configkit.Keys("prompts"), } } func resourceSets(config *document) []resourceSet { sets := make([]resourceSet, 0, len(config.Collections)+1) sets = append(sets, resourceSet{ bases: rootResourceBases(), backends: config.Backends, profiles: config.Profiles, prompts: config.Prompts, }) for _, namespace := range sortedKeys(config.Collections) { collection := config.Collections[namespace] base := configkit.Keys("collections", namespace) sets = append(sets, resourceSet{ isCollection: true, namespace: namespace, bases: resourceBases{ backends: base.Key("backends"), profiles: base.Key("profiles"), prompts: base.Key("prompts"), }, backends: collection.Backends, profiles: collection.Profiles, prompts: collection.Prompts, }) } return sets } func indexedResourceID(namespace, localKey string) string { qualified, valid := domain.QualifyLocalResourceID(namespace, localKey) if valid { return qualified } if namespace == "" { return localKey } return namespace + "/" + localKey } func indexedBackends(config *document) map[string]backendConfig { backends := make(map[string]backendConfig) for _, resources := range resourceSets(config) { for localKey, definition := range resources.backends { backends[indexedResourceID(resources.namespace, localKey)] = definition } } return backends } func indexedProfiles(config *document) map[string]profileConfig { profiles := make(map[string]profileConfig) for _, resources := range resourceSets(config) { for localKey, definition := range resources.profiles { profiles[indexedResourceID(resources.namespace, localKey)] = definition } } return profiles } 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) { 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, SourceVersionMode: configkit.SourceVersionModeIfPresent, Definition: configkit.Definition[document]{ Version: &configkit.VersionRule{ Path: configkit.Keys("version"), Check: checkConfigurationVersion, }, Secret: secretProviderPath, Resolve: func(config *document, resolution *configkit.Resolution) error { for _, resources := range resourceSets(config) { captureFields(resources.backends, resources.bases.backends, resources.namespace, backendFields, resolution, state.backends) captureFields(resources.profiles, resources.bases.profiles, resources.namespace, profileFields, resolution, state.profiles) capturePromptMetadata(resources.prompts, resources.bases.prompts, resources.namespace, resolution, state.promptMetadata) resolveBackends(resources.backends, resources.bases.backends, resolution) resolveProfiles(resources.profiles, resources.bases.profiles, resolution) resolvePrompts(resources.prompts, resources.bases.prompts, resolution) } return nil }, Validate: func(config *document, validation *configkit.Validation) { validateEffective(config, validation) }, }, }) if err != nil { return nil, fmt.Errorf("load configuration: %w", err) } state.result = result if err := state.materializePrompts(); 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.backendIDs = sortedKeys(indexedBackends(&result.Config)) 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)}) } state.namespaces = map[string][]string{ "backend": resourceNamespaces(state.backendIDs), "profile": resourceNamespaces(state.profileIDs), "prompt": promptNamespaces(state.promptIDs), } for _, namespace := range sortedKeys(result.Config.Collections) { collection := result.Config.Collections[namespace] state.collections = append(state.collections, CollectionIdentity{ Namespace: namespace, ReleaseVersion: stringPointerValue(collection.ReleaseVersion), }) } return state, nil } func resourceNamespaces(ids []string) []string { namespaces := make(map[string]struct{}) for _, id := range ids { if namespace, _, qualified := strings.Cut(id, "/"); qualified { namespaces[namespace] = struct{}{} } } return sortedKeys(namespaces) } func promptNamespaces(prompts []domain.PromptIdentity) []string { ids := make([]string, len(prompts)) for index := range prompts { ids[index] = prompts[index].ID } return resourceNamespaces(ids) } func checkConfigurationVersion(version configkit.Version, validation *configkit.Validation) error { if !version.Present { return nil } if version.Null || version.Text != "1" { validation.Error(configkit.Keys("version"), "unsupported configuration specification version") } return nil } type promptInputPresence struct { contentType fieldState description fieldState } func capturePromptMetadata(prompts map[string]promptConfig, basePath configkit.Path, namespace string, 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[indexedResourceID(namespace, 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, namespace string, 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[indexedResourceID(namespace, 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, validation *configkit.Validation) { sets := resourceSets(config) backendIDs := make(map[string]struct{}) for _, resources := range sets { if resources.isCollection && !domain.IsValidResourceNamespace(resources.namespace) { validation.Error(configkit.Keys("collections", resources.namespace), "collection namespace must be nonblank and contain no slash") } for localKey := range resources.backends { backendIDs[indexedResourceID(resources.namespace, localKey)] = struct{}{} } } for _, resources := range sets { for _, id := range sortedKeys(resources.backends) { backend := resources.backends[id] base := resources.bases.backends.Key(id) if !domain.IsValidLocalResourceKey(id) { validation.Error(base, "backend key must be a nonblank local key without a slash") } 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 { if strings.TrimSpace(*smokeTests.DefaultProfile) == "" { validation.Error(smokePath.Key("default_profile"), "default profile must not be blank") } else if !domain.IsValidResourceID(*smokeTests.DefaultProfile) { validation.Error(smokePath.Key("default_profile"), "default profile reference is malformed") } } 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(resources.profiles) { profile := resources.profiles[id] base := resources.bases.profiles.Key(id) if !domain.IsValidLocalResourceKey(id) { validation.Error(base, "profile key must be a nonblank local key without a slash") } if profile.Backend != nil && *profile.Backend != "" { if !domain.IsValidResourceID(*profile.Backend) { validation.Error(base.Key("backend"), "backend reference is malformed") } else if _, exists := backendIDs[*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) } for _, id := range sortedKeys(resources.prompts) { prompt := resources.prompts[id] base := resources.bases.prompts.Key(id) if !domain.IsValidLocalResourceKey(id) { validation.Error(base, "prompt key must be a nonblank local key without a slash") } if prompt.DefaultProfile != nil && *prompt.DefaultProfile != "" && !domain.IsValidResourceID(*prompt.DefaultProfile) { validation.Error(base.Key("default_profile"), "default profile reference is malformed") } } } } 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") } } } }