package config import ( "encoding/hex" "fmt" "strings" "github.com/spf13/viper" ) // validateOpenAIRoutes rejects duplicate and empty model ids in the route catalog. func validateOpenAIRoutes(routes []OpenAIRouteEntry) error { seen := make(map[string]struct{}, len(routes)) for i, r := range routes { model := strings.TrimSpace(r.Model) if model == "" { return fmt.Errorf("openai.model_routes[%d]: model must not be empty", i) } if _, dup := seen[model]; dup { return fmt.Errorf("openai.model_routes: duplicate model %q", model) } seen[model] = struct{}{} if r.MaxQueue < 0 { return fmt.Errorf("openai.model_routes[%d].max_queue must be non-negative", i) } if r.QueueTimeoutMS < 0 { return fmt.Errorf("openai.model_routes[%d].queue_timeout_ms must be non-negative", i) } } return nil } // validateOpenAIPrincipalTokens validates the raw-token-free bearer token // operation mapping. Each entry must resolve to a unique, 64-char lowercase // hex SHA-256 hash and a non-empty principal_ref; token_ref must be unique. func validateOpenAIPrincipalTokens(tokens []OpenAIPrincipalTokenConf) error { seenTokenRef := make(map[string]struct{}, len(tokens)) seenHash := make(map[string]struct{}, len(tokens)) for i, t := range tokens { ref := strings.TrimSpace(t.TokenRef) if ref == "" { return fmt.Errorf("openai.principal_tokens[%d]: token_ref must not be empty", i) } if _, dup := seenTokenRef[ref]; dup { return fmt.Errorf("openai.principal_tokens: duplicate token_ref %q", ref) } seenTokenRef[ref] = struct{}{} hash := strings.ToLower(strings.TrimSpace(t.TokenHashSHA256)) if len(hash) != 64 { return fmt.Errorf("openai.principal_tokens[%q]: token_hash_sha256 must be a 64-character hex SHA-256 hash", ref) } if _, err := hex.DecodeString(hash); err != nil { return fmt.Errorf("openai.principal_tokens[%q]: token_hash_sha256 must be hex-encoded: %w", ref, err) } if _, dup := seenHash[hash]; dup { return fmt.Errorf("openai.principal_tokens: duplicate token_hash_sha256 for token_ref %q", ref) } seenHash[hash] = struct{}{} if strings.TrimSpace(t.PrincipalRef) == "" { return fmt.Errorf("openai.principal_tokens[%q]: principal_ref must not be empty", ref) } } return nil } func normalizeOpenAIProviderAuth(v *viper.Viper, auth *EdgeOpenAIProviderAuthConf) error { if !auth.Enabled { return nil } if v.InConfig("openai.provider_auth.from_header") { auth.FromHeader = strings.TrimSpace(auth.FromHeader) if auth.FromHeader == "" { return fmt.Errorf("openai.provider_auth.from_header must not be empty when provider_auth is enabled") } } else { auth.FromHeader = "X-IOP-Provider-Authorization" } if v.InConfig("openai.provider_auth.target_header") { auth.TargetHeader = strings.TrimSpace(auth.TargetHeader) if auth.TargetHeader == "" { return fmt.Errorf("openai.provider_auth.target_header must not be empty when provider_auth is enabled") } } else { auth.TargetHeader = "Authorization" } auth.Scheme = strings.TrimSpace(auth.Scheme) if auth.Scheme == "" { auth.Scheme = "Bearer" } if !v.InConfig("openai.provider_auth.required") { auth.Required = true } return nil } // validateProviderLongContextBudget enforces the long-context admission budget: // every provider referenced by a model group with a positive context window must // have total_context_tokens >= context_window_tokens * long_context_capacity. // Providers with long_context_capacity == 0 declare no long-slot budget and are // therefore unconstrained by this check. func validateProviderLongContextBudget(m ModelCatalogEntry, providerByID map[string]NodeProviderConf) error { if m.ContextWindowTokens <= 0 { return nil } for pid := range m.Providers { p := strings.TrimSpace(pid) prov, ok := providerByID[p] if !ok { continue } if prov.LongContextCapacity <= 0 { continue } required := m.ContextWindowTokens * prov.LongContextCapacity if prov.TotalContextTokens < required { return fmt.Errorf("providers[%q].total_context_tokens (%d) must be >= context_window_tokens (%d) * long_context_capacity (%d) = %d", p, prov.TotalContextTokens, m.ContextWindowTokens, prov.LongContextCapacity, required) } } return nil } func checkUniqueNames(field string, name func(int) string, n int) error { seen := make(map[string]struct{}, n) for i := 0; i < n; i++ { k := name(i) if k == "" { return fmt.Errorf("%s[%d]: name must not be empty", field, i) } if _, dup := seen[k]; dup { return fmt.Errorf("%s: duplicate name %q", field, k) } seen[k] = struct{}{} } return nil } func validateProviderQueueConfig(field string, capacity, maxQueue, queueTimeoutMS, requestTimeoutMS int) error { if capacity < 0 { return fmt.Errorf("%s.capacity must be non-negative", field) } if maxQueue < 0 { return fmt.Errorf("%s.max_queue must be non-negative", field) } if queueTimeoutMS < 0 { return fmt.Errorf("%s.queue_timeout_ms must be non-negative", field) } if requestTimeoutMS < 0 { return fmt.Errorf("%s.request_timeout_ms must be non-negative", field) } return nil } // CheckProviderLegacyConflict validates that when a provider-first resource // declares an ID matching an enabled legacy adapter instance name, the // provider's execution fields match the legacy adapter's fields. A mismatch // produces a validation error to prevent silent precedence between the two // config paths. func CheckProviderLegacyConflict(nodeIdx int, nodeName string, node *NodeDefinition) error { // Build a map of enabled adapter instance names to their configs per type. legacyByKey := make(map[string]*legacyAdapterRef) for _, inst := range node.Adapters.OllamaInstances { if inst.Enabled { legacyByKey[inst.Name] = &legacyAdapterRef{ Type: "ollama", BaseURL: inst.BaseURL, ContextSize: inst.ContextSize, Capacity: inst.Capacity, } } } for _, inst := range node.Adapters.VllmInstances { if inst.Enabled { legacyByKey[inst.Name] = &legacyAdapterRef{ Type: "vllm", Endpoint: inst.Endpoint, Capacity: inst.Capacity, } } } for _, inst := range node.Adapters.OpenAICompatInstances { if inst.Enabled { legacyByKey[inst.Name] = &legacyAdapterRef{ Type: "openai_compat", Provider: inst.Provider, Endpoint: inst.Endpoint, Headers: inst.Headers, Capacity: inst.Capacity, MaxQueue: inst.MaxQueue, QueueTimeoutMS: inst.QueueTimeoutMS, } } } // CLI legacy: enabled cli adapter with its profiles. if node.Adapters.CLI.Enabled { for name, profile := range node.Adapters.CLI.Profiles { legacyByKey[name] = &legacyAdapterRef{ Type: "cli", Command: profile.Command, Args: profile.Args, } } } for j, p := range node.Providers { legacy, exists := legacyByKey[p.ID] if !exists { continue } normType := NormalizeProviderType(p.Type) switch normType { case "ollama": if legacy.Type != "ollama" { continue } if p.BaseURL != "" && p.BaseURL != legacy.BaseURL { return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.ollama instance %q: base_url mismatch (provider=%q, legacy=%q)", nodeIdx, j, p.ID, p.ID, p.BaseURL, legacy.BaseURL) } if p.ContextSize != 0 && p.ContextSize != legacy.ContextSize { return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.ollama instance %q: context_size mismatch (provider=%d, legacy=%d)", nodeIdx, j, p.ID, p.ID, p.ContextSize, legacy.ContextSize) } if p.Capacity != legacy.Capacity { return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.ollama instance %q: capacity mismatch (provider=%d, legacy=%d)", nodeIdx, j, p.ID, p.ID, p.Capacity, legacy.Capacity) } case "vllm", "openai_compat": if legacy.Type != "vllm" && legacy.Type != "openai_compat" { continue } if p.Endpoint != "" && p.Endpoint != legacy.Endpoint { return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.%s instance %q: endpoint mismatch (provider=%q, legacy=%q)", nodeIdx, j, p.ID, legacy.Type, p.ID, p.Endpoint, legacy.Endpoint) } if p.Capacity != legacy.Capacity { return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.%s instance %q: capacity mismatch (provider=%d, legacy=%d)", nodeIdx, j, p.ID, legacy.Type, p.ID, p.Capacity, legacy.Capacity) } case "cli": if legacy.Type != "cli" { continue } if p.Command != "" && p.Command != legacy.Command { return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.cli profile %q: command mismatch (provider=%q, legacy=%q)", nodeIdx, j, p.ID, p.ID, p.Command, legacy.Command) } } } return nil } type legacyAdapterRef struct { Type string Endpoint string BaseURL string Provider string Headers map[string]string ContextSize int Capacity int MaxQueue int QueueTimeoutMS int Command string Args []string }