package config import ( "errors" "fmt" "net/url" "regexp" "sort" "strings" "unicode" ) const maxCatalogModels = 64 var catalogKeyPattern = regexp.MustCompile(`^[a-z][a-z0-9._-]{0,63}$`) var catalogModelIDPattern = regexp.MustCompile(`^[a-z][a-z0-9._-]{0,63}/[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$`) var catalogUpstreamModelPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$`) var catalogAPIVersionPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`) // ModelCatalog is the only operator-authored source for model identity and runtime eligibility. type ModelCatalog struct { Defaults ModelCatalogDefaults `yaml:"defaults" json:"defaults"` Embedding ModelCatalogEmbedding `yaml:"embedding" json:"embedding"` Providers map[string]ModelProvider `yaml:"providers" json:"providers"` } type ModelCatalogDefaults struct { 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 { ID string `yaml:"id" json:"id"` Dimensions int `yaml:"dimensions" json:"dimensions"` } type ModelEndpoint struct { BaseURL string `yaml:"baseUrl" json:"baseUrl"` APIVersion string `yaml:"apiVersion,omitempty" json:"apiVersion,omitempty"` } type ModelAuthentication struct { Mode string `yaml:"mode" json:"mode"` APIKeyEnv string `yaml:"apiKeyEnv,omitempty" json:"apiKeyEnv,omitempty"` } type ModelSessionAdapter struct { Mode string `yaml:"mode" json:"mode"` } type ModelMetadataAdapter struct { LiteLLMProvider string `yaml:"litellmProvider" json:"litellmProvider"` } type ModelProvider struct { Endpoint *ModelEndpoint `yaml:"endpoint,omitempty" json:"endpoint,omitempty"` Authentication ModelAuthentication `yaml:"authentication" json:"authentication"` Session *ModelSessionAdapter `yaml:"session,omitempty" json:"session,omitempty"` MetadataGeneration *ModelMetadataAdapter `yaml:"metadataGeneration,omitempty" json:"metadataGeneration,omitempty"` Models map[string]CatalogModel `yaml:"models" json:"models"` } type ModelCost struct { Input float64 `yaml:"input" json:"input"` Output float64 `yaml:"output" json:"output"` CacheRead float64 `yaml:"cacheRead" json:"cacheRead"` CacheWrite float64 `yaml:"cacheWrite" json:"cacheWrite"` } type ModelCompatibility struct { SupportsDeveloperRole bool `yaml:"supportsDeveloperRole" json:"supportsDeveloperRole"` SupportsReasoningEffort bool `yaml:"supportsReasoningEffort" json:"supportsReasoningEffort"` SupportsStore bool `yaml:"supportsStore" json:"supportsStore"` MaxTokensField string `yaml:"maxTokensField" json:"maxTokensField,omitempty"` ThinkingFormat string `yaml:"thinkingFormat,omitempty" json:"thinkingFormat,omitempty"` } type SessionModel struct { Reasoning bool `yaml:"reasoning,omitempty" json:"reasoning"` Input []string `yaml:"input,omitempty" json:"input,omitempty"` Cost *ModelCost `yaml:"cost,omitempty" json:"cost,omitempty"` ContextWindow int `yaml:"contextWindow,omitempty" json:"contextWindow,omitempty"` MaxTokens int `yaml:"maxTokens,omitempty" json:"maxTokens,omitempty"` Compatibility *ModelCompatibility `yaml:"compatibility,omitempty" json:"compatibility,omitempty"` } type MetadataGenerationModel struct { DisableThinking bool `yaml:"disableThinking,omitempty" json:"disableThinking"` } type CatalogModel struct { Label string `yaml:"label,omitempty" json:"label,omitempty"` UpstreamModel string `yaml:"upstreamModel,omitempty" json:"upstreamModel,omitempty"` Session *SessionModel `yaml:"session,omitempty" json:"session,omitempty"` MetadataGeneration *MetadataGenerationModel `yaml:"metadataGeneration,omitempty" json:"metadataGeneration,omitempty"` } // RuntimeModel is the flattened, canonical representation shared by runtime projections. type RuntimeModel struct { ID string Provider string Model string Label string UpstreamModel string Endpoint *ModelEndpoint Authentication ModelAuthentication SessionAdapter *ModelSessionAdapter MetadataAdapter *ModelMetadataAdapter Session *SessionModel MetadataGeneration *MetadataGenerationModel } // RuntimeModels returns all catalog models in canonical identity order. func (c ModelCatalog) RuntimeModels() []RuntimeModel { models := make([]RuntimeModel, 0) for providerID, provider := range c.Providers { for modelID, model := range provider.Models { id := providerID + "/" + modelID label := model.Label if label == "" { label = id } upstream := model.UpstreamModel if upstream == "" { upstream = modelID } models = append(models, RuntimeModel{ ID: id, Provider: providerID, Model: modelID, Label: label, UpstreamModel: upstream, Endpoint: provider.Endpoint, Authentication: provider.Authentication, SessionAdapter: provider.Session, MetadataAdapter: provider.MetadataGeneration, Session: model.Session, MetadataGeneration: model.MetadataGeneration, }) } } sort.Slice(models, func(left, right int) bool { return models[left].ID < models[right].ID }) return models } // Validate rejects ambiguous, duplicated, or runtime-incompatible catalog declarations. func (c ModelCatalog) Validate(environment map[string]string) error { 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") } if len(c.Providers) == 0 { return errors.New("modelCatalog.providers must not be empty") } if countCatalogModels(c) > maxCatalogModels { return fmt.Errorf("modelCatalog must contain at most %d models", maxCatalogModels) } hasMetadata := false secretsNeeded := make(map[string][]string) for providerID, provider := range c.Providers { if !catalogKeyPattern.MatchString(providerID) { return fmt.Errorf("modelCatalog provider %q is invalid", providerID) } if len(provider.Models) == 0 { return fmt.Errorf("modelCatalog provider %q has no models", providerID) } if err := validateCatalogEndpoint(providerID, provider.Endpoint); err != nil { return err } hasSession, providerHasMetadata := false, false upstreamModels := make(map[string]string) for modelID, model := range provider.Models { if !catalogKeyPattern.MatchString(modelID) { return fmt.Errorf("modelCatalog model %q/%q is invalid", providerID, modelID) } canonical := providerID + "/" + modelID upstream := model.UpstreamModel if upstream == "" { upstream = modelID } if !catalogUpstreamModelPattern.MatchString(upstream) { return fmt.Errorf("modelCatalog model %q upstreamModel is invalid", canonical) } if existing, duplicate := upstreamModels[upstream]; duplicate { return fmt.Errorf("modelCatalog models %q and %q use the same upstreamModel", existing, canonical) } upstreamModels[upstream] = canonical if model.Session == nil && model.MetadataGeneration == nil { return fmt.Errorf("modelCatalog model %q has no runtime use", canonical) } if model.Label != "" && (len(model.Label) > 128 || strings.TrimSpace(model.Label) != model.Label || strings.IndexFunc(model.Label, unicode.IsControl) >= 0) { return fmt.Errorf("modelCatalog model %q label is invalid", canonical) } if model.Session != nil { hasSession = true } if model.MetadataGeneration != nil { hasMetadata, providerHasMetadata = true, true if model.MetadataGeneration.DisableThinking && provider.Endpoint == nil { return fmt.Errorf("modelCatalog model %q disableThinking requires an explicit endpoint", canonical) } } } if err := validateCatalogProvider(providerID, provider, hasSession, providerHasMetadata); err != nil { return err } if provider.Authentication.Mode == "secret_env" { secretsNeeded[provider.Authentication.APIKeyEnv] = append(secretsNeeded[provider.Authentication.APIKeyEnv], providerID) } } models := c.RuntimeModels() 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) } func validateCatalogProvider(id string, provider ModelProvider, hasSession, hasMetadata bool) error { auth := provider.Authentication switch auth.Mode { case "secret_env": if !metadataSecretBundleKeyPattern.MatchString(auth.APIKeyEnv) { return fmt.Errorf("modelCatalog provider %q authentication.apiKeyEnv is invalid", id) } if _, allowed := metadataAPIKeyEnvironments[auth.APIKeyEnv]; !allowed { return fmt.Errorf("modelCatalog provider %q authentication.apiKeyEnv is invalid", id) } case "pi_auth": if auth.APIKeyEnv != "" || hasMetadata || !hasSession || provider.Session == nil || provider.Session.Mode != "pi_builtin" { return fmt.Errorf("modelCatalog provider %q pi_auth is valid only for session-only pi_builtin providers", id) } case "none": if auth.APIKeyEnv != "" || provider.Endpoint == nil { return fmt.Errorf("modelCatalog provider %q authentication none requires an explicit endpoint", id) } default: return fmt.Errorf("modelCatalog provider %q authentication.mode is invalid", id) } if hasSession { if provider.Session == nil || (provider.Session.Mode != "pi_builtin" && provider.Session.Mode != "openai_compatible") { return fmt.Errorf("modelCatalog provider %q requires a supported session adapter", id) } if provider.Session.Mode == "openai_compatible" && provider.Endpoint == nil { return fmt.Errorf("modelCatalog provider %q openai_compatible requires an explicit endpoint", id) } if provider.Session.Mode == "pi_builtin" { for modelID, model := range provider.Models { if model.Session != nil && (model.Session.ContextWindow != 0 || model.Session.MaxTokens != 0 || len(model.Session.Input) != 0 || model.Session.Cost != nil || model.Session.Compatibility != nil || model.Session.Reasoning) { return fmt.Errorf("modelCatalog model %q/%q must use an empty session block for pi_builtin", id, modelID) } } } else { for modelID, model := range provider.Models { if model.Session != nil && (model.Session.ContextWindow <= 0 || model.Session.MaxTokens <= 0) { return fmt.Errorf("modelCatalog model %q/%q requires contextWindow and maxTokens", id, modelID) } if model.Session != nil && model.Session.Compatibility != nil { format := model.Session.Compatibility.ThinkingFormat if format != "" && format != "qwen" && format != "qwen-chat-template" { return fmt.Errorf("modelCatalog model %q/%q compatibility.thinkingFormat is invalid", id, modelID) } if format != "" && !model.Session.Reasoning { return fmt.Errorf("modelCatalog model %q/%q compatibility.thinkingFormat requires reasoning: true", id, modelID) } } } } } else if provider.Session != nil { return fmt.Errorf("modelCatalog provider %q has a session adapter but no session models", id) } if hasMetadata { if provider.MetadataGeneration == nil || provider.MetadataGeneration.LiteLLMProvider == "" { return fmt.Errorf("modelCatalog provider %q requires metadataGeneration.litellmProvider", id) } } else if provider.MetadataGeneration != nil { return fmt.Errorf("modelCatalog provider %q has a metadata adapter but no metadata models", id) } return nil } func validateCatalogEndpoint(provider string, endpoint *ModelEndpoint) error { if endpoint == nil { return nil } parsed, err := url.Parse(endpoint.BaseURL) if err != nil || strings.TrimSpace(endpoint.BaseURL) != endpoint.BaseURL || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Hostname() == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" { return fmt.Errorf("modelCatalog provider %q endpoint.baseUrl is invalid", provider) } if endpoint.APIVersion != "" && !catalogAPIVersionPattern.MatchString(endpoint.APIVersion) { return fmt.Errorf("modelCatalog provider %q endpoint.apiVersion is invalid", provider) } return nil } func validateCatalogSecrets(environment map[string]string, needed map[string][]string) error { if len(needed) == 0 { return nil } bundle := environment["THT_SECRETS_FILE"] if bundle == "" { return errors.New("modelCatalog secret_env providers require THT_SECRETS_FILE") } secrets, err := readMetadataGenerationSecrets(bundle) if err != nil { return errors.New("modelCatalog secrets in THT_SECRETS_FILE are unavailable or invalid") } for key := range needed { value, exists := secrets[key] if !exists || !usableMetadataGenerationSecret(value) { return fmt.Errorf("modelCatalog secret %q is missing or unusable", key) } } return nil } func runtimeModelEligible(models []RuntimeModel, id, usage string) bool { for _, model := range models { if model.ID == id && ((usage == "session" && model.Session != nil) || (usage == "metadata" && model.MetadataGeneration != nil)) { return true } } return false } func countCatalogModels(c ModelCatalog) int { count := 0 for _, provider := range c.Providers { count += len(provider.Models) } return count }