package edgevalidate import ( "fmt" 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) } } return nil } 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 }