From a5b27579ecff4c8ba29b2bd71154e8b2ec806208 Mon Sep 17 00:00:00 2001 From: braginini Date: Thu, 30 Jul 2026 15:33:44 +0200 Subject: [PATCH] [combined, management] Load agent-network pricing defaults in combined server; resolve relative paths against datadir --- combined/cmd/root.go | 52 +++++++++++++++++++ combined/config.yaml.example | 19 +++---- management/cmd/management.go | 14 +++-- .../modules/agentnetwork/pricing/override.go | 5 ++ management/internals/server/config/config.go | 7 +-- 5 files changed, 81 insertions(+), 16 deletions(-) diff --git a/combined/cmd/root.go b/combined/cmd/root.go index 5f2564e3a..7eac84ce5 100644 --- a/combined/cmd/root.go +++ b/combined/cmd/root.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "os/signal" + "path/filepath" "strconv" "strings" "sync" @@ -24,6 +25,7 @@ import ( "google.golang.org/grpc" "github.com/netbirdio/netbird/encryption" + agentnetworkpricing "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/pricing" mgmtServer "github.com/netbirdio/netbird/management/internals/server" nbconfig "github.com/netbirdio/netbird/management/internals/server/config" "github.com/netbirdio/netbird/management/server/telemetry" @@ -288,6 +290,11 @@ func (s *serverInstances) createManagementServer(ctx context.Context, cfg *Combi return fmt.Errorf("failed to ensure encryption key: %w", err) } + if err := loadAgentNetworkPricing(ctx, mgmtConfig); err != nil { + cleanupSTUNListeners(s.stunListeners) + return fmt.Errorf("failed to load agent-network pricing defaults: %w", err) + } + LogConfigInfo(mgmtConfig) s.mgmtSrv, err = createManagementServer(cfg, mgmtConfig) @@ -622,6 +629,32 @@ func handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func( acceptFn(conn) } +// loadAgentNetworkPricing loads the management-side LLM pricing defaults +// file for the combined server and starts its periodic reloader. An +// explicitly configured PricingDefaultsFile is required to load (a typo +// must fail startup rather than silently bill with built-ins the operator +// believes they replaced); a relative path is resolved against the data +// directory so a bare filename like "pricing.yaml" lands in the datadir +// alongside the store. With no path configured, / +// is probed and may be absent (compiled-in defaults serve). +func loadAgentNetworkPricing(ctx context.Context, mgmtConfig *nbconfig.Config) error { + pricingPath := mgmtConfig.AgentNetwork.PricingDefaultsFile + required := pricingPath != "" + if !required { + pricingPath = agentnetworkpricing.DefaultFileName + } + if !filepath.IsAbs(pricingPath) { + pricingPath = filepath.Join(mgmtConfig.Datadir, pricingPath) + } + + log.Infof("loading agent-network pricing defaults from %s (required: %v)", pricingPath, required) + if err := agentnetworkpricing.LoadFile(pricingPath, required); err != nil { + return err + } + agentnetworkpricing.StartReloader(ctx, agentnetworkpricing.ReloadInterval) + return nil +} + // logConfig prints all configuration parameters for debugging func logConfig(cfg *CombinedConfig) { log.Info("=== Configuration ===") @@ -698,6 +731,25 @@ func logManagementConfig(cfg *CombinedConfig) { log.Infof(" Relay addresses: %v", cfg.Management.Relays.Addresses) log.Infof(" Relay credentials TTL: %s", cfg.Management.Relays.CredentialsTTL) } + + logAgentNetworkConfig(cfg) +} + +func logAgentNetworkConfig(cfg *CombinedConfig) { + log.Info(" Agent Network:") + pricingPath := cfg.Server.AgentNetwork.PricingDefaultsFile + configured := pricingPath != "" + if !configured { + pricingPath = agentnetworkpricing.DefaultFileName + } + if !filepath.IsAbs(pricingPath) { + pricingPath = filepath.Join(cfg.Management.DataDir, pricingPath) + } + if configured { + log.Infof(" Pricing defaults file: %s", pricingPath) + } else { + log.Infof(" Pricing defaults file: %s (default, optional)", pricingPath) + } } // logEnvVars logs all NB_ environment variables that are currently set diff --git a/combined/config.yaml.example b/combined/config.yaml.example index 783ae01b6..085e4344f 100644 --- a/combined/config.yaml.example +++ b/combined/config.yaml.example @@ -137,12 +137,13 @@ server: # Agent network (LLM gateway) settings (optional) # agentNetwork: - # # Path to the YAML file holding the default LLM pricing table. When empty, - # # {dataDir}/defaults_llm_pricing.yaml is probed; if no file is present the - # # compiled-in defaults are used. Schema: surface ("openai"/"anthropic"/ - # # "bedrock") -> model -> rates in USD per 1k tokens (input_per_1k, - # # output_per_1k, and the optional cached_input_per_1k / cache_read_per_1k / - # # cache_creation_per_1k). The file is re-read periodically (mtime poll). An - # # explicitly configured path that fails to load fails startup; runtime - # # reload errors keep the previous table. - # pricingDefaultsFile: "/var/lib/netbird/defaults_llm_pricing.yaml" + # # Path to the YAML file holding the default LLM pricing table. A relative + # # path is resolved against dataDir, so a bare filename like "pricing.yaml" + # # lands in the data directory. When empty, {dataDir}/defaults_llm_pricing.yaml + # # is probed; if no file is present the compiled-in defaults are used. + # # Schema: surface ("openai"/"anthropic"/"bedrock") -> model -> rates in USD + # # per 1k tokens (input_per_1k, output_per_1k, and the optional + # # cached_input_per_1k / cache_read_per_1k / cache_creation_per_1k). The file + # # is re-read periodically (mtime poll). An explicitly configured path that + # # fails to load fails startup; runtime reload errors keep the previous table. + # pricingDefaultsFile: "pricing.yaml" diff --git a/management/cmd/management.go b/management/cmd/management.go index 181f68f66..147985314 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -117,14 +117,20 @@ var ( // explicitly configured path is required to load (a typo must // fail startup — the operator believes those rates are live); // otherwise /defaults_llm_pricing.yaml is probed and - // may be absent (compiled-in defaults serve). Either way the - // path stays watched: the reloader picks up edits — and the - // file appearing later — without a restart. + // may be absent (compiled-in defaults serve). A relative path + // is resolved against the datadir so a bare filename lands + // alongside the store. Either way the path stays watched: the + // reloader picks up edits — and the file appearing later — + // without a restart. pricingPath := config.AgentNetwork.PricingDefaultsFile pricingRequired := pricingPath != "" if !pricingRequired { - pricingPath = filepath.Join(config.Datadir, agentnetworkpricing.DefaultFileName) + pricingPath = agentnetworkpricing.DefaultFileName } + if !filepath.IsAbs(pricingPath) { + pricingPath = filepath.Join(config.Datadir, pricingPath) + } + log.Infof("loading agent-network pricing defaults from %s (required: %v)", pricingPath, pricingRequired) if err := agentnetworkpricing.LoadFile(pricingPath, pricingRequired); err != nil { return fmt.Errorf("load agent-network pricing defaults: %v", err) } diff --git a/management/internals/modules/agentnetwork/pricing/override.go b/management/internals/modules/agentnetwork/pricing/override.go index f11e4ec1c..07677dbaa 100644 --- a/management/internals/modules/agentnetwork/pricing/override.go +++ b/management/internals/modules/agentnetwork/pricing/override.go @@ -70,11 +70,13 @@ func LoadFile(path string, required bool) error { table, mtime, err := readFile(path) if err != nil { if errors.Is(err, fs.ErrNotExist) && !required { + log.Infof("agent-network pricing defaults file %s not present; serving built-in defaults", path) return nil } return err } storeFileTable(table, mtime) + log.Infof("agent-network pricing defaults loaded from %s", path) return nil } @@ -113,6 +115,8 @@ func reload() { path, lastMtime := fileState.path, fileState.mtime fileState.mu.Unlock() + log.Debugf("agent-network pricing defaults reload: checking %s for changes", path) + st, err := os.Stat(path) if err != nil { if errors.Is(err, fs.ErrNotExist) { @@ -128,6 +132,7 @@ func reload() { return } if st.ModTime().UnixNano() == lastMtime { + log.Debugf("agent-network pricing defaults %s unchanged since last check", path) return } diff --git a/management/internals/server/config/config.go b/management/internals/server/config/config.go index b076abc8a..dc60ed822 100644 --- a/management/internals/server/config/config.go +++ b/management/internals/server/config/config.go @@ -190,9 +190,10 @@ type StoreConfig struct { // AgentNetwork contains agent-network (LLM gateway) configuration. type AgentNetwork struct { // PricingDefaultsFile is the path to the YAML file holding the default - // LLM pricing table (defaults_llm_pricing.yaml). Empty falls back to - // probing /defaults_llm_pricing.yaml; with no file present the - // compiled-in defaults serve. Schema: surface ("openai"/"anthropic"/ + // LLM pricing table (defaults_llm_pricing.yaml). A relative path is + // resolved against , so a bare filename lands alongside the + // store. Empty falls back to probing /defaults_llm_pricing.yaml; + // with no file present the compiled-in defaults serve. Schema: surface ("openai"/"anthropic"/ // "bedrock") -> model -> rates in USD per 1k tokens (input_per_1k, // output_per_1k, and the optional cached_input_per_1k / // cache_read_per_1k / cache_creation_per_1k). File entries replace the