package config import ( "fmt" "os" "strings" "gopkg.in/yaml.v3" ) type Server struct { Host string `yaml:"host"` Port int `yaml:"port"` ReadTimeoutMS int `yaml:"read_timeout_ms"` CORSOrigins []string `yaml:"cors_origins"` RateLimit RateLimit `yaml:"rate_limit"` } type RateLimit struct { RequestsPerMinute int `yaml:"requests_per_minute"` Burst int `yaml:"burst"` } type Provider struct { Name string `yaml:"name"` Type string `yaml:"type"` Model string `yaml:"model,omitempty"` ModelPath string `yaml:"model_path,omitempty"` Endpoint string `yaml:"endpoint,omitempty"` ContextSize int `yaml:"context_size,omitempty"` MaxTokens int `yaml:"max_tokens,omitempty"` NGPULayers int `yaml:"n_gpu_layers,omitempty"` Temperature float32 `yaml:"temperature,omitempty"` APIKeyEnv string `yaml:"api_key_env,omitempty"` Default bool `yaml:"default,omitempty"` } type RAG struct { Enabled bool `yaml:"enabled"` DataPath string `yaml:"data_path"` ChunkSize int `yaml:"chunk_size"` ChunkOverlap int `yaml:"chunk_overlap"` DBPath string `yaml:"db_path"` TopK int `yaml:"top_k"` Tokenize string `yaml:"tokenize"` } type Persona struct { Name string `yaml:"name"` Tone string `yaml:"tone"` Language string `yaml:"language"` Constraints []string `yaml:"constraints"` Intro string `yaml:"intro"` } type Logging struct { Level string `yaml:"level"` Format string `yaml:"format"` Output string `yaml:"output"` } // Compaction controls automatic context-window management. When the most // recent request's input tokens exceed threshold_ratio × the provider's // reported MaxContextWindow, the older portion of the conversation is // summarized by the LLM and replaced with a single system-role note. // Recent turns (keep_recent_turns) are always preserved verbatim. // // Disabled by default; flip enabled: true in the YAML to opt in. The // model's MaxContextWindow comes from llm.LLMClient.Capabilities(), so // the same setting works for llamacpp, ollama, openai and anthropic // without per-provider configuration. type Compaction struct { Enabled bool `yaml:"enabled"` // ThresholdRatio is the fraction of the context window that, once // exceeded by the previous turn's input tokens, triggers compaction. // Range 0–1; default 0.75 (75% of the window). ThresholdRatio float64 `yaml:"threshold_ratio"` // KeepRecentTurns is how many of the most recent user turns (each // being a user message plus everything up to the next user message) // to keep verbatim after compaction. Older turns are folded into the // summary. Default 4. KeepRecentTurns int `yaml:"keep_recent_turns"` // SummarySystemPrompt is the instruction sent to the LLM when it // summarizes the older turns. Empty falls back to a built-in // bilingual prompt that auto-matches the conversation language. SummarySystemPrompt string `yaml:"summary_system_prompt"` } type Config struct { Server Server `yaml:"server"` Providers []Provider `yaml:"providers"` RAG RAG `yaml:"rag"` Persona Persona `yaml:"persona"` SystemPrompt string `yaml:"system_prompt"` Compaction Compaction `yaml:"compaction"` Logging Logging `yaml:"logging"` } func Load(path string) (*Config, error) { raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config %s: %w", path, err) } expanded := os.ExpandEnv(string(raw)) var cfg Config if err := yaml.Unmarshal([]byte(expanded), &cfg); err != nil { return nil, fmt.Errorf("parse config %s: %w", path, err) } if err := cfg.validate(); err != nil { return nil, err } return &cfg, nil } func (c *Config) validate() error { if len(c.Providers) == 0 { return fmt.Errorf("config: at least one provider must be configured") } defaultCount := 0 for _, p := range c.Providers { if p.Default { defaultCount++ } } if defaultCount == 0 { c.Providers[0].Default = true } else if defaultCount > 1 { return fmt.Errorf("config: multiple providers marked as default") } if c.Server.Port == 0 { c.Server.Port = 7331 } if c.Server.ReadTimeoutMS == 0 { c.Server.ReadTimeoutMS = 30000 } if c.RAG.ChunkSize == 0 { c.RAG.ChunkSize = 500 } if c.RAG.ChunkOverlap == 0 { c.RAG.ChunkOverlap = 50 } if c.RAG.TopK == 0 { c.RAG.TopK = 5 } if c.RAG.Tokenize == "" { c.RAG.Tokenize = "unicode61" } if c.Compaction.Enabled { if c.Compaction.ThresholdRatio <= 0 || c.Compaction.ThresholdRatio > 1 { c.Compaction.ThresholdRatio = 0.75 } if c.Compaction.KeepRecentTurns <= 0 { c.Compaction.KeepRecentTurns = 4 } } return nil } func (c *Config) DefaultProvider() *Provider { for i := range c.Providers { if c.Providers[i].Default { return &c.Providers[i] } } return &c.Providers[0] } func (c *Config) Addr() string { return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port) } func splitPath(path string) []string { return strings.Split(path, string(os.PathSeparator)) }