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)
}
@@ -174,7 +174,7 @@ func Run(request Request) error {
func reconcileCatalog(legacy legacyDescriptor, piModels piCatalog, settings piSettings, request Request) (config.ModelCatalog, error) {
catalog := config.ModelCatalog{
Defaults: config.ModelCatalogDefaults{Session: request.SessionDefault},
Defaults: config.ModelCatalogDefaults{Interaction: request.SessionDefault},
Embedding: config.ModelCatalogEmbedding{ID: request.EmbeddingID, Dimensions: request.EmbeddingDimensions},
Providers: make(map[string]config.ModelProvider),
}
@@ -251,7 +251,9 @@ func reconcileCatalog(legacy legacyDescriptor, piModels piCatalog, settings piSe
if !ok {
return catalog, errors.New("metadataGeneration.default does not identify one legacy model")
}
catalog.Defaults.MetadataGeneration = mapped
if mapped != request.SessionDefault {
return catalog, errors.New("migration_required: legacy metadata default differs from the requested interaction model; align the legacy default explicitly before migration")
}
} else if legacy.MetadataGeneration.Default != "" {
return catalog, errors.New("metadataGeneration.default is set without models")
}
@@ -35,10 +35,10 @@ func TestRunEmitsValidatedCandidateWithoutChangingLegacyInputs(t *testing.T) {
if err != nil {
t.Fatalf("candidate config.Load() error = %v", err)
}
if candidate.SchemaVersion != 2 || candidate.ModelCatalog.Defaults.Session != "zai/glm-5.3" {
if candidate.SchemaVersion != 2 || candidate.ModelCatalog.Defaults.Interaction != "zai/glm-5.3" {
t.Fatalf("candidate = %#v", candidate.ModelCatalog)
}
if got := candidate.ModelCatalog.Defaults.MetadataGeneration; got != "zai/glm-5.3" {
if got := candidate.ModelCatalog.Defaults.Interaction; got != "zai/glm-5.3" {
t.Fatalf("metadata default = %q", got)
}
if candidate.ModelCatalog.Providers["deepseek"].Session.Mode != "pi_builtin" {
@@ -24,11 +24,10 @@ const (
var renameProjectionDirectory = os.Rename
type runtimeCatalog struct {
SchemaVersion int `json:"schemaVersion"`
DefaultSession string `json:"defaultSession"`
DefaultMetadataGeneration string `json:"defaultMetadataGeneration,omitempty"`
Embedding config.ModelCatalogEmbedding `json:"embedding"`
Models []runtimeCatalogModel `json:"models"`
SchemaVersion int `json:"schemaVersion"`
DefaultInteraction string `json:"defaultInteraction"`
Embedding config.ModelCatalogEmbedding `json:"embedding"`
Models []runtimeCatalogModel `json:"models"`
}
type runtimeCatalogModel struct {
@@ -74,19 +73,33 @@ type piSettings struct {
// Render builds every projection in memory so no partial candidate can be published.
func Render(installation config.Installation) (map[string][]byte, error) {
if err := installation.ModelCatalog.NormalizeDefaults(); err != nil {
return nil, err
}
models := installation.ModelCatalog.RuntimeModels()
hasMetadata := false
for _, model := range models {
hasMetadata = hasMetadata || model.MetadataGeneration != nil
}
runtimeModels := make([]runtimeCatalogModel, 0, len(models))
customProviders := make(map[string]piProvider)
enabled := make([]string, 0)
for _, model := range models {
sessionAdapter, metadataAdapter := model.SessionAdapter, model.MetadataAdapter
if model.Session == nil {
sessionAdapter = nil
}
if model.MetadataGeneration == nil {
metadataAdapter = nil
}
runtimeModels = append(runtimeModels, runtimeCatalogModel{
ID: model.ID, Provider: model.Provider, Model: model.Model, Label: model.Label,
UpstreamModel: model.UpstreamModel, Endpoint: model.Endpoint,
Authentication: model.Authentication, SessionAdapter: model.SessionAdapter,
MetadataAdapter: model.MetadataAdapter, Session: model.Session,
Authentication: model.Authentication, SessionAdapter: sessionAdapter,
MetadataAdapter: metadataAdapter, Session: model.Session,
MetadataGeneration: model.MetadataGeneration,
})
if model.Session == nil {
if model.Session == nil || (hasMetadata && model.MetadataGeneration == nil) {
continue
}
if model.SessionAdapter == nil || model.SessionAdapter.Mode != "openai_compatible" {
@@ -117,9 +130,8 @@ func Render(installation config.Installation) (map[string][]byte, error) {
sort.Strings(enabled)
catalogBytes, err := marshalJSON(runtimeCatalog{
SchemaVersion: 1, DefaultSession: installation.ModelCatalog.Defaults.Session,
DefaultMetadataGeneration: installation.ModelCatalog.Defaults.MetadataGeneration,
Embedding: installation.ModelCatalog.Embedding, Models: runtimeModels,
SchemaVersion: 2, DefaultInteraction: installation.ModelCatalog.Defaults.Interaction,
Embedding: installation.ModelCatalog.Embedding, Models: runtimeModels,
})
if err != nil {
return nil, fmt.Errorf("render runtime model catalog: %w", err)
@@ -294,7 +306,7 @@ func renderCompose(installation config.Installation, fingerprint string) []byte
embedding-model-init:
environment:
OLLAMA_MODEL: %s
`, strconv.Quote(fingerprint), strconv.Quote(installation.ModelCatalog.Defaults.Session),
`, strconv.Quote(fingerprint), strconv.Quote(installation.ModelCatalog.Defaults.Interaction),
strconv.Quote(embedding.ID), strconv.Quote(embeddingModelName(embedding.ID)), strconv.Quote(strconv.Itoa(embedding.Dimensions)),
strconv.Quote(installation.GeneratedModelCatalogPath()), strconv.Quote(installation.GeneratedPiModelsPath()),
strconv.Quote(installation.GeneratedPiSettingsPath()), strconv.Quote(embedding.ID),
@@ -2,6 +2,7 @@ package modelprojection
import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
@@ -32,7 +33,7 @@ func TestRenderProducesDeterministicCatalogPiAndComposeProjections(t *testing.T)
}
catalog := string(first[CatalogFile])
for _, expected := range []string{`"id": "local/qwen"`, `"defaultSession": "zai/glm-5.3"`, `"embedding"`} {
for _, expected := range []string{`"id": "local/qwen"`, `"defaultInteraction": "zai/glm-5.3"`, `"embedding"`} {
if !strings.Contains(catalog, expected) {
t.Fatalf("catalog projection missing %s:\n%s", expected, catalog)
}
@@ -42,8 +43,8 @@ func TestRenderProducesDeterministicCatalogPiAndComposeProjections(t *testing.T)
t.Fatalf("Pi models projection must contain only custom providers:\n%s", piModels)
}
settings := string(first[PiSettingsFile])
if !strings.Contains(settings, `"deepseek/deepseek-v4-pro"`) || !strings.Contains(settings, `"local/qwen2.5:7b"`) {
t.Fatalf("Pi settings projection missing enabled session models:\n%s", settings)
if strings.Contains(settings, `"deepseek/deepseek-v4-pro"`) || !strings.Contains(settings, `"local/qwen2.5:7b"`) {
t.Fatalf("Pi settings must enable only shared Core/Admin models:\n%s", settings)
}
if !strings.Contains(piModels, `"id": "qwen2.5:7b"`) || !strings.Contains(catalog, `"id": "local/qwen"`) {
t.Fatalf("Pi projection must map canonical catalog keys to upstream model IDs")
@@ -56,6 +57,69 @@ func TestRenderProducesDeterministicCatalogPiAndComposeProjections(t *testing.T)
}
}
func TestDeepSeekBuiltinAndMetadataShareCatalogIdentityAndSecretReference(t *testing.T) {
i := projectionFixture(t)
p := i.ModelCatalog.Providers["deepseek"]
p.Authentication = config.ModelAuthentication{Mode: "secret_env", APIKeyEnv: "DEEPSEEK_API_KEY"}
p.MetadataGeneration = &config.ModelMetadataAdapter{LiteLLMProvider: "deepseek"}
for _, id := range []string{"deepseek-v4-pro", "deepseek-v4-flash"} {
p.Models[id] = config.CatalogModel{Session: &config.SessionModel{}, MetadataGeneration: &config.MetadataGenerationModel{}}
}
i.ModelCatalog.Providers["deepseek"] = p
i.ModelCatalog.Defaults.Interaction = "deepseek/deepseek-v4-pro"
files, err := Render(i)
if err != nil {
t.Fatal(err)
}
var catalog runtimeCatalog
if err := json.Unmarshal(files[CatalogFile], &catalog); err != nil {
t.Fatal(err)
}
count := 0
for _, model := range catalog.Models {
if model.Provider != "deepseek" {
continue
}
count++
if model.Authentication.APIKeyEnv != "DEEPSEEK_API_KEY" || model.SessionAdapter == nil || model.MetadataAdapter == nil {
t.Fatalf("DeepSeek must share auth and both adapters: %#v", model)
}
if !bytes.Contains(files[PiSettingsFile], []byte(model.ID)) {
t.Fatalf("shared model missing from Pi selection: %s", model.ID)
}
}
if count != 2 || bytes.Contains(files[PiModelsFile], []byte("deepseek")) {
t.Fatal("both DeepSeek models must use native Pi definitions, not duplicated custom definitions")
}
}
func TestProjectionKeepsSingleUseInventoryOutOfSharedPiSelection(t *testing.T) {
i := projectionFixture(t)
p := i.ModelCatalog.Providers["zai"]
p.Models["core-only"] = config.CatalogModel{Session: &config.SessionModel{ContextWindow: 32768, MaxTokens: 8192}}
p.Models["admin-only"] = config.CatalogModel{MetadataGeneration: &config.MetadataGenerationModel{}}
i.ModelCatalog.Providers["zai"] = p
files, err := Render(i)
if err != nil {
t.Fatal(err)
}
var catalog runtimeCatalog
if err := json.Unmarshal(files[CatalogFile], &catalog); err != nil {
t.Fatal(err)
}
for _, model := range catalog.Models {
if model.ID == "zai/core-only" && model.MetadataAdapter != nil {
t.Fatal("projected an unused metadata adapter")
}
if model.ID == "zai/admin-only" && model.SessionAdapter != nil {
t.Fatal("projected an unused session adapter")
}
}
if bytes.Contains(files[PiSettingsFile], []byte("core-only")) || bytes.Contains(files[PiSettingsFile], []byte("admin-only")) {
t.Fatal("single-use inventory leaked into global Pi selection")
}
}
func TestGenerateDoesNotReplaceUnchangedProjection(t *testing.T) {
installation := projectionFixture(t)
if err := Generate(installation); err != nil {
@@ -167,11 +231,11 @@ func projectionFixture(t *testing.T) config.Installation {
return config.Installation{
Path: filepath.Join(root, "thothii-installation.yaml"),
ModelCatalog: config.ModelCatalog{
Defaults: config.ModelCatalogDefaults{Session: "zai/glm-5.3", MetadataGeneration: "local/qwen"},
Defaults: config.ModelCatalogDefaults{Interaction: "zai/glm-5.3"},
Embedding: config.ModelCatalogEmbedding{ID: "ollama/qwen3-embedding:0.6b", Dimensions: 1024},
Providers: map[string]config.ModelProvider{
"deepseek": {Authentication: config.ModelAuthentication{Mode: "pi_auth"}, Session: &config.ModelSessionAdapter{Mode: "pi_builtin"}, Models: map[string]config.CatalogModel{"deepseek-v4-pro": {Session: &config.SessionModel{}}}},
"zai": {Endpoint: &config.ModelEndpoint{BaseURL: "https://api.z.ai/v1"}, Authentication: config.ModelAuthentication{Mode: "none"}, Session: &config.ModelSessionAdapter{Mode: "openai_compatible"}, Models: map[string]config.CatalogModel{"glm-5.3": {Label: "GLM-5.3", Session: &config.SessionModel{Reasoning: true, ContextWindow: 200000, MaxTokens: 131072}}}},
"zai": {Endpoint: &config.ModelEndpoint{BaseURL: "https://api.z.ai/v1"}, Authentication: config.ModelAuthentication{Mode: "none"}, Session: &config.ModelSessionAdapter{Mode: "openai_compatible"}, MetadataGeneration: &config.ModelMetadataAdapter{LiteLLMProvider: "openai"}, Models: map[string]config.CatalogModel{"glm-5.3": {Label: "GLM-5.3", Session: &config.SessionModel{Reasoning: true, ContextWindow: 200000, MaxTokens: 131072}, MetadataGeneration: &config.MetadataGenerationModel{}}}},
"local": {Endpoint: &config.ModelEndpoint{BaseURL: "http://ollama:11434/v1"}, Authentication: config.ModelAuthentication{Mode: "none"}, Session: &config.ModelSessionAdapter{Mode: "openai_compatible"}, MetadataGeneration: &config.ModelMetadataAdapter{LiteLLMProvider: "openai"}, Models: map[string]config.CatalogModel{"qwen": {UpstreamModel: "qwen2.5:7b", Session: &config.SessionModel{ContextWindow: 32768, MaxTokens: 8192}, MetadataGeneration: &config.MetadataGenerationModel{DisableThinking: true}}}},
},
},
+1 -1
View File
@@ -377,7 +377,7 @@ func render(root, descriptorPath string, value answers) ([]byte, []byte, error)
func defaultModelCatalog() config.ModelCatalog {
return config.ModelCatalog{
Defaults: config.ModelCatalogDefaults{Session: "deepseek/deepseek-v4-pro"},
Defaults: config.ModelCatalogDefaults{Interaction: "deepseek/deepseek-v4-pro"},
Embedding: config.ModelCatalogEmbedding{ID: "ollama/qwen3-embedding:0.6b", Dimensions: 1024},
Providers: map[string]config.ModelProvider{
"deepseek": {