Add custom backend configuration
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/defaults"
|
||||
@@ -32,11 +33,29 @@ var (
|
||||
|
||||
// Config is the on-disk YAML shape for application-level settings.
|
||||
type Config struct {
|
||||
PromptDir string `yaml:"prompt_dir"`
|
||||
ProfileDir string `yaml:"profile_dir"`
|
||||
SchemaDir string `yaml:"schema_dir"`
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Defaults DefaultsConfig `yaml:"defaults"`
|
||||
PromptDir string `yaml:"prompt_dir"`
|
||||
ProfileDir string `yaml:"profile_dir"`
|
||||
SchemaDir string `yaml:"schema_dir"`
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Defaults DefaultsConfig `yaml:"defaults"`
|
||||
Backends map[string]BackendConfig `yaml:"backends"`
|
||||
}
|
||||
|
||||
type BackendConfig struct {
|
||||
Endpoint string `yaml:"endpoint"`
|
||||
APIKeyEnv string `yaml:"api_key_env"`
|
||||
ExtraParams map[string]any `yaml:"extra_params"`
|
||||
ConcurrencyLimit int `yaml:"concurrency_limit"`
|
||||
QueueCapacity *int `yaml:"queue_capacity"`
|
||||
}
|
||||
|
||||
type BackendSettings struct {
|
||||
ID string
|
||||
Endpoint string
|
||||
APIKeyEnv string
|
||||
ExtraParams map[string]any
|
||||
ConcurrencyLimit int
|
||||
QueueCapacity *int
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -62,6 +81,7 @@ type AppSettings struct {
|
||||
MaxArtifactBytes int64
|
||||
MaxResponseBytes int64
|
||||
DefaultRenderFormat renderformat.PreparedRunOutputFormat
|
||||
Backends []BackendSettings
|
||||
}
|
||||
|
||||
// CLIOverrides can be applied after config load to enforce precedence.
|
||||
@@ -245,6 +265,25 @@ func applyConfig(base AppSettings, cfg Config) (AppSettings, error) {
|
||||
}
|
||||
out.DefaultRenderFormat = parsed
|
||||
}
|
||||
if len(cfg.Backends) > 0 {
|
||||
ids := make([]string, 0, len(cfg.Backends))
|
||||
for id := range cfg.Backends {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Strings(ids)
|
||||
out.Backends = make([]BackendSettings, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
backend := cfg.Backends[id]
|
||||
out.Backends = append(out.Backends, BackendSettings{
|
||||
ID: id,
|
||||
Endpoint: backend.Endpoint,
|
||||
APIKeyEnv: backend.APIKeyEnv,
|
||||
ExtraParams: backend.ExtraParams,
|
||||
ConcurrencyLimit: backend.ConcurrencyLimit,
|
||||
QueueCapacity: backend.QueueCapacity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/defaults"
|
||||
@@ -20,7 +21,7 @@ func TestLoadConfigMissingImplicitPathUsesBuiltInDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
want := BuiltInDefaults()
|
||||
if got != want {
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected settings: got=%+v want=%+v", got, want)
|
||||
}
|
||||
}
|
||||
@@ -100,6 +101,78 @@ func TestLoadConfigAPIKeyFieldIsRejectedAsUnknown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigBackendsRetainsResolvedSettingsInSortedOrder(t *testing.T) {
|
||||
path := writeConfigFile(t, "config.yml", `
|
||||
backends:
|
||||
zebra:
|
||||
endpoint: https://zebra.example/v1
|
||||
api_key_env: ZEBRA_API_KEY
|
||||
extra_params:
|
||||
provider_option: enabled
|
||||
nested:
|
||||
enabled: true
|
||||
attempts: 2
|
||||
concurrency_limit: 2
|
||||
alpha:
|
||||
endpoint: http://alpha.example/v1
|
||||
queue_capacity: 0
|
||||
`)
|
||||
|
||||
got, err := LoadConfig(path, true)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if len(got.Backends) != 2 {
|
||||
t.Fatalf("expected two backends, got %#v", got.Backends)
|
||||
}
|
||||
if got.Backends[0].ID != "alpha" || got.Backends[1].ID != "zebra" {
|
||||
t.Fatalf("expected sorted backend IDs, got %#v", got.Backends)
|
||||
}
|
||||
if got.Backends[0].QueueCapacity == nil || *got.Backends[0].QueueCapacity != 0 {
|
||||
t.Fatalf("expected explicit zero queue capacity, got %#v", got.Backends[0].QueueCapacity)
|
||||
}
|
||||
if got.Backends[1].QueueCapacity != nil {
|
||||
t.Fatalf("expected omitted queue capacity to remain nil, got %#v", got.Backends[1].QueueCapacity)
|
||||
}
|
||||
wantParams := map[string]any{
|
||||
"provider_option": "enabled",
|
||||
"nested": map[string]any{
|
||||
"enabled": true,
|
||||
"attempts": 2,
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(got.Backends[1].ExtraParams, wantParams) {
|
||||
t.Fatalf("unexpected extra params: got=%#v want=%#v", got.Backends[1].ExtraParams, wantParams)
|
||||
}
|
||||
if got.Backends[1].Endpoint != "https://zebra.example/v1" || got.Backends[1].APIKeyEnv != "ZEBRA_API_KEY" || got.Backends[1].ConcurrencyLimit != 2 {
|
||||
t.Fatalf("unexpected zebra backend: %#v", got.Backends[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigRejectsUnknownOrSecretBackendFields(t *testing.T) {
|
||||
for name, body := range map[string]string{
|
||||
"unknown": "backends:\n local:\n endpoint: http://localhost:11434/v1\n unexpected: value\n",
|
||||
"secret": "backends:\n local:\n endpoint: http://localhost:11434/v1\n api_key: secret\n",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
path := writeConfigFile(t, "config.yml", body)
|
||||
_, err := LoadConfig(path, true)
|
||||
if !errors.Is(err, ErrInvalidConfigYAML) {
|
||||
t.Fatalf("expected ErrInvalidConfigYAML, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigRejectsInvalidBackendFieldTypes(t *testing.T) {
|
||||
path := writeConfigFile(t, "config.yml", "backends:\n local:\n endpoint: http://localhost:11434/v1\n queue_capacity: not-a-number\n")
|
||||
|
||||
_, err := LoadConfig(path, true)
|
||||
if !errors.Is(err, ErrInvalidConfigYAML) {
|
||||
t.Fatalf("expected ErrInvalidConfigYAML, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigValidConfigSetsDirectoriesAndServerAddr(t *testing.T) {
|
||||
path := writeConfigFile(t, "config.yml", `
|
||||
prompt_dir: ./prompts
|
||||
@@ -196,7 +269,7 @@ func TestLoadConfigEmptyFileResolvesToBuiltInDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
want := BuiltInDefaults()
|
||||
if got != want {
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected settings: got=%+v want=%+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user