32bc781326
Over time the llama-swap configuration file can get really long and challenging to work with. The -config-dir flag is used for a directory of configuration YAML fragments. These fragments are merged together and into a full configuration and tested for validity. All previous configuration functionality remains unchanged.
258 lines
6.8 KiB
Go
258 lines
6.8 KiB
Go
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
|
|
}
|