package config import ( "fmt" "net/url" "sort" "strings" ) const AgentKindGenericNode = "generic-node" // resolveProtocolProfiles resolves the top-level protocol_profiles catalog // (built-in + custom overlays) and writes an immutable ConcreteProtocolProfile // snapshot onto each provider that has a profile selector or a legacy type // mapping. It is called once during Edge config normalization. func resolveProtocolProfiles(cfg *EdgeConfig) error { catalog, err := buildProfileCatalog(cfg) if err != nil { return err } profileIDs := make([]string, 0, len(catalog)) for id := range catalog { profileIDs = append(profileIDs, id) } sort.Strings(profileIDs) for _, id := range profileIDs { if _, err := ResolveProtocolProfile(id, "", catalog); err != nil { return fmt.Errorf("protocol_profiles[%q]: %w", id, err) } } for i := range cfg.Nodes { for j := range cfg.Nodes[i].Providers { p := &cfg.Nodes[i].Providers[j] // Skip providers that have no explicit profile selector and no // legacy type mapping. Only OpenAI-compatible aliases and explicit // profile selectors produce a concrete profile snapshot; other // provider types (ollama, cli, etc.) are handled by their own // adapter paths. if strings.TrimSpace(p.Profile) == "" && LegacyProviderTypeToProfile(p.Type) == "" { continue } resolved, err := ResolveProtocolProfile(p.Profile, p.Type, catalog) if err != nil { return fmt.Errorf("nodes[%d].providers[%d] %q: %w", i, j, p.ID, err) } resolved = resolved.Clone() if adapterRef := strings.TrimSpace(p.Adapter); adapterRef != "" { backingEndpoint, err := resolveProtocolBackingEndpoint(cfg.Nodes[i].Adapters, adapterRef) if err != nil { nodeID := cfg.Nodes[i].ID if nodeID == "" { nodeID = cfg.Nodes[i].Alias } return fmt.Errorf("node %q provider %q adapter %q: %w", nodeID, p.ID, adapterRef, err) } normalized, err := overlayProtocolEndpoint(resolved.BaseURL, backingEndpoint) if err != nil { return fmt.Errorf("nodes[%d].providers[%d] %q: backing adapter endpoint: %w", i, j, p.ID, err) } resolved.BaseURL = normalized } if endpoint := effectiveProtocolEndpoint(*p); endpoint != "" { normalized, err := overlayProtocolEndpoint(resolved.BaseURL, endpoint) if err != nil { return fmt.Errorf("nodes[%d].providers[%d] %q: protocol endpoint: %w", i, j, p.ID, err) } resolved.BaseURL = normalized } snapshot := resolved.Clone() p.RuntimeProfile = &snapshot } } return nil } // resolveProtocolBackingEndpoint resolves the legacy HTTP adapter instance // that supplies transport settings for an adapter-backed protocol profile. // Exact instance names win; a type-name route is valid only with one enabled // instance, matching the Node router's ambiguity contract. func resolveProtocolBackingEndpoint(adapters AdaptersConf, ref string) (string, error) { ref = strings.TrimSpace(ref) type exactMatch struct { enabled bool endpoint string typeName string } var exact []exactMatch for _, inst := range adapters.VllmInstances { if inst.Name == ref { exact = append(exact, exactMatch{inst.Enabled, inst.Endpoint, "vllm"}) } } for _, inst := range adapters.OpenAICompatInstances { if inst.Name == ref { exact = append(exact, exactMatch{inst.Enabled, inst.Endpoint, "openai_compat"}) } } if len(exact) > 1 { return "", fmt.Errorf("backing adapter reference is ambiguous across supported HTTP adapter types") } if len(exact) == 1 { if !exact[0].enabled { return "", fmt.Errorf("backing %s adapter instance is disabled", exact[0].typeName) } return exact[0].endpoint, nil } var enabled []string disabled := 0 switch ref { case "vllm": for _, inst := range adapters.VllmInstances { if inst.Enabled { enabled = append(enabled, inst.Endpoint) } else { disabled++ } } case "openai_compat": for _, inst := range adapters.OpenAICompatInstances { if inst.Enabled { enabled = append(enabled, inst.Endpoint) } else { disabled++ } } default: return "", fmt.Errorf("backing adapter reference does not match a vllm or openai_compat instance") } switch len(enabled) { case 1: return enabled[0], nil case 0: if disabled > 0 { return "", fmt.Errorf("backing adapter type has no enabled instance") } return "", fmt.Errorf("backing adapter reference is missing") default: return "", fmt.Errorf("backing adapter type is ambiguous (%d enabled instances)", len(enabled)) } } // buildProfileCatalog combines the immutable built-in profiles with the // top-level custom overlays from EdgeConfig. Custom overlays may extend // built-ins via the Base field. func buildProfileCatalog(cfg *EdgeConfig) (map[string]ProtocolProfileConf, error) { catalog := BuiltInProtocolProfileCatalog() for id, p := range cfg.ProtocolProfiles { id = strings.TrimSpace(id) if id == "" { return nil, fmt.Errorf("protocol_profiles: profile id must not be empty") } if _, reserved := catalog[id]; reserved { return nil, fmt.Errorf("protocol_profiles[%q]: built-in profile id is reserved", id) } catalog[id] = deepCopyProfileConf(p) } return catalog, nil } // effectiveProtocolEndpoint applies provider-first precedence: endpoint is the // HTTP adapter endpoint, while base_url is accepted as a compatibility alias. func effectiveProtocolEndpoint(p NodeProviderConf) string { if endpoint := strings.TrimSpace(p.Endpoint); endpoint != "" { return endpoint } return strings.TrimSpace(p.BaseURL) } func normalizeProtocolEndpoint(raw string) (string, error) { parsed, err := url.Parse(strings.TrimSpace(raw)) if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { return "", fmt.Errorf("%q must be an absolute http(s) URL", raw) } parsed.Path = strings.TrimRight(parsed.Path, "/") return parsed.String(), nil } // overlayProtocolEndpoint replaces the upstream origin and honors an explicit // provider path. A root-only endpoint retains the profile's documented base // path (for example /v1), preserving legacy root endpoint compatibility. func overlayProtocolEndpoint(profileBaseURL, rawEndpoint string) (string, error) { endpoint, err := normalizeProtocolEndpoint(rawEndpoint) if err != nil { return "", err } parsedEndpoint, _ := url.Parse(endpoint) if parsedEndpoint.Path == "" || parsedEndpoint.Path == "/" { parsedProfile, parseErr := url.Parse(profileBaseURL) if parseErr != nil { return "", fmt.Errorf("profile base_url %q is invalid: %w", profileBaseURL, parseErr) } parsedEndpoint.Path = strings.TrimRight(parsedProfile.Path, "/") } return parsedEndpoint.String(), nil } // 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 }