Assemble collection resources by qualified ID
This commit is contained in:
@@ -17,10 +17,18 @@ import (
|
||||
)
|
||||
|
||||
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"`
|
||||
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 {
|
||||
@@ -126,6 +134,15 @@ type resourceBases struct {
|
||||
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"),
|
||||
@@ -134,6 +151,61 @@ func rootResourceBases() resourceBases {
|
||||
}
|
||||
}
|
||||
|
||||
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"}
|
||||
@@ -146,7 +218,6 @@ func Load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
||||
|
||||
// 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),
|
||||
@@ -162,16 +233,18 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
||||
},
|
||||
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)
|
||||
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, bases, validation)
|
||||
validateEffective(config, validation)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -179,8 +252,10 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
||||
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)
|
||||
for _, resources := range resourceSets(&result.Config) {
|
||||
if err := state.materializePrompts(resources.prompts, resources.bases.prompts, resources.namespace); err != nil {
|
||||
return nil, fmt.Errorf("load configuration: %w", err)
|
||||
}
|
||||
}
|
||||
if err := state.buildExecutionProfiles(); err != nil {
|
||||
return nil, fmt.Errorf("load configuration: %w", err)
|
||||
@@ -207,7 +282,7 @@ type promptInputPresence struct {
|
||||
description fieldState
|
||||
}
|
||||
|
||||
func capturePromptMetadata(prompts map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string][]promptInputPresence) {
|
||||
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))
|
||||
@@ -219,7 +294,7 @@ func capturePromptMetadata(prompts map[string]promptConfig, basePath configkit.P
|
||||
description: captureFieldState(path.Key("description"), resolution),
|
||||
}
|
||||
}
|
||||
destination[id] = states
|
||||
destination[indexedResourceID(namespace, id)] = states
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +310,7 @@ func captureFieldState(path configkit.Path, resolution *configkit.Resolution) fi
|
||||
}
|
||||
}
|
||||
|
||||
func captureFields[T any](entries map[string]T, basePath configkit.Path, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) {
|
||||
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 {
|
||||
@@ -249,7 +324,7 @@ func captureFields[T any](entries map[string]T, basePath configkit.Path, fields
|
||||
states[field] = fieldValue
|
||||
}
|
||||
}
|
||||
destination[id] = states
|
||||
destination[indexedResourceID(namespace, id)] = states
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,83 +430,111 @@ func resolveProfiles(profiles map[string]profileConfig, basePath configkit.Path,
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
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")
|
||||
}
|
||||
if backend.Endpoint == nil || *backend.Endpoint == "" {
|
||||
validation.Error(base.Key("endpoint"), "endpoint is required")
|
||||
for localKey := range resources.backends {
|
||||
backendIDs[indexedResourceID(resources.namespace, localKey)] = struct{}{}
|
||||
}
|
||||
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")
|
||||
}
|
||||
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.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")
|
||||
if backend.Endpoint == nil || *backend.Endpoint == "" {
|
||||
validation.Error(base.Key("endpoint"), "endpoint is required")
|
||||
}
|
||||
}
|
||||
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 backend.APIKeyEnv != nil && *backend.APIKeyEnv != "" && !envName.MatchString(*backend.APIKeyEnv) {
|
||||
validation.Error(base.Key("api_key_env"), "environment variable name is invalid")
|
||||
}
|
||||
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")
|
||||
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(config.Profiles) {
|
||||
profile := config.Profiles[id]
|
||||
base := bases.profiles.Key(id)
|
||||
if strings.TrimSpace(id) == "" {
|
||||
validation.Error(base, "profile ID must not be blank")
|
||||
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)
|
||||
}
|
||||
if profile.Backend != nil && *profile.Backend != "" {
|
||||
if _, exists := config.Backends[*profile.Backend]; !exists {
|
||||
validation.Error(base.Key("backend"), "backend is not defined")
|
||||
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")
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user