agents.default and agents.nodes.<id> in the cluster's settings hold everything agent.yaml can say but the agent's id, control planes, address and certificate, which stay in the file and are refused there. The control plane sends each node its settings on register and, a second apart, when they change; an agent whose settings changed checks they load beside its file, keeps them and starts again on them, engines serving throughout. An edited agent.yaml that loads restarts the agent too. History, audit, ctl config, the console and the portal's settings page cover them as the rest of the settings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
94 lines
3.0 KiB
Go
94 lines
3.0 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/knadh/koanf/parsers/yaml"
|
|
env "github.com/knadh/koanf/providers/env/v2"
|
|
"github.com/knadh/koanf/providers/file"
|
|
"github.com/knadh/koanf/v2"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/agent"
|
|
)
|
|
|
|
// LoadAgent reads an agent configuration, overlaying it on the defaults and
|
|
// then applying LLMGW_-prefixed environment overrides.
|
|
//
|
|
// An empty path loads the defaults, which with --server and --node-id on the
|
|
// command line is enough for a smoke test on a node with no IB.
|
|
func LoadAgent(path string) (agent.Config, error) { return LoadAgentWith(path, nil) }
|
|
|
|
// LoadAgentWith is LoadAgent with the cluster's settings for this node
|
|
// (agents.default and agents.nodes.<id>, as YAML) laid over the file: the
|
|
// file is what the agent needs to start, the settings what it runs on.
|
|
func LoadAgentWith(path string, settings []byte) (agent.Config, error) {
|
|
cfg := agent.DefaultConfig()
|
|
k := koanf.New(".")
|
|
|
|
if path != "" {
|
|
if _, err := os.Stat(path); err != nil {
|
|
return cfg, fmt.Errorf("config file: %w", err)
|
|
}
|
|
if err := k.Load(file.Provider(path), yaml.Parser()); err != nil {
|
|
return cfg, fmt.Errorf("parsing %s: %w", path, err)
|
|
}
|
|
}
|
|
if len(settings) > 0 {
|
|
if err := k.Load(bytesProvider(settings), yaml.Parser()); err != nil {
|
|
return cfg, fmt.Errorf("parsing the cluster's settings for this agent: %w", err)
|
|
}
|
|
}
|
|
|
|
err := k.Load(env.Provider(".", env.Opt{
|
|
Prefix: "LLMGW_",
|
|
TransformFunc: func(key, value string) (string, any) {
|
|
key = strings.ToLower(strings.TrimPrefix(key, "LLMGW_"))
|
|
return strings.ReplaceAll(key, "__", "."), value
|
|
},
|
|
}), nil)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("environment overrides: %w", err)
|
|
}
|
|
|
|
if err := k.UnmarshalWithConf("", &cfg, koanf.UnmarshalConf{Tag: "koanf"}); err != nil {
|
|
return cfg, fmt.Errorf("decoding configuration: %w", err)
|
|
}
|
|
return cfg, validateAgent(cfg)
|
|
}
|
|
|
|
// validateAgent rejects configurations that would fail confusingly at runtime.
|
|
func validateAgent(c agent.Config) error {
|
|
seen := map[string]struct{}{}
|
|
for i, f := range c.Fabrics {
|
|
if f.Fabric == "" {
|
|
return fmt.Errorf("fabrics[%d]: name is required", i)
|
|
}
|
|
if _, dup := seen[f.Fabric]; dup {
|
|
return fmt.Errorf("fabric %q is declared twice", f.Fabric)
|
|
}
|
|
seen[f.Fabric] = struct{}{}
|
|
if len(f.HCAs) == 0 {
|
|
// A fabric with no adapters is worse than no fabric at all: the
|
|
// scheduler would place multi-node replicas here and every shard
|
|
// would come up with NCCL unable to find a path (plan §5.3).
|
|
return fmt.Errorf("fabric %q lists no HCAs", f.Fabric)
|
|
}
|
|
}
|
|
|
|
// The same adapter on two fabrics means one of the labels is wrong, and the
|
|
// consequence is a replica that starts and then hangs on its first
|
|
// all-reduce — the failure mode this labelling exists to prevent.
|
|
byHCA := map[string]string{}
|
|
for _, f := range c.Fabrics {
|
|
for _, h := range f.HCAs {
|
|
if prev, dup := byHCA[h]; dup {
|
|
return fmt.Errorf("HCA %q is claimed by both %q and %q", h, prev, f.Fabric)
|
|
}
|
|
byHCA[h] = f.Fabric
|
|
}
|
|
}
|
|
return nil
|
|
}
|