package cmd import ( "log/slog" "strings" "github.com/nextlevelbuilder/goclaw/internal/bus" "github.com/nextlevelbuilder/goclaw/internal/config" "github.com/nextlevelbuilder/goclaw/internal/memory" "github.com/nextlevelbuilder/goclaw/internal/providers" "github.com/nextlevelbuilder/goclaw/internal/sandbox" "github.com/nextlevelbuilder/goclaw/internal/tools" "github.com/nextlevelbuilder/goclaw/internal/tts" ) // resolveEmbeddingProvider auto-selects an embedding provider based on config and available API keys. // Resolution order: // 1. Explicit provider name from memCfg → hardcoded match → registry lookup (DB providers) // 2. Auto-detect: openai → openrouter → gemini (config-file keys) // // The optional providerReg allows resolving DB-stored provider names (e.g. "openai-embedding") // that don't match the hardcoded provider names. func resolveEmbeddingProvider(cfg *config.Config, memCfg *config.MemoryConfig, providerReg *providers.Registry) memory.EmbeddingProvider { // Explicit provider in config if memCfg != nil && memCfg.EmbeddingProvider != "" { if p := createEmbeddingProvider(memCfg.EmbeddingProvider, cfg, memCfg, providerReg); p != nil { return p } // Explicit name set but no match — don't auto-detect, log and return nil slog.Warn("embedding provider not found", "name", memCfg.EmbeddingProvider) return nil } // Auto-select: openai → openrouter → gemini for _, name := range []string{"openai", "openrouter", "gemini"} { if p := createEmbeddingProvider(name, cfg, memCfg, nil); p != nil { return p } } return nil } func createEmbeddingProvider(name string, cfg *config.Config, memCfg *config.MemoryConfig, providerReg *providers.Registry) memory.EmbeddingProvider { model := "text-embedding-3-small" apiBase := "" if memCfg != nil { if memCfg.EmbeddingModel != "" { model = memCfg.EmbeddingModel } if memCfg.EmbeddingAPIBase != "" { apiBase = memCfg.EmbeddingAPIBase } } switch name { case "openai": if cfg.Providers.OpenAI.APIKey == "" { return nil } if apiBase == "" { apiBase = "https://api.openai.com/v1" } return memory.NewOpenAIEmbeddingProvider("openai", cfg.Providers.OpenAI.APIKey, apiBase, model) case "openrouter": if cfg.Providers.OpenRouter.APIKey == "" { return nil } // OpenRouter requires provider prefix: "openai/text-embedding-3-small" orModel := model if !strings.Contains(orModel, "/") { orModel = "openai/" + orModel } return memory.NewOpenAIEmbeddingProvider("openrouter", cfg.Providers.OpenRouter.APIKey, "https://openrouter.ai/api/v1", orModel) case "gemini": if cfg.Providers.Gemini.APIKey == "" { return nil } geminiModel := "gemini-embedding-001" if memCfg != nil && memCfg.EmbeddingModel != "" { geminiModel = memCfg.EmbeddingModel } return memory.NewOpenAIEmbeddingProvider("gemini", cfg.Providers.Gemini.APIKey, "https://generativelanguage.googleapis.com/v1beta/openai", geminiModel). WithDimensions(1536) } // Fallback: resolve from provider registry (DB-stored providers like "openai-embedding"). // Any OpenAI-compatible provider in the registry can serve embeddings. if providerReg != nil { if regProv, err := providerReg.Get(name); err == nil { if op, ok := regProv.(*providers.OpenAIProvider); ok { embBase := op.APIBase() if apiBase != "" { embBase = apiBase // memCfg override takes precedence } slog.Info("embedding provider resolved from registry", "name", name, "base", embBase, "model", model) return memory.NewOpenAIEmbeddingProvider(name, op.APIKey(), embBase, model) } slog.Warn("embedding provider in registry is not OpenAI-compatible", "name", name) } } return nil } func setupSubagents(providerReg *providers.Registry, cfg *config.Config, msgBus *bus.MessageBus, toolsReg *tools.Registry, workspace string, sandboxMgr sandbox.Manager) *tools.SubagentManager { names := providerReg.List() if len(names) == 0 { return nil } agentCfg := cfg.ResolveAgent("default") provider, err := providerReg.Get(agentCfg.Provider) if err != nil { provider, _ = providerReg.Get(names[0]) } if provider == nil { return nil } subCfg := tools.DefaultSubagentConfig() // Apply config file overrides if present (matching TS agents.defaults.subagents). if sc := agentCfg.Subagents; sc != nil { if sc.MaxConcurrent > 0 { subCfg.MaxConcurrent = sc.MaxConcurrent } if sc.MaxSpawnDepth > 0 { subCfg.MaxSpawnDepth = min(sc.MaxSpawnDepth, 5) // TS: max 5 } if sc.MaxChildrenPerAgent > 0 { subCfg.MaxChildrenPerAgent = min(sc.MaxChildrenPerAgent, 20) // TS: max 20 } if sc.ArchiveAfterMinutes > 0 { subCfg.ArchiveAfterMinutes = sc.ArchiveAfterMinutes } if sc.Model != "" { subCfg.Model = sc.Model } } // Tool factory: clone parent registry (inherits web_fetch, web_search, browser, MCP tools, etc.) // then override file/exec tools with workspace-scoped versions. // NOTE: SubagentManager.applyDenyList() handles deny lists after createTools(), // so we don't apply deny lists here. toolsFactory := func() *tools.Registry { reg := toolsReg.Clone() if sandboxMgr != nil { reg.Register(tools.NewSandboxedReadFileTool(workspace, agentCfg.RestrictToWorkspace, sandboxMgr)) reg.Register(tools.NewSandboxedWriteFileTool(workspace, agentCfg.RestrictToWorkspace, sandboxMgr)) reg.Register(tools.NewSandboxedListFilesTool(workspace, agentCfg.RestrictToWorkspace, sandboxMgr)) reg.Register(tools.NewSandboxedExecTool(workspace, agentCfg.RestrictToWorkspace, sandboxMgr)) } else { reg.Register(tools.NewReadFileTool(workspace, agentCfg.RestrictToWorkspace)) reg.Register(tools.NewWriteFileTool(workspace, agentCfg.RestrictToWorkspace)) reg.Register(tools.NewListFilesTool(workspace, agentCfg.RestrictToWorkspace)) reg.Register(tools.NewExecTool(workspace, agentCfg.RestrictToWorkspace)) } return reg } return tools.NewSubagentManager(provider, providerReg, agentCfg.Model, msgBus, toolsFactory, subCfg) } // setupTTS creates the TTS manager from config and registers providers. // Edge TTS is always registered (free, no API key required). // Always returns a non-nil manager with at least one provider. func setupTTS(cfg *config.Config) *tts.Manager { ttsCfg := cfg.Tts mgr := tts.NewManager(tts.ManagerConfig{ Primary: ttsCfg.Provider, Auto: tts.AutoMode(ttsCfg.Auto), Mode: tts.Mode(ttsCfg.Mode), MaxLength: ttsCfg.MaxLength, TimeoutMs: ttsCfg.TimeoutMs, }) // Register providers that have API keys configured if key := ttsCfg.OpenAI.APIKey; key != "" { mgr.RegisterProvider(tts.NewOpenAIProvider(tts.OpenAIConfig{ APIKey: key, APIBase: ttsCfg.OpenAI.APIBase, Model: ttsCfg.OpenAI.Model, Voice: ttsCfg.OpenAI.Voice, TimeoutMs: ttsCfg.TimeoutMs, })) } if key := ttsCfg.ElevenLabs.APIKey; key != "" { mgr.RegisterProvider(tts.NewElevenLabsProvider(tts.ElevenLabsConfig{ APIKey: key, BaseURL: ttsCfg.ElevenLabs.BaseURL, VoiceID: ttsCfg.ElevenLabs.VoiceID, ModelID: ttsCfg.ElevenLabs.ModelID, TimeoutMs: ttsCfg.TimeoutMs, })) } // Edge TTS is free (no API key) — always register so it's available as primary or fallback. mgr.RegisterProvider(tts.NewEdgeProvider(tts.EdgeConfig{ Voice: ttsCfg.Edge.Voice, Rate: ttsCfg.Edge.Rate, TimeoutMs: ttsCfg.TimeoutMs, })) if key := ttsCfg.MiniMax.APIKey; key != "" { mgr.RegisterProvider(tts.NewMiniMaxProvider(tts.MiniMaxConfig{ APIKey: key, GroupID: ttsCfg.MiniMax.GroupID, APIBase: ttsCfg.MiniMax.APIBase, Model: ttsCfg.MiniMax.Model, VoiceID: ttsCfg.MiniMax.VoiceID, TimeoutMs: ttsCfg.TimeoutMs, })) } if !mgr.HasProviders() { return nil } return mgr }