feat: establish unified administration and model context baseline

This commit is contained in:
Codex
2026-09-12 18:03:15 +02:00
parent 840344706f
commit f52bf22e05
74 changed files with 2334 additions and 523 deletions
@@ -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 -15
View File
@@ -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)
}