package config import ( "fmt" "os" "sort" "gopkg.in/yaml.v3" ) const DEFAULT_GROUP_ID = "(default)" const ( LogToStdoutProxy = "proxy" LogToStdoutUpstream = "upstream" LogToStdoutBoth = "both" LogToStdoutNone = "none" ) type MacroEntry struct { Name string Value any } type MacroList []MacroEntry // UnmarshalYAML implements custom YAML unmarshaling that preserves macro definition order func (ml *MacroList) UnmarshalYAML(value *yaml.Node) error { if value.Kind != yaml.MappingNode { return fmt.Errorf("macros must be a mapping") } // yaml.Node.Content for a mapping contains alternating key/value nodes entries := make([]MacroEntry, 0, len(value.Content)/2) for i := 0; i < len(value.Content); i += 2 { keyNode := value.Content[i] valueNode := value.Content[i+1] var name string if err := keyNode.Decode(&name); err != nil { return fmt.Errorf("failed to decode macro name: %w", err) } var val any if err := valueNode.Decode(&val); err != nil { return fmt.Errorf("failed to decode macro value for '%s': %w", name, err) } entries = append(entries, MacroEntry{Name: name, Value: val}) } *ml = entries return nil } // Get retrieves a macro value by name func (ml MacroList) Get(name string) (any, bool) { for _, entry := range ml { if entry.Name == name { return entry.Value, true } } return nil, false } // ToMap converts MacroList to a map (for backward compatibility if needed) func (ml MacroList) ToMap() map[string]any { result := make(map[string]any, len(ml)) for _, entry := range ml { result[entry.Name] = entry.Value } return result } type GroupConfig struct { Swap bool `yaml:"swap"` Exclusive bool `yaml:"exclusive"` Persistent bool `yaml:"persistent"` Members []string `yaml:"members"` } // set default values for GroupConfig func (c *GroupConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type rawGroupConfig GroupConfig defaults := rawGroupConfig{ Swap: true, Exclusive: true, Persistent: false, Members: []string{}, } if err := unmarshal(&defaults); err != nil { return err } *c = GroupConfig(defaults) return nil } type HooksConfig struct { OnStartup HookOnStartup `yaml:"on_startup"` } type HookOnStartup struct { Preload []string `yaml:"preload"` } type Config struct { HealthCheckTimeout int `yaml:"healthCheckTimeout"` LogRequests bool `yaml:"logRequests"` LogLevel string `yaml:"logLevel"` LogTimeFormat string `yaml:"logTimeFormat"` LogToStdout string `yaml:"logToStdout"` MetricsMaxInMemory int `yaml:"metricsMaxInMemory"` CaptureBuffer int `yaml:"captureBuffer"` Performance PerformanceConfig `yaml:"performance"` GlobalTTL int `yaml:"globalTTL"` Models map[string]ModelConfig `yaml:"models"` /* key is model ID */ Profiles map[string][]string `yaml:"profiles"` // routing is the canonical source for swap/scheduling configuration. // New code must read Routing, never the backwards-compat fields below. Routing RoutingConfig `yaml:"routing"` // Groups and Matrix are permanent backwards-compat input fields for the // legacy top-level `groups:`/`matrix:` keys. They are normalized into // Routing by LoadConfigFromReader. New code must not read them directly. Groups map[string]GroupConfig `yaml:"groups"` /* key is group ID */ Matrix *MatrixConfig `yaml:"matrix"` // for key/value replacements in model's cmd, cmdStop, proxy, checkEndPoint Macros MacroList `yaml:"macros"` // map aliases to actual model IDs aliases map[string]string // automatic port assignments StartPort int `yaml:"startPort"` // hooks, see: #209 Hooks HooksConfig `yaml:"hooks"` // send loading state in reasoning SendLoadingState bool `yaml:"sendLoadingState"` // present aliases to /v1/models OpenAI API listing IncludeAliasesInList bool `yaml:"includeAliasesInList"` // support API keys, see issue #433, #50, #251 RequiredAPIKeys []string `yaml:"apiKeys"` // support remote peers, see issue #433, #296 Peers PeerDictionaryConfig `yaml:"peers"` // upstream controls behaviour of the /upstream passthrough endpoint Upstream UpstreamConfig `yaml:"upstream"` } // RoutingConfig is the canonical, normalized routing/scheduling configuration. type RoutingConfig struct { Scheduler SchedulerConfig `yaml:"scheduler"` Router RouterConfig `yaml:"router"` } type SchedulerConfig struct { Use string `yaml:"use"` // default "fifo" Settings SchedulerSettings `yaml:"settings"` } type SchedulerSettings struct { Fifo FifoConfig `yaml:"fifo"` } type FifoConfig struct { Priority map[string]int `yaml:"priority"` // model ID -> priority, default 0 } type RouterConfig struct { Use string `yaml:"use"` // "group" (default) | "matrix" Settings RouterSettings `yaml:"settings"` } type RouterSettings struct { Groups map[string]GroupConfig `yaml:"groups"` Matrix *MatrixConfig `yaml:"matrix"` } func (c *Config) RealModelName(search string) (string, bool) { if _, found := c.Models[search]; found { return search, true } else if name, found := c.aliases[search]; found { return name, found } else { return "", false } } func (c *Config) FindConfig(modelName string) (ModelConfig, string, bool) { if realName, found := c.RealModelName(modelName); !found { return ModelConfig{}, "", false } else { return c.Models[realName], realName, true } } func LoadConfig(path string) (Config, error) { file, err := os.Open(path) if err != nil { return Config{}, err } defer file.Close() return LoadConfigFromReader(file) } // rewrites the yaml to include a default group with any orphaned models func AddDefaultGroupToConfig(config Config) Config { if config.Groups == nil { config.Groups = make(map[string]GroupConfig) } defaultGroup := GroupConfig{ Swap: true, Exclusive: true, Members: []string{}, } // if groups is empty, create a default group and put // all models into it if len(config.Groups) == 0 { for modelName := range config.Models { defaultGroup.Members = append(defaultGroup.Members, modelName) } } else { // iterate over existing group members and add non-grouped models into the default group for modelName := range config.Models { foundModel := false found: // search for the model in existing groups for _, groupConfig := range config.Groups { for _, member := range groupConfig.Members { if member == modelName { foundModel = true break found } } } if !foundModel { defaultGroup.Members = append(defaultGroup.Members, modelName) } } } sort.Strings(defaultGroup.Members) // make consistent ordering for testing config.Groups[DEFAULT_GROUP_ID] = defaultGroup return config }