package adapters import ( "fmt" "go.uber.org/zap" "google.golang.org/protobuf/types/known/structpb" "iop/apps/node/internal/adapters/cli" "iop/apps/node/internal/adapters/mock" "iop/apps/node/internal/adapters/ollama" "iop/apps/node/internal/adapters/openai_compat" "iop/apps/node/internal/adapters/vllm" "iop/packages/go/config" iop "iop/proto/gen/iop" ) // BuildFromPayload creates a Registry from a NodeConfigPayload received from edge. func BuildFromPayload(payload *iop.NodeConfigPayload, logger *zap.Logger) (*Registry, error) { reg := NewRegistry() for _, ac := range payload.GetAdapters() { if !ac.GetEnabled() { continue } instanceKey := ac.GetName() typeName := ac.GetType() switch typeName { case "mock": reg.RegisterKeyed(instanceKey, typeName, mock.New(logger)) case "ollama": var cfg config.OllamaConf if m := ac.GetOllama(); m != nil { cfg = ollamaConfFromProto(m) } else { cfg = ollamaConfFromStruct(ac.GetSettings()) } reg.RegisterKeyed(instanceKey, typeName, ollama.New(cfg, logger, instanceKey)) case "vllm": var cfg config.VllmConf if m := ac.GetVllm(); m != nil { cfg = vllmConfFromProto(m) } else { cfg = vllmConfFromStruct(ac.GetSettings()) } reg.RegisterKeyed(instanceKey, typeName, vllm.New(cfg, logger, instanceKey)) case "openai_compat": var cfg config.OpenAICompatConf if m := ac.GetOpenaiCompat(); m != nil { cfg = openAICompatConfFromProto(m) } else { cfg = openAICompatConfFromStruct(ac.GetSettings()) } reg.RegisterKeyed(instanceKey, typeName, openai_compat.New(cfg, logger, instanceKey)) case "cli": var cfg config.CLIConf if m := ac.GetCli(); m != nil { cfg = cliConfFromProto(m) } else { cfg = cliConfFromStruct(ac.GetSettings()) } reg.RegisterKeyed(instanceKey, typeName, cli.New(cfg, logger)) default: return nil, fmt.Errorf("adapters: unknown adapter type %q", typeName) } } return reg, nil } func ollamaConfFromProto(m *iop.OllamaAdapterConfig) config.OllamaConf { return config.OllamaConf{ Enabled: true, BaseURL: m.GetBaseUrl(), ContextSize: int(m.GetContextSize()), Capacity: int(m.GetCapacity()), MaxQueue: int(m.GetMaxQueue()), QueueTimeoutMS: int(m.GetQueueTimeoutMs()), RequestTimeoutMS: int(m.GetRequestTimeoutMs()), } } func vllmConfFromProto(m *iop.VllmAdapterConfig) config.VllmConf { return config.VllmConf{ Enabled: true, Endpoint: m.GetEndpoint(), Capacity: int(m.GetCapacity()), MaxQueue: int(m.GetMaxQueue()), QueueTimeoutMS: int(m.GetQueueTimeoutMs()), RequestTimeoutMS: int(m.GetRequestTimeoutMs()), } } func openAICompatConfFromProto(m *iop.OpenAICompatAdapterConfig) config.OpenAICompatConf { return config.OpenAICompatConf{ Enabled: true, Provider: m.GetProvider(), Endpoint: m.GetEndpoint(), Headers: m.GetHeaders(), Capacity: int(m.GetCapacity()), MaxQueue: int(m.GetMaxQueue()), QueueTimeoutMS: int(m.GetQueueTimeoutMs()), RequestTimeoutMS: int(m.GetRequestTimeoutMs()), } } func cliConfFromProto(m *iop.CLIAdapterConfig) config.CLIConf { cfg := config.CLIConf{Enabled: true, Profiles: make(map[string]config.CLIProfileConf)} for name, p := range m.GetProfiles() { cfg.Profiles[name] = cliProfileFromProto(p) } return cfg } func cliProfileFromProto(p *iop.CLIProfileConfig) config.CLIProfileConf { if p == nil { return config.CLIProfileConf{} } prof := config.CLIProfileConf{ Command: p.GetCommand(), Args: append([]string(nil), p.GetArgs()...), Env: append([]string(nil), p.GetEnv()...), Persistent: p.GetPersistent(), Terminal: p.GetTerminal(), ResponseIdleTimeoutMS: int(p.GetResponseIdleTimeoutMs()), StartupIdleTimeoutMS: int(p.GetStartupIdleTimeoutMs()), OutputFormat: p.GetOutputFormat(), Mode: p.GetMode(), ResumeArgs: append([]string(nil), p.GetResumeArgs()...), } if marker := p.GetCompletionMarker(); marker != nil { prof.CompletionMarker = config.CompletionMarkerConf{ Line: marker.GetLine(), Regex: marker.GetRegex(), } } return prof } // Legacy structpb fallback paths for payloads from older edge versions // that populate AdapterConfig.settings instead of the typed oneof. func ollamaConfFromStruct(s *structpb.Struct) config.OllamaConf { cfg := config.OllamaConf{Enabled: true} if s == nil { return cfg } m := s.AsMap() if v, ok := m["base_url"].(string); ok { cfg.BaseURL = v } if v, ok := intSetting(m, "context_size"); ok { cfg.ContextSize = v } else if v, ok := intSetting(m, "num_ctx"); ok { cfg.ContextSize = v } if v, ok := intSetting(m, "capacity"); ok { cfg.Capacity = v } if v, ok := intSetting(m, "max_queue"); ok { cfg.MaxQueue = v } if v, ok := intSetting(m, "queue_timeout_ms"); ok { cfg.QueueTimeoutMS = v } if v, ok := intSetting(m, "request_timeout_ms"); ok { cfg.RequestTimeoutMS = v } return cfg } func intSetting(m map[string]any, key string) (int, bool) { v, ok := m[key] if !ok { return 0, false } switch n := v.(type) { case int: return n, true case int32: return int(n), true case int64: return int(n), true case float64: return int(n), true default: return 0, false } } func vllmConfFromStruct(s *structpb.Struct) config.VllmConf { cfg := config.VllmConf{Enabled: true} if s == nil { return cfg } m := s.AsMap() if v, ok := m["endpoint"].(string); ok { cfg.Endpoint = v } if v, ok := intSetting(m, "capacity"); ok { cfg.Capacity = v } if v, ok := intSetting(m, "max_queue"); ok { cfg.MaxQueue = v } if v, ok := intSetting(m, "queue_timeout_ms"); ok { cfg.QueueTimeoutMS = v } if v, ok := intSetting(m, "request_timeout_ms"); ok { cfg.RequestTimeoutMS = v } return cfg } func openAICompatConfFromStruct(s *structpb.Struct) config.OpenAICompatConf { cfg := config.OpenAICompatConf{Enabled: true} if s == nil { return cfg } m := s.AsMap() if v, ok := m["provider"].(string); ok { cfg.Provider = v } if v, ok := m["endpoint"].(string); ok { cfg.Endpoint = v } if headers, ok := m["headers"].(map[string]any); ok { cfg.Headers = make(map[string]string, len(headers)) for k, hv := range headers { if s, ok := hv.(string); ok { cfg.Headers[k] = s } } } if v, ok := intSetting(m, "capacity"); ok { cfg.Capacity = v } if v, ok := intSetting(m, "max_queue"); ok { cfg.MaxQueue = v } if v, ok := intSetting(m, "queue_timeout_ms"); ok { cfg.QueueTimeoutMS = v } if v, ok := intSetting(m, "request_timeout_ms"); ok { cfg.RequestTimeoutMS = v } return cfg } func cliConfFromStruct(s *structpb.Struct) config.CLIConf { cfg := config.CLIConf{Enabled: true, Profiles: make(map[string]config.CLIProfileConf)} if s == nil { return cfg } m := s.AsMap() profiles, ok := m["profiles"].(map[string]any) if !ok { return cfg } for name, pAny := range profiles { p, ok := pAny.(map[string]any) if !ok { continue } prof := config.CLIProfileConf{} if v, ok := p["command"].(string); ok { prof.Command = v } prof.Args = stringsFromAny(p["args"]) prof.Env = stringsFromAny(p["env"]) if v, ok := boolFromAny(p["persistent"]); ok { prof.Persistent = v } if v, ok := boolFromAny(p["terminal"]); ok { prof.Terminal = v } if v, ok := intFromAny(p["response_idle_timeout_ms"]); ok { prof.ResponseIdleTimeoutMS = v } if v, ok := intFromAny(p["startup_idle_timeout_ms"]); ok { prof.StartupIdleTimeoutMS = v } if v, ok := p["output_format"].(string); ok { prof.OutputFormat = v } if v, ok := p["mode"].(string); ok { prof.Mode = v } prof.ResumeArgs = stringsFromAny(p["resume_args"]) if marker, ok := p["completion_marker"].(map[string]any); ok { if v, ok := marker["line"].(string); ok { prof.CompletionMarker.Line = v } if v, ok := marker["regex"].(string); ok { prof.CompletionMarker.Regex = v } } cfg.Profiles[name] = prof } return cfg } func stringsFromAny(v any) []string { arr, ok := v.([]any) if !ok { return nil } out := make([]string, 0, len(arr)) for _, a := range arr { if s, ok := a.(string); ok { out = append(out, s) } } return out } func boolFromAny(v any) (bool, bool) { if b, ok := v.(bool); ok { return b, true } return false, false } func intFromAny(v any) (int, bool) { if f, ok := v.(float64); ok { return int(f), true } if i, ok := v.(int); ok { return i, true } return 0, false }