package config import ( "fmt" "strings" "github.com/spf13/viper" ) func Load(cfgFile string) (*NodeConfig, error) { v := viper.New() v.SetConfigFile(cfgFile) setDefaults(v) if err := v.ReadInConfig(); err != nil { return nil, err } var cfg NodeConfig if err := v.Unmarshal(&cfg); err != nil { return nil, err } return &cfg, nil } func LoadEdge(cfgFile string) (*EdgeConfig, error) { v := viper.New() v.SetConfigFile(cfgFile) setEdgeDefaults(v) if err := v.ReadInConfig(); err != nil { return nil, err } var cfg EdgeConfig if err := v.Unmarshal(&cfg); err != nil { return nil, err } if !v.InConfig("console.target") { if v.InConfig("console.agent") { cfg.Console.Target = cfg.Console.Agent } else if v.InConfig("console.model") { cfg.Console.Target = cfg.Console.Model } } if err := validateOpenAIRoutes(cfg.OpenAI.ModelRoutes); err != nil { return nil, err } if err := validateOpenAIPrincipalTokens(cfg.OpenAI.PrincipalTokens); err != nil { return nil, err } if err := normalizeOpenAIProviderAuth(v, &cfg.OpenAI.ProviderAuth); err != nil { return nil, err } if cfg.LongContextThresholdTokens <= 0 { return nil, fmt.Errorf("long_context_threshold_tokens must be positive") } // Collect all provider IDs from nodes[].providers[] for cross-referencing // and validate uniqueness within each node. providerIDs := make(map[string]struct{}) providerByID := make(map[string]NodeProviderConf) for i := range cfg.Nodes { kind, err := NormalizeAgentKind(cfg.Nodes[i].AgentKind) if err != nil { return nil, fmt.Errorf("nodes[%d] alias=%q: %w", i, cfg.Nodes[i].Alias, err) } cfg.Nodes[i].AgentKind = kind if err := normalizeAdapters(&cfg.Nodes[i].Adapters); err != nil { name := cfg.Nodes[i].ID if name == "" { name = cfg.Nodes[i].Alias } return nil, fmt.Errorf("nodes[%d] %q adapters: %w", i, name, err) } // Validate unique provider IDs within this node and across all nodes. seenProviderIDs := make(map[string]struct{}, len(cfg.Nodes[i].Providers)) for j, p := range cfg.Nodes[i].Providers { if err := p.Validate(); err != nil { return nil, fmt.Errorf("nodes[%d].providers[%d]: %w", i, j, err) } if _, dup := seenProviderIDs[p.ID]; dup { return nil, fmt.Errorf("nodes[%d].providers: duplicate provider id %q within node", i, p.ID) } seenProviderIDs[p.ID] = struct{}{} if _, globalDup := providerIDs[p.ID]; globalDup { return nil, fmt.Errorf("nodes[%d].providers[%d]: duplicate provider id %q across nodes (global uniqueness violation)", i, j, p.ID) } providerIDs[p.ID] = struct{}{} providerByID[p.ID] = p } } for i := range cfg.Nodes { if err := CheckProviderLegacyConflict(i, cfg.Nodes[i].Alias, &cfg.Nodes[i]); err != nil { return nil, err } } // Build the provider->servedModels index so Validate can check membership. serveModels := buildProviderServedModelsIndex(cfg.Nodes) // Validate models[].providers reference valid provider IDs. seenModelIDs := make(map[string]struct{}, len(cfg.Models)) for i, m := range cfg.Models { id := strings.TrimSpace(m.ID) if id == "" { return nil, fmt.Errorf("models[%d]: id must not be empty", i) } if _, dup := seenModelIDs[id]; dup { return nil, fmt.Errorf("models: duplicate model id %q", id) } seenModelIDs[id] = struct{}{} if err := m.Validate(providerIDs, serveModels); err != nil { return nil, fmt.Errorf("models[%d]: %w", i, err) } if err := validateProviderLongContextBudget(m, providerByID); err != nil { return nil, fmt.Errorf("models[%d]: %w", i, err) } } return &cfg, nil } func setDefaults(v *viper.Viper) { v.SetDefault("transport.edge_addr", "localhost:9090") v.SetDefault("reconnect.interval_sec", 10) v.SetDefault("reconnect.max_attempts", 10) v.SetDefault("logging.level", "info") v.SetDefault("metrics.port", 9091) } func setEdgeDefaults(v *viper.Viper) { v.SetDefault("server.listen", "0.0.0.0:9090") v.SetDefault("bootstrap.listen", "0.0.0.0:18080") v.SetDefault("bootstrap.artifact_dir", "artifacts") v.SetDefault("openai.enabled", false) v.SetDefault("openai.listen", "0.0.0.0:18081") v.SetDefault("openai.bearer_token", "") v.SetDefault("openai.adapter", "ollama") v.SetDefault("openai.session_id", "openai") v.SetDefault("openai.timeout_sec", 120) v.SetDefault("openai.strict_output", true) v.SetDefault("openai.strict_stream_buffer", false) v.SetDefault("a2a.enabled", false) v.SetDefault("a2a.listen", "0.0.0.0:8081") v.SetDefault("a2a.path", "/a2a") v.SetDefault("a2a.adapter", "cli") v.SetDefault("a2a.session_id", "a2a") v.SetDefault("a2a.timeout_sec", 120) v.SetDefault("logging.level", "info") v.SetDefault("metrics.port", 19092) v.SetDefault("tls.enabled", false) v.SetDefault("console.adapter", "cli") v.SetDefault("console.target", "claude") v.SetDefault("console.session_id", "default") v.SetDefault("console.background", false) v.SetDefault("console.timeout_sec", 120) v.SetDefault("control_plane.enabled", false) v.SetDefault("control_plane.wire_addr", "") v.SetDefault("control_plane.reconnect_interval_sec", 5) v.SetDefault("refresh.enabled", false) v.SetDefault("refresh.listen", "127.0.0.1:19093") v.SetDefault("long_context_threshold_tokens", 100000) }