Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9f6aad305 | |||
| fda0338134 | |||
| 2b3d165ce2 | |||
| 43f55edb98 | |||
| 4950e1dee2 | |||
| d240364776 | |||
| 6f57f2f30a | |||
| 2c50eca033 | |||
| ac44a4f325 | |||
| 9e05c3ac8b | |||
| 2c83925c88 | |||
| e0cd63923d | |||
| 75204d6134 | |||
| 5ce6e01f27 | |||
| 5cec25a40c | |||
| 855aae6daa | |||
| 43abbcb414 | |||
| 9f33fed823 | |||
| 7a98feb399 | |||
| 626134e5e1 | |||
| e7ed315671 | |||
| d7d9a7dd65 | |||
| 18f6a474e0 | |||
| fb7ebe6a6c |
@@ -26,6 +26,8 @@ source-relative prompt content, supplies an inline input, and prepares through
|
|||||||
a scoped view. The example also demonstrates collection inventories and
|
a scoped view. The example also demonstrates collection inventories and
|
||||||
namespace-filtered discovery, without contacting a provider or requiring
|
namespace-filtered discovery, without contacting a provider or requiring
|
||||||
credentials.
|
credentials.
|
||||||
|
The example disables the bundled OpenRouter configuration to keep its
|
||||||
|
inventories limited to its own offline fixtures.
|
||||||
The [consumer guide](docs/consumers/pkg-promptkit.md) also shows a local backend,
|
The [consumer guide](docs/consumers/pkg-promptkit.md) also shows a local backend,
|
||||||
ordered file/embedded sources, programmatic mappings, and how to replace an
|
ordered file/embedded sources, programmatic mappings, and how to replace an
|
||||||
engine when source files change. Applications adapting the prior keyed
|
engine when source files change. Applications adapting the prior keyed
|
||||||
@@ -43,6 +45,11 @@ boundary and constraints that framework work must preserve.
|
|||||||
|
|
||||||
## Release Guidance
|
## Release Guidance
|
||||||
|
|
||||||
|
The [v0.18.0 release notes](docs/releases/v0.18.0.md) cover the default-on
|
||||||
|
OpenRouter backend and profiles, recorded configuration normalization, and
|
||||||
|
ConfigKit v1.1. The notes explain inventory changes and the bundle opt-out;
|
||||||
|
the Go 1.27 minimum remains unchanged.
|
||||||
|
|
||||||
The [v0.17.0 release notes](docs/releases/v0.17.0.md) cover the vanity module
|
The [v0.17.0 release notes](docs/releases/v0.17.0.md) cover the vanity module
|
||||||
path, ConfigKit v1, identified resource lists, removal of prompt-version
|
path, ConfigKit v1, identified resource lists, removal of prompt-version
|
||||||
metadata, and the Go 1.27 minimum. This is a breaking pre-v1 migration.
|
metadata, and the Go 1.27 minimum. This is a breaking pre-v1 migration.
|
||||||
|
|||||||
115
bundled_composition_contract_test.go
Normal file
115
bundled_composition_contract_test.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBundledCompositionReplacementAndInheritance(t *testing.T) {
|
||||||
|
underlay := configkit.Input{Source: configkit.Mapping("caller underlay", map[string]any{
|
||||||
|
"profiles": []any{map[string]any{"id": "gpt-sol-latest", "model": "underlay-model"}},
|
||||||
|
})}
|
||||||
|
underlayEngine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
underlayProfile, err := underlayEngine.InspectProfile(context.Background(), "gpt-sol-latest")
|
||||||
|
if err != nil || underlayProfile.EffectiveModelParams.Model != "underlay-model" || underlayProfile.EffectiveModelParams.BackendID != "" {
|
||||||
|
t.Fatalf("underlay replacement = %+v, %v", underlayProfile, err)
|
||||||
|
}
|
||||||
|
main := configkit.Input{Source: configkit.Mapping("caller main", map[string]any{
|
||||||
|
"backends": []any{map[string]any{"id": "openrouter", "endpoint": "https://local.invalid/v1"}, map[string]any{"id": "other", "endpoint": "https://other.invalid/v1"}},
|
||||||
|
"profiles": []any{
|
||||||
|
map[string]any{"id": "gpt-sol-latest", "model": "main-model"},
|
||||||
|
map[string]any{"id": "custom", "extends": map[string]any{"id": "gpt-luna-latest"}, "model": "custom-model"},
|
||||||
|
map[string]any{"id": "auto", "backend": "other", "model": "other-model"},
|
||||||
|
},
|
||||||
|
"collections": map[string]any{"openrouter": map[string]any{
|
||||||
|
"backends": []any{map[string]any{"id": "local", "endpoint": "https://collection.invalid/v1"}},
|
||||||
|
"profiles": []any{map[string]any{"id": "profile", "backend": "openrouter/local", "model": "collection-model"}},
|
||||||
|
}},
|
||||||
|
})}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}, Main: &main}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
inspect := func(id string) promptkit.ExecutionTarget {
|
||||||
|
t.Helper()
|
||||||
|
profile, err := engine.InspectProfile(context.Background(), id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return profile.EffectiveModelParams
|
||||||
|
}
|
||||||
|
if target := inspect("gpt-sol-latest"); target.Model != "main-model" || target.BackendID != "" {
|
||||||
|
t.Fatalf("replacement = %+v", target)
|
||||||
|
}
|
||||||
|
if target := inspect("custom"); target.Model != "custom-model" || target.BackendID != "openrouter" || target.Endpoint != "https://local.invalid/v1" {
|
||||||
|
t.Fatalf("inherited = %+v", target)
|
||||||
|
}
|
||||||
|
if target := inspect("gpt-luna-latest"); target.Endpoint != "https://local.invalid/v1" || target.APIKeyEnv != "" {
|
||||||
|
t.Fatalf("backend replacement = %+v", target)
|
||||||
|
}
|
||||||
|
if target := inspect("openrouter/profile"); target.BackendID != "openrouter/local" || target.Model != "collection-model" {
|
||||||
|
t.Fatalf("collection = %+v", target)
|
||||||
|
}
|
||||||
|
for _, tier := range []string{"low", "medium", "high", "xhigh", "max"} {
|
||||||
|
target := inspect("auto-" + tier)
|
||||||
|
if target.BackendID != "other" || target.Model != "other-model" || !strings.Contains(fmt.Sprint(target.ExtraParams), "cost_tier:"+tier) || !strings.Contains(fmt.Sprint(target.ExtraParams), "id:auto-router") {
|
||||||
|
t.Errorf("tier %s = %+v", tier, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledCompositionSequenceModes(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name, mode, id string
|
||||||
|
wantErr bool
|
||||||
|
count int
|
||||||
|
}{
|
||||||
|
{"duplicate append", "append", "auto", true, 0},
|
||||||
|
{"distinct append", "append", "custom", false, 18},
|
||||||
|
{"replace", "replace", "custom", false, 1},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("sequence", map[string]any{
|
||||||
|
"config": map[string]any{"sequences": []any{map[string]any{"destination": []any{"profiles"}, "mode": test.mode}}},
|
||||||
|
"profiles": []any{map[string]any{"id": test.id, "model": "custom-model"}},
|
||||||
|
})}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}})
|
||||||
|
if test.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected duplicate rejection")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
profiles, err := engine.ListProfiles(context.Background())
|
||||||
|
if err != nil || len(profiles) != test.count {
|
||||||
|
t.Fatalf("profiles = %v, %v", profiles, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledCompositionRejectsStrictConflictsAndMissingBackends(t *testing.T) {
|
||||||
|
profile := configkit.Input{Source: configkit.Mapping("profile", map[string]any{"profiles": []any{map[string]any{"id": "auto", "model": "caller"}}})}
|
||||||
|
_, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{profile, profile}}}}})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("strict group accepted duplicate records")
|
||||||
|
}
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("replace backends", map[string]any{
|
||||||
|
"config": map[string]any{"sequences": []any{map[string]any{"destination": []any{"backends"}, "mode": "replace"}}},
|
||||||
|
"backends": []any{map[string]any{"id": "other", "endpoint": "https://other.invalid/v1"}},
|
||||||
|
})}
|
||||||
|
if _, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}); err == nil {
|
||||||
|
t.Fatal("missing bundled backend was accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
90
bundled_execution_contract_test.go
Normal file
90
bundled_execution_contract_test.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func bundledPromptConfig(client *http.Client) promptkit.Config {
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("prompt", map[string]any{"prompts": []any{map[string]any{
|
||||||
|
"id": "prompt", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
|
}}})}
|
||||||
|
return promptkit.Config{HTTPClient: client, Sources: promptkit.ConfigurationSources{Main: &input}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledProfilePreparationAndExecution(t *testing.T) {
|
||||||
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}}
|
||||||
|
engine, err := promptkit.NewEngine(bundledPromptConfig(nil), promptkit.WithLLMClient(client))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := engine.PrepareExecution(context.Background(), promptkit.RunRequest{PromptID: "prompt"}); !errors.Is(err, promptkit.ErrProfileRequired) {
|
||||||
|
t.Fatalf("missing profile = %v", err)
|
||||||
|
}
|
||||||
|
temperature := 0.3
|
||||||
|
request := promptkit.RunRequest{PromptID: "prompt", ProfileID: "gpt-luna-latest", Execution: &promptkit.ExecutionTargetOverride{Temperature: &temperature}}
|
||||||
|
prepared, err := engine.PrepareExecution(context.Background(), request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
details := prepared.Details()
|
||||||
|
if details.SelectedProfileID != "gpt-luna-latest" || details.EffectiveModelParams.Model != "~openai/gpt-luna-latest" || details.EffectiveModelParams.BackendID != "openrouter" || details.EffectiveModelParams.Temperature != temperature {
|
||||||
|
t.Fatalf("prepared target = %+v", details.EffectiveModelParams)
|
||||||
|
}
|
||||||
|
if _, err := engine.Run(context.Background(), request); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(client.requests) != 1 || client.requests[0].Target.Model != "~openai/gpt-luna-latest" || client.requests[0].Target.BackendID != "openrouter" {
|
||||||
|
t.Fatalf("requests = %+v", client.requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledProfileHTTPPayloads(t *testing.T) {
|
||||||
|
t.Setenv("OPENROUTER_API_KEY", "synthetic-key")
|
||||||
|
for _, test := range []struct{ id, model, tier string }{
|
||||||
|
{"auto-low", "openrouter/auto", "low"}, {"auto-medium", "openrouter/auto", "medium"}, {"auto-high", "openrouter/auto", "high"}, {"auto-xhigh", "openrouter/auto", "xhigh"}, {"auto-max", "openrouter/auto", "max"},
|
||||||
|
{"auto", "openrouter/auto", ""}, {"free", "openrouter/free", ""}, {"gpt-luna-latest", "~openai/gpt-luna-latest", ""},
|
||||||
|
} {
|
||||||
|
t.Run(test.id, func(t *testing.T) {
|
||||||
|
var body map[string]any
|
||||||
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
||||||
|
if request.Header.Get("Authorization") != "Bearer synthetic-key" {
|
||||||
|
t.Errorf("authorization = %q", request.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"choices":[{"message":{"content":"ok"}}]}`))}, nil
|
||||||
|
})
|
||||||
|
engine, err := promptkit.NewEngine(bundledPromptConfig(&http.Client{Transport: transport}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := engine.Run(context.Background(), promptkit.RunRequest{PromptID: "prompt", ProfileID: test.id}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if body["model"] != test.model || body["extra_params"] != nil {
|
||||||
|
t.Fatalf("request = %+v", body)
|
||||||
|
}
|
||||||
|
if test.tier == "" {
|
||||||
|
if _, exists := body["plugins"]; exists {
|
||||||
|
t.Fatalf("unexpected plugins = %+v", body)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
want := []any{map[string]any{"id": "auto-router", "cost_tier": test.tier}}
|
||||||
|
if !reflect.DeepEqual(body["plugins"], want) {
|
||||||
|
t.Fatalf("plugins = %#v, want %#v", body["plugins"], want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
124
bundled_openrouter_contract_test.go
Normal file
124
bundled_openrouter_contract_test.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rejectBundleTransport struct{}
|
||||||
|
|
||||||
|
func (rejectBundleTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||||
|
return nil, errors.New("unexpected HTTP request")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledOpenRouterOptionsAndInventory(t *testing.T) {
|
||||||
|
t.Setenv("OPENROUTER_API_KEY", "")
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
options []promptkit.Option
|
||||||
|
enabled bool
|
||||||
|
}{
|
||||||
|
{"default", nil, true}, {"enabled", []promptkit.Option{promptkit.WithBundledOpenRouter(true)}, true},
|
||||||
|
{"disabled", []promptkit.Option{promptkit.WithBundledOpenRouter(false)}, false},
|
||||||
|
{"last disables", []promptkit.Option{promptkit.WithBundledOpenRouter(true), promptkit.WithBundledOpenRouter(false)}, false},
|
||||||
|
{"last enables", []promptkit.Option{promptkit.WithBundledOpenRouter(false), promptkit.WithBundledOpenRouter(true)}, true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{HTTPClient: &http.Client{Transport: rejectBundleTransport{}}}, test.options...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
profiles, err := engine.ListProfiles(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
backends, err := engine.ListBackends(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
prompts, err := engine.ListPrompts(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
collections, err := engine.ListCollections(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := 0
|
||||||
|
if test.enabled {
|
||||||
|
want = 17
|
||||||
|
}
|
||||||
|
if len(profiles) != want || len(backends) != boolCount(test.enabled) || len(prompts) != 0 || len(collections) != 0 {
|
||||||
|
t.Fatalf("inventories: %v %v %v %v", profiles, backends, prompts, collections)
|
||||||
|
}
|
||||||
|
if test.enabled {
|
||||||
|
wantProfiles := []string{"auto", "auto-high", "auto-low", "auto-max", "auto-medium", "auto-xhigh", "claude-fable-latest", "claude-haiku-latest", "claude-opus-latest", "claude-sonnet-latest", "free", "gemini-flash-latest", "gemini-pro-latest", "gpt-astra-latest", "gpt-luna-latest", "gpt-sol-latest", "gpt-terra-latest"}
|
||||||
|
if !reflect.DeepEqual(profiles, wantProfiles) || !reflect.DeepEqual(backends, []string{"openrouter"}) {
|
||||||
|
t.Fatalf("bundle inventory: %v %v", profiles, backends)
|
||||||
|
}
|
||||||
|
for id, model := range map[string]string{"auto": "openrouter/auto", "auto-max": "openrouter/auto", "gpt-luna-latest": "~openai/gpt-luna-latest", "free": "openrouter/free"} {
|
||||||
|
profile, err := engine.InspectProfile(context.Background(), id)
|
||||||
|
if err != nil || profile.EffectiveModelParams.Model != model || profile.EffectiveModelParams.BackendID != "openrouter" {
|
||||||
|
t.Fatalf("profile %s = %+v, %v", id, profile, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(fmtSources(view.Sources()), "Promptkit bundled OpenRouter") {
|
||||||
|
t.Fatal("bundle source missing")
|
||||||
|
}
|
||||||
|
if explanation, found := view.Explain(configkit.Keys("backends").Index(0).Key("endpoint")); !found || !strings.Contains(explanation.Effective.YAML, "https://openrouter.ai/api/v1") || len(explanation.History) == 0 {
|
||||||
|
t.Fatalf("bundle explanation = %+v", explanation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolCount(value bool) int {
|
||||||
|
if value {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
func fmtSources(sources []configkit.SourceInfo) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for _, source := range sources {
|
||||||
|
b.WriteString(source.Label)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundlePreservesCallerUnderlayBackingArray(t *testing.T) {
|
||||||
|
source := configkit.Mapping("caller", map[string]any{"profiles": []any{map[string]any{"id": "caller", "model": "model"}}})
|
||||||
|
groups := make([]configkit.Underlay, 1, 2)
|
||||||
|
groups[0] = configkit.Underlay{Inputs: []configkit.Input{{Source: source}}}
|
||||||
|
before := append([]configkit.Underlay(nil), groups...)
|
||||||
|
if _, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: groups}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(groups, before) || !reflect.DeepEqual(groups[:2][1], configkit.Underlay{}) {
|
||||||
|
t.Fatal("caller underlays were modified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisabledBundleKeepsCallerOpenRouter(t *testing.T) {
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("caller", map[string]any{"backends": []any{map[string]any{"id": "openrouter", "endpoint": "https://local.invalid/v1"}}})}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}, promptkit.WithBundledOpenRouter(false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
backends, err := engine.ListBackends(context.Background())
|
||||||
|
if err != nil || !reflect.DeepEqual(backends, []string{"openrouter"}) {
|
||||||
|
t.Fatalf("backends = %v, %v", backends, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
127
bundled_smoke_contract_test.go
Normal file
127
bundled_smoke_contract_test.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package promptkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBundledSmokeHTTPDefaults(t *testing.T) {
|
||||||
|
t.Setenv("OPENROUTER_API_KEY", "synthetic-key")
|
||||||
|
var urls []string
|
||||||
|
var models []string
|
||||||
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
||||||
|
urls = append(urls, request.URL.String())
|
||||||
|
body := `{"data":{"label":"member","is_free_tier":false,"usage":0}}`
|
||||||
|
if request.Method == http.MethodPost {
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
models = append(models, payload["model"].(string))
|
||||||
|
content := "PROMPTKIT_OK"
|
||||||
|
if _, structured := payload["response_format"]; structured {
|
||||||
|
content = `{"status":"ok"}`
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(map[string]any{"choices": []any{map[string]any{"message": map[string]any{"content": content}}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body = string(encoded)
|
||||||
|
} else if strings.HasSuffix(request.URL.Path, "/key") && request.Header.Get("Authorization") != "Bearer synthetic-key" {
|
||||||
|
t.Errorf("authentication header missing")
|
||||||
|
}
|
||||||
|
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body))}, nil
|
||||||
|
})
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{HTTPClient: &http.Client{Transport: transport}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
before, _ := engine.ListProfiles(context.Background())
|
||||||
|
inspectionBefore, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, kind := range []promptkit.SmokeTestKind{promptkit.SmokeTestReachability, promptkit.SmokeTestAuthentication, promptkit.SmokeTestTextInference, promptkit.SmokeTestStructuredOutput} {
|
||||||
|
report, err := engine.SmokeTest(context.Background(), promptkit.SmokeTestRequest{Kind: kind, BackendID: "openrouter"})
|
||||||
|
if err != nil || report == nil || !report.Completed || report.Failure != "" {
|
||||||
|
t.Fatalf("%s report = %+v, %v", kind, report, err)
|
||||||
|
}
|
||||||
|
if kind == promptkit.SmokeTestAuthentication && (report.Facts.CredentialsAccepted == nil || !*report.Facts.CredentialsAccepted) {
|
||||||
|
t.Fatalf("authentication facts = %+v", report.Facts)
|
||||||
|
}
|
||||||
|
if kind == promptkit.SmokeTestTextInference || kind == promptkit.SmokeTestStructuredOutput {
|
||||||
|
if report.Target.ProfileID != "gpt-luna-latest" || report.Target.EffectiveSettings.Model != "~openai/gpt-luna-latest" {
|
||||||
|
t.Fatalf("target = %+v", report.Target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wantURLs := []string{"https://openrouter.ai/api/v1", "https://openrouter.ai/api/v1/key", "https://openrouter.ai/api/v1/chat/completions", "https://openrouter.ai/api/v1/chat/completions"}
|
||||||
|
if !reflect.DeepEqual(urls, wantURLs) || !reflect.DeepEqual(models, []string{"~openai/gpt-luna-latest", "~openai/gpt-luna-latest"}) {
|
||||||
|
t.Fatalf("urls %v, models %v", urls, models)
|
||||||
|
}
|
||||||
|
after, _ := engine.ListProfiles(context.Background())
|
||||||
|
if !reflect.DeepEqual(before, after) {
|
||||||
|
t.Fatalf("inventory changed: %v %v", before, after)
|
||||||
|
}
|
||||||
|
inspectionAfter, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
diff, err := inspectionBefore.Diff(inspectionAfter)
|
||||||
|
if err != nil || len(diff) != 0 {
|
||||||
|
t.Fatalf("inspection changed: %+v, %v", diff, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundledSmokeSelectionUsesEffectiveConfiguration(t *testing.T) {
|
||||||
|
replacement := configkit.Input{Source: configkit.Mapping("replacement", map[string]any{"profiles": []any{map[string]any{"id": "gpt-luna-latest", "backend": "openrouter", "model": "replacement"}}})}
|
||||||
|
replacementClient := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "PROMPTKIT_OK"}}
|
||||||
|
replacementEngine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &replacement}}, promptkit.WithLLMClient(replacementClient))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
replacementReport, err := replacementEngine.SmokeTest(context.Background(), promptkit.SmokeTestRequest{Kind: promptkit.SmokeTestTextInference, BackendID: "openrouter"})
|
||||||
|
if err != nil || replacementReport.Target.ProfileID != "gpt-luna-latest" || replacementReport.Target.EffectiveSettings.Model != "replacement" {
|
||||||
|
t.Fatalf("replaced default = %+v, %v", replacementReport, err)
|
||||||
|
}
|
||||||
|
replacementClient.response.Content = "unexpected"
|
||||||
|
mismatch, err := replacementEngine.SmokeTest(context.Background(), promptkit.SmokeTestRequest{Kind: promptkit.SmokeTestTextInference, BackendID: "openrouter"})
|
||||||
|
if err != nil || mismatch == nil || !mismatch.Completed {
|
||||||
|
t.Fatalf("concluded mismatch = %+v, %v", mismatch, err)
|
||||||
|
}
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("application", map[string]any{
|
||||||
|
"backends": []any{map[string]any{"id": "openrouter", "endpoint": "https://example.invalid/v1", "smoke_tests": map[string]any{"default_profile": "custom"}}},
|
||||||
|
"profiles": []any{map[string]any{"id": "gpt-luna-latest", "backend": "openrouter", "model": "replacement"}, map[string]any{"id": "custom", "backend": "openrouter", "model": "custom-model"}},
|
||||||
|
"prompts": []any{map[string]any{"id": "prompt", "default_profile": "free", "messages": []any{map[string]any{"role": "user", "content": "private application marker"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}},
|
||||||
|
})}
|
||||||
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "PROMPTKIT_OK"}}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}, promptkit.WithLLMClient(client))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, test := range []struct {
|
||||||
|
request promptkit.SmokeTestRequest
|
||||||
|
profile, model string
|
||||||
|
}{
|
||||||
|
{promptkit.SmokeTestRequest{BackendID: "openrouter"}, "custom", "custom-model"},
|
||||||
|
{promptkit.SmokeTestRequest{BackendID: "openrouter", PromptID: "prompt"}, "free", "openrouter/free"},
|
||||||
|
{promptkit.SmokeTestRequest{BackendID: "openrouter", PromptID: "prompt", ProfileID: "gpt-luna-latest"}, "gpt-luna-latest", "replacement"},
|
||||||
|
} {
|
||||||
|
test.request.Kind = promptkit.SmokeTestTextInference
|
||||||
|
report, err := engine.SmokeTest(context.Background(), test.request)
|
||||||
|
if err != nil || report.Target.ProfileID != test.profile || report.Target.EffectiveSettings.Model != test.model {
|
||||||
|
t.Fatalf("report = %+v, %v", report, err)
|
||||||
|
}
|
||||||
|
if strings.Contains(client.requests[len(client.requests)-1].Prompt.Messages[0].Content, "private application marker") {
|
||||||
|
t.Fatal("application content used by diagnostic")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,34 @@ func TestConfigurationInspectionDiffIgnoresSourceHistory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionDiffUsesNormalizedValues(t *testing.T) {
|
||||||
|
inspect := func(endpoint string) promptkit.ConfigurationInspection {
|
||||||
|
t.Helper()
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("backend", map[string]any{
|
||||||
|
"backends": []any{map[string]any{"id": "local", "endpoint": endpoint}},
|
||||||
|
})}}}}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
view, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
left := inspect(" HTTPS://example.invalid/v1 ")
|
||||||
|
equal := inspect("https://example.invalid/v1")
|
||||||
|
differences, err := left.Diff(equal)
|
||||||
|
if err != nil || len(differences) != 0 {
|
||||||
|
t.Fatalf("normalized diff = %+v, %v", differences, err)
|
||||||
|
}
|
||||||
|
changed := inspect("https://other.invalid/v1")
|
||||||
|
differences, err = left.Diff(changed)
|
||||||
|
if err != nil || len(differences) != 1 || differences[0].Path.String() != "/backends" {
|
||||||
|
t.Fatalf("changed diff = %+v, %v", differences, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigurationInspectionDiffReportsPresenceAndSequences(t *testing.T) {
|
func TestConfigurationInspectionDiffReportsPresenceAndSequences(t *testing.T) {
|
||||||
left := comparisonInspectionFrom(t, "before", "same-model", map[string]any{
|
left := comparisonInspectionFrom(t, "before", "same-model", map[string]any{
|
||||||
"sequence": []any{"earlier", "unchanged"},
|
"sequence": []any{"earlier", "unchanged"},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func TestConfigurationInspectionReportsLiteralKeysAndRedactedHistory(t *testing.
|
|||||||
Underlays: []configkit.Underlay{
|
Underlays: []configkit.Underlay{
|
||||||
{Inputs: []configkit.Input{{Source: configkit.Mapping("defaults", map[string]any{
|
{Inputs: []configkit.Input{{Source: configkit.Mapping("defaults", map[string]any{
|
||||||
"backends": []any{map[string]any{
|
"backends": []any{map[string]any{
|
||||||
"id": "local", "endpoint": "https://example.invalid/v1", "api_key_env": "OPTIONAL_KEY",
|
"id": "local", "endpoint": " HTTPS://example.invalid/v1 ", "api_key_env": "OPTIONAL_KEY",
|
||||||
"extra_params": map[string]any{"token": "private-sentinel", "visible": "public-value"},
|
"extra_params": map[string]any{"token": "private-sentinel", "visible": "public-value"},
|
||||||
}},
|
}},
|
||||||
"collections": map[string]any{"literal": map[string]any{
|
"collections": map[string]any{"literal": map[string]any{
|
||||||
@@ -42,7 +42,7 @@ func TestConfigurationInspectionReportsLiteralKeysAndRedactedHistory(t *testing.
|
|||||||
}},
|
}},
|
||||||
})}}},
|
})}}},
|
||||||
},
|
},
|
||||||
}})
|
}}, promptkit.WithBundledOpenRouter(false))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("construct layered engine: %v", err)
|
t.Fatalf("construct layered engine: %v", err)
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,11 @@ func TestConfigurationInspectionReportsLiteralKeysAndRedactedHistory(t *testing.
|
|||||||
t.Fatalf("literal-key path = %q", inheritedPath)
|
t.Fatalf("literal-key path = %q", inheritedPath)
|
||||||
}
|
}
|
||||||
secretPath := configkit.Keys("backends").Index(0).Key("extra_params").Key("token")
|
secretPath := configkit.Keys("backends").Index(0).Key("extra_params").Key("token")
|
||||||
|
endpointPath := configkit.Keys("backends").Index(0).Key("endpoint")
|
||||||
|
endpoint, found := view.Explain(endpointPath)
|
||||||
|
if !found || !strings.Contains(endpoint.Effective.YAML, "https://example.invalid/v1") || strings.Contains(fmt.Sprintf("%+v", view.Metadata().History(endpointPath)), "private-sentinel") {
|
||||||
|
t.Fatalf("normalized endpoint explanation = %+v", endpoint)
|
||||||
|
}
|
||||||
secret, found := view.Explain(secretPath)
|
secret, found := view.Explain(secretPath)
|
||||||
if !found || !secret.Effective.Redacted || strings.Contains(secret.Effective.YAML, "private-sentinel") {
|
if !found || !secret.Effective.Redacted || strings.Contains(secret.Effective.YAML, "private-sentinel") {
|
||||||
t.Fatalf("credential-shaped provider value was not redacted: (%+v, %t)", secret, found)
|
t.Fatalf("credential-shaped provider value was not redacted: (%+v, %t)", secret, found)
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ func TestBackendSourceOwnsSmokeTestValuesAndDefersDefaultLookup(t *testing.T) {
|
|||||||
"https://backend.example/v1",
|
"https://backend.example/v1",
|
||||||
"not-yet-listed",
|
"not-yet-listed",
|
||||||
"https://probe.example/ready?check=1",
|
"https://probe.example/ready?check=1",
|
||||||
" OPENROUTER_KEY ",
|
"openrouter_key",
|
||||||
"https://probe.example/key?scope=current",
|
"https://probe.example/key?scope=current",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(contents, want) {
|
if !strings.Contains(contents, want) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
This breaking pre-v1 minor migration adopts Promptkit's vanity module path,
|
This breaking pre-v1 minor migration adopts Promptkit's vanity module path,
|
||||||
changes the v0.16-era keyed resources to identified lists, adopts ConfigKit v1,
|
changes the v0.16-era keyed resources to identified lists, adopts ConfigKit v1,
|
||||||
and removes prompt-version labels.
|
and removes prompt-version labels.
|
||||||
Promptkit does not install a provider catalog; applications choose configuration
|
Promptkit installs a default-on bundled OpenRouter catalog; applications choose configuration
|
||||||
inputs explicitly, and execution selects one prompt by its exact ID. This guide
|
inputs explicitly, and execution selects one prompt by its exact ID. This guide
|
||||||
explains the shape changes and decisions an application must make; it is not an
|
explains the shape changes and decisions an application must make; it is not an
|
||||||
automatic migration tool. The [framework format reference](../formats.md) owns
|
automatic migration tool. The [framework format reference](../formats.md) owns
|
||||||
@@ -134,9 +134,9 @@ Replace former maps passed to `BackendSource` in the same way, setting each
|
|||||||
`BackendConfig.ID`. Duplicates are retained for loading to reject. Resource
|
`BackendConfig.ID`. Duplicates are retained for loading to reject. Resource
|
||||||
inspection paths now address list indexes, for example
|
inspection paths now address list indexes, for example
|
||||||
`configkit.Keys("profiles").Index(0).Key("model")`. Configuration inspection
|
`configkit.Keys("profiles").Index(0).Key("model")`. Configuration inspection
|
||||||
and diffs expose composed values and provenance, while `InspectProfile` and
|
and diffs expose resolved values, recorded normalization and provenance.
|
||||||
other interpreted views expose Promptkit normalization and defaults; the views
|
`InspectProfile` additionally applies backend completion; request preparation
|
||||||
are not promised to be identical.
|
can apply further overrides, so the views are not promised to be identical.
|
||||||
|
|
||||||
Remove `PromptVersion` and `PromptIdentity.Version` uses and the corresponding
|
Remove `PromptVersion` and `PromptIdentity.Version` uses and the corresponding
|
||||||
`prompt_version` JSON members. Prompt hashes are opaque equality values and may
|
`prompt_version` JSON members. Prompt hashes are opaque equality values and may
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import "go.maximumdirect.net/promptkit"
|
|||||||
For migration from the former module path, see
|
For migration from the former module path, see
|
||||||
[Update Go Imports](configuration-migration.md#update-go-imports).
|
[Update Go Imports](configuration-migration.md#update-go-imports).
|
||||||
|
|
||||||
Configuration APIs use `go.maximumdirect.net/configkit` v1.0.0. Its types are
|
Configuration APIs use `go.maximumdirect.net/configkit` v1.1.0. Its types are
|
||||||
not interchangeable with the old ConfigKit module path.
|
not interchangeable with the old ConfigKit module path.
|
||||||
|
|
||||||
The maintained [offline preparation example](../../examples/go-library/prepare/main.go)
|
The maintained [offline preparation example](../../examples/go-library/prepare/main.go)
|
||||||
@@ -52,6 +52,32 @@ Create an engine with [`NewEngine`](../../engine.go). Select each directory
|
|||||||
explicitly and place its inputs in an ordered ConfigKit underlay. A group
|
explicitly and place its inputs in an ordered ConfigKit underlay. A group
|
||||||
strictly composes its files; later groups can overlay earlier ones:
|
strictly composes its files; later groups can overlay earlier ones:
|
||||||
|
|
||||||
|
The bundled OpenRouter backend and profiles are installed in a lower underlay
|
||||||
|
by default. Select a profile explicitly, for example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
input := configkit.Input{Source: configkit.Mapping("application", map[string]any{
|
||||||
|
"prompts": []any{map[string]any{
|
||||||
|
"id": "my-prompt",
|
||||||
|
"messages": []any{map[string]any{"role": "user", "content": "Hello"}},
|
||||||
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
|
}},
|
||||||
|
})}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}})
|
||||||
|
if err != nil { return err }
|
||||||
|
result, err := engine.Run(ctx, promptkit.RunRequest{PromptID: "my-prompt", ProfileID: "gpt-luna-latest"})
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend looks up
|
||||||
|
`OPENROUTER_API_KEY` when available; credentials are not required at engine
|
||||||
|
construction. For the complete inventory and diagnostic defaults see the
|
||||||
|
[format reference](../formats.md#bundled-openrouter-configuration). Use
|
||||||
|
`promptkit.WithBundledOpenRouter(false)` for application-only inventories.
|
||||||
|
Caller records replace matching bundled records in full; use `extends` with a
|
||||||
|
distinct ID to inherit. Replacing `auto` also changes its cost-tier children's
|
||||||
|
inherited target, while their plugins remain. External collections remain
|
||||||
|
supported.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
promptInputs, err := promptkit.DirectoryInputs("prompts")
|
promptInputs, err := promptkit.DirectoryInputs("prompts")
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
@@ -211,7 +237,7 @@ sensitive application data.
|
|||||||
## Inspect The Captured Configuration
|
## Inspect The Captured Configuration
|
||||||
|
|
||||||
Use [`Engine.InspectConfiguration`](../../configuration_inspection.go) for an
|
Use [`Engine.InspectConfiguration`](../../configuration_inspection.go) for an
|
||||||
operator-selected report of composed configuration, source descriptions,
|
operator-selected report of resolved configuration, source descriptions,
|
||||||
diagnostics, and path history. Configuration inspection uses list indexes in
|
diagnostics, and path history. Configuration inspection uses list indexes in
|
||||||
the collection's nested path rather than qualified runtime IDs. For a profile
|
the collection's nested path rather than qualified runtime IDs. For a profile
|
||||||
at index `i` in collection `team`, construct the path with `configkit.Keys`:
|
at index `i` in collection `team`, construct the path with `configkit.Keys`:
|
||||||
@@ -230,8 +256,9 @@ if found && explanation.Effective.Redacted {
|
|||||||
the application decides whether those reports may be disclosed. Credential-
|
the application decides whether those reports may be disclosed. Credential-
|
||||||
shaped provider keys are redacted, but arbitrary prompt and provider content
|
shaped provider keys are redacted, but arbitrary prompt and provider content
|
||||||
is not necessarily safe to share. Inspection uses the engine's captured result
|
is not necessarily safe to share. Inspection uses the engine's captured result
|
||||||
and does not reopen sources or resolve credentials. It retains composed values
|
and does not reopen sources or resolve credentials. It retains effective values,
|
||||||
and provenance rather than Promptkit's normalized/defaulted interpretation.
|
including recorded field normalization and queue defaulting, alongside source
|
||||||
|
provenance.
|
||||||
`InspectProfile` instead shows backend-completed execution settings for one profile, and `Prepare` or
|
`InspectProfile` instead shows backend-completed execution settings for one profile, and `Prepare` or
|
||||||
`PreparedExecution.Details` shows request-specific rendered messages, inputs,
|
`PreparedExecution.Details` shows request-specific rendered messages, inputs,
|
||||||
and validation resources. None of those views is a substitute for the others.
|
and validation resources. None of those views is a substitute for the others.
|
||||||
@@ -263,11 +290,13 @@ for _, difference := range differences {
|
|||||||
```
|
```
|
||||||
|
|
||||||
See the [`ConfigurationInspection.Diff` GoDoc](../../configuration_inspection.go)
|
See the [`ConfigurationInspection.Diff` GoDoc](../../configuration_inspection.go)
|
||||||
and ConfigKit's [v1.0.0 effective-configuration diff guidance](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/docs/config.md#effective-configuration-diffs)
|
and ConfigKit's [v1.1.0 effective-configuration diff guidance](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.1.0/docs/config.md#effective-configuration-diffs)
|
||||||
for the exact comparison contract. The operation compares only frozen effective
|
for the exact comparison contract. The operation compares only frozen effective
|
||||||
configuration values: it does not reread external files or compare their
|
configuration values: it does not reread external files or compare their
|
||||||
contents, resolved credential contents, later execution preparation, or
|
contents, resolved credential contents, later execution preparation, or
|
||||||
request-specific overrides. An empty diff is not proof of equivalent execution.
|
request-specific overrides. Inputs normalizing to the same effective value do
|
||||||
|
not create a value diff; lists are compared atomically. An empty diff is not
|
||||||
|
proof of equivalent execution.
|
||||||
Records can contain ordinary application data, so use the application's
|
Records can contain ordinary application data, so use the application's
|
||||||
disclosure policy when presenting them.
|
disclosure policy when presenting them.
|
||||||
|
|
||||||
@@ -662,9 +691,9 @@ the engine to pick up changed definitions or message files.
|
|||||||
### Choose A Profile Ownership Model
|
### Choose A Profile Ownership Model
|
||||||
|
|
||||||
Supply backend and profile definitions through the configuration sources your
|
Supply backend and profile definitions through the configuration sources your
|
||||||
application selects. PromptKit does not install provider catalogs or built-in
|
application selects, alongside the default-on bundled OpenRouter inventory.
|
||||||
profile inventories; conventional backend ID constants are names only. Define
|
Conventional backend ID constants are names only. Define or replace the model,
|
||||||
the model, endpoint, credentials lookup, and settings that your application
|
endpoint, credentials lookup, and settings that your application
|
||||||
intends to support. A profile may inherit a sibling profile by its local key in
|
intends to support. A profile may inherit a sibling profile by its local key in
|
||||||
the same root or collection list; its backend reference uses the exact
|
the same root or collection list; its backend reference uses the exact
|
||||||
configured identity. Caller-selected source layers determine the final values. The
|
configured identity. Caller-selected source layers determine the final values. The
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This is the canonical reference for Promptkit's backend, profile, prompt, and
|
|||||||
schema definitions. The [Go consumer guide](consumers/pkg-promptkit.md) shows
|
schema definitions. The [Go consumer guide](consumers/pkg-promptkit.md) shows
|
||||||
how to select sources and use an engine; the
|
how to select sources and use an engine; the
|
||||||
[OpenAI-compatible integration contract](integrations/openai-compatible-chat.md)
|
[OpenAI-compatible integration contract](integrations/openai-compatible-chat.md)
|
||||||
owns outbound wire behavior. [ConfigKit](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/docs/config.md)
|
owns outbound wire behavior. [ConfigKit](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.1.0/docs/config.md)
|
||||||
owns the generic YAML grammar, imports, layering, inheritance, and diagnostics.
|
owns the generic YAML grammar, imports, layering, inheritance, and diagnostics.
|
||||||
This reference describes what Promptkit does with the final effective values.
|
This reference describes what Promptkit does with the final effective values.
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ A configuration document may contain top-level `backends`, `profiles`, and
|
|||||||
`prompts` lists, plus a `collections` mapping of namespaced resource groups.
|
`prompts` lists, plus a `collections` mapping of namespaced resource groups.
|
||||||
Each resource has an explicit literal `id`, not a filename-derived identity.
|
Each resource has an explicit literal `id`, not a filename-derived identity.
|
||||||
IDs and references must be nonblank and remain exact: Promptkit does not trim or
|
IDs and references must be nonblank and remain exact: Promptkit does not trim or
|
||||||
re-key them. Unknown fields are errors. Promptkit installs no provider or
|
re-key them. Unknown fields are errors. Promptkit installs a default-on bundled
|
||||||
profile catalog; callers select the definitions they need.
|
OpenRouter underlay; callers still explicitly select a profile.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
backends:
|
backends:
|
||||||
@@ -86,9 +86,10 @@ list. It is not a qualified or cross-collection reference.
|
|||||||
no label; an explicitly empty string is retained. It does not select a resource
|
no label; an explicitly empty string is retained. It does not select a resource
|
||||||
or change execution. Runtime lookups
|
or change execution. Runtime lookups
|
||||||
use the captured effective resource indexes. `InspectConfiguration` is an
|
use the captured effective resource indexes. `InspectConfiguration` is an
|
||||||
explicit operator view of composed settings and provenance. Normalization and
|
explicit operator view of resolved settings and provenance. Selected field
|
||||||
defaults appear in Promptkit's interpreted execution views, not as changes to
|
normalizations and the derived queue default are recorded in that tree, with
|
||||||
that composed tree. Source history is not a second runtime resolver, and the
|
source history retained. Request-time completion remains separate. Source
|
||||||
|
history is not a second runtime resolver, and the
|
||||||
retained effective shape is not flattened or rewritten into those indexes.
|
retained effective shape is not flattened or rewritten into those indexes.
|
||||||
|
|
||||||
For examples of explicitly selecting native collection inputs and inspecting
|
For examples of explicitly selecting native collection inputs and inspecting
|
||||||
@@ -331,9 +332,58 @@ For example, a runtime `extra_params["extends"]` key is ordinary provider
|
|||||||
data, not a configuration inheritance instruction. An explicit zero timeout
|
data, not a configuration inheritance instruction. An explicit zero timeout
|
||||||
does not disable the caller context or HTTP transport timeout.
|
does not disable the caller context or HTTP transport timeout.
|
||||||
|
|
||||||
|
### Bundled OpenRouter Configuration
|
||||||
|
|
||||||
|
The default bundle installs root backend `openrouter`, endpoint
|
||||||
|
`https://openrouter.ai/api/v1`, optional credential environment name
|
||||||
|
`OPENROUTER_API_KEY`, and smoke metadata selecting `gpt-luna-latest` with the
|
||||||
|
`openrouter_key` authentication adapter. Existing diagnostic endpoint fallback
|
||||||
|
applies. No prompt, implicit profile selection, or tuning defaults are installed.
|
||||||
|
Disable the entire bundle with `WithBundledOpenRouter(false)`.
|
||||||
|
|
||||||
|
The root profile IDs and their models are:
|
||||||
|
|
||||||
|
| Profile | Model |
|
||||||
|
| --- | --- |
|
||||||
|
| `gpt-astra-latest` | `~openai/gpt-astra-latest` |
|
||||||
|
| `gpt-sol-latest` | `~openai/gpt-sol-latest` |
|
||||||
|
| `gpt-terra-latest` | `~openai/gpt-terra-latest` |
|
||||||
|
| `gpt-luna-latest` | `~openai/gpt-luna-latest` |
|
||||||
|
| `claude-fable-latest` | `~anthropic/claude-fable-latest` |
|
||||||
|
| `claude-opus-latest` | `~anthropic/claude-opus-latest` |
|
||||||
|
| `claude-sonnet-latest` | `~anthropic/claude-sonnet-latest` |
|
||||||
|
| `claude-haiku-latest` | `~anthropic/claude-haiku-latest` |
|
||||||
|
| `gemini-pro-latest` | `~google/gemini-pro-latest` |
|
||||||
|
| `gemini-flash-latest` | `~google/gemini-flash-latest` |
|
||||||
|
| `auto` | `openrouter/auto` |
|
||||||
|
| `free` | `openrouter/free` |
|
||||||
|
|
||||||
|
The profiles `auto-low`, `auto-medium`, `auto-high`, `auto-xhigh`, and
|
||||||
|
`auto-max` extend `auto` and add an `auto-router` plugin with the corresponding
|
||||||
|
`cost_tier`. Replacing `auto` changes their inherited backend and model while
|
||||||
|
retaining their plugins. These are root IDs even when a model contains `/`.
|
||||||
|
For example, a caller can replace `auto` as a complete record and extend another
|
||||||
|
bundled profile under a new ID:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
profiles:
|
||||||
|
- id: auto
|
||||||
|
backend: openrouter
|
||||||
|
model: openrouter/free
|
||||||
|
- id: my-luna
|
||||||
|
extends: {id: gpt-luna-latest}
|
||||||
|
timeout_seconds: 60
|
||||||
|
```
|
||||||
|
|
||||||
|
See OpenRouter's [alias examples](https://openrouter.ai/~openai/gpt-luna-latest),
|
||||||
|
[auto router and cost tiers](https://openrouter.ai/docs/guides/routing/routers/auto-router),
|
||||||
|
and [free router](https://openrouter.ai/docs/guides/routing/routers/free-router)
|
||||||
|
for provider behavior and availability.
|
||||||
|
|
||||||
### Source And Profile Precedence
|
### Source And Profile Precedence
|
||||||
|
|
||||||
`NewEngine` composes caller underlays in their given order, followed by an
|
`NewEngine` composes the bundled OpenRouter underlay first by default, then
|
||||||
|
caller underlays in their given order, followed by an
|
||||||
optional main input. Later layers overlay earlier ones. Inputs within one
|
optional main input. Later layers overlay earlier ones. Inputs within one
|
||||||
underlay group are strictly composed: conflicting contributions are errors,
|
underlay group are strictly composed: conflicting contributions are errors,
|
||||||
not priority signals. The directory helpers discover `.yaml` and `.yml` files recursively
|
not priority signals. The directory helpers discover `.yaml` and `.yml` files recursively
|
||||||
@@ -363,7 +413,7 @@ references are exact IDs and use their qualified form when they select a
|
|||||||
collection resource. There is no Promptkit-specific depth limit or second
|
collection resource. There is no Promptkit-specific depth limit or second
|
||||||
base-chain resolver. Missing references and cycles fail at construction. For
|
base-chain resolver. Missing references and cycles fail at construction. For
|
||||||
generic inheritance and layer mechanics, consult
|
generic inheritance and layer mechanics, consult
|
||||||
[ConfigKit](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/docs/config.md).
|
[ConfigKit](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.1.0/docs/config.md).
|
||||||
|
|
||||||
## Schemas
|
## Schemas
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,12 @@ source semantics. Internal packages are not consumer APIs.
|
|||||||
|
|
||||||
`internal/configuration` is the single adapter from ConfigKit's final
|
`internal/configuration` is the single adapter from ConfigKit's final
|
||||||
effective configuration to execution-owned definitions. It performs one
|
effective configuration to execution-owned definitions. It performs one
|
||||||
layered load, captures effective presence (including explicit null), builds
|
layered load, captures effective presence (including explicit null), records
|
||||||
detached normalized interpretations during validation, and validates all effective
|
field-level normalization and derived defaults during resolution, and validates all effective
|
||||||
root and collection backend, profile, and prompt values. Replaced lower-layer
|
root and collection backend, profile, and prompt values. Replaced lower-layer
|
||||||
domain values are not separately validated. It indexes root resources under
|
domain values are not separately validated. It indexes root resources under
|
||||||
their unqualified IDs and collection resources under their full
|
their unqualified IDs and collection resources under their full
|
||||||
`namespace/local-key` IDs while retaining ConfigKit's effective tree in its
|
`namespace/local-key` IDs while retaining ConfigKit's resolved tree. It builds an immutable backend index and presence-aware profile
|
||||||
authored shape. It builds an immutable backend index and presence-aware profile
|
|
||||||
targets, applying final backend defaults only after profile inheritance. Runtime
|
targets, applying final backend defaults only after profile inheritance. Runtime
|
||||||
lookups use those final indexes and exact stored references; they do not rewrite
|
lookups use those final indexes and exact stored references; they do not rewrite
|
||||||
or resolve resources from ConfigKit source history. The runtime/domain packages
|
or resolve resources from ConfigKit source history. The runtime/domain packages
|
||||||
@@ -27,7 +26,7 @@ do not depend on ConfigKit.
|
|||||||
ConfigKit owns source headers, imports, composition, and source evidence.
|
ConfigKit owns source headers, imports, composition, and source evidence.
|
||||||
Promptkit does not preparse contributions: it validates the final effective
|
Promptkit does not preparse contributions: it validates the final effective
|
||||||
values once, captures the indexed identities and their original resource
|
values once, captures the indexed identities and their original resource
|
||||||
contexts, and retains ConfigKit's composed result for explicit operator inspection.
|
contexts, and retains ConfigKit's resolved result for explicit operator inspection.
|
||||||
Accepted non-main source headers are consumed during composition; the main
|
Accepted non-main source headers are consumed during composition; the main
|
||||||
source declaration remains available in that inspection view. Collection
|
source declaration remains available in that inspection view. Collection
|
||||||
release labels are descriptive metadata, not selection inputs. The
|
release labels are descriptive metadata, not selection inputs. The
|
||||||
@@ -37,12 +36,12 @@ owns the exact source-header and collection contracts.
|
|||||||
The `ConfigurationInspection.Diff` facade method compares two retained results
|
The `ConfigurationInspection.Diff` facade method compares two retained results
|
||||||
through a private wrapper and a narrow `internal/configuration.Diff` bridge.
|
through a private wrapper and a narrow `internal/configuration.Diff` bridge.
|
||||||
The wrapper keeps `Result[document]` inside the adapter; the bridge delegates
|
The wrapper keeps `Result[document]` inside the adapter; the bridge delegates
|
||||||
once to ConfigKit v1.0.0 and returns its public difference records. Comparison
|
once to ConfigKit v1.1.0 and returns its public difference records. Comparison
|
||||||
uses only frozen effective values and does not reopen sources, resolve runtime
|
uses only frozen effective values and does not reopen sources, resolve runtime
|
||||||
credentials, or enter the execution path. ConfigKit applies sensitivity from
|
credentials, or enter the execution path. ConfigKit applies sensitivity from
|
||||||
both results; reports may still contain ordinary application data. See the
|
both results; reports may still contain ordinary application data. See the
|
||||||
[consumer comparison guide](../consumers/pkg-promptkit.md#inspect-the-captured-configuration)
|
[consumer comparison guide](../consumers/pkg-promptkit.md#inspect-the-captured-configuration)
|
||||||
and ConfigKit's [v1.0.0 comparison contract](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/docs/config.md#effective-configuration-diffs).
|
and ConfigKit's [v1.1.0 comparison contract](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.1.0/docs/config.md#effective-configuration-diffs).
|
||||||
|
|
||||||
After resource materialization and effective-profile snapshot construction, the
|
After resource materialization and effective-profile snapshot construction, the
|
||||||
adapter captures lexically ordered identity inventories for backends, profiles,
|
adapter captures lexically ordered identity inventories for backends, profiles,
|
||||||
@@ -53,7 +52,9 @@ captured values without consulting ConfigKit or copying resource definitions.
|
|||||||
The root `DirectoryInputs` and `FilesystemInputs` helpers discover sorted YAML
|
The root `DirectoryInputs` and `FilesystemInputs` helpers discover sorted YAML
|
||||||
files under an explicit root, provide resource context, and leave imports
|
files under an explicit root, provide resource context, and leave imports
|
||||||
disabled. Each returned slice belongs to one strict-composition group; file
|
disabled. Each returned slice belongs to one strict-composition group; file
|
||||||
order does not confer priority. Caller underlays are composed in their given
|
order does not confer priority. Engine assembly prepends a dedicated embedded
|
||||||
|
OpenRouter underlay unless disabled, without modifying caller-owned slices.
|
||||||
|
Caller underlays are composed in their given
|
||||||
order, followed by the optional main input. The
|
order, followed by the optional main input. The
|
||||||
[configuration tests](../../internal/configuration/load_test.go) exercise the
|
[configuration tests](../../internal/configuration/load_test.go) exercise the
|
||||||
real loader and final-value handoff.
|
real loader and final-value handoff.
|
||||||
@@ -85,12 +86,11 @@ returning and retains no source capability for public inspection.
|
|||||||
Prompt materialization shares a lazy metadata snapshot across root and collection
|
Prompt materialization shares a lazy metadata snapshot across root and collection
|
||||||
resources during construction, avoiding a full metadata copy per collection.
|
resources during construction, avoiding a full metadata copy per collection.
|
||||||
|
|
||||||
Normalization and defaults are applied to execution-owned interpretations,
|
Normalization and defaults are recorded at individual resolved field paths,
|
||||||
including message roles and cache controls. ConfigKit retains the composed
|
including message roles and cache controls. List indexes identify fields for
|
||||||
values and origins, rather than synthetic Promptkit normalization events. List
|
presence, validation, sensitivity, and resource context; full IDs select
|
||||||
indexes identify fields for presence, validation, sensitivity, and resource
|
captured runtime indexes. Unchanged content-file fields retain their origins.
|
||||||
context; full IDs select captured runtime indexes. Recording a sequence-wide
|
Backend-to-profile and request-time completion remain separate runtime work.
|
||||||
normalization would replace origins of unchanged content-file fields.
|
|
||||||
|
|
||||||
The runner selects full definitions from this index for preparation and uses
|
The runner selects full definitions from this index for preparation and uses
|
||||||
captured prompt metadata for `InspectPrompt` and `InspectRequirements`; the
|
captured prompt metadata for `InspectPrompt` and `InspectRequirements`; the
|
||||||
|
|||||||
@@ -26,17 +26,31 @@ License and notices: [CC BY-SA 4.0](licenses/CC-BY-SA-4.0.txt) |
|
|||||||
|
|
||||||
## ConfigKit
|
## ConfigKit
|
||||||
|
|
||||||
Selected module: `go.maximumdirect.net/configkit` at `v1.0.0`.
|
Selected module: `go.maximumdirect.net/configkit` at `v1.1.0`.
|
||||||
|
|
||||||
Copyright (C) 2026 Eric Rakestraw
|
Copyright (C) 2026 Eric Rakestraw
|
||||||
|
|
||||||
SPDX-License-Identifier: LGPL-3.0-or-later
|
SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
License and notices: [LGPL 3.0](../../LICENSE) |
|
License and notices: [LGPL 3.0](../../LICENSE) |
|
||||||
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/configkit/src/commit/c88aac990b23f48a31f9046b830d4d0954f57f53/COPYRIGHT) |
|
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/configkit/src/commit/191015e289d7f2b91abb9fdd9c4f2f5cf75b4de9/COPYRIGHT) |
|
||||||
[LICENSE](https://forge.maximumdirect.net/base-layer/configkit/src/commit/c88aac990b23f48a31f9046b830d4d0954f57f53/LICENSE).
|
[LICENSE](https://forge.maximumdirect.net/base-layer/configkit/src/commit/191015e289d7f2b91abb9fdd9c4f2f5cf75b4de9/LICENSE).
|
||||||
|
|
||||||
[Source](https://forge.maximumdirect.net/base-layer/configkit/src/commit/c88aac990b23f48a31f9046b830d4d0954f57f53).
|
[Source](https://forge.maximumdirect.net/base-layer/configkit/src/commit/191015e289d7f2b91abb9fdd9c4f2f5cf75b4de9).
|
||||||
|
|
||||||
|
## ReleaseKit
|
||||||
|
|
||||||
|
Selected module: `go.maximumdirect.net/releasekit` at `v0.9.0` (maintainer tool).
|
||||||
|
|
||||||
|
Copyright (C) 2026 Eric Rakestraw
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
License and notices: [LGPL 3.0](../../LICENSE) |
|
||||||
|
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/releasekit/src/commit/2180aa868ec2caa95fc8279fd59d5f0ebac41bd4/COPYRIGHT) |
|
||||||
|
[LICENSE](https://forge.maximumdirect.net/base-layer/releasekit/src/commit/2180aa868ec2caa95fc8279fd59d5f0ebac41bd4/LICENSE).
|
||||||
|
|
||||||
|
[Source](https://forge.maximumdirect.net/base-layer/releasekit/src/commit/2180aa868ec2caa95fc8279fd59d5f0ebac41bd4).
|
||||||
|
|
||||||
## jsonschema
|
## jsonschema
|
||||||
|
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ Successful publication needs no separate remote read-back or CI wait.
|
|||||||
|
|
||||||
For optional local diagnosis, use `./scripts/releasekit.sh check VERSION`.
|
For optional local diagnosis, use `./scripts/releasekit.sh check VERSION`.
|
||||||
It is not a required step before publication. The upstream
|
It is not a required step before publication. The upstream
|
||||||
[candidate-check guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/docs/consumers/check.md)
|
[candidate-check guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/consumers/check.md)
|
||||||
and [CLI contract](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/docs/cli.md)
|
and [CLI contract](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/cli.md)
|
||||||
own shared checks and exit-status definitions.
|
own shared checks and exit-status definitions.
|
||||||
|
|
||||||
## Deliver The Hosted Release
|
## Deliver The Hosted Release
|
||||||
@@ -97,14 +97,26 @@ For a later retry, use a clean checkout of the original tag with its recorded
|
|||||||
module files and supply the token there. In a separate checkout, for example:
|
module files and supply the token there. In a separate checkout, for example:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git switch --detach vMAJOR.MINOR.PATCH
|
(
|
||||||
./scripts/releasekit.sh deliver vMAJOR.MINOR.PATCH
|
set -e
|
||||||
|
release_version=vMAJOR.MINOR.PATCH
|
||||||
|
release_tag_ref="refs/tags/$release_version"
|
||||||
|
git fetch origin "$release_tag_ref:$release_tag_ref"
|
||||||
|
release_tag_commit=$(git rev-parse --verify "$release_tag_ref^{commit}")
|
||||||
|
git switch --detach "$release_tag_commit"
|
||||||
|
release_worktree_status=$(git status --porcelain --untracked-files=all)
|
||||||
|
if [ -n "$release_worktree_status" ]; then
|
||||||
|
printf '%s\n' 'Delivery retry requires a clean tagged checkout.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
./scripts/releasekit.sh deliver "$release_version"
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Resolve uncertain hosted writes before retrying the same immutable version.
|
Resolve uncertain hosted writes before retrying the same immutable version.
|
||||||
Matching content is accepted; conflicts are not overwritten. Do not rerun
|
Matching content is accepted; conflicts are not overwritten. Do not rerun
|
||||||
`publish` or move tags to repair delivery. Follow the upstream
|
`publish` or move tags to repair delivery. Follow the upstream
|
||||||
[delivery guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/docs/consumers/delivery.md)
|
[delivery guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/consumers/delivery.md)
|
||||||
for result interpretation and recovery. These commands apply to tags containing
|
for result interpretation and recovery. These commands apply to tags containing
|
||||||
this delivery configuration; older releases retain their original workflows.
|
this delivery configuration; older releases retain their original workflows.
|
||||||
|
|
||||||
@@ -131,13 +143,13 @@ and explicitly select the matching attempt:
|
|||||||
Discovery is local; reconciliation observes the original destination without
|
Discovery is local; reconciliation observes the original destination without
|
||||||
rerunning validation, changing tags, or delivering hosted content. Neither is
|
rerunning validation, changing tags, or delivering hosted content. Neither is
|
||||||
a routine gate after successful publication. Follow the upstream
|
a routine gate after successful publication. Follow the upstream
|
||||||
[recovery procedure](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/docs/release.md#resolve-publication-failures)
|
[recovery procedure](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/release.md#resolve-publication-failures)
|
||||||
for outcomes, incomplete evidence, and older attempts without records.
|
for outcomes, incomplete evidence, and older attempts without records.
|
||||||
|
|
||||||
Records are stored under Git's common directory by default and can contain
|
Records are stored under Git's common directory by default and can contain
|
||||||
credential-bearing endpoints. Keep them private, out of source control and
|
credential-bearing endpoints. Keep them private, out of source control and
|
||||||
shared logs; retain them while needed for recovery and remove them when that
|
shared logs; retain them while needed for recovery and remove them when that
|
||||||
value expires. See the [evidence guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/docs/consumers/publish.md#retain-and-reconcile-publication-evidence)
|
value expires. See the [evidence guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/consumers/publish.md#retain-and-reconcile-publication-evidence)
|
||||||
for storage and retention details.
|
for storage and retention details.
|
||||||
|
|
||||||
Current absence does not prove a version was never published. Remove a retained
|
Current absence does not prove a version was never published. Remove a retained
|
||||||
|
|||||||
65
docs/releases/v0.18.0.md
Normal file
65
docs/releases/v0.18.0.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Promptkit v0.18.0
|
||||||
|
|
||||||
|
Configuration inspection now exposes Promptkit's recorded field normalization
|
||||||
|
and derived queue-capacity defaults through ConfigKit v1.1.0. Engines also
|
||||||
|
include a default-on OpenRouter configuration. The minimum supported Go
|
||||||
|
version remains 1.27.0.
|
||||||
|
|
||||||
|
## Bundled OpenRouter Backend And Profiles
|
||||||
|
|
||||||
|
New engines include an embedded OpenRouter backend and seventeen root profiles:
|
||||||
|
ten latest-family aliases, the auto and free routers, and five auto-router
|
||||||
|
cost-tier variants. Select a profile explicitly in a prompt or request; the
|
||||||
|
bundle does not supply an implicit execution profile or contact providers
|
||||||
|
during construction. The backend uses `OPENROUTER_API_KEY` when available under
|
||||||
|
the existing optional-credential rules.
|
||||||
|
|
||||||
|
The bundle is the lowest-priority underlay. Applications can replace its records
|
||||||
|
or derive named variants through ordinary configuration, and external collections
|
||||||
|
remain supported. Cost-tier variants inherit the effective `auto` profile, so
|
||||||
|
replacing that parent changes their inherited target while retaining plugins.
|
||||||
|
The [bundle reference](../formats.md#bundled-openrouter-configuration) owns the
|
||||||
|
inventory and composition details.
|
||||||
|
|
||||||
|
Explicit smoke diagnostics now have OpenRouter authentication metadata and an
|
||||||
|
inference fallback profile. They remain opt-in and may incur provider costs.
|
||||||
|
Latest aliases and routers can change upstream; configured slugs do not pin
|
||||||
|
the concrete model, pricing, or availability. See the
|
||||||
|
[consumer guide](../consumers/pkg-promptkit.md#construct-an-engine) for selection
|
||||||
|
and opt-out examples.
|
||||||
|
|
||||||
|
## Recorded Configuration Normalization
|
||||||
|
|
||||||
|
Inspection shows normalized backend and profile settings, prompt metadata, and
|
||||||
|
configured message roles and cache controls, with field-level resolution
|
||||||
|
history. Existing source evidence remains available, and unchanged content-file
|
||||||
|
origins continue to select their resource context.
|
||||||
|
|
||||||
|
Configuration diffs compare normalized effective values, so spelling-only
|
||||||
|
changes that normalize identically may no longer appear. Lists remain atomic in
|
||||||
|
diffs. Inspection does not include backend-to-profile completion or request
|
||||||
|
overrides; see the [configuration inspection guide](../consumers/pkg-promptkit.md#inspect-the-captured-configuration).
|
||||||
|
|
||||||
|
## Compatibility And Upgrade
|
||||||
|
|
||||||
|
New engines install a bundled OpenRouter backend and root profiles by default.
|
||||||
|
Inventory listings and inspection sources therefore change, and references to
|
||||||
|
bundled IDs that previously failed may now resolve. Pass
|
||||||
|
`promptkit.WithBundledOpenRouter(false)` to retain application-only behavior.
|
||||||
|
Caller records can replace bundled definitions through ordinary composition;
|
||||||
|
same-ID replacements replace complete records rather than merging selected fields.
|
||||||
|
No configuration-format migration is required from v0.17.0.
|
||||||
|
Consumers that display or compare configuration inspection results should account
|
||||||
|
for normalized values and derived defaults replacing their earlier authored
|
||||||
|
representations. Request-time overrides and caller-appended messages retain
|
||||||
|
their existing processing.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get go.maximumdirect.net/promptkit@v0.18.0
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [framework format reference](../formats.md) for current configuration
|
||||||
|
semantics. Consumers upgrading from versions before v0.17.0 should also follow
|
||||||
|
the [configuration and API migration guide](../consumers/configuration-migration.md).
|
||||||
|
|
||||||
|
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.18.0/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.18.0/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.18.0/docs/oss/attribution.md)
|
||||||
@@ -38,30 +38,8 @@ consumers.
|
|||||||
|
|
||||||
## Ideas
|
## Ideas
|
||||||
|
|
||||||
### Field-Level Normalization With Preserved List Provenance
|
No unselected ideas are currently recorded here. Deliberately postponed ideas
|
||||||
|
remain in the [deferred catalog](deferred.md).
|
||||||
Reevaluate normalizing resource fields directly in ConfigKit's effective
|
|
||||||
configuration after upstream ConfigKit supports recording changes within a
|
|
||||||
list without replacing the source attribution of unchanged fields.
|
|
||||||
|
|
||||||
This could let configuration inspection show PromptKit-normalized values and
|
|
||||||
field-level change history while preserving the origins needed to resolve
|
|
||||||
relative resources such as `content_file`. Assess whether that inspection
|
|
||||||
benefit justifies changing the execution-owned normalization approach.
|
|
||||||
|
|
||||||
Include normalization of roles and cache-control values in configured messages
|
|
||||||
in this reevaluation. This could make inspection reflect the values used for
|
|
||||||
execution and reduce separate normalization paths. Upstream support would need
|
|
||||||
to preserve untouched field origins within nested message lists as well as outer
|
|
||||||
resource lists. PromptKit would continue owning the normalization rules;
|
|
||||||
ConfigKit would record their effects and provenance. Caller-appended messages
|
|
||||||
would still need request-time normalization because they do not pass through
|
|
||||||
ConfigKit.
|
|
||||||
Consider moving these transformations only if it improves inspection consistency
|
|
||||||
and simplifies implementation without changing execution behavior.
|
|
||||||
|
|
||||||
Upstream support is the reconsideration trigger, not a dependency of the
|
|
||||||
current ConfigKit migration or a commitment to adopt the enhancement.
|
|
||||||
|
|
||||||
## Entry Format
|
## Entry Format
|
||||||
|
|
||||||
|
|||||||
18
engine.go
18
engine.go
@@ -101,8 +101,9 @@ type Engine struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ConfigurationSources selects ordered ConfigKit underlays and an optional
|
// ConfigurationSources selects ordered ConfigKit underlays and an optional
|
||||||
// highest-priority main input. Each underlay's inputs strictly compose within
|
// highest-priority main input. By default a bundled OpenRouter underlay precedes
|
||||||
// their group; later underlays overlay earlier ones. A nil Main uses
|
// the caller underlays; [WithBundledOpenRouter] can disable it. Inputs within
|
||||||
|
// each underlay strictly compose; later underlays overlay earlier ones. A nil Main uses
|
||||||
// ConfigKit's empty source. A non-nil Main is passed through unchanged and
|
// ConfigKit's empty source. A non-nil Main is passed through unchanged and
|
||||||
// may fail construction if invalid.
|
// may fail construction if invalid.
|
||||||
type ConfigurationSources struct {
|
type ConfigurationSources struct {
|
||||||
@@ -154,6 +155,15 @@ type engineOptions struct {
|
|||||||
internal internalengine.Options
|
internal internalengine.Options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithBundledOpenRouter enables or disables the built-in OpenRouter underlay.
|
||||||
|
// It is enabled by default; when repeated, the last option wins.
|
||||||
|
func WithBundledOpenRouter(enabled bool) Option {
|
||||||
|
return optionFunc(func(options *engineOptions) error {
|
||||||
|
options.internal.SetBundledOpenRouter(enabled)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// WithLLMClient replaces the built-in model client used by [Engine.Run] and
|
// WithLLMClient replaces the built-in model client used by [Engine.Run] and
|
||||||
// [Engine.RunPrepared].
|
// [Engine.RunPrepared].
|
||||||
//
|
//
|
||||||
@@ -213,7 +223,9 @@ func WithSchemaFile(path string) Option {
|
|||||||
// NewEngine constructs an Engine from configuration and options.
|
// NewEngine constructs an Engine from configuration and options.
|
||||||
//
|
//
|
||||||
// ConfigKit composes caller-selected underlays in their given order, followed
|
// ConfigKit composes caller-selected underlays in their given order, followed
|
||||||
// by the optional main input. Construction validates the final effective
|
// by the optional main input, after the default-on bundled OpenRouter underlay.
|
||||||
|
// The bundle adds a root backend and profiles to inventories, but no prompts
|
||||||
|
// or implicit profile selection. Construction validates the final effective
|
||||||
// backend, profile, and prompt definitions and captures referenced prompt
|
// backend, profile, and prompt definitions and captures referenced prompt
|
||||||
// content. A missing prompt collection is allowed. Schema documents and
|
// content. A missing prompt collection is allowed. Schema documents and
|
||||||
// request artifacts remain runtime resources.
|
// request artifacts remain runtime resources.
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func run() (summary, error) {
|
|||||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||||
Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: inputs}}},
|
Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: inputs}}},
|
||||||
HTTPClient: &http.Client{Transport: transport},
|
HTTPClient: &http.Client{Transport: transport},
|
||||||
}, promptkit.WithSchemaFS(assets, "schemas"))
|
}, promptkit.WithBundledOpenRouter(false), promptkit.WithSchemaFS(assets, "schemas")) // Keep the example's configuration self-contained.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return summary{}, err
|
return summary{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func run(ctx context.Context, exampleDir string, out io.Writer) error {
|
|||||||
{Inputs: []configkit.Input{support, review}},
|
{Inputs: []configkit.Input{support, review}},
|
||||||
{Inputs: []configkit.Input{override}},
|
{Inputs: []configkit.Input{override}},
|
||||||
},
|
},
|
||||||
}})
|
}}, promptkit.WithBundledOpenRouter(false)) // Keep this example's inventory caller-owned.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func run(ctx context.Context, exampleDir string, out io.Writer) error {
|
|||||||
{ID: "offline-example", Endpoint: &endpoint, Model: &model},
|
{ID: "offline-example", Endpoint: &endpoint, Model: &model},
|
||||||
})}}}},
|
})}}}},
|
||||||
Main: &configkit.Input{Source: configkit.File(filepath.Join(exampleDir, "prompt.yaml"))},
|
Main: &configkit.Input{Source: configkit.File(filepath.Join(exampleDir, "prompt.yaml"))},
|
||||||
}}, promptkit.WithLLMClient(deterministicClient{}))
|
}}, promptkit.WithBundledOpenRouter(false), promptkit.WithLLMClient(deterministicClient{})) // Use only this example's offline configuration.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -4,12 +4,12 @@ go 1.27.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
|
||||||
go.maximumdirect.net/configkit v1.0.0
|
go.maximumdirect.net/configkit v1.1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/yuin/goldmark/v2 v2.1.6 // indirect
|
github.com/yuin/goldmark/v2 v2.1.6 // indirect
|
||||||
go.maximumdirect.net/releasekit v0.8.0 // indirect
|
go.maximumdirect.net/releasekit v0.9.0 // indirect
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
|
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
|
||||||
golang.org/x/mod v0.40.0 // indirect
|
golang.org/x/mod v0.40.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -4,10 +4,10 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEV
|
|||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||||
github.com/yuin/goldmark/v2 v2.1.6 h1:igy0q9JcqovoFXFx/XjtU8sHVhcUHavPj2sA3EoKJUE=
|
github.com/yuin/goldmark/v2 v2.1.6 h1:igy0q9JcqovoFXFx/XjtU8sHVhcUHavPj2sA3EoKJUE=
|
||||||
github.com/yuin/goldmark/v2 v2.1.6/go.mod h1:G6M4/qOFtfn01/o14BU1UR2Lo5N3S9Qo7xCuz/sHjGQ=
|
github.com/yuin/goldmark/v2 v2.1.6/go.mod h1:G6M4/qOFtfn01/o14BU1UR2Lo5N3S9Qo7xCuz/sHjGQ=
|
||||||
go.maximumdirect.net/configkit v1.0.0 h1:cguQnxkaRk3ClQOZNzzhTdc4bfzbraGKv8CkoCp+HLc=
|
go.maximumdirect.net/configkit v1.1.0 h1:UzXN2ZMnEkcL0O4YSLqD9qd9M1gb/NWCHQR/H8AaQzA=
|
||||||
go.maximumdirect.net/configkit v1.0.0/go.mod h1:B6fJ5xBLBF1nuywqfLeEI4sLm29UkIcDLu8i6I6gGXE=
|
go.maximumdirect.net/configkit v1.1.0/go.mod h1:s1zLJ9GGB/1Q4a+aOHMLISonJxWFgNWpA6SpPlOafeA=
|
||||||
go.maximumdirect.net/releasekit v0.8.0 h1:hSaHPbUI7tAhnolfbu7UMj2fvmm9FGBY+jhkygdbxR0=
|
go.maximumdirect.net/releasekit v0.9.0 h1:sK+Aoj6gS6ATU/1reocjwBekp4YETYENwkcX5ti6mrE=
|
||||||
go.maximumdirect.net/releasekit v0.8.0/go.mod h1:QE4BLrAhVJTzi+/YTcVl21K/qZp9ojRLg5kuoy3Zb44=
|
go.maximumdirect.net/releasekit v0.9.0/go.mod h1:QE4BLrAhVJTzi+/YTcVl21K/qZp9ojRLg5kuoy3Zb44=
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||||
|
|||||||
16
internal/configuration/bundled.go
Normal file
16
internal/configuration/bundled.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed bundled/openrouter.yaml
|
||||||
|
var bundledFiles embed.FS
|
||||||
|
|
||||||
|
// BundledOpenRouterUnderlay returns the built-in OpenRouter configuration source.
|
||||||
|
func BundledOpenRouterUnderlay() configkit.Underlay {
|
||||||
|
filesystem := configkit.NewFilesystem("Promptkit bundled OpenRouter", bundledFiles)
|
||||||
|
return configkit.Underlay{Inputs: []configkit.Input{{Source: filesystem.File("bundled/openrouter.yaml")}}}
|
||||||
|
}
|
||||||
60
internal/configuration/bundled/openrouter.yaml
Normal file
60
internal/configuration/bundled/openrouter.yaml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
backends:
|
||||||
|
- id: openrouter
|
||||||
|
endpoint: https://openrouter.ai/api/v1
|
||||||
|
api_key_env: OPENROUTER_API_KEY
|
||||||
|
smoke_tests:
|
||||||
|
default_profile: gpt-luna-latest
|
||||||
|
authentication:
|
||||||
|
kind: openrouter_key
|
||||||
|
profiles:
|
||||||
|
- id: gpt-astra-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: ~openai/gpt-astra-latest
|
||||||
|
- id: gpt-sol-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: ~openai/gpt-sol-latest
|
||||||
|
- id: gpt-terra-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: ~openai/gpt-terra-latest
|
||||||
|
- id: gpt-luna-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: ~openai/gpt-luna-latest
|
||||||
|
- id: claude-fable-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: ~anthropic/claude-fable-latest
|
||||||
|
- id: claude-opus-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: ~anthropic/claude-opus-latest
|
||||||
|
- id: claude-sonnet-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: ~anthropic/claude-sonnet-latest
|
||||||
|
- id: claude-haiku-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: ~anthropic/claude-haiku-latest
|
||||||
|
- id: gemini-pro-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: ~google/gemini-pro-latest
|
||||||
|
- id: gemini-flash-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: ~google/gemini-flash-latest
|
||||||
|
- id: auto
|
||||||
|
backend: openrouter
|
||||||
|
model: openrouter/auto
|
||||||
|
- id: free
|
||||||
|
backend: openrouter
|
||||||
|
model: openrouter/free
|
||||||
|
- id: auto-low
|
||||||
|
extends: {id: auto}
|
||||||
|
extra_params: {plugins: [{id: auto-router, cost_tier: low}]}
|
||||||
|
- id: auto-medium
|
||||||
|
extends: {id: auto}
|
||||||
|
extra_params: {plugins: [{id: auto-router, cost_tier: medium}]}
|
||||||
|
- id: auto-high
|
||||||
|
extends: {id: auto}
|
||||||
|
extra_params: {plugins: [{id: auto-router, cost_tier: high}]}
|
||||||
|
- id: auto-xhigh
|
||||||
|
extends: {id: auto}
|
||||||
|
extra_params: {plugins: [{id: auto-router, cost_tier: xhigh}]}
|
||||||
|
- id: auto-max
|
||||||
|
extends: {id: auto}
|
||||||
|
extra_params: {plugins: [{id: auto-router, cost_tier: max}]}
|
||||||
47
internal/configuration/bundled_test.go
Normal file
47
internal/configuration/bundled_test.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBundledOpenRouterUnderlay(t *testing.T) {
|
||||||
|
state, err := Load(configkit.Input{Source: configkit.NoSource()}, []configkit.Underlay{BundledOpenRouterUnderlay()})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
backend, err := state.GetBackend("openrouter")
|
||||||
|
if err != nil || backend.Endpoint != "https://openrouter.ai/api/v1" || backend.APIKeyEnv != "OPENROUTER_API_KEY" || backend.SmokeTests == nil || backend.SmokeTests.DefaultProfile == nil || *backend.SmokeTests.DefaultProfile != "gpt-luna-latest" || backend.SmokeTests.Authentication == nil || *backend.SmokeTests.Authentication.Kind != "openrouter_key" {
|
||||||
|
t.Fatalf("backend = %+v, %v", backend, err)
|
||||||
|
}
|
||||||
|
models := map[string]string{
|
||||||
|
"gpt-astra-latest": "~openai/gpt-astra-latest", "gpt-sol-latest": "~openai/gpt-sol-latest", "gpt-terra-latest": "~openai/gpt-terra-latest", "gpt-luna-latest": "~openai/gpt-luna-latest",
|
||||||
|
"claude-fable-latest": "~anthropic/claude-fable-latest", "claude-opus-latest": "~anthropic/claude-opus-latest", "claude-sonnet-latest": "~anthropic/claude-sonnet-latest", "claude-haiku-latest": "~anthropic/claude-haiku-latest",
|
||||||
|
"gemini-pro-latest": "~google/gemini-pro-latest", "gemini-flash-latest": "~google/gemini-flash-latest", "auto": "openrouter/auto", "free": "openrouter/free",
|
||||||
|
}
|
||||||
|
for _, tier := range []string{"low", "medium", "high", "xhigh", "max"} {
|
||||||
|
models["auto-"+tier] = "openrouter/auto"
|
||||||
|
}
|
||||||
|
if len(state.profileIDs) != len(models) {
|
||||||
|
t.Fatalf("profiles = %v", state.profileIDs)
|
||||||
|
}
|
||||||
|
for id, model := range models {
|
||||||
|
profile, err := state.GetProfile(context.Background(), id)
|
||||||
|
if err != nil || profile.ResolvedTarget.Model != model || profile.ResolvedTarget.BackendID != "openrouter" {
|
||||||
|
t.Fatalf("profile %s = %+v, %v", id, profile, err)
|
||||||
|
}
|
||||||
|
if tier, ok := strings.CutPrefix(id, "auto-"); ok {
|
||||||
|
plugins := fmt.Sprint(profile.ResolvedTarget.ExtraParams)
|
||||||
|
if !strings.Contains(plugins, "cost_tier:"+tier) || !strings.Contains(plugins, "id:auto-router") {
|
||||||
|
t.Fatalf("profile %s plugins = %+v", id, profile.ResolvedTarget.ExtraParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(fmt.Sprint(state.result.Sources()), "Promptkit bundled OpenRouter") {
|
||||||
|
t.Fatalf("source evidence = %+v", state.result.Sources())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"go.maximumdirect.net/configkit"
|
"go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTargetInterpretationPreservesComposedNestedValues(t *testing.T) {
|
func TestTargetResolutionNormalizesNestedValues(t *testing.T) {
|
||||||
state, err := load(mapping("padded target", map[string]any{
|
state, err := load(mapping("padded target", map[string]any{
|
||||||
"backends": []any{map[string]any{
|
"backends": []any{map[string]any{
|
||||||
"id": "local", "endpoint": " HTTPS://example.invalid/v1 ", "concurrency_limit": 1,
|
"id": "local", "endpoint": " HTTPS://example.invalid/v1 ", "concurrency_limit": 1,
|
||||||
@@ -33,20 +33,32 @@ func TestTargetInterpretationPreservesComposedNestedValues(t *testing.T) {
|
|||||||
t.Fatalf("interpreted profile = %+v, %v", profile, err)
|
t.Fatalf("interpreted profile = %+v, %v", profile, err)
|
||||||
}
|
}
|
||||||
composed := state.result.Config.Backends[0]
|
composed := state.result.Config.Backends[0]
|
||||||
if *composed.Endpoint != " HTTPS://example.invalid/v1 " || composed.QueueCapacity != nil ||
|
if *composed.Endpoint != "https://example.invalid/v1" || composed.QueueCapacity == nil || *composed.QueueCapacity != 1024 ||
|
||||||
*composed.SmokeTests.Reachability.Endpoint != " https://example.invalid/health " ||
|
*composed.SmokeTests.Reachability.Endpoint != "https://example.invalid/health" ||
|
||||||
*composed.SmokeTests.Authentication.Kind != " NONE " || *composed.SmokeTests.Authentication.Endpoint != " https://example.invalid/key " ||
|
*composed.SmokeTests.Authentication.Kind != "none" || *composed.SmokeTests.Authentication.Endpoint != "https://example.invalid/key" ||
|
||||||
*state.result.Config.Profiles[0].ServiceTier != " standard " || *state.result.Config.Profiles[0].APIKeyEnv != " TOKEN " {
|
*state.result.Config.Profiles[0].ServiceTier != "standard" || *state.result.Config.Profiles[0].APIKeyEnv != "TOKEN" {
|
||||||
t.Fatalf("composed values were mutated: %+v", state.result.Config)
|
t.Fatalf("resolved values were not normalized: %+v", state.result.Config)
|
||||||
}
|
}
|
||||||
path := configkit.Keys("backends").Index(0).Key("smoke_tests").Key("authentication").Key("endpoint")
|
path := configkit.Keys("backends").Index(0).Key("smoke_tests").Key("authentication").Key("endpoint")
|
||||||
field, found := state.result.Metadata().Lookup(path)
|
field, found := state.result.Metadata().Lookup(path)
|
||||||
if !found || len(field.Origin.Locations) == 0 || field.Origin.Operation == configkit.ChangeNormalization {
|
if !found || field.Origin.Operation != configkit.ChangeNormalization {
|
||||||
t.Fatalf("nested field origin changed: %+v", field)
|
t.Fatalf("nested field origin changed: %+v", field)
|
||||||
}
|
}
|
||||||
|
for _, path := range []configkit.Path{
|
||||||
|
configkit.Keys("backends").Index(0).Key("endpoint"),
|
||||||
|
configkit.Keys("backends").Index(0).Key("smoke_tests").Key("reachability").Key("endpoint"),
|
||||||
|
configkit.Keys("backends").Index(0).Key("smoke_tests").Key("authentication").Key("kind"),
|
||||||
|
configkit.Keys("profiles").Index(0).Key("service_tier"),
|
||||||
|
configkit.Keys("profiles").Index(0).Key("api_key_env"),
|
||||||
|
} {
|
||||||
|
field, found := state.result.Metadata().Lookup(path)
|
||||||
|
if !found || field.Origin.Operation != configkit.ChangeNormalization {
|
||||||
|
t.Errorf("normalization at %v: %+v", path, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPromptInterpretationPreservesComposedOutputAndRejectsDuplicateNames(t *testing.T) {
|
func TestPromptResolutionNormalizesOutputAndRejectsDuplicateNames(t *testing.T) {
|
||||||
definition := map[string]any{
|
definition := map[string]any{
|
||||||
"id": "example", "inputs": []any{map[string]any{"name": " document "}},
|
"id": "example", "inputs": []any{map[string]any{"name": " document "}},
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
||||||
@@ -61,8 +73,8 @@ func TestPromptInterpretationPreservesComposedOutputAndRejectsDuplicateNames(t *
|
|||||||
t.Fatalf("interpreted prompt = %+v, %+v, %v", selected, inspection, err)
|
t.Fatalf("interpreted prompt = %+v, %+v, %v", selected, inspection, err)
|
||||||
}
|
}
|
||||||
composed := state.result.Config.Prompts[0]
|
composed := state.result.Config.Prompts[0]
|
||||||
if *composed.Output.SchemaPath != " schema.json " || composed.Inputs[0].Name != " document " {
|
if *composed.Output.SchemaPath != "schema.json" || composed.Inputs[0].Name != "document" {
|
||||||
t.Fatalf("composed prompt changed: %+v", composed)
|
t.Fatalf("prompt was not normalized: %+v", composed)
|
||||||
}
|
}
|
||||||
definition["inputs"] = []any{map[string]any{"name": " document "}, map[string]any{"name": "document"}}
|
definition["inputs"] = []any{map[string]any{"name": " document "}, map[string]any{"name": "document"}}
|
||||||
_, err = load(mapping("duplicate inputs", map[string]any{"prompts": []any{definition}}), nil)
|
_, err = load(mapping("duplicate inputs", map[string]any{"prompts": []any{definition}}), nil)
|
||||||
@@ -70,3 +82,71 @@ func TestPromptInterpretationPreservesComposedOutputAndRejectsDuplicateNames(t *
|
|||||||
t.Fatalf("duplicate normalized names were not rejected: %v", err)
|
t.Fatalf("duplicate normalized names were not rejected: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPromptResolutionRecordsMessageFields(t *testing.T) {
|
||||||
|
loadPrompt := func(role, cacheType, ttl string) *loaded {
|
||||||
|
t.Helper()
|
||||||
|
state, err := load(mapping("prompt", map[string]any{"prompts": []any{map[string]any{
|
||||||
|
"id": "example", "inputs": []any{map[string]any{"name": " input ", "content_type": nil, "description": " text "}},
|
||||||
|
"messages": []any{map[string]any{"role": role, "content": "", "cache_control": map[string]any{"type": cacheType, "ttl": ttl}}},
|
||||||
|
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||||
|
}}}), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
padded := loadPrompt(" USER ", " ephemeral ", " 1h ")
|
||||||
|
canonical := loadPrompt("user", "ephemeral", "1h")
|
||||||
|
definition, inspection, err := padded.prompt("example")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
other, otherInspection, err := canonical.prompt("example")
|
||||||
|
if err != nil || inspection.PromptHash != otherInspection.PromptHash || definition.Templates[0].Role != other.Templates[0].Role || definition.Templates[0].Content != "" {
|
||||||
|
t.Fatalf("equivalent prompt mismatch: %+v %+v %v", definition, other, err)
|
||||||
|
}
|
||||||
|
base := configkit.Keys("prompts").Index(0)
|
||||||
|
for _, path := range []configkit.Path{base.Key("inputs").Index(0).Key("name"), base.Key("inputs").Index(0).Key("description"), base.Key("messages").Index(0).Key("role"), base.Key("messages").Index(0).Key("cache_control").Key("type"), base.Key("messages").Index(0).Key("cache_control").Key("ttl")} {
|
||||||
|
field, found := padded.result.Metadata().Lookup(path)
|
||||||
|
if !found || field.Origin.Operation != configkit.ChangeNormalization {
|
||||||
|
t.Errorf("normalization at %v: %+v", path, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if padded.promptMetadata["example"][0].contentType != fieldNull {
|
||||||
|
t.Fatal("null input metadata was not retained")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolutionRecordsTargetDependencies(t *testing.T) {
|
||||||
|
backend := func(id string) map[string]any {
|
||||||
|
return map[string]any{"id": id, "endpoint": " HTTPS://example.invalid/v1 ", "concurrency_limit": 2, "queue_capacity": nil}
|
||||||
|
}
|
||||||
|
state, err := load(mapping("targets", map[string]any{
|
||||||
|
"backends": []any{backend("root")},
|
||||||
|
"collections": map[string]any{"scoped": map[string]any{"backends": []any{backend("local")}}},
|
||||||
|
}), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, base := range []configkit.Path{configkit.Keys("backends").Index(0), configkit.Keys("collections", "scoped", "backends").Index(0)} {
|
||||||
|
endpoint, ok := state.result.Explain(base.Key("endpoint"))
|
||||||
|
if !ok || !strings.Contains(endpoint.Effective.YAML, "https://example.invalid/v1") {
|
||||||
|
t.Fatalf("endpoint explanation: %+v", endpoint)
|
||||||
|
}
|
||||||
|
history := state.result.Metadata().History(base.Key("queue_capacity"))
|
||||||
|
found := false
|
||||||
|
for _, change := range history {
|
||||||
|
if change.Origin.Operation == configkit.ChangeDerivedDefault {
|
||||||
|
for _, dependency := range change.Dependencies {
|
||||||
|
if dependency.String() == base.Key("concurrency_limit").String() {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("missing queue dependency: %+v", history)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package configuration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -120,7 +119,6 @@ const (
|
|||||||
|
|
||||||
type loaded struct {
|
type loaded struct {
|
||||||
result *configkit.Result[document]
|
result *configkit.Result[document]
|
||||||
interpreted document
|
|
||||||
backends map[string]map[string]fieldState
|
backends map[string]map[string]fieldState
|
||||||
profiles map[string]map[string]fieldState
|
profiles map[string]map[string]fieldState
|
||||||
promptMetadata map[string][]promptInputPresence
|
promptMetadata map[string][]promptInputPresence
|
||||||
@@ -243,11 +241,15 @@ func configurationDefinition(state *loaded) configkit.Definition[document] {
|
|||||||
captureFields(resources.profiles, func(v profileConfig) string { return v.ID }, resources.bases.profiles, resources.namespace, profileFields, resolution, state.profiles)
|
captureFields(resources.profiles, func(v profileConfig) string { return v.ID }, resources.bases.profiles, resources.namespace, profileFields, resolution, state.profiles)
|
||||||
capturePromptMetadata(resources.prompts, resources.bases.prompts, resources.namespace, resolution, state.promptMetadata)
|
capturePromptMetadata(resources.prompts, resources.bases.prompts, resources.namespace, resolution, state.promptMetadata)
|
||||||
}
|
}
|
||||||
|
for _, resources := range resourceSets(config) {
|
||||||
|
resolveBackends(resources.backends, resources.bases.backends, resolution)
|
||||||
|
resolveProfiles(resources.profiles, resources.bases.profiles, resolution)
|
||||||
|
resolvePrompts(resources.prompts, resources.bases.prompts, resolution)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Validate: func(config *document, validation *configkit.Validation) {
|
Validate: func(config *document, validation *configkit.Validation) {
|
||||||
state.interpreted = interpretConfiguration(config, validation)
|
validateEffective(config, validation)
|
||||||
validateEffective(&state.interpreted, validation)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,7 +276,7 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
|||||||
if err := state.buildExecutionProfiles(); err != nil {
|
if err := state.buildExecutionProfiles(); err != nil {
|
||||||
return nil, fmt.Errorf("load configuration: %w", err)
|
return nil, fmt.Errorf("load configuration: %w", err)
|
||||||
}
|
}
|
||||||
state.backendIDs = sortedKeys(indexedBackends(&state.interpreted))
|
state.backendIDs = sortedKeys(indexedBackends(&state.result.Config))
|
||||||
state.profileIDs = sortedKeys(state.indexedProfiles)
|
state.profileIDs = sortedKeys(state.indexedProfiles)
|
||||||
for _, id := range sortedKeys(state.prompts) {
|
for _, id := range sortedKeys(state.prompts) {
|
||||||
state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id})
|
state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id})
|
||||||
@@ -372,6 +374,29 @@ func captureFields[T any](entries []T, identity func(T) string, basePath configk
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eligibleResourceList leaves invalid identities to the existing validation
|
||||||
|
// boundaries, without mutating a list that cannot be recorded by identity.
|
||||||
|
func eligibleResourceList[T any](entries []T, identity func(T) string) bool {
|
||||||
|
seen := make(map[string]struct{}, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
id := identity(entry)
|
||||||
|
if !domain.IsValidLocalResourceKey(id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, exists := seen[id]; exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordStringNormalization(value **string, path configkit.Path, resolution *configkit.Resolution, normalized string) {
|
||||||
|
if *value != nil && **value != normalized {
|
||||||
|
configkit.Normalize(resolution, path, value, &normalized, "Normalize configured value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sortedKeys[T any](entries map[string]T) []string {
|
func sortedKeys[T any](entries map[string]T) []string {
|
||||||
keys := make([]string, 0, len(entries))
|
keys := make([]string, 0, len(entries))
|
||||||
for key := range entries {
|
for key := range entries {
|
||||||
@@ -381,113 +406,6 @@ func sortedKeys[T any](entries map[string]T) []string {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
func interpretConfiguration(config *document, validation *configkit.Validation) document {
|
|
||||||
interpreted := *config
|
|
||||||
interpreted.Backends = interpretBackends(config.Backends, configkit.Keys("backends"), validation)
|
|
||||||
interpreted.Profiles = interpretProfiles(config.Profiles, configkit.Keys("profiles"), validation)
|
|
||||||
interpreted.Prompts = interpretPrompts(config.Prompts)
|
|
||||||
if config.Collections != nil {
|
|
||||||
interpreted.Collections = make(map[string]collectionConfig, len(config.Collections))
|
|
||||||
for namespace, original := range config.Collections {
|
|
||||||
collection := original
|
|
||||||
base := configkit.Keys("collections", namespace)
|
|
||||||
collection.Backends = interpretBackends(original.Backends, base.Key("backends"), validation)
|
|
||||||
collection.Profiles = interpretProfiles(original.Profiles, base.Key("profiles"), validation)
|
|
||||||
collection.Prompts = interpretPrompts(original.Prompts)
|
|
||||||
interpreted.Collections[namespace] = collection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return interpreted
|
|
||||||
}
|
|
||||||
|
|
||||||
func interpretBackends(source []backendConfig, basePath configkit.Path, validation *configkit.Validation) []backendConfig {
|
|
||||||
if source == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
backends := append([]backendConfig{}, source...)
|
|
||||||
for index := range backends {
|
|
||||||
backend := &backends[index]
|
|
||||||
base := basePath.Index(index)
|
|
||||||
if backend.Endpoint != nil {
|
|
||||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*backend.Endpoint)
|
|
||||||
if err != nil {
|
|
||||||
validation.Error(base.Key("endpoint"), err.Error())
|
|
||||||
} else {
|
|
||||||
backend.Endpoint = &endpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if backend.APIKeyEnv != nil {
|
|
||||||
value := strings.TrimSpace(*backend.APIKeyEnv)
|
|
||||||
backend.APIKeyEnv = &value
|
|
||||||
}
|
|
||||||
if backend.SmokeTests != nil {
|
|
||||||
smokeTests := *backend.SmokeTests
|
|
||||||
backend.SmokeTests = &smokeTests
|
|
||||||
smokePath := base.Key("smoke_tests")
|
|
||||||
if smokeTests.Reachability != nil {
|
|
||||||
reachability := *smokeTests.Reachability
|
|
||||||
smokeTests.Reachability = &reachability
|
|
||||||
normalizeDiagnosticEndpoint(&reachability.Endpoint, smokePath.Key("reachability").Key("endpoint"), validation)
|
|
||||||
}
|
|
||||||
if smokeTests.Authentication != nil {
|
|
||||||
authentication := *smokeTests.Authentication
|
|
||||||
smokeTests.Authentication = &authentication
|
|
||||||
if authentication.Kind != nil {
|
|
||||||
kind := strings.ToLower(strings.TrimSpace(*authentication.Kind))
|
|
||||||
authentication.Kind = &kind
|
|
||||||
}
|
|
||||||
normalizeDiagnosticEndpoint(&authentication.Endpoint, smokePath.Key("authentication").Key("endpoint"), validation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if backend.ConcurrencyLimit != nil && *backend.ConcurrencyLimit > 0 && backend.QueueCapacity == nil {
|
|
||||||
capacity := 1024
|
|
||||||
backend.QueueCapacity = &capacity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return backends
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeDiagnosticEndpoint(value **string, path configkit.Path, validation *configkit.Validation) {
|
|
||||||
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() == "" {
|
|
||||||
validation.Error(path, "diagnostic endpoint must be an absolute HTTP(S) URL with a host")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
*value = &normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
func interpretProfiles(source []profileConfig, basePath configkit.Path, validation *configkit.Validation) []profileConfig {
|
|
||||||
if source == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
profiles := append([]profileConfig{}, source...)
|
|
||||||
for index := range profiles {
|
|
||||||
profile := &profiles[index]
|
|
||||||
base := basePath.Index(index)
|
|
||||||
if profile.Endpoint != nil && *profile.Endpoint != "" {
|
|
||||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint)
|
|
||||||
if err != nil {
|
|
||||||
validation.Error(base.Key("endpoint"), err.Error())
|
|
||||||
} else {
|
|
||||||
profile.Endpoint = &endpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, value := range []**string{&profile.ServiceTier, &profile.ReasoningEffort, &profile.APIKeyEnv} {
|
|
||||||
if *value != nil {
|
|
||||||
normalized := strings.TrimSpace(**value)
|
|
||||||
*value = &normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return profiles
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateEffective(config *document, validation *configkit.Validation) {
|
func validateEffective(config *document, validation *configkit.Validation) {
|
||||||
sets := resourceSets(config)
|
sets := resourceSets(config)
|
||||||
backendIDs := make(map[string]struct{})
|
backendIDs := make(map[string]struct{})
|
||||||
@@ -502,6 +420,7 @@ func validateEffective(config *document, validation *configkit.Validation) {
|
|||||||
for _, resources := range sets {
|
for _, resources := range sets {
|
||||||
for index, backend := range resources.backends {
|
for index, backend := range resources.backends {
|
||||||
base := resources.bases.backends.Index(index)
|
base := resources.bases.backends.Index(index)
|
||||||
|
validateBackendEndpoints(backend, base, validation)
|
||||||
if !domain.IsValidLocalResourceKey(backend.ID) {
|
if !domain.IsValidLocalResourceKey(backend.ID) {
|
||||||
validation.Error(base.Key("id"), "backend ID must be a nonblank local key without a slash")
|
validation.Error(base.Key("id"), "backend ID must be a nonblank local key without a slash")
|
||||||
}
|
}
|
||||||
@@ -546,6 +465,11 @@ func validateEffective(config *document, validation *configkit.Validation) {
|
|||||||
}
|
}
|
||||||
for index, profile := range resources.profiles {
|
for index, profile := range resources.profiles {
|
||||||
base := resources.bases.profiles.Index(index)
|
base := resources.bases.profiles.Index(index)
|
||||||
|
if profile.Endpoint != nil && *profile.Endpoint != "" {
|
||||||
|
if _, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*profile.Endpoint); err != nil {
|
||||||
|
validation.Error(base.Key("endpoint"), err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
if !domain.IsValidLocalResourceKey(profile.ID) {
|
if !domain.IsValidLocalResourceKey(profile.ID) {
|
||||||
validation.Error(base.Key("id"), "profile ID must be a nonblank local key without a slash")
|
validation.Error(base.Key("id"), "profile ID must be a nonblank local key without a slash")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,50 @@ func TestIdentifiedResourcesRejectDuplicateStrictPeers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInvalidIdentitiesWithNormalizableFieldsAreRejected(t *testing.T) {
|
||||||
|
for _, kind := range []string{"backends", "profiles", "prompts"} {
|
||||||
|
for _, id := range []string{" ", "bad/id"} {
|
||||||
|
t.Run(kind+"/"+id, func(t *testing.T) {
|
||||||
|
record := map[string]any{"id": id}
|
||||||
|
switch kind {
|
||||||
|
case "backends":
|
||||||
|
record["endpoint"] = " HTTPS://example.invalid/v1 "
|
||||||
|
record["concurrency_limit"] = 1
|
||||||
|
case "profiles":
|
||||||
|
record["service_tier"] = " standard "
|
||||||
|
case "prompts":
|
||||||
|
record["description"] = " padded "
|
||||||
|
}
|
||||||
|
_, err := load(mapping("invalid", map[string]any{kind: []any{record}}), nil)
|
||||||
|
if err == nil || strings.Contains(strings.ToLower(err.Error()), "unrecorded") {
|
||||||
|
t.Fatalf("invalid identity error = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDuplicateIdentitiesWithNormalizableFieldsAreRejected(t *testing.T) {
|
||||||
|
for _, kind := range []string{"backends", "profiles", "prompts"} {
|
||||||
|
t.Run(kind, func(t *testing.T) {
|
||||||
|
record := map[string]any{"id": "same"}
|
||||||
|
switch kind {
|
||||||
|
case "backends":
|
||||||
|
record["endpoint"] = " HTTPS://example.invalid/v1 "
|
||||||
|
record["concurrency_limit"] = 1
|
||||||
|
case "profiles":
|
||||||
|
record["service_tier"] = " standard "
|
||||||
|
case "prompts":
|
||||||
|
record["description"] = " padded "
|
||||||
|
}
|
||||||
|
_, err := load(mapping("duplicates", map[string]any{kind: []any{record, record}}), nil)
|
||||||
|
if err == nil || strings.Contains(strings.ToLower(err.Error()), "unrecorded") {
|
||||||
|
t.Fatalf("duplicate identity error = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFinalIdentitiesAreCheckedAfterApplicationResolution(t *testing.T) {
|
func TestFinalIdentitiesAreCheckedAfterApplicationResolution(t *testing.T) {
|
||||||
for _, kind := range []string{"backends", "profiles", "prompts"} {
|
for _, kind := range []string{"backends", "profiles", "prompts"} {
|
||||||
for _, namespace := range []string{"", "*"} {
|
for _, namespace := range []string{"", "*"} {
|
||||||
@@ -534,17 +578,25 @@ func TestLoadInterpretsBackendNormalizationAndNullDefault(t *testing.T) {
|
|||||||
t.Fatalf("backend normalization/default was not applied: %+v", backend)
|
t.Fatalf("backend normalization/default was not applied: %+v", backend)
|
||||||
}
|
}
|
||||||
composed := loaded.result.Config.Backends[0]
|
composed := loaded.result.Config.Backends[0]
|
||||||
if composed.Endpoint == nil || *composed.Endpoint != " HTTP://localhost:8080/v1 " ||
|
if composed.Endpoint == nil || *composed.Endpoint != "http://localhost:8080/v1" ||
|
||||||
composed.APIKeyEnv == nil || *composed.APIKeyEnv != " TOKEN_NAME " || composed.QueueCapacity != nil {
|
composed.APIKeyEnv == nil || *composed.APIKeyEnv != "TOKEN_NAME" || composed.QueueCapacity == nil || *composed.QueueCapacity != 1024 {
|
||||||
t.Fatalf("composed backend was changed: %+v", composed)
|
t.Fatalf("resolved backend was not normalized: %+v", composed)
|
||||||
}
|
}
|
||||||
if loaded.backends["local"]["queue_capacity"] != fieldNull {
|
if loaded.backends["local"]["queue_capacity"] != fieldNull {
|
||||||
t.Fatalf("explicit null was not retained: %+v", loaded.backends["local"])
|
t.Fatalf("explicit null was not retained: %+v", loaded.backends["local"])
|
||||||
}
|
}
|
||||||
field, found := loaded.result.Metadata().Lookup(configkit.Keys("backends").Index(0).Key("endpoint"))
|
field, found := loaded.result.Metadata().Lookup(configkit.Keys("backends").Index(0).Key("endpoint"))
|
||||||
if !found || field.Origin.Operation == configkit.ChangeNormalization || len(field.Origin.Locations) == 0 {
|
if !found || field.Origin.Operation != configkit.ChangeNormalization {
|
||||||
t.Fatalf("endpoint origin was not preserved: %+v", field)
|
t.Fatalf("endpoint origin was not preserved: %+v", field)
|
||||||
}
|
}
|
||||||
|
field, found = loaded.result.Metadata().Lookup(configkit.Keys("backends").Index(0).Key("queue_capacity"))
|
||||||
|
if !found || field.Origin.Operation != configkit.ChangeDerivedDefault {
|
||||||
|
t.Fatalf("queue capacity default was not recorded: %+v", field)
|
||||||
|
}
|
||||||
|
field, found = loaded.result.Metadata().Lookup(configkit.Keys("backends").Index(0).Key("concurrency_limit"))
|
||||||
|
if !found || field.Origin.Operation == configkit.ChangeNormalization || field.Origin.Operation == configkit.ChangeDerivedDefault {
|
||||||
|
t.Fatalf("unchanged concurrency acquired a resolution record: %+v", field)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadBackendSmokeTestsOmissionAndNull(t *testing.T) {
|
func TestLoadBackendSmokeTestsOmissionAndNull(t *testing.T) {
|
||||||
@@ -711,7 +763,7 @@ func TestLoadDefaultsAbsentQueueCapacity(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
backend, err := loaded.GetBackend("local")
|
backend, err := loaded.GetBackend("local")
|
||||||
if err != nil || backend.APIKeyEnv != "" || backend.QueueCapacity != 1024 || loaded.result.Config.Backends[0].QueueCapacity != nil || loaded.backends["local"]["queue_capacity"] != fieldAbsent {
|
if err != nil || backend.APIKeyEnv != "" || backend.QueueCapacity != 1024 || loaded.result.Config.Backends[0].QueueCapacity == nil || *loaded.result.Config.Backends[0].QueueCapacity != 1024 || loaded.backends["local"]["queue_capacity"] != fieldAbsent {
|
||||||
t.Fatalf("absent queue capacity did not receive its default: %+v", backend)
|
t.Fatalf("absent queue capacity did not receive its default: %+v", backend)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,44 +28,6 @@ type promptEntry struct {
|
|||||||
messages []*template.Template
|
messages []*template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func interpretPrompts(source []promptConfig) []promptConfig {
|
|
||||||
if source == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
prompts := append([]promptConfig{}, source...)
|
|
||||||
for index := range prompts {
|
|
||||||
definition := &prompts[index]
|
|
||||||
for _, value := range []**string{&definition.Description, &definition.SessionID} {
|
|
||||||
if *value != nil {
|
|
||||||
normalized := strings.TrimSpace(**value)
|
|
||||||
*value = &normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if definition.Inputs != nil {
|
|
||||||
definition.Inputs = append([]promptInputConfig{}, definition.Inputs...)
|
|
||||||
for index := range definition.Inputs {
|
|
||||||
input := &definition.Inputs[index]
|
|
||||||
input.Name = strings.TrimSpace(input.Name)
|
|
||||||
for _, value := range []**string{&input.ContentType, &input.Description} {
|
|
||||||
if *value != nil {
|
|
||||||
normalized := strings.TrimSpace(**value)
|
|
||||||
*value = &normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if definition.Output != nil {
|
|
||||||
output := *definition.Output
|
|
||||||
definition.Output = &output
|
|
||||||
if output.SchemaPath != nil {
|
|
||||||
normalized := strings.TrimSpace(*output.SchemaPath)
|
|
||||||
output.SchemaPath = &normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return prompts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (state *loaded) materializePrompts() error {
|
func (state *loaded) materializePrompts() error {
|
||||||
state.prompts = make(map[string]*promptEntry)
|
state.prompts = make(map[string]*promptEntry)
|
||||||
var metadata *configkit.Metadata
|
var metadata *configkit.Metadata
|
||||||
@@ -76,7 +38,7 @@ func (state *loaded) materializePrompts() error {
|
|||||||
}
|
}
|
||||||
return metadata.Lookup(path)
|
return metadata.Lookup(path)
|
||||||
}
|
}
|
||||||
for _, resources := range resourceSets(&state.interpreted) {
|
for _, resources := range resourceSets(&state.result.Config) {
|
||||||
for index, raw := range resources.prompts {
|
for index, raw := range resources.prompts {
|
||||||
promptBase := resources.bases.prompts.Index(index)
|
promptBase := resources.bases.prompts.Index(index)
|
||||||
if strings.TrimSpace(raw.ID) == "" {
|
if strings.TrimSpace(raw.ID) == "" {
|
||||||
@@ -152,8 +114,7 @@ func (state *loaded) materializePrompt(id string, raw promptConfig, base configk
|
|||||||
if message.Role == nil {
|
if message.Role == nil {
|
||||||
return nil, fmt.Errorf("%s: role is required", path.Key("role"))
|
return nil, fmt.Errorf("%s: role is required", path.Key("role"))
|
||||||
}
|
}
|
||||||
role, err := domain.NormalizeMessageRole(*message.Role)
|
if _, err := domain.NormalizeMessageRole(*message.Role); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", path.Key("role"), err)
|
return nil, fmt.Errorf("%s: %w", path.Key("role"), err)
|
||||||
}
|
}
|
||||||
if (message.Content != nil) == (message.ContentFile != nil) {
|
if (message.Content != nil) == (message.ContentFile != nil) {
|
||||||
@@ -178,9 +139,7 @@ func (state *loaded) materializePrompt(id string, raw promptConfig, base configk
|
|||||||
if message.CacheControl.TTL != nil {
|
if message.CacheControl.TTL != nil {
|
||||||
control.TTL = *message.CacheControl.TTL
|
control.TTL = *message.CacheControl.TTL
|
||||||
}
|
}
|
||||||
var err error
|
if _, err := domain.NormalizeCacheControl(control); err != nil {
|
||||||
control, err = domain.NormalizeCacheControl(control)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", path.Key("cache_control"), err)
|
return nil, fmt.Errorf("%s: %w", path.Key("cache_control"), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +149,7 @@ func (state *loaded) materializePrompt(id string, raw promptConfig, base configk
|
|||||||
}
|
}
|
||||||
entry.messages = append(entry.messages, compiled)
|
entry.messages = append(entry.messages, compiled)
|
||||||
definition.Templates = append(definition.Templates, domain.PromptMessageTemplate{
|
definition.Templates = append(definition.Templates, domain.PromptMessageTemplate{
|
||||||
Role: role, Content: content, CacheControl: control,
|
Role: *message.Role, Content: content, CacheControl: control,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if raw.Output == nil {
|
if raw.Output == nil {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
func TestLoadCapturesInheritedImportedContent(t *testing.T) {
|
func TestLoadCapturesInheritedImportedContent(t *testing.T) {
|
||||||
files := fstest.MapFS{
|
files := fstest.MapFS{
|
||||||
"app/main.yaml": {Data: []byte("config:\n imports:\n - path: ../catalog/base.yaml\n required: true\ncollections:\n docs:\n prompts:\n - id: child\n extends: {id: base}\n other:\n prompts:\n - id: child\n messages:\n - role: user\n content_file: ./body.md\n output:\n format: text\n validation_mode: none\n")},
|
"app/main.yaml": {Data: []byte("config:\n imports:\n - path: ../catalog/base.yaml\n required: true\ncollections:\n docs:\n prompts:\n - id: child\n extends: {id: base}\n description: ' description '\n other:\n prompts:\n - id: child\n messages:\n - role: user\n content_file: ./body.md\n output:\n format: text\n validation_mode: none\n")},
|
||||||
"app/body.md": {Data: []byte("Replacement {{.name}}")},
|
"app/body.md": {Data: []byte("Replacement {{.name}}")},
|
||||||
"catalog/base.yaml": {Data: []byte("collections:\n docs:\n release_version: r1\n prompts:\n - id: base\n description: ' description '\n session_id: ' session '\n inputs:\n - name: ' document '\n content_type: ' text/plain '\n description: ' document source '\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")},
|
"catalog/base.yaml": {Data: []byte("collections:\n docs:\n release_version: r1\n prompts:\n - id: base\n description: ' description '\n session_id: ' session '\n inputs:\n - name: ' document '\n content_type: ' text/plain '\n description: ' document source '\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")},
|
||||||
"catalog/body.md": {Data: []byte("Captured {{.name}}")},
|
"catalog/body.md": {Data: []byte("Captured {{.name}}")},
|
||||||
@@ -60,9 +60,9 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
|
|||||||
t.Fatal("inherited prompt missing from effective list")
|
t.Fatal("inherited prompt missing from effective list")
|
||||||
}
|
}
|
||||||
composed := state.result.Config.Collections["docs"].Prompts[childIndex]
|
composed := state.result.Config.Collections["docs"].Prompts[childIndex]
|
||||||
if *composed.Description != " description " || *composed.SessionID != " session " || composed.Inputs[0].Name != " document " ||
|
if *composed.Description != "description" || *composed.SessionID != "session" || composed.Inputs[0].Name != "document" ||
|
||||||
*composed.Inputs[0].ContentType != " text/plain " || *composed.Inputs[0].Description != " document source " {
|
*composed.Inputs[0].ContentType != "text/plain" || *composed.Inputs[0].Description != "document source" {
|
||||||
t.Fatalf("composed metadata was changed: %+v", composed)
|
t.Fatalf("resolved metadata was not normalized: %+v", composed)
|
||||||
}
|
}
|
||||||
field, ok := state.result.Metadata().Lookup(configkit.Keys("collections", "docs", "prompts").Index(childIndex).Key("messages").Index(0).Key("content_file"))
|
field, ok := state.result.Metadata().Lookup(configkit.Keys("collections", "docs", "prompts").Index(childIndex).Key("messages").Index(0).Key("content_file"))
|
||||||
if !ok || len(field.Origin.Locations) != 1 {
|
if !ok || len(field.Origin.Locations) != 1 {
|
||||||
|
|||||||
68
internal/configuration/resolve_prompts.go
Normal file
68
internal/configuration/resolve_prompts.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolvePrompts(entries []promptConfig, base configkit.Path, resolution *configkit.Resolution) {
|
||||||
|
if !eligibleResourceList(entries, func(v promptConfig) string { return v.ID }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range entries {
|
||||||
|
entry := &entries[i]
|
||||||
|
path := base.Index(i)
|
||||||
|
for _, field := range []struct {
|
||||||
|
value **string
|
||||||
|
name string
|
||||||
|
}{{&entry.Description, "description"}, {&entry.SessionID, "session_id"}} {
|
||||||
|
if *field.value != nil {
|
||||||
|
recordStringNormalization(field.value, path.Key(field.name), resolution, strings.TrimSpace(**field.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for j := range entry.Inputs {
|
||||||
|
input := &entry.Inputs[j]
|
||||||
|
inputPath := path.Key("inputs").Index(j)
|
||||||
|
if normalized := strings.TrimSpace(input.Name); normalized != input.Name {
|
||||||
|
configkit.Normalize(resolution, inputPath.Key("name"), &input.Name, normalized, "Normalize input name")
|
||||||
|
}
|
||||||
|
for _, field := range []struct {
|
||||||
|
value **string
|
||||||
|
name string
|
||||||
|
}{{&input.ContentType, "content_type"}, {&input.Description, "description"}} {
|
||||||
|
if *field.value != nil {
|
||||||
|
recordStringNormalization(field.value, inputPath.Key(field.name), resolution, strings.TrimSpace(**field.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if entry.Output != nil && entry.Output.SchemaPath != nil {
|
||||||
|
recordStringNormalization(&entry.Output.SchemaPath, path.Key("output").Key("schema_path"), resolution, strings.TrimSpace(*entry.Output.SchemaPath))
|
||||||
|
}
|
||||||
|
for j := range entry.Messages {
|
||||||
|
message := &entry.Messages[j]
|
||||||
|
messagePath := path.Key("messages").Index(j)
|
||||||
|
if message.Role != nil {
|
||||||
|
if value, err := domain.NormalizeMessageRole(*message.Role); err == nil {
|
||||||
|
recordStringNormalization(&message.Role, messagePath.Key("role"), resolution, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if message.CacheControl == nil || message.CacheControl.Type == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cache := message.CacheControl
|
||||||
|
control := &domain.CacheControl{Type: domain.CacheControlType(*cache.Type)}
|
||||||
|
if cache.TTL != nil {
|
||||||
|
control.TTL = *cache.TTL
|
||||||
|
}
|
||||||
|
if normalized, err := domain.NormalizeCacheControl(control); err == nil {
|
||||||
|
cachePath := messagePath.Key("cache_control")
|
||||||
|
recordStringNormalization(&cache.Type, cachePath.Key("type"), resolution, string(normalized.Type))
|
||||||
|
if cache.TTL != nil {
|
||||||
|
recordStringNormalization(&cache.TTL, cachePath.Key("ttl"), resolution, normalized.TTL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
internal/configuration/resolve_targets.go
Normal file
102
internal/configuration/resolve_targets.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit"
|
||||||
|
"go.maximumdirect.net/promptkit/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolveBackends(entries []backendConfig, base configkit.Path, resolution *configkit.Resolution) {
|
||||||
|
if !eligibleResourceList(entries, func(v backendConfig) string { return v.ID }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range entries {
|
||||||
|
entry := &entries[i]
|
||||||
|
path := base.Index(i)
|
||||||
|
if entry.Endpoint != nil {
|
||||||
|
if normalized, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*entry.Endpoint); err == nil {
|
||||||
|
recordStringNormalization(&entry.Endpoint, path.Key("endpoint"), resolution, normalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if entry.APIKeyEnv != nil {
|
||||||
|
recordStringNormalization(&entry.APIKeyEnv, path.Key("api_key_env"), resolution, strings.TrimSpace(*entry.APIKeyEnv))
|
||||||
|
}
|
||||||
|
if entry.ConcurrencyLimit != nil && *entry.ConcurrencyLimit > 0 && entry.QueueCapacity == nil {
|
||||||
|
capacity := 1024
|
||||||
|
entry.QueueCapacity = &capacity
|
||||||
|
resolution.RecordDerivedDefault(path.Key("queue_capacity"), "Derive queue capacity", path.Key("concurrency_limit"))
|
||||||
|
}
|
||||||
|
if entry.SmokeTests == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
smoke := entry.SmokeTests
|
||||||
|
smokePath := path.Key("smoke_tests")
|
||||||
|
if smoke.Reachability != nil {
|
||||||
|
resolveDiagnosticEndpoint(&smoke.Reachability.Endpoint, smokePath.Key("reachability").Key("endpoint"), resolution)
|
||||||
|
}
|
||||||
|
if smoke.Authentication != nil {
|
||||||
|
auth := smoke.Authentication
|
||||||
|
authPath := smokePath.Key("authentication")
|
||||||
|
if auth.Kind != nil {
|
||||||
|
recordStringNormalization(&auth.Kind, authPath.Key("kind"), resolution, strings.ToLower(strings.TrimSpace(*auth.Kind)))
|
||||||
|
}
|
||||||
|
resolveDiagnosticEndpoint(&auth.Endpoint, authPath.Key("endpoint"), resolution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func diagnosticEndpointValid(value string) bool {
|
||||||
|
normalized := strings.TrimSpace(value)
|
||||||
|
request, err := http.NewRequest(http.MethodGet, normalized, nil)
|
||||||
|
return normalized != "" && err == nil && request.URL != nil && (strings.EqualFold(request.URL.Scheme, "http") || strings.EqualFold(request.URL.Scheme, "https")) && request.URL.Hostname() != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateBackendEndpoints(entry backendConfig, path configkit.Path, validation *configkit.Validation) {
|
||||||
|
if entry.Endpoint != nil {
|
||||||
|
if _, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*entry.Endpoint); err != nil {
|
||||||
|
validation.Error(path.Key("endpoint"), err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if entry.SmokeTests == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
smoke := entry.SmokeTests
|
||||||
|
path = path.Key("smoke_tests")
|
||||||
|
if smoke.Reachability != nil && smoke.Reachability.Endpoint != nil && !diagnosticEndpointValid(*smoke.Reachability.Endpoint) {
|
||||||
|
validation.Error(path.Key("reachability").Key("endpoint"), "diagnostic endpoint must be an absolute HTTP(S) URL with a host")
|
||||||
|
}
|
||||||
|
if smoke.Authentication != nil && smoke.Authentication.Endpoint != nil && !diagnosticEndpointValid(*smoke.Authentication.Endpoint) {
|
||||||
|
validation.Error(path.Key("authentication").Key("endpoint"), "diagnostic endpoint must be an absolute HTTP(S) URL with a host")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDiagnosticEndpoint(value **string, path configkit.Path, resolution *configkit.Resolution) {
|
||||||
|
if *value != nil && diagnosticEndpointValid(**value) {
|
||||||
|
recordStringNormalization(value, path, resolution, strings.TrimSpace(**value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveProfiles(entries []profileConfig, base configkit.Path, resolution *configkit.Resolution) {
|
||||||
|
if !eligibleResourceList(entries, func(v profileConfig) string { return v.ID }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range entries {
|
||||||
|
entry := &entries[i]
|
||||||
|
path := base.Index(i)
|
||||||
|
if entry.Endpoint != nil && *entry.Endpoint != "" {
|
||||||
|
if normalized, err := domain.NormalizeOpenAICompatibleBaseEndpoint(*entry.Endpoint); err == nil {
|
||||||
|
recordStringNormalization(&entry.Endpoint, path.Key("endpoint"), resolution, normalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, field := range []struct {
|
||||||
|
value **string
|
||||||
|
name string
|
||||||
|
}{{&entry.ServiceTier, "service_tier"}, {&entry.ReasoningEffort, "reasoning_effort"}, {&entry.APIKeyEnv, "api_key_env"}} {
|
||||||
|
if *field.value != nil {
|
||||||
|
recordStringNormalization(field.value, path.Key(field.name), resolution, strings.TrimSpace(**field.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (state *loaded) buildExecutionProfiles() error {
|
func (state *loaded) buildExecutionProfiles() error {
|
||||||
backendConfigs := indexedBackends(&state.interpreted)
|
backendConfigs := indexedBackends(&state.result.Config)
|
||||||
profileConfigs := indexedProfiles(&state.interpreted)
|
profileConfigs := indexedProfiles(&state.result.Config)
|
||||||
definitions := make(map[string]domain.Backend, len(backendConfigs))
|
definitions := make(map[string]domain.Backend, len(backendConfigs))
|
||||||
for _, id := range sortedKeys(backendConfigs) {
|
for _, id := range sortedKeys(backendConfigs) {
|
||||||
config := backendConfigs[id]
|
config := backendConfigs[id]
|
||||||
|
|||||||
@@ -41,13 +41,17 @@ type ConfigurationSources struct {
|
|||||||
|
|
||||||
// Options collects construction choices from the public facade.
|
// Options collects construction choices from the public facade.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
llmClient llm.Client
|
llmClient llm.Client
|
||||||
artifactReader artifact.Reader
|
artifactReader artifact.Reader
|
||||||
validator validate.Validator
|
validator validate.Validator
|
||||||
validatorSource bool
|
validatorSource bool
|
||||||
artifactSource bool
|
artifactSource bool
|
||||||
|
disableBundledOpenRouter bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetBundledOpenRouter selects whether the built-in OpenRouter source is installed.
|
||||||
|
func (o *Options) SetBundledOpenRouter(enabled bool) { o.disableBundledOpenRouter = !enabled }
|
||||||
|
|
||||||
// SetLLMClient selects the client collaborator for one engine.
|
// SetLLMClient selects the client collaborator for one engine.
|
||||||
func (o *Options) SetLLMClient(client llm.Client) { o.llmClient = client }
|
func (o *Options) SetLLMClient(client llm.Client) { o.llmClient = client }
|
||||||
|
|
||||||
@@ -124,6 +128,11 @@ func New(cfg Config, options Options) (*Engine, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
underlays := cfg.Sources.Underlays
|
underlays := cfg.Sources.Underlays
|
||||||
|
if !options.disableBundledOpenRouter {
|
||||||
|
underlays = make([]configkit.Underlay, 1, len(cfg.Sources.Underlays)+1)
|
||||||
|
underlays[0] = configuration.BundledOpenRouterUnderlay()
|
||||||
|
underlays = append(underlays, cfg.Sources.Underlays...)
|
||||||
|
}
|
||||||
main := configkit.Input{Source: configkit.NoSource()}
|
main := configkit.Input{Source: configkit.NoSource()}
|
||||||
if cfg.Sources.Main != nil {
|
if cfg.Sources.Main != nil {
|
||||||
main = *cfg.Sources.Main
|
main = *cfg.Sources.Main
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ func newNamespaceListingEngine(t *testing.T) *promptkit.Engine {
|
|||||||
"profiles": []any{map[string]any{"id": "root-profile", "backend": "root-backend", "model": "root-model"}},
|
"profiles": []any{map[string]any{"id": "root-profile", "backend": "root-backend", "model": "root-model"}},
|
||||||
"prompts": []any{listingContractPrompt("root-prompt")},
|
"prompts": []any{listingContractPrompt("root-prompt")},
|
||||||
})}
|
})}
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}})
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}, promptkit.WithBundledOpenRouter(false))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var loadErr *configkit.LoadError
|
var loadErr *configkit.LoadError
|
||||||
if errors.As(err, &loadErr) {
|
if errors.As(err, &loadErr) {
|
||||||
@@ -464,7 +464,7 @@ func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestListIdentitiesEmptyEffectiveConfiguration(t *testing.T) {
|
func TestListIdentitiesEmptyEffectiveConfiguration(t *testing.T) {
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{})
|
engine, err := promptkit.NewEngine(promptkit.Config{}, promptkit.WithBundledOpenRouter(false))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestProgrammaticSourcesPreserveOrderAndRejectDuplicates(t *testing.T) {
|
|||||||
params["visible"] = "changed"
|
params["visible"] = "changed"
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: backendSource}, {Source: profileSource}}}},
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: backendSource}, {Source: profileSource}}}},
|
||||||
}})
|
}}, promptkit.WithBundledOpenRouter(false))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user