iop/apps/edge/internal/edgevalidate/validate.go

197 lines
5.5 KiB
Go

package edgevalidate
import (
"fmt"
"strings"
edgenode "iop/apps/edge/internal/node"
"iop/packages/go/config"
)
// ValidateEdgeConfig validates Edge config fields that config.LoadEdge does
// not check: adapter endpoint presence, per-node adapter-enabled requirements,
// and node store consistency (token uniqueness). It is the shared validation
// path used by both `config check` and the refresh candidate loader so that
// the same ruleset applies regardless of the entry point.
func ValidateEdgeConfig(cfg *config.EdgeConfig) error {
_, err := edgenode.LoadFromConfig(cfg.Nodes)
if err != nil {
return err
}
for i, n := range cfg.Nodes {
name := n.ID
if name == "" {
name = n.Alias
}
if name == "" {
name = fmt.Sprintf("index %d", i)
}
hasOllama := anyOllamaEnabled(n.Adapters)
hasVllm := anyVllmEnabled(n.Adapters)
hasOpenAICompat := anyOpenAICompatEnabled(n.Adapters)
if !hasOllama && !n.Adapters.CLI.Enabled && !hasVllm && !hasOpenAICompat {
return fmt.Errorf("node %q: at least one adapter must be enabled", name)
}
if n.Adapters.Ollama.Enabled && n.Adapters.Ollama.BaseURL == "" {
return fmt.Errorf("node %q: ollama adapter base_url must not be empty", name)
}
for j, inst := range n.Adapters.OllamaInstances {
if inst.Enabled && inst.BaseURL == "" {
return fmt.Errorf("node %q: ollama_instances[%d] %q: base_url must not be empty", name, j, inst.Name)
}
}
if n.Adapters.Vllm.Enabled && n.Adapters.Vllm.Endpoint == "" {
return fmt.Errorf("node %q: vllm adapter endpoint must not be empty", name)
}
for j, inst := range n.Adapters.VllmInstances {
if inst.Enabled && inst.Endpoint == "" {
return fmt.Errorf("node %q: vllm_instances[%d] %q: endpoint must not be empty", name, j, inst.Name)
}
}
if n.Adapters.OpenAICompat.Enabled && n.Adapters.OpenAICompat.Endpoint == "" {
return fmt.Errorf("node %q: openai_compat adapter endpoint must not be empty", name)
}
for j, inst := range n.Adapters.OpenAICompatInstances {
if inst.Enabled && inst.Endpoint == "" {
return fmt.Errorf("node %q: openai_compat_instances[%d] %q: endpoint must not be empty", name, j, inst.Name)
}
}
if n.Runtime.Concurrency < 0 {
return fmt.Errorf("node %q: runtime concurrency must be non-negative", name)
}
if err := validateProviderAdapterReferences(name, n); err != nil {
return err
}
}
return nil
}
func validateProviderAdapterReferences(nodeName string, n config.NodeDefinition) error {
idx := buildAdapterIndex(n.Adapters)
for i, p := range n.Providers {
adapter := strings.TrimSpace(p.Adapter)
if adapter == "" {
return fmt.Errorf("node %q: providers[%d] %q: adapter must not be empty", nodeName, i, p.ID)
}
if p.Category == config.CategoryCLI && !n.Adapters.CLI.Enabled {
return fmt.Errorf("node %q: providers[%d] %q: category cli requires enabled cli adapter", nodeName, i, p.ID)
}
if idx.exact[adapter] {
continue
}
if count, knownType := idx.typeCount[adapter]; knownType {
switch count {
case 0:
return fmt.Errorf("node %q: providers[%d] %q: adapter %q does not resolve to an enabled adapter instance", nodeName, i, p.ID, adapter)
case 1:
continue
default:
return fmt.Errorf("node %q: providers[%d] %q: adapter %q is ambiguous; use an enabled adapter instance key", nodeName, i, p.ID, adapter)
}
}
return fmt.Errorf("node %q: providers[%d] %q: adapter %q does not resolve to an enabled adapter instance", nodeName, i, p.ID, adapter)
}
return nil
}
type adapterIndex struct {
exact map[string]bool
typeKeys map[string]map[string]struct{}
typeCount map[string]int
}
func buildAdapterIndex(a config.AdaptersConf) adapterIndex {
idx := adapterIndex{
exact: make(map[string]bool),
typeKeys: make(map[string]map[string]struct{}),
typeCount: map[string]int{"ollama": 0, "vllm": 0, "openai_compat": 0, "cli": 0},
}
if a.Ollama.Enabled {
idx.add("ollama", "ollama")
}
for _, inst := range a.OllamaInstances {
if inst.Enabled {
idx.add("ollama", defaultAdapterKey(inst.Name, "ollama"))
}
}
if a.Vllm.Enabled {
idx.add("vllm", "vllm")
}
for _, inst := range a.VllmInstances {
if inst.Enabled {
idx.add("vllm", defaultAdapterKey(inst.Name, "vllm"))
}
}
if a.OpenAICompat.Enabled {
idx.add("openai_compat", "openai_compat")
}
for _, inst := range a.OpenAICompatInstances {
if inst.Enabled {
idx.add("openai_compat", defaultAdapterKey(inst.Name, "openai_compat"))
}
}
if a.CLI.Enabled {
idx.add("cli", "cli")
}
for typeName, keys := range idx.typeKeys {
idx.typeCount[typeName] = len(keys)
}
return idx
}
func (idx adapterIndex) add(typeName, key string) {
key = strings.TrimSpace(key)
if key == "" {
return
}
idx.exact[key] = true
if idx.typeKeys[typeName] == nil {
idx.typeKeys[typeName] = make(map[string]struct{})
}
idx.typeKeys[typeName][key] = struct{}{}
}
func defaultAdapterKey(name, typeName string) string {
if strings.TrimSpace(name) == "" {
return typeName
}
return name
}
func anyOllamaEnabled(a config.AdaptersConf) bool {
if a.Ollama.Enabled {
return true
}
for _, inst := range a.OllamaInstances {
if inst.Enabled {
return true
}
}
return false
}
func anyVllmEnabled(a config.AdaptersConf) bool {
if a.Vllm.Enabled {
return true
}
for _, inst := range a.VllmInstances {
if inst.Enabled {
return true
}
}
return false
}
func anyOpenAICompatEnabled(a config.AdaptersConf) bool {
if a.OpenAICompat.Enabled {
return true
}
for _, inst := range a.OpenAICompatInstances {
if inst.Enabled {
return true
}
}
return false
}