// Package configuration adapts ConfigKit's effective configuration to // Promptkit's definition and execution boundaries. package configuration import ( "fmt" "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]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"` } 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 { 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]map[string][]promptInputPresence prompts map[string]map[string]*promptEntry ambiguous map[string]bool registry *backend.Registry indexedProfiles map[string]domain.ExecutionProfile } var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) var backendFields = []string{"endpoint", "api_key_env", "concurrency_limit", "queue_capacity", "extra_params"} 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]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, "backends", backendFields, resolution, state.backends) captureFields(config.Profiles, "profiles", profileFields, resolution, state.profiles) capturePromptMetadata(config.Prompts, resolution, state.promptMetadata) resolveBackends(config.Backends, resolution) resolveProfiles(config.Profiles, resolution) resolvePrompts(config.Prompts, resolution) return nil }, Validate: validateEffective, }, }) 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) } return state, nil } type promptInputPresence struct { contentType fieldState description fieldState } func capturePromptMetadata(prompts map[string]map[string]promptConfig, resolution *configkit.Resolution, destination map[string]map[string][]promptInputPresence) { for _, id := range sortedKeys(prompts) { if destination[id] == nil { destination[id] = make(map[string][]promptInputPresence, len(prompts[id])) } for _, version := range sortedKeys(prompts[id]) { inputs := prompts[id][version].Inputs states := make([]promptInputPresence, len(inputs)) base := configkit.Keys("prompts", id, version, "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][version] = 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, collection 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(configkit.Keys(collection, id, 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, resolution *configkit.Resolution) { for _, id := range sortedKeys(backends) { backend := backends[id] base := configkit.Keys("backends", 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 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 resolveProfiles(profiles map[string]profileConfig, resolution *configkit.Resolution) { for _, id := range sortedKeys(profiles) { profile := profiles[id] base := configkit.Keys("profiles", 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) { for _, id := range sortedKeys(config.Backends) { backend := config.Backends[id] base := configkit.Keys("backends", 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) } for _, id := range sortedKeys(config.Profiles) { profile := config.Profiles[id] base := configkit.Keys("profiles", 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") } } } }