Use explicit IDs in configuration source builders
This commit is contained in:
17
backends.go
17
backends.go
@@ -14,13 +14,15 @@ const BackendRakestrawHome = backend.RakestrawHomeID
|
||||
// BackendLocal is a conventional ID for a caller-configured local backend.
|
||||
const BackendLocal = "local"
|
||||
|
||||
// BackendConfig is a presence-aware convenience value for a keyed backend
|
||||
// BackendConfig is a presence-aware convenience value for an identified backend
|
||||
// definition. Nil pointer fields are omitted, while non-nil pointers preserve
|
||||
// explicit zero or blank values. A nil ExtraParams map is omitted; a non-nil
|
||||
// empty map supplies an empty mapping. A nil SmokeTests pointer is omitted; a
|
||||
// non-nil pointer supplies a smoke_tests mapping. Use raw ConfigKit Mapping
|
||||
// inputs for explicit null. BackendConfig has no stable JSON representation.
|
||||
type BackendConfig struct {
|
||||
// ID is the local backend identity within its resource list.
|
||||
ID string
|
||||
// Endpoint is the required absolute HTTP(S) base URL after all layers merge.
|
||||
Endpoint *string
|
||||
// APIKeyEnv optionally names an environment variable, not a credential.
|
||||
@@ -71,15 +73,16 @@ type AuthenticationProbeConfig struct {
|
||||
Endpoint *string
|
||||
}
|
||||
|
||||
// BackendSource creates a labeled ConfigKit mapping from keyed backend values.
|
||||
// BackendSource creates a labeled ConfigKit mapping containing backend records
|
||||
// in caller order. IDs are explicit and duplicates are left for loading to reject.
|
||||
// It encodes fields without validating them; NewEngine validates the final
|
||||
// effective configuration after all selected source layers are combined.
|
||||
// Mapping captures a copy of supported input values at creation; unsupported
|
||||
// values fail when the source is loaded.
|
||||
func BackendSource(label string, backends map[string]BackendConfig) configkit.Source {
|
||||
definitions := make(map[string]any, len(backends))
|
||||
for id, config := range backends {
|
||||
fields := make(map[string]any)
|
||||
func BackendSource(label string, backends []BackendConfig) configkit.Source {
|
||||
definitions := make([]any, 0, len(backends))
|
||||
for _, config := range backends {
|
||||
fields := map[string]any{"id": config.ID}
|
||||
if config.Endpoint != nil {
|
||||
fields["endpoint"] = *config.Endpoint
|
||||
}
|
||||
@@ -119,7 +122,7 @@ func BackendSource(label string, backends map[string]BackendConfig) configkit.So
|
||||
}
|
||||
fields["smoke_tests"] = smokeTests
|
||||
}
|
||||
definitions[id] = fields
|
||||
definitions = append(definitions, fields)
|
||||
}
|
||||
return configkit.Mapping(label, map[string]any{"backends": definitions})
|
||||
}
|
||||
|
||||
@@ -40,16 +40,16 @@ func TestProfileEnvironmentLookupAcceptsNonShellNames(t *testing.T) {
|
||||
cfg.Sources.Underlays = []configkit.Underlay{{Inputs: []configkit.Input{*cfg.Sources.Main}}}
|
||||
lowerName := name
|
||||
cfg.Sources.Underlays = append(cfg.Sources.Underlays, configkit.Underlay{Inputs: []configkit.Input{{
|
||||
Source: promptkit.ProfileSource("default credentials", map[string]promptkit.ProfileConfig{
|
||||
"profile": {APIKeyEnv: &lowerName},
|
||||
Source: promptkit.ProfileSource("default credentials", []promptkit.ProfileConfig{
|
||||
{ID: "profile", APIKeyEnv: &lowerName},
|
||||
}),
|
||||
}}})
|
||||
env := " " + name + " "
|
||||
if test.clear {
|
||||
env = ""
|
||||
}
|
||||
cfg.Sources.Main = &configkit.Input{Source: promptkit.ProfileSource("credentials", map[string]promptkit.ProfileConfig{
|
||||
"profile": {APIKeyEnv: &env},
|
||||
cfg.Sources.Main = &configkit.Input{Source: promptkit.ProfileSource("credentials", []promptkit.ProfileConfig{
|
||||
{ID: "profile", APIKeyEnv: &env},
|
||||
})}
|
||||
var calls int
|
||||
var auth string
|
||||
@@ -81,13 +81,15 @@ func TestPublicConstructionWithLocalBackendAndProgrammaticProfile(t *testing.T)
|
||||
}},
|
||||
})}
|
||||
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "reply"}}
|
||||
localBackend := promptkit.LocalBackend("http://localhost:8000/v1", 2)
|
||||
localBackend.ID = backendID
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
||||
{Source: promptkit.BackendSource("local backend", map[string]promptkit.BackendConfig{
|
||||
backendID: promptkit.LocalBackend("http://localhost:8000/v1", 2),
|
||||
{Source: promptkit.BackendSource("local backend", []promptkit.BackendConfig{
|
||||
localBackend,
|
||||
})},
|
||||
{Source: promptkit.ProfileSource("profile", map[string]promptkit.ProfileConfig{
|
||||
"offline": {Backend: &backendID, Model: &model},
|
||||
{Source: promptkit.ProfileSource("profile", []promptkit.ProfileConfig{
|
||||
{ID: "offline", Backend: &backendID, Model: &model},
|
||||
})},
|
||||
}}},
|
||||
Main: &prompt,
|
||||
@@ -113,13 +115,13 @@ func TestFormerCatalogBackendIDsWorkWhenConfigured(t *testing.T) {
|
||||
model := "configured-model"
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
||||
{Source: promptkit.BackendSource("application backends", map[string]promptkit.BackendConfig{
|
||||
openRouterID: {Endpoint: &openRouterEndpoint},
|
||||
rakestrawhomeID: {Endpoint: &rakestrawhomeEndpoint},
|
||||
{Source: promptkit.BackendSource("application backends", []promptkit.BackendConfig{
|
||||
{ID: openRouterID, Endpoint: &openRouterEndpoint},
|
||||
{ID: rakestrawhomeID, Endpoint: &rakestrawhomeEndpoint},
|
||||
})},
|
||||
{Source: promptkit.ProfileSource("application profiles", map[string]promptkit.ProfileConfig{
|
||||
"openrouter-profile": {Backend: &openRouterID, Model: &model},
|
||||
"rakestrawhome-profile": {Backend: &rakestrawhomeID, Model: &model},
|
||||
{Source: promptkit.ProfileSource("application profiles", []promptkit.ProfileConfig{
|
||||
{ID: "openrouter-profile", Backend: &openRouterID, Model: &model},
|
||||
{ID: "rakestrawhome-profile", Backend: &rakestrawhomeID, Model: &model},
|
||||
})},
|
||||
}}},
|
||||
}})
|
||||
@@ -168,8 +170,8 @@ prompts:
|
||||
t.Fatalf("select operator inputs: %v", err)
|
||||
}
|
||||
programmaticModel := "programmatic-model"
|
||||
programmatic := configkit.Input{Source: promptkit.ProfileSource("programmatic settings", map[string]promptkit.ProfileConfig{
|
||||
"application": {Model: &programmaticModel},
|
||||
programmatic := configkit.Input{Source: promptkit.ProfileSource("programmatic settings", []promptkit.ProfileConfig{
|
||||
{ID: "application", Model: &programmaticModel},
|
||||
})}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
@@ -225,11 +227,11 @@ func TestPublicConstructionUsesProgrammaticBackendAndProfileSources(t *testing.T
|
||||
endpoint, backendID, model := "http://application.example/v1", "application-backend", "synthetic-model"
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
||||
{Source: promptkit.BackendSource("operator backend", map[string]promptkit.BackendConfig{
|
||||
backendID: {Endpoint: &endpoint},
|
||||
{Source: promptkit.BackendSource("operator backend", []promptkit.BackendConfig{
|
||||
{ID: backendID, Endpoint: &endpoint},
|
||||
})},
|
||||
{Source: promptkit.ProfileSource("operator profile", map[string]promptkit.ProfileConfig{
|
||||
"application-profile": {Backend: &backendID, Model: &model},
|
||||
{Source: promptkit.ProfileSource("operator profile", []promptkit.ProfileConfig{
|
||||
{ID: "application-profile", Backend: &backendID, Model: &model},
|
||||
})},
|
||||
}}},
|
||||
}})
|
||||
@@ -251,8 +253,9 @@ func TestBackendSourceOwnsSmokeTestValuesAndDefersDefaultLookup(t *testing.T) {
|
||||
reachabilityEndpoint := " https://probe.example/ready?check=1 "
|
||||
authenticationKind := " OPENROUTER_KEY "
|
||||
authenticationEndpoint := "https://probe.example/key?scope=current"
|
||||
source := promptkit.BackendSource("diagnostic backend", map[string]promptkit.BackendConfig{
|
||||
"local": {
|
||||
source := promptkit.BackendSource("diagnostic backend", []promptkit.BackendConfig{
|
||||
{
|
||||
ID: "local",
|
||||
Endpoint: &backendEndpoint,
|
||||
SmokeTests: &promptkit.BackendSmokeTestConfig{
|
||||
DefaultProfile: &defaultProfile,
|
||||
|
||||
@@ -384,6 +384,14 @@ tests. Other callers are migrated in later stages.
|
||||
Validation: the public construction/source-builder tests demonstrate the new
|
||||
input shape and ownership. Do not add map compatibility or builder-only ID rules.
|
||||
|
||||
Completion: profile and backend builders now accept ordered explicit-ID slices,
|
||||
preserve duplicates and owned values, and encode profile inheritance by sibling
|
||||
identity. Migrated their owning construction callers and added focused tests
|
||||
for order, copying, duplicate rejection, and real profile inheritance. Validation
|
||||
passed: the isolated source-builder contract tests and `go build .` on Go 1.27.0,
|
||||
formatting, and `git diff --check`. The full root test package remains deferred
|
||||
until later callers are converted to the new signatures.
|
||||
|
||||
## Stage 9: Remove Internal Prompt-Version Propagation
|
||||
|
||||
Scope: domain values, prompt requirement snapshots, hashes, use cases, and
|
||||
|
||||
@@ -13,19 +13,20 @@ import (
|
||||
)
|
||||
|
||||
func aliasPrompt() map[string]any {
|
||||
return map[string]any{"prompts": map[string]any{"prepared": promptDefinitionWithVersion("1", map[string]any{
|
||||
return map[string]any{"prompts": []any{map[string]any{
|
||||
"id": "prepared",
|
||||
"default_profile": "child",
|
||||
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||
})}}
|
||||
}}}
|
||||
}
|
||||
|
||||
func TestProfileInheritanceConfiguredBaseAliasWorkflow(t *testing.T) {
|
||||
baseID, baseEndpoint, baseModel, reasoning, timeout := "base", "https://profile.example/v1", "synthetic-model", "high", 120
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: promptkit.ProfileSource("application alias", map[string]promptkit.ProfileConfig{
|
||||
"base": {Endpoint: &baseEndpoint, Model: &baseModel},
|
||||
"child": {Extends: &baseID, ReasoningEffort: &reasoning, TimeoutSeconds: &timeout},
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: promptkit.ProfileSource("application alias", []promptkit.ProfileConfig{
|
||||
{ID: "base", Endpoint: &baseEndpoint, Model: &baseModel},
|
||||
{ID: "child", Extends: &baseID, ReasoningEffort: &reasoning, TimeoutSeconds: &timeout},
|
||||
})}}}},
|
||||
Main: &configkit.Input{Source: configkit.Mapping("prompt", aliasPrompt())},
|
||||
}})
|
||||
|
||||
20
profiles.go
20
profiles.go
@@ -2,12 +2,15 @@ package promptkit
|
||||
|
||||
import "go.maximumdirect.net/configkit"
|
||||
|
||||
// ProfileConfig is a presence-aware convenience value for a keyed execution
|
||||
// ProfileConfig is a presence-aware convenience value for an identified execution
|
||||
// profile. Nil fields are omitted; non-nil pointers preserve zero and blank
|
||||
// values. A nil ExtraParams map is omitted; a non-nil empty map supplies an
|
||||
// empty mapping, not a clear operation. Raw ConfigKit Mapping inputs can
|
||||
// express explicit null. ProfileConfig has no stable JSON representation.
|
||||
// Extends, when supplied, names a sibling profile in the same resource list.
|
||||
type ProfileConfig struct {
|
||||
// ID is the local profile identity within its resource list.
|
||||
ID string
|
||||
Extends, Backend, Endpoint, Model *string
|
||||
Temperature, TopP *float64
|
||||
MaxTokens, TimeoutSeconds *int
|
||||
@@ -17,17 +20,18 @@ type ProfileConfig struct {
|
||||
ExtraParams map[string]any
|
||||
}
|
||||
|
||||
// ProfileSource creates a labeled ConfigKit mapping from keyed profile values.
|
||||
// ProfileSource creates a labeled ConfigKit mapping containing profile records
|
||||
// in caller order. IDs are explicit and duplicates are left for loading to reject.
|
||||
// It encodes fields without validating them; NewEngine validates the final
|
||||
// effective configuration after all selected source layers are combined.
|
||||
// Mapping captures a copy of supported input values at creation; unsupported
|
||||
// values fail when the source is loaded.
|
||||
func ProfileSource(label string, profiles map[string]ProfileConfig) configkit.Source {
|
||||
definitions := make(map[string]any, len(profiles))
|
||||
for id, config := range profiles {
|
||||
fields := make(map[string]any)
|
||||
func ProfileSource(label string, profiles []ProfileConfig) configkit.Source {
|
||||
definitions := make([]any, 0, len(profiles))
|
||||
for _, config := range profiles {
|
||||
fields := map[string]any{"id": config.ID}
|
||||
if config.Extends != nil {
|
||||
fields["extends"] = *config.Extends
|
||||
fields["extends"] = map[string]any{"id": *config.Extends}
|
||||
}
|
||||
if config.Backend != nil {
|
||||
fields["backend"] = *config.Backend
|
||||
@@ -65,7 +69,7 @@ func ProfileSource(label string, profiles map[string]ProfileConfig) configkit.So
|
||||
if config.ExtraParams != nil {
|
||||
fields["extra_params"] = providerMapForMapping(config.ExtraParams)
|
||||
}
|
||||
definitions[id] = fields
|
||||
definitions = append(definitions, fields)
|
||||
}
|
||||
return configkit.Mapping(label, map[string]any{"profiles": definitions})
|
||||
}
|
||||
|
||||
75
sources_contract_test.go
Normal file
75
sources_contract_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package promptkit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
"go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestProgrammaticSourcesPreserveOrderAndRejectDuplicates(t *testing.T) {
|
||||
endpoint := "https://example.invalid/v1"
|
||||
params := map[string]any{"visible": "original"}
|
||||
backendSource := promptkit.BackendSource("backends", []promptkit.BackendConfig{
|
||||
{ID: "zeta", Endpoint: &endpoint, ExtraParams: params}, {ID: "alpha", Endpoint: &endpoint},
|
||||
})
|
||||
profileSource := promptkit.ProfileSource("profiles", []promptkit.ProfileConfig{
|
||||
{ID: "zeta", ExtraParams: params}, {ID: "alpha"},
|
||||
})
|
||||
params["visible"] = "changed"
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: backendSource}, {Source: profileSource}}}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
view, err := engine.InspectConfiguration()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var report bytes.Buffer
|
||||
if err := view.WriteEffective(&report); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents := report.String()
|
||||
if strings.Count(contents, "original") != 2 || strings.Contains(contents, "changed") {
|
||||
t.Fatalf("source ownership was lost: %s", contents)
|
||||
}
|
||||
for _, kind := range []string{"backends", "profiles"} {
|
||||
for index, id := range []string{"zeta", "alpha"} {
|
||||
entry, found := view.Explain(configkit.Keys(kind).Index(index).Key("id"))
|
||||
if !found || !strings.Contains(entry.Effective.YAML, id) {
|
||||
t.Fatalf("%s entry %d = %+v, want %s", kind, index, entry, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, source := range []configkit.Source{
|
||||
promptkit.BackendSource("duplicate backends", []promptkit.BackendConfig{{ID: "duplicate", Endpoint: &endpoint}, {ID: "duplicate", Endpoint: &endpoint}}),
|
||||
promptkit.ProfileSource("duplicate profiles", []promptkit.ProfileConfig{{ID: "duplicate"}, {ID: "duplicate"}}),
|
||||
} {
|
||||
_, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: source}}})
|
||||
if !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||
t.Fatalf("duplicate source error = %v, want ErrInvalidConfig", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgrammaticProfileVariantInherits(t *testing.T) {
|
||||
parent, endpoint, model, timeout := "base", "https://example.invalid/v1", "synthetic-model", 15
|
||||
source := promptkit.ProfileSource("profiles", []promptkit.ProfileConfig{
|
||||
{ID: parent, Endpoint: &endpoint, Model: &model},
|
||||
{ID: "child", Extends: &parent, TimeoutSeconds: &timeout},
|
||||
})
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: source}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
profile, err := engine.InspectProfile(context.Background(), "child")
|
||||
if err != nil || profile.EffectiveModelParams.Model != model || profile.EffectiveModelParams.Endpoint != endpoint || profile.EffectiveModelParams.TimeoutSeconds != timeout {
|
||||
t.Fatalf("inherited profile = %+v, %v", profile, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user