[combined, management] Load agent-network pricing defaults in combined server; resolve relative paths against datadir

This commit is contained in:
braginini
2026-07-30 15:33:44 +02:00
parent 2b7703a54f
commit a5b27579ec
5 changed files with 81 additions and 16 deletions

View File

@@ -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, <datadir>/<DefaultFileName>
// 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

View File

@@ -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"

View File

@@ -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 <datadir>/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)
}

View File

@@ -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
}

View File

@@ -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 <Datadir>/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 <Datadir>, so a bare filename lands alongside the
// store. Empty falls back to probing <Datadir>/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