Files
Ilker ManapandClaude Opus 5.5 dc243f8840 The nodes' agent settings kept with the cluster's, agent.yaml down to what an agent needs to start
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>
2026-10-08 18:06:36 +02:00

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
}