Load backend smoke-test metadata
This commit is contained in:
66
backends.go
66
backends.go
@@ -17,8 +17,9 @@ const BackendLocal = "local"
|
|||||||
// BackendConfig is a presence-aware convenience value for a keyed backend
|
// BackendConfig is a presence-aware convenience value for a keyed backend
|
||||||
// definition. Nil pointer fields are omitted, while non-nil pointers preserve
|
// definition. Nil pointer fields are omitted, while non-nil pointers preserve
|
||||||
// explicit zero or blank values. A nil ExtraParams map is omitted; a non-nil
|
// explicit zero or blank values. A nil ExtraParams map is omitted; a non-nil
|
||||||
// empty map supplies an empty mapping. Use raw ConfigKit Mapping inputs for
|
// empty map supplies an empty mapping. A nil SmokeTests pointer is omitted; a
|
||||||
// explicit null. BackendConfig has no stable JSON representation.
|
// non-nil pointer supplies a smoke_tests mapping. Use raw ConfigKit Mapping
|
||||||
|
// inputs for explicit null. BackendConfig has no stable JSON representation.
|
||||||
type BackendConfig struct {
|
type BackendConfig struct {
|
||||||
// Endpoint is the required absolute HTTP(S) base URL after all layers merge.
|
// Endpoint is the required absolute HTTP(S) base URL after all layers merge.
|
||||||
Endpoint *string
|
Endpoint *string
|
||||||
@@ -31,6 +32,43 @@ type BackendConfig struct {
|
|||||||
QueueCapacity *int
|
QueueCapacity *int
|
||||||
// ExtraParams supplies JSON-compatible provider request defaults.
|
// ExtraParams supplies JSON-compatible provider request defaults.
|
||||||
ExtraParams map[string]any
|
ExtraParams map[string]any
|
||||||
|
// SmokeTests supplies optional diagnostic defaults and HTTP probe settings.
|
||||||
|
SmokeTests *BackendSmokeTestConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackendSmokeTestConfig contains optional diagnostic settings associated with
|
||||||
|
// a backend. Nil nested fields are omitted by BackendSource.
|
||||||
|
type BackendSmokeTestConfig struct {
|
||||||
|
// DefaultProfile is the exact profile ID selected as the backend's
|
||||||
|
// diagnostic inference default. A supplied value must not be blank; the ID
|
||||||
|
// is resolved only when that default is selected.
|
||||||
|
DefaultProfile *string
|
||||||
|
// Reachability optionally selects a full HTTP destination for reachability
|
||||||
|
// diagnostics. A nil endpoint uses the effective backend endpoint.
|
||||||
|
Reachability *ReachabilityProbeConfig
|
||||||
|
// Authentication optionally declares a named authentication diagnostic.
|
||||||
|
// A supplied object requires Kind.
|
||||||
|
Authentication *AuthenticationProbeConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReachabilityProbeConfig configures an optional reachability destination.
|
||||||
|
type ReachabilityProbeConfig struct {
|
||||||
|
// Endpoint is an absolute HTTP(S) URL, including any desired path or query.
|
||||||
|
// Nil selects the effective backend endpoint; an explicit blank value is
|
||||||
|
// invalid.
|
||||||
|
Endpoint *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticationProbeConfig configures an optional named authentication
|
||||||
|
// diagnostic.
|
||||||
|
type AuthenticationProbeConfig struct {
|
||||||
|
// Kind selects the authentication adapter. Supported values are "none" and
|
||||||
|
// "openrouter_key"; surrounding whitespace and case are normalized.
|
||||||
|
Kind *string
|
||||||
|
// Endpoint is an absolute HTTP(S) URL, including any desired path or query.
|
||||||
|
// Nil selects the adapter's backend-relative destination; an explicit blank
|
||||||
|
// value is invalid.
|
||||||
|
Endpoint *string
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackendSource creates a labeled ConfigKit mapping from keyed backend values.
|
// BackendSource creates a labeled ConfigKit mapping from keyed backend values.
|
||||||
@@ -57,6 +95,30 @@ func BackendSource(label string, backends map[string]BackendConfig) configkit.So
|
|||||||
if config.ExtraParams != nil {
|
if config.ExtraParams != nil {
|
||||||
fields["extra_params"] = providerMapForMapping(config.ExtraParams)
|
fields["extra_params"] = providerMapForMapping(config.ExtraParams)
|
||||||
}
|
}
|
||||||
|
if config.SmokeTests != nil {
|
||||||
|
smokeTests := make(map[string]any)
|
||||||
|
if config.SmokeTests.DefaultProfile != nil {
|
||||||
|
smokeTests["default_profile"] = *config.SmokeTests.DefaultProfile
|
||||||
|
}
|
||||||
|
if config.SmokeTests.Reachability != nil {
|
||||||
|
reachability := make(map[string]any)
|
||||||
|
if config.SmokeTests.Reachability.Endpoint != nil {
|
||||||
|
reachability["endpoint"] = *config.SmokeTests.Reachability.Endpoint
|
||||||
|
}
|
||||||
|
smokeTests["reachability"] = reachability
|
||||||
|
}
|
||||||
|
if config.SmokeTests.Authentication != nil {
|
||||||
|
authentication := make(map[string]any)
|
||||||
|
if config.SmokeTests.Authentication.Kind != nil {
|
||||||
|
authentication["kind"] = *config.SmokeTests.Authentication.Kind
|
||||||
|
}
|
||||||
|
if config.SmokeTests.Authentication.Endpoint != nil {
|
||||||
|
authentication["endpoint"] = *config.SmokeTests.Authentication.Endpoint
|
||||||
|
}
|
||||||
|
smokeTests["authentication"] = authentication
|
||||||
|
}
|
||||||
|
fields["smoke_tests"] = smokeTests
|
||||||
|
}
|
||||||
definitions[id] = fields
|
definitions[id] = fields
|
||||||
}
|
}
|
||||||
return configkit.Mapping(label, map[string]any{"backends": definitions})
|
return configkit.Mapping(label, map[string]any{"backends": definitions})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package promptkit_test
|
package promptkit_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
@@ -208,6 +209,66 @@ func TestPublicConstructionCanOverlayMaintainedBackend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackendSourceOwnsSmokeTestValuesAndDefersDefaultLookup(t *testing.T) {
|
||||||
|
backendEndpoint := "https://backend.example/v1"
|
||||||
|
defaultProfile := "not-yet-listed"
|
||||||
|
reachabilityEndpoint := " https://probe.example/ready?check=1 "
|
||||||
|
authenticationKind := " OPENROUTER_KEY "
|
||||||
|
authenticationEndpoint := "https://probe.example/key?scope=current"
|
||||||
|
source := promptkit.BackendSource("diagnostic backend", map[string]promptkit.BackendConfig{
|
||||||
|
"local": {
|
||||||
|
Endpoint: &backendEndpoint,
|
||||||
|
SmokeTests: &promptkit.BackendSmokeTestConfig{
|
||||||
|
DefaultProfile: &defaultProfile,
|
||||||
|
Reachability: &promptkit.ReachabilityProbeConfig{Endpoint: &reachabilityEndpoint},
|
||||||
|
Authentication: &promptkit.AuthenticationProbeConfig{
|
||||||
|
Kind: &authenticationKind, Endpoint: &authenticationEndpoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
backendEndpoint = "https://mutated.example/v1"
|
||||||
|
defaultProfile = "mutated-profile"
|
||||||
|
reachabilityEndpoint = "https://mutated.example/ready"
|
||||||
|
authenticationKind = "none"
|
||||||
|
authenticationEndpoint = "https://mutated.example/key"
|
||||||
|
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: source}}}},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine with dangling diagnostic default: %v", err)
|
||||||
|
}
|
||||||
|
inspection, err := engine.InspectConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var effective bytes.Buffer
|
||||||
|
if err := inspection.WriteEffective(&effective); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
contents := effective.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
"https://backend.example/v1",
|
||||||
|
"not-yet-listed",
|
||||||
|
"https://probe.example/ready?check=1",
|
||||||
|
"openrouter_key",
|
||||||
|
"https://probe.example/key?scope=current",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(contents, want) {
|
||||||
|
t.Errorf("effective configuration does not contain %q:\n%s", want, contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, unwanted := range []string{
|
||||||
|
"mutated.example",
|
||||||
|
"mutated-profile",
|
||||||
|
} {
|
||||||
|
if strings.Contains(contents, unwanted) {
|
||||||
|
t.Errorf("effective configuration unexpectedly contains %q:\n%s", unwanted, contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPreparationRejectsAmbiguousPromptVersion(t *testing.T) {
|
func TestPreparationRejectsAmbiguousPromptVersion(t *testing.T) {
|
||||||
definition := func(content string) map[string]any {
|
definition := func(content string) map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
|
|||||||
@@ -168,16 +168,46 @@ are not reserved against configuration changes.
|
|||||||
| `rakestrawhome` | `https://inference.ai.rakestrawhome.com/v1` | `RAKESTRAWHOME_INFERENCE_API_KEY` | 4 |
|
| `rakestrawhome` | `https://inference.ai.rakestrawhome.com/v1` | `RAKESTRAWHOME_INFERENCE_API_KEY` | 4 |
|
||||||
|
|
||||||
A backend definition may have `endpoint`, `api_key_env`,
|
A backend definition may have `endpoint`, `api_key_env`,
|
||||||
`concurrency_limit`, `queue_capacity`, and `extra_params`. The final endpoint
|
`concurrency_limit`, `queue_capacity`, `extra_params`, and `smoke_tests`. The
|
||||||
is required and must be an absolute HTTP(S) URL with a host, no user
|
final endpoint is required and must be an absolute HTTP(S) URL with a host, no
|
||||||
information, query, or fragment. An omitted or null API-key environment name
|
user information, query, or fragment. An omitted or null API-key environment
|
||||||
means no lookup. Omitted/null concurrency is zero (unlimited); a supplied
|
name means no lookup. Omitted/null concurrency is zero (unlimited); a supplied
|
||||||
negative value is invalid. For positive concurrency, omitted/null queue
|
negative value is invalid. For positive concurrency, omitted/null queue
|
||||||
capacity defaults to 1024. An explicit queue capacity, including zero,
|
capacity defaults to 1024. An explicit queue capacity, including zero,
|
||||||
requires positive concurrency and must be nonnegative. Unlimited concurrency
|
requires positive concurrency and must be nonnegative. Unlimited concurrency
|
||||||
has no queue. Provider maps contain JSON-compatible values; null entries are
|
has no queue. Provider maps contain JSON-compatible values; null entries are
|
||||||
ordinary JSON null. Unknown fields, including a raw `api_key`, fail.
|
ordinary JSON null. Unknown fields, including a raw `api_key`, fail.
|
||||||
|
|
||||||
|
`smoke_tests` is optional backend metadata for explicit diagnostic operations.
|
||||||
|
It may contain `default_profile`, `reachability`, and `authentication`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backends:
|
||||||
|
openrouter:
|
||||||
|
smoke_tests:
|
||||||
|
default_profile: promptkit-openrouter-standard
|
||||||
|
reachability:
|
||||||
|
endpoint: https://status.example/ready?check=1
|
||||||
|
authentication:
|
||||||
|
kind: openrouter_key
|
||||||
|
endpoint: https://openrouter.ai/api/v1/key
|
||||||
|
```
|
||||||
|
|
||||||
|
An omitted or null `smoke_tests` block disables these metadata defaults.
|
||||||
|
`default_profile` is preserved as an exact, nonblank profile ID; its existence
|
||||||
|
is checked only if a diagnostic selects that default. Omitted/null
|
||||||
|
`reachability` or its endpoint uses the effective backend endpoint. An explicit
|
||||||
|
reachability endpoint is trimmed and must be an absolute HTTP(S) URL with a
|
||||||
|
host; unlike the backend base endpoint, its path and query are allowed. Null
|
||||||
|
means fallback, while an explicit blank value is invalid.
|
||||||
|
|
||||||
|
Omitted/null `authentication` means no authentication adapter is declared. A
|
||||||
|
non-null object requires `kind`, which is trimmed and case-normalized; the
|
||||||
|
recognized values are `none` and `openrouter_key`. Its omitted/null endpoint
|
||||||
|
uses the adapter's backend-relative destination; an explicit endpoint follows
|
||||||
|
the same full-URL rules as reachability. These values are configuration only:
|
||||||
|
engine construction does not contact a backend.
|
||||||
|
|
||||||
## Profile Definitions
|
## Profile Definitions
|
||||||
|
|
||||||
Profiles are keyed by literal ID and may inherit a named parent with
|
Profiles are keyed by literal ID and may inherit a named parent with
|
||||||
|
|||||||
@@ -264,6 +264,10 @@ Validate: `go test ./internal/configuration ./internal/backend` and focused root
|
|||||||
backend-source tests. Update backend-field GoDoc and `docs/formats.md` as these
|
backend-source tests. Update backend-field GoDoc and `docs/formats.md` as these
|
||||||
configuration fields land; runtime smoke-test guidance waits for Stage 18.
|
configuration fields land; runtime smoke-test guidance waits for Stage 18.
|
||||||
|
|
||||||
|
**Completed: 2026-10-13.** Validation passed:
|
||||||
|
`go test ./internal/configuration ./internal/backend`, `go test .`, and
|
||||||
|
`git diff --check`.
|
||||||
|
|
||||||
## Stage 3: Resolve Diagnostic Targets Without Preparing Applications
|
## Stage 3: Resolve Diagnostic Targets Without Preparing Applications
|
||||||
|
|
||||||
Scope: selection helpers in `internal/usecase`; no transport.
|
Scope: selection helpers in `internal/usecase`; no transport.
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/jsonvalue"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -31,13 +30,12 @@ type Registry struct {
|
|||||||
func NewEffectiveRegistry(definitions map[string]domain.Backend) (*Registry, error) {
|
func NewEffectiveRegistry(definitions map[string]domain.Backend) (*Registry, error) {
|
||||||
registry := &Registry{backends: make(map[string]domain.Backend, len(definitions))}
|
registry := &Registry{backends: make(map[string]domain.Backend, len(definitions))}
|
||||||
for id, definition := range definitions {
|
for id, definition := range definitions {
|
||||||
params, err := jsonvalue.CopyMap(definition.ExtraParams)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("backend %q extra parameters: %w", id, err)
|
|
||||||
}
|
|
||||||
definition.ID = id
|
definition.ID = id
|
||||||
definition.ExtraParams = params
|
cloned, err := domain.CloneBackend(definition)
|
||||||
registry.backends[id] = definition
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("backend %q: %w", id, err)
|
||||||
|
}
|
||||||
|
registry.backends[id] = cloned
|
||||||
}
|
}
|
||||||
return registry, nil
|
return registry, nil
|
||||||
}
|
}
|
||||||
@@ -51,12 +49,11 @@ func (r *Registry) GetBackend(id string) (domain.Backend, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return domain.Backend{}, fmt.Errorf("%w: %q", ErrBackendNotFound, id)
|
return domain.Backend{}, fmt.Errorf("%w: %q", ErrBackendNotFound, id)
|
||||||
}
|
}
|
||||||
extraParams, err := jsonvalue.CopyMap(definition.ExtraParams)
|
cloned, err := domain.CloneBackend(definition)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Backend{}, fmt.Errorf("copy backend %q: %w", id, err)
|
return domain.Backend{}, fmt.Errorf("copy backend %q: %w", id, err)
|
||||||
}
|
}
|
||||||
definition.ExtraParams = extraParams
|
return cloned, nil
|
||||||
return definition, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CapacityPolicies returns a copy of the normalized policies for limited
|
// CapacityPolicies returns a copy of the normalized policies for limited
|
||||||
|
|||||||
@@ -11,25 +11,61 @@ import (
|
|||||||
|
|
||||||
func TestEffectiveRegistryOwnsDefinitionsAndCapacityPolicies(t *testing.T) {
|
func TestEffectiveRegistryOwnsDefinitionsAndCapacityPolicies(t *testing.T) {
|
||||||
params := map[string]any{"nested": map[string]any{"enabled": true}}
|
params := map[string]any{"nested": map[string]any{"enabled": true}}
|
||||||
|
defaultProfile := "catalog-profile"
|
||||||
|
reachabilityEndpoint := "https://probe.example/ready"
|
||||||
|
authenticationKind := "openrouter_key"
|
||||||
|
authenticationEndpoint := "https://probe.example/key"
|
||||||
|
smokeTests := &domain.BackendSmokeTestConfig{
|
||||||
|
DefaultProfile: &defaultProfile,
|
||||||
|
Reachability: &domain.ReachabilityProbeConfig{Endpoint: &reachabilityEndpoint},
|
||||||
|
Authentication: &domain.AuthenticationProbeConfig{
|
||||||
|
Kind: &authenticationKind, Endpoint: &authenticationEndpoint,
|
||||||
|
},
|
||||||
|
}
|
||||||
registry, err := backend.NewEffectiveRegistry(map[string]domain.Backend{
|
registry, err := backend.NewEffectiveRegistry(map[string]domain.Backend{
|
||||||
" local.v1 ": {
|
" local.v1 ": {
|
||||||
Endpoint: "https://backend.example/v1", ExtraParams: params,
|
Endpoint: "https://backend.example/v1", ExtraParams: params,
|
||||||
ConcurrencyLimit: 2, QueueCapacity: 3, QueueCapacitySet: true,
|
ConcurrencyLimit: 2, QueueCapacity: 3, QueueCapacitySet: true,
|
||||||
|
SmokeTests: smokeTests,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
params["nested"].(map[string]any)["enabled"] = false
|
params["nested"].(map[string]any)["enabled"] = false
|
||||||
|
defaultProfile = "mutated-profile"
|
||||||
|
reachabilityEndpoint = "https://mutated.example/ready"
|
||||||
|
authenticationKind = "none"
|
||||||
|
authenticationEndpoint = "https://mutated.example/key"
|
||||||
selected, err := registry.GetBackend(" local.v1 ")
|
selected, err := registry.GetBackend(" local.v1 ")
|
||||||
if err != nil || selected.ID != " local.v1 " || selected.ExtraParams["nested"].(map[string]any)["enabled"] != true {
|
if err != nil || selected.ID != " local.v1 " || selected.ExtraParams["nested"].(map[string]any)["enabled"] != true {
|
||||||
t.Fatalf("effective backend was not captured under its literal ID: %+v, %v", selected, err)
|
t.Fatalf("effective backend was not captured under its literal ID: %+v, %v", selected, err)
|
||||||
}
|
}
|
||||||
|
if selected.SmokeTests == nil || selected.SmokeTests.DefaultProfile == nil ||
|
||||||
|
*selected.SmokeTests.DefaultProfile != "catalog-profile" || selected.SmokeTests.Reachability == nil ||
|
||||||
|
selected.SmokeTests.Reachability.Endpoint == nil || *selected.SmokeTests.Reachability.Endpoint != "https://probe.example/ready" ||
|
||||||
|
selected.SmokeTests.Authentication == nil || selected.SmokeTests.Authentication.Kind == nil ||
|
||||||
|
*selected.SmokeTests.Authentication.Kind != "openrouter_key" || selected.SmokeTests.Authentication.Endpoint == nil ||
|
||||||
|
*selected.SmokeTests.Authentication.Endpoint != "https://probe.example/key" {
|
||||||
|
t.Fatalf("registry did not capture nested diagnostic metadata: %+v", selected.SmokeTests)
|
||||||
|
}
|
||||||
selected.ExtraParams["nested"].(map[string]any)["enabled"] = false
|
selected.ExtraParams["nested"].(map[string]any)["enabled"] = false
|
||||||
|
*selected.SmokeTests.DefaultProfile = "mutated-return"
|
||||||
|
*selected.SmokeTests.Reachability.Endpoint = "https://mutated-return.example/ready"
|
||||||
|
*selected.SmokeTests.Authentication.Kind = "none"
|
||||||
|
*selected.SmokeTests.Authentication.Endpoint = "https://mutated-return.example/key"
|
||||||
again, err := registry.GetBackend(" local.v1 ")
|
again, err := registry.GetBackend(" local.v1 ")
|
||||||
if err != nil || again.ExtraParams["nested"].(map[string]any)["enabled"] != true {
|
if err != nil || again.ExtraParams["nested"].(map[string]any)["enabled"] != true {
|
||||||
t.Fatalf("lookup exposed registry values: %+v, %v", again, err)
|
t.Fatalf("lookup exposed registry values: %+v, %v", again, err)
|
||||||
}
|
}
|
||||||
|
if again.SmokeTests == nil || again.SmokeTests.DefaultProfile == nil ||
|
||||||
|
*again.SmokeTests.DefaultProfile != "catalog-profile" || again.SmokeTests.Reachability == nil ||
|
||||||
|
again.SmokeTests.Reachability.Endpoint == nil || *again.SmokeTests.Reachability.Endpoint != "https://probe.example/ready" ||
|
||||||
|
again.SmokeTests.Authentication == nil || again.SmokeTests.Authentication.Kind == nil ||
|
||||||
|
*again.SmokeTests.Authentication.Kind != "openrouter_key" || again.SmokeTests.Authentication.Endpoint == nil ||
|
||||||
|
*again.SmokeTests.Authentication.Endpoint != "https://probe.example/key" {
|
||||||
|
t.Fatalf("lookup exposed nested diagnostic metadata: %+v", again.SmokeTests)
|
||||||
|
}
|
||||||
policies := registry.CapacityPolicies()
|
policies := registry.CapacityPolicies()
|
||||||
if !reflect.DeepEqual(policies, map[string]domain.BackendCapacityPolicy{
|
if !reflect.DeepEqual(policies, map[string]domain.BackendCapacityPolicy{
|
||||||
" local.v1 ": {ConcurrencyLimit: 2, QueueCapacity: 3},
|
" local.v1 ": {ConcurrencyLimit: 2, QueueCapacity: 3},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package configuration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -27,6 +28,22 @@ type backendConfig struct {
|
|||||||
ConcurrencyLimit *int `yaml:"concurrency_limit"`
|
ConcurrencyLimit *int `yaml:"concurrency_limit"`
|
||||||
QueueCapacity *int `yaml:"queue_capacity"`
|
QueueCapacity *int `yaml:"queue_capacity"`
|
||||||
ExtraParams map[string]any `yaml:"extra_params"`
|
ExtraParams map[string]any `yaml:"extra_params"`
|
||||||
|
SmokeTests *backendSmokeTestConfig `yaml:"smoke_tests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type backendSmokeTestConfig struct {
|
||||||
|
DefaultProfile *string `yaml:"default_profile"`
|
||||||
|
Reachability *reachabilityProbeConfig `yaml:"reachability"`
|
||||||
|
Authentication *authenticationProbeConfig `yaml:"authentication"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type reachabilityProbeConfig struct {
|
||||||
|
Endpoint *string `yaml:"endpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticationProbeConfig struct {
|
||||||
|
Kind *string `yaml:"kind"`
|
||||||
|
Endpoint *string `yaml:"endpoint"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type profileConfig struct {
|
type profileConfig struct {
|
||||||
@@ -104,7 +121,7 @@ type loaded struct {
|
|||||||
|
|
||||||
var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||||
|
|
||||||
var backendFields = []string{"endpoint", "api_key_env", "concurrency_limit", "queue_capacity", "extra_params"}
|
var backendFields = []string{"endpoint", "api_key_env", "concurrency_limit", "queue_capacity", "extra_params", "smoke_tests"}
|
||||||
var profileFields = []string{"backend", "endpoint", "model", "temperature", "top_p", "max_tokens", "timeout_seconds", "service_tier", "reasoning_effort", "api_key_env", "api_key_required", "extra_params"}
|
var profileFields = []string{"backend", "endpoint", "model", "temperature", "top_p", "max_tokens", "timeout_seconds", "service_tier", "reasoning_effort", "api_key_env", "api_key_required", "extra_params"}
|
||||||
|
|
||||||
// Load captures one immutable effective configuration for internal consumers.
|
// Load captures one immutable effective configuration for internal consumers.
|
||||||
@@ -242,6 +259,21 @@ func resolveBackends(backends map[string]backendConfig, resolution *configkit.Re
|
|||||||
if backend.APIKeyEnv != nil {
|
if backend.APIKeyEnv != nil {
|
||||||
normalizeString(&backend.APIKeyEnv, base.Key("api_key_env"), resolution, strings.TrimSpace(*backend.APIKeyEnv))
|
normalizeString(&backend.APIKeyEnv, base.Key("api_key_env"), resolution, strings.TrimSpace(*backend.APIKeyEnv))
|
||||||
}
|
}
|
||||||
|
if smokeTests := backend.SmokeTests; smokeTests != nil {
|
||||||
|
smokePath := base.Key("smoke_tests")
|
||||||
|
if smokeTests.Reachability != nil {
|
||||||
|
normalizeDiagnosticEndpoint(&smokeTests.Reachability.Endpoint, smokePath.Key("reachability").Key("endpoint"), resolution)
|
||||||
|
}
|
||||||
|
if smokeTests.Authentication != nil {
|
||||||
|
if smokeTests.Authentication.Kind != nil {
|
||||||
|
normalizeString(&smokeTests.Authentication.Kind,
|
||||||
|
smokePath.Key("authentication").Key("kind"), resolution,
|
||||||
|
strings.ToLower(strings.TrimSpace(*smokeTests.Authentication.Kind)))
|
||||||
|
}
|
||||||
|
normalizeDiagnosticEndpoint(&smokeTests.Authentication.Endpoint,
|
||||||
|
smokePath.Key("authentication").Key("endpoint"), resolution)
|
||||||
|
}
|
||||||
|
}
|
||||||
if backend.ConcurrencyLimit != nil && *backend.ConcurrencyLimit > 0 && backend.QueueCapacity == nil {
|
if backend.ConcurrencyLimit != nil && *backend.ConcurrencyLimit > 0 && backend.QueueCapacity == nil {
|
||||||
capacity := 1024
|
capacity := 1024
|
||||||
if resolution.IsExplicit(base.Key("queue_capacity")) {
|
if resolution.IsExplicit(base.Key("queue_capacity")) {
|
||||||
@@ -254,6 +286,21 @@ func resolveBackends(backends map[string]backendConfig, resolution *configkit.Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeDiagnosticEndpoint(value **string, path configkit.Path, resolution *configkit.Resolution) {
|
||||||
|
if value == nil || *value == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
normalized := strings.TrimSpace(**value)
|
||||||
|
request, err := http.NewRequest(http.MethodGet, normalized, nil)
|
||||||
|
if normalized == "" || err != nil || request.URL == nil ||
|
||||||
|
(strings.ToLower(request.URL.Scheme) != "http" && strings.ToLower(request.URL.Scheme) != "https") ||
|
||||||
|
request.URL.Hostname() == "" {
|
||||||
|
resolution.Error(path, "diagnostic endpoint must be an absolute HTTP(S) URL with a host")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
normalizeString(value, path, resolution, normalized)
|
||||||
|
}
|
||||||
|
|
||||||
func resolveProfiles(profiles map[string]profileConfig, resolution *configkit.Resolution) {
|
func resolveProfiles(profiles map[string]profileConfig, resolution *configkit.Resolution) {
|
||||||
for _, id := range sortedKeys(profiles) {
|
for _, id := range sortedKeys(profiles) {
|
||||||
profile := profiles[id]
|
profile := profiles[id]
|
||||||
@@ -309,6 +356,20 @@ func validateEffective(config *document, validation *configkit.Validation) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
validateExtraParams(backend.ExtraParams, base.Key("extra_params"), true, validation)
|
validateExtraParams(backend.ExtraParams, base.Key("extra_params"), true, validation)
|
||||||
|
if smokeTests := backend.SmokeTests; smokeTests != nil {
|
||||||
|
smokePath := base.Key("smoke_tests")
|
||||||
|
if smokeTests.DefaultProfile != nil && strings.TrimSpace(*smokeTests.DefaultProfile) == "" {
|
||||||
|
validation.Error(smokePath.Key("default_profile"), "default profile must not be blank")
|
||||||
|
}
|
||||||
|
if authentication := smokeTests.Authentication; authentication != nil {
|
||||||
|
kindPath := smokePath.Key("authentication").Key("kind")
|
||||||
|
if authentication.Kind == nil {
|
||||||
|
validation.Error(kindPath, "authentication kind is required")
|
||||||
|
} else if *authentication.Kind != "none" && *authentication.Kind != "openrouter_key" {
|
||||||
|
validation.Error(kindPath, "authentication kind must be none or openrouter_key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, id := range sortedKeys(config.Profiles) {
|
for _, id := range sortedKeys(config.Profiles) {
|
||||||
profile := config.Profiles[id]
|
profile := config.Profiles[id]
|
||||||
|
|||||||
@@ -78,6 +78,159 @@ func TestLoadRecordsBackendNormalizationAndNullDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadBackendSmokeTestsOmissionAndNull(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
smokeTests any
|
||||||
|
include bool
|
||||||
|
inherited bool
|
||||||
|
}{
|
||||||
|
{name: "omitted"},
|
||||||
|
{name: "null", smokeTests: nil, include: true, inherited: true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
backend := map[string]any{"endpoint": "https://backend.example/v1"}
|
||||||
|
if test.include {
|
||||||
|
backend["smoke_tests"] = test.smokeTests
|
||||||
|
}
|
||||||
|
main := mapping("backend", map[string]any{
|
||||||
|
"backends": map[string]any{"local": backend},
|
||||||
|
})
|
||||||
|
var underlays []configkit.Underlay
|
||||||
|
if test.inherited {
|
||||||
|
underlays = []configkit.Underlay{{Inputs: []configkit.Input{mapping("defaults", map[string]any{
|
||||||
|
"backends": map[string]any{"local": map[string]any{
|
||||||
|
"endpoint": "https://backend.example/v1",
|
||||||
|
"smoke_tests": map[string]any{"default_profile": "inherited-profile"},
|
||||||
|
}},
|
||||||
|
})}}}
|
||||||
|
}
|
||||||
|
loaded, err := load(main, underlays)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := loaded.result.Config.Backends["local"].SmokeTests; got != nil {
|
||||||
|
t.Fatalf("smoke_tests = %+v, want nil", got)
|
||||||
|
}
|
||||||
|
selected, err := loaded.registry.GetBackend("local")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if selected.SmokeTests != nil {
|
||||||
|
t.Fatalf("indexed smoke_tests = %+v, want nil", selected.SmokeTests)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadMergesBackendSmokeTestsAndRetainsFallbacks(t *testing.T) {
|
||||||
|
base := mapping("defaults", map[string]any{
|
||||||
|
"backends": map[string]any{"local": map[string]any{
|
||||||
|
"endpoint": "https://backend.example/v1",
|
||||||
|
"smoke_tests": map[string]any{
|
||||||
|
"default_profile": "catalog-profile",
|
||||||
|
"reachability": map[string]any{"endpoint": " https://probe.example/ready?check=1 "},
|
||||||
|
"authentication": map[string]any{
|
||||||
|
"kind": " OPENROUTER_KEY ",
|
||||||
|
"endpoint": "https://probe.example/key?scope=current",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
overlay := mapping("operator", map[string]any{
|
||||||
|
"backends": map[string]any{"local": map[string]any{
|
||||||
|
"smoke_tests": map[string]any{
|
||||||
|
"default_profile": "not-yet-listed",
|
||||||
|
"authentication": map[string]any{"kind": " NONE ", "endpoint": nil},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
loaded, err := load(configkit.Input{Source: configkit.NoSource()}, []configkit.Underlay{
|
||||||
|
{Inputs: []configkit.Input{base}},
|
||||||
|
{Inputs: []configkit.Input{overlay}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load effective diagnostic metadata: %v", err)
|
||||||
|
}
|
||||||
|
selected, err := loaded.registry.GetBackend("local")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
metadata := selected.SmokeTests
|
||||||
|
if selected.Endpoint != "https://backend.example/v1" {
|
||||||
|
t.Fatalf("effective backend endpoint = %q", selected.Endpoint)
|
||||||
|
}
|
||||||
|
if metadata == nil || metadata.DefaultProfile == nil || *metadata.DefaultProfile != "not-yet-listed" {
|
||||||
|
t.Fatalf("default profile overlay was not retained: %+v", metadata)
|
||||||
|
}
|
||||||
|
if metadata.Reachability == nil || metadata.Reachability.Endpoint == nil ||
|
||||||
|
*metadata.Reachability.Endpoint != "https://probe.example/ready?check=1" {
|
||||||
|
t.Fatalf("full reachability destination was not normalized: %+v", metadata.Reachability)
|
||||||
|
}
|
||||||
|
if metadata.Authentication == nil || metadata.Authentication.Kind == nil || *metadata.Authentication.Kind != "none" ||
|
||||||
|
metadata.Authentication.Endpoint != nil {
|
||||||
|
t.Fatalf("authentication overlay or null fallback was not retained: %+v", metadata.Authentication)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadRejectsInvalidFinalBackendSmokeTests(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
data map[string]any
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "blank default profile",
|
||||||
|
data: map[string]any{"default_profile": " \t "},
|
||||||
|
path: "/backends/local/smoke_tests/default_profile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "authentication kind required",
|
||||||
|
data: map[string]any{"authentication": map[string]any{}},
|
||||||
|
path: "/backends/local/smoke_tests/authentication/kind",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown authentication kind",
|
||||||
|
data: map[string]any{"authentication": map[string]any{"kind": "bearer"}},
|
||||||
|
path: "/backends/local/smoke_tests/authentication/kind",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-http reachability URL",
|
||||||
|
data: map[string]any{"reachability": map[string]any{"endpoint": "ftp://probe.example/ready"}},
|
||||||
|
path: "/backends/local/smoke_tests/reachability/endpoint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "blank authentication URL",
|
||||||
|
data: map[string]any{"authentication": map[string]any{"kind": "none", "endpoint": " "}},
|
||||||
|
path: "/backends/local/smoke_tests/authentication/endpoint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative authentication URL",
|
||||||
|
data: map[string]any{"authentication": map[string]any{"kind": "none", "endpoint": "/key"}},
|
||||||
|
path: "/backends/local/smoke_tests/authentication/endpoint",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
_, err := load(mapping("backend", map[string]any{
|
||||||
|
"backends": map[string]any{"local": map[string]any{
|
||||||
|
"endpoint": "https://backend.example/v1",
|
||||||
|
"smoke_tests": test.data,
|
||||||
|
}},
|
||||||
|
}), nil)
|
||||||
|
var loadError *configkit.LoadError
|
||||||
|
if !errors.As(err, &loadError) {
|
||||||
|
t.Fatalf("expected final-value validation error, got %v", err)
|
||||||
|
}
|
||||||
|
for _, diagnostic := range loadError.Diagnostics() {
|
||||||
|
if diagnostic.Path.String() == test.path {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("expected diagnostic at %s, got %+v", test.path, loadError.Diagnostics())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadDefaultsAbsentQueueCapacity(t *testing.T) {
|
func TestLoadDefaultsAbsentQueueCapacity(t *testing.T) {
|
||||||
loaded, err := load(mapping("backend", map[string]any{"backends": map[string]any{
|
loaded, err := load(mapping("backend", map[string]any{"backends": map[string]any{
|
||||||
"local": map[string]any{"endpoint": "http://localhost:8080/v1", "concurrency_limit": 1},
|
"local": map[string]any{"endpoint": "http://localhost:8080/v1", "concurrency_limit": 1},
|
||||||
@@ -128,6 +281,40 @@ func TestLoadValidatesOnlyFinalDomainValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadValidatesOnlyFinalBackendSmokeTestValues(t *testing.T) {
|
||||||
|
underlay := mapping("defaults", map[string]any{
|
||||||
|
"backends": map[string]any{"local": map[string]any{
|
||||||
|
"endpoint": "https://backend.example/v1",
|
||||||
|
"smoke_tests": map[string]any{
|
||||||
|
"default_profile": " ",
|
||||||
|
"reachability": map[string]any{"endpoint": "ftp://invalid.example/"},
|
||||||
|
"authentication": map[string]any{"kind": "unsupported"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
main := mapping("operator", map[string]any{
|
||||||
|
"backends": map[string]any{"local": map[string]any{
|
||||||
|
"smoke_tests": map[string]any{
|
||||||
|
"default_profile": "dangling-profile",
|
||||||
|
"reachability": map[string]any{"endpoint": nil},
|
||||||
|
"authentication": map[string]any{"kind": "none"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
loaded, err := load(main, []configkit.Underlay{{Inputs: []configkit.Input{underlay}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("replaced lower metadata should not fail: %v", err)
|
||||||
|
}
|
||||||
|
selected, err := loaded.registry.GetBackend("local")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if selected.SmokeTests == nil || selected.SmokeTests.DefaultProfile == nil ||
|
||||||
|
*selected.SmokeTests.DefaultProfile != "dangling-profile" {
|
||||||
|
t.Fatalf("final dangling default profile was not retained: %+v", selected.SmokeTests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadKeepsSyntaxErrorsFromLowerTiers(t *testing.T) {
|
func TestLoadKeepsSyntaxErrorsFromLowerTiers(t *testing.T) {
|
||||||
_, err := load(mapping("operator", map[string]any{"backends": map[string]any{"local": map[string]any{"endpoint": "http://localhost:8080"}}}),
|
_, err := load(mapping("operator", map[string]any{"backends": map[string]any{"local": map[string]any{"endpoint": "http://localhost:8080"}}}),
|
||||||
[]configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Bytes("broken", []byte("backends: ["))}}}})
|
[]configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Bytes("broken", []byte("backends: ["))}}}})
|
||||||
|
|||||||
@@ -15,11 +15,20 @@ func (state *loaded) buildExecutionProfiles() error {
|
|||||||
definitions := make(map[string]domain.Backend, len(state.result.Config.Backends))
|
definitions := make(map[string]domain.Backend, len(state.result.Config.Backends))
|
||||||
for _, id := range sortedKeys(state.result.Config.Backends) {
|
for _, id := range sortedKeys(state.result.Config.Backends) {
|
||||||
config := state.result.Config.Backends[id]
|
config := state.result.Config.Backends[id]
|
||||||
params, err := jsonvalue.CopyMap(config.ExtraParams)
|
definition := domain.Backend{ID: id, ExtraParams: config.ExtraParams}
|
||||||
if err != nil {
|
if config.SmokeTests != nil {
|
||||||
return fmt.Errorf("backend %q extra parameters: %w", id, err)
|
smokeTests := &domain.BackendSmokeTestConfig{DefaultProfile: config.SmokeTests.DefaultProfile}
|
||||||
|
if config.SmokeTests.Reachability != nil {
|
||||||
|
smokeTests.Reachability = &domain.ReachabilityProbeConfig{Endpoint: config.SmokeTests.Reachability.Endpoint}
|
||||||
|
}
|
||||||
|
if config.SmokeTests.Authentication != nil {
|
||||||
|
smokeTests.Authentication = &domain.AuthenticationProbeConfig{
|
||||||
|
Kind: config.SmokeTests.Authentication.Kind,
|
||||||
|
Endpoint: config.SmokeTests.Authentication.Endpoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
definition.SmokeTests = smokeTests
|
||||||
}
|
}
|
||||||
definition := domain.Backend{ID: id, ExtraParams: params}
|
|
||||||
if config.Endpoint != nil {
|
if config.Endpoint != nil {
|
||||||
definition.Endpoint = *config.Endpoint
|
definition.Endpoint = *config.Endpoint
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user