package config import ( "fmt" "strings" ) const AgentKindGenericNode = "generic-node" // NormalizeAgentKind returns the canonical agent kind for a node definition. // An empty value defaults to generic-node; any other unsupported value is // rejected so misconfigured kinds fail at config load time. func NormalizeAgentKind(kind string) (string, error) { switch kind { case "": return AgentKindGenericNode, nil case AgentKindGenericNode: return kind, nil default: return "", fmt.Errorf("invalid agent_kind %q (allowed: %q)", kind, AgentKindGenericNode) } } // NormalizeProviderType returns the canonical driver name for provider type. func NormalizeProviderType(t string) string { switch strings.ToLower(strings.TrimSpace(t)) { case "openai_compat", "openai_api", "vllm", "vllm-mlx", "lemonade", "sglang", "seulgivibe_claude", "seulgivibe_openai": return "openai_compat" case "ollama": return "ollama" case "cli": return "cli" default: return t } } // NormalizeAdapters promotes legacy single-instance Ollama/Vllm fields into the // typed instance slices and validates that all instance names are unique. func NormalizeAdapters(a *AdaptersConf) error { return normalizeAdapters(a) } func normalizeAdapters(a *AdaptersConf) error { if a.Ollama.Enabled { existing := findOllamaInstance(a.OllamaInstances, "ollama") if existing == nil { a.OllamaInstances = append([]OllamaInstanceConf{{ Name: "ollama", Enabled: a.Ollama.Enabled, BaseURL: a.Ollama.BaseURL, ContextSize: a.Ollama.ContextSize, Capacity: a.Ollama.Capacity, MaxQueue: a.Ollama.MaxQueue, QueueTimeoutMS: a.Ollama.QueueTimeoutMS, RequestTimeoutMS: a.Ollama.RequestTimeoutMS, }}, a.OllamaInstances...) } else if !sameOllamaInstance(*existing, a.Ollama) { return fmt.Errorf("ollama: legacy field conflicts with explicit instance %q: enabled/base_url/context_size/capacity/max_queue/queue_timeout_ms/request_timeout_ms mismatch", "ollama") } } if a.Vllm.Enabled { existing := findVllmInstance(a.VllmInstances, "vllm") if existing == nil { a.VllmInstances = append([]VllmInstanceConf{{ Name: "vllm", Enabled: a.Vllm.Enabled, Endpoint: a.Vllm.Endpoint, Capacity: a.Vllm.Capacity, MaxQueue: a.Vllm.MaxQueue, QueueTimeoutMS: a.Vllm.QueueTimeoutMS, RequestTimeoutMS: a.Vllm.RequestTimeoutMS, }}, a.VllmInstances...) } else if !sameVllmInstance(*existing, a.Vllm) { return fmt.Errorf("vllm: legacy field conflicts with explicit instance %q: enabled/endpoint/capacity/max_queue/queue_timeout_ms/request_timeout_ms mismatch", "vllm") } } if a.OpenAICompat.Enabled { existing := findOpenAICompatInstance(a.OpenAICompatInstances, "openai_compat") if existing == nil { a.OpenAICompatInstances = append([]OpenAICompatInstanceConf{{ Name: "openai_compat", Enabled: a.OpenAICompat.Enabled, Provider: a.OpenAICompat.Provider, Endpoint: a.OpenAICompat.Endpoint, Headers: a.OpenAICompat.Headers, Capacity: a.OpenAICompat.Capacity, MaxQueue: a.OpenAICompat.MaxQueue, QueueTimeoutMS: a.OpenAICompat.QueueTimeoutMS, RequestTimeoutMS: a.OpenAICompat.RequestTimeoutMS, }}, a.OpenAICompatInstances...) } else if !sameOpenAICompatInstance(*existing, a.OpenAICompat) { return fmt.Errorf("openai_compat: legacy field conflicts with explicit instance %q: enabled/provider/endpoint/headers/capacity/max_queue/queue_timeout_ms/request_timeout_ms mismatch", "openai_compat") } } if err := checkUniqueNames("ollama_instances", func(i int) string { return a.OllamaInstances[i].Name }, len(a.OllamaInstances)); err != nil { return err } if err := checkUniqueNames("vllm_instances", func(i int) string { return a.VllmInstances[i].Name }, len(a.VllmInstances)); err != nil { return err } if err := checkUniqueNames("openai_compat_instances", func(i int) string { return a.OpenAICompatInstances[i].Name }, len(a.OpenAICompatInstances)); err != nil { return err } for i, inst := range a.OllamaInstances { field := fmt.Sprintf("ollama_instances[%d]", i) if err := validateProviderQueueConfig(field, inst.Capacity, inst.MaxQueue, inst.QueueTimeoutMS, inst.RequestTimeoutMS); err != nil { return err } } for i, inst := range a.VllmInstances { field := fmt.Sprintf("vllm_instances[%d]", i) if err := validateProviderQueueConfig(field, inst.Capacity, inst.MaxQueue, inst.QueueTimeoutMS, inst.RequestTimeoutMS); err != nil { return err } } for i, inst := range a.OpenAICompatInstances { field := fmt.Sprintf("openai_compat_instances[%d]", i) if err := validateProviderQueueConfig(field, inst.Capacity, inst.MaxQueue, inst.QueueTimeoutMS, inst.RequestTimeoutMS); err != nil { return err } } return nil } func sameOllamaInstance(inst OllamaInstanceConf, legacy OllamaConf) bool { return inst.Enabled == legacy.Enabled && inst.BaseURL == legacy.BaseURL && inst.ContextSize == legacy.ContextSize && inst.Capacity == legacy.Capacity && inst.MaxQueue == legacy.MaxQueue && inst.QueueTimeoutMS == legacy.QueueTimeoutMS && inst.RequestTimeoutMS == legacy.RequestTimeoutMS } func sameVllmInstance(inst VllmInstanceConf, legacy VllmConf) bool { return inst.Enabled == legacy.Enabled && inst.Endpoint == legacy.Endpoint && inst.Capacity == legacy.Capacity && inst.MaxQueue == legacy.MaxQueue && inst.QueueTimeoutMS == legacy.QueueTimeoutMS && inst.RequestTimeoutMS == legacy.RequestTimeoutMS } func findOllamaInstance(instances []OllamaInstanceConf, name string) *OllamaInstanceConf { for i := range instances { if instances[i].Name == name { return &instances[i] } } return nil } func findVllmInstance(instances []VllmInstanceConf, name string) *VllmInstanceConf { for i := range instances { if instances[i].Name == name { return &instances[i] } } return nil } func sameOpenAICompatInstance(inst OpenAICompatInstanceConf, legacy OpenAICompatConf) bool { if inst.Enabled != legacy.Enabled || inst.Provider != legacy.Provider || inst.Endpoint != legacy.Endpoint || inst.Capacity != legacy.Capacity || inst.MaxQueue != legacy.MaxQueue || inst.QueueTimeoutMS != legacy.QueueTimeoutMS || inst.RequestTimeoutMS != legacy.RequestTimeoutMS { return false } if len(inst.Headers) != len(legacy.Headers) { return false } for k, v := range inst.Headers { if lv, ok := legacy.Headers[k]; !ok || lv != v { return false } } return true } func findOpenAICompatInstance(instances []OpenAICompatInstanceConf, name string) *OpenAICompatInstanceConf { for i := range instances { if instances[i].Name == name { return &instances[i] } } return nil }