feat: establish unified administration and model context baseline
This commit is contained in:
@@ -125,6 +125,9 @@ func Load(path string) (Installation, error) {
|
||||
if err := ensureOnlyOneDocument(decoder); err != nil {
|
||||
return Installation{}, err
|
||||
}
|
||||
if err := raw.ModelCatalog.NormalizeDefaults(); err != nil {
|
||||
return Installation{}, err
|
||||
}
|
||||
|
||||
if raw.SchemaVersion != 2 {
|
||||
return Installation{}, errors.New("migration_required: installation schemaVersion must be 2")
|
||||
|
||||
@@ -19,8 +19,8 @@ func TestLoadAcceptsInstallationModelCatalog(t *testing.T) {
|
||||
if installation.SchemaVersion != 2 {
|
||||
t.Fatalf("SchemaVersion = %d, want 2", installation.SchemaVersion)
|
||||
}
|
||||
if installation.ModelCatalog.Defaults.Session != "zai/glm-5.3" {
|
||||
t.Fatalf("session default = %q", installation.ModelCatalog.Defaults.Session)
|
||||
if installation.ModelCatalog.Defaults.Interaction != "zai/glm-5.3" {
|
||||
t.Fatalf("session default = %q", installation.ModelCatalog.Defaults.Interaction)
|
||||
}
|
||||
models := installation.ModelCatalog.RuntimeModels()
|
||||
if len(models) != 2 || models[0].ID != "local/qwen" || models[1].ID != "zai/glm-5.3" {
|
||||
@@ -28,6 +28,43 @@ func TestLoadAcceptsInstallationModelCatalog(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAcceptsSharedDeepSeekBuiltinWithBundleAuthentication(t *testing.T) {
|
||||
path, _, envFile, _ := writeInstallation(t, "local")
|
||||
catalog := strings.Replace(validModelCatalogYAML(), "interaction: zai/glm-5.3", "interaction: deepseek/deepseek-v4-pro", 1)
|
||||
catalog += ` deepseek:
|
||||
authentication:
|
||||
mode: secret_env
|
||||
apiKeyEnv: DEEPSEEK_API_KEY
|
||||
session:
|
||||
mode: pi_builtin
|
||||
metadataGeneration:
|
||||
litellmProvider: deepseek
|
||||
models:
|
||||
deepseek-v4-pro:
|
||||
session: {}
|
||||
metadataGeneration: {}
|
||||
deepseek-v4-flash:
|
||||
session: {}
|
||||
metadataGeneration: {}
|
||||
`
|
||||
rewriteInstallationCatalog(t, path, catalog)
|
||||
secretPath := filepath.Join(filepath.Dir(path), "secrets.env")
|
||||
if err := os.WriteFile(secretPath, []byte("DEEPSEEK_API_KEY=shared-test-key\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(envFile, []byte("THT_AUTH_CONFIG_ROOT="+filepath.Join(filepath.Dir(path), "auth")+"\nTHT_INSTALLATION_CONFIG_SOURCE="+path+"\nTHT_SECRETS_FILE="+secretPath+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
i, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := i.ModelCatalog.Providers["deepseek"]
|
||||
if p.Authentication.APIKeyEnv != "DEEPSEEK_API_KEY" || p.Session.Mode != "pi_builtin" || p.MetadataGeneration.LiteLLMProvider != "deepseek" || len(p.Models) != 2 {
|
||||
t.Fatalf("unexpected shared DeepSeek provider: %#v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsLegacyMetadataGenerationWithMigrationRequired(t *testing.T) {
|
||||
path, _, _, _ := writeInstallation(t, "local")
|
||||
contents, err := os.ReadFile(path)
|
||||
@@ -70,8 +107,8 @@ func TestLoadRejectsInvalidModelCatalogDefaultsAndAuthentication(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "unknown session default",
|
||||
catalog: strings.Replace(validModelCatalogYAML(), "session: zai/glm-5.3", "session: zai/missing", 1),
|
||||
wantError: "defaults.session",
|
||||
catalog: strings.Replace(validModelCatalogYAML(), "interaction: zai/glm-5.3", "interaction: zai/missing", 1),
|
||||
wantError: "defaults.interaction",
|
||||
},
|
||||
{
|
||||
name: "embedding must use the installation Ollama service",
|
||||
@@ -86,9 +123,9 @@ func TestLoadRejectsInvalidModelCatalogDefaultsAndAuthentication(t *testing.T) {
|
||||
wantError: "same upstreamModel",
|
||||
},
|
||||
{
|
||||
name: "metadata default required",
|
||||
catalog: strings.Replace(validModelCatalogYAML(), " metadataGeneration: local/qwen\n", "", 1),
|
||||
wantError: "defaults.metadataGeneration",
|
||||
name: "interaction default required",
|
||||
catalog: strings.Replace(validModelCatalogYAML(), " interaction: zai/glm-5.3\n", "", 1),
|
||||
wantError: "defaults.interaction",
|
||||
},
|
||||
{
|
||||
name: "pi auth cannot serve metadata",
|
||||
@@ -145,8 +182,7 @@ func rewriteInstallationCatalog(t *testing.T, path, catalog string) {
|
||||
func validModelCatalogYAML() string {
|
||||
return `modelCatalog:
|
||||
defaults:
|
||||
session: zai/glm-5.3
|
||||
metadataGeneration: local/qwen
|
||||
interaction: zai/glm-5.3
|
||||
embedding:
|
||||
id: ollama/qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
@@ -158,8 +194,11 @@ func validModelCatalogYAML() string {
|
||||
mode: none
|
||||
session:
|
||||
mode: openai_compatible
|
||||
metadataGeneration:
|
||||
litellmProvider: openai
|
||||
models:
|
||||
glm-5.3:
|
||||
metadataGeneration: {}
|
||||
session:
|
||||
reasoning: true
|
||||
contextWindow: 200000
|
||||
@@ -182,3 +221,48 @@ func validModelCatalogYAML() string {
|
||||
disableThinking: true
|
||||
`
|
||||
}
|
||||
|
||||
func TestInteractionDefaultLegacyMigration(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name, defaults string
|
||||
wantError bool
|
||||
}{
|
||||
{"equal legacy defaults", " session: zai/glm-5.3\n metadataGeneration: zai/glm-5.3\n", false},
|
||||
{"divergent legacy defaults", " session: zai/glm-5.3\n metadataGeneration: local/qwen\n", true},
|
||||
{"mixed new and legacy fields", " interaction: zai/glm-5.3\n session: zai/glm-5.3\n", true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
path, _, _, _ := writeInstallation(t, "local")
|
||||
rewriteInstallationCatalog(t, path, strings.Replace(validModelCatalogYAML(), " interaction: zai/glm-5.3\n", test.defaults, 1))
|
||||
before, _ := os.ReadFile(path)
|
||||
installation, err := Load(path)
|
||||
if test.wantError {
|
||||
if err == nil || !strings.Contains(err.Error(), "migration_required") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if installation.ModelCatalog.Defaults.Interaction != "zai/glm-5.3" || installation.ModelCatalog.Defaults.Session != "" || installation.ModelCatalog.Defaults.MetadataGeneration != "" {
|
||||
t.Fatalf("defaults were not normalized: %#v", installation.ModelCatalog.Defaults)
|
||||
}
|
||||
after, _ := os.ReadFile(path)
|
||||
if string(before) != string(after) {
|
||||
t.Fatal("Load rewrote the operator configuration")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInteractionDefaultMustSupportBothConfiguredUses(t *testing.T) {
|
||||
path, _, _, _ := writeInstallation(t, "local")
|
||||
// Keep Admin configured for local/qwen but remove that use from the selected default.
|
||||
catalog := strings.Replace(validModelCatalogYAML(), " metadataGeneration:\n litellmProvider: openai\n", "", 1)
|
||||
catalog = strings.Replace(catalog, " metadataGeneration: {}\n", "", 1)
|
||||
rewriteInstallationCatalog(t, path, catalog)
|
||||
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), "defaults.interaction") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,26 @@ type ModelCatalog struct {
|
||||
}
|
||||
|
||||
type ModelCatalogDefaults struct {
|
||||
Session string `yaml:"session" json:"session"`
|
||||
MetadataGeneration string `yaml:"metadataGeneration,omitempty" json:"metadataGeneration,omitempty"`
|
||||
Interaction string `yaml:"interaction" json:"interaction"`
|
||||
// Legacy input only. NormalizeDefaults clears these before projections or serialization.
|
||||
Session string `yaml:"session,omitempty" json:"-"`
|
||||
MetadataGeneration string `yaml:"metadataGeneration,omitempty" json:"-"`
|
||||
}
|
||||
|
||||
// NormalizeDefaults accepts unambiguous legacy configuration without rewriting the source file.
|
||||
func (c *ModelCatalog) NormalizeDefaults() error {
|
||||
d := &c.Defaults
|
||||
if d.Interaction != "" && (d.Session != "" || d.MetadataGeneration != "") {
|
||||
return errors.New("migration_required: use only modelCatalog.defaults.interaction, not legacy defaults")
|
||||
}
|
||||
if d.Interaction == "" {
|
||||
if d.Session != "" && d.MetadataGeneration != "" && d.Session != d.MetadataGeneration {
|
||||
return errors.New("migration_required: session and metadataGeneration defaults differ; explicitly choose modelCatalog.defaults.interaction and remove both legacy defaults")
|
||||
}
|
||||
d.Interaction = d.Session
|
||||
}
|
||||
d.Session, d.MetadataGeneration = "", ""
|
||||
return nil
|
||||
}
|
||||
|
||||
type ModelCatalogEmbedding struct {
|
||||
@@ -138,8 +156,8 @@ func (c ModelCatalog) RuntimeModels() []RuntimeModel {
|
||||
|
||||
// Validate rejects ambiguous, duplicated, or runtime-incompatible catalog declarations.
|
||||
func (c ModelCatalog) Validate(environment map[string]string) error {
|
||||
if c.Defaults.Session == "" {
|
||||
return errors.New("modelCatalog.defaults.session is required")
|
||||
if c.Defaults.Interaction == "" {
|
||||
return errors.New("modelCatalog.defaults.interaction is required")
|
||||
}
|
||||
if !catalogModelIDPattern.MatchString(c.Embedding.ID) || !strings.HasPrefix(c.Embedding.ID, "ollama/") || c.Embedding.Dimensions <= 0 {
|
||||
return errors.New("modelCatalog.embedding requires an ollama canonical id and positive dimensions")
|
||||
@@ -206,17 +224,9 @@ func (c ModelCatalog) Validate(environment map[string]string) error {
|
||||
}
|
||||
|
||||
models := c.RuntimeModels()
|
||||
if !runtimeModelEligible(models, c.Defaults.Session, "session") {
|
||||
return fmt.Errorf("modelCatalog.defaults.session %q is not a session model", c.Defaults.Session)
|
||||
}
|
||||
if hasMetadata && c.Defaults.MetadataGeneration == "" {
|
||||
return errors.New("modelCatalog.defaults.metadataGeneration is required when metadata models are configured")
|
||||
}
|
||||
if !hasMetadata && c.Defaults.MetadataGeneration != "" {
|
||||
return errors.New("modelCatalog.defaults.metadataGeneration must be empty when metadata generation is unavailable")
|
||||
}
|
||||
if hasMetadata && !runtimeModelEligible(models, c.Defaults.MetadataGeneration, "metadata") {
|
||||
return fmt.Errorf("modelCatalog.defaults.metadataGeneration %q is not a metadata-generation model", c.Defaults.MetadataGeneration)
|
||||
if !runtimeModelEligible(models, c.Defaults.Interaction, "session") ||
|
||||
(hasMetadata && !runtimeModelEligible(models, c.Defaults.Interaction, "metadata")) {
|
||||
return fmt.Errorf("modelCatalog.defaults.interaction %q must support Core and every configured Admin LLM use", c.Defaults.Interaction)
|
||||
}
|
||||
return validateCatalogSecrets(environment, secretsNeeded)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user