diff options
| author | adamdottv <[email protected]> | 2025-05-29 15:18:47 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-05-29 15:18:47 -0500 |
| commit | 005d6e0bde9a42e2bebee7b712b0fe9a7be23499 (patch) | |
| tree | 3e667d3237d99a95a123b8659c4f1a5c370b9e5e /internal/config | |
| parent | 37c0c1f358cadbc918319500cd2b1b3fcbe41a9e (diff) | |
| download | opencode-005d6e0bde9a42e2bebee7b712b0fe9a7be23499.tar.gz opencode-005d6e0bde9a42e2bebee7b712b0fe9a7be23499.zip | |
wip: refactoring tui
Diffstat (limited to 'internal/config')
| -rw-r--r-- | internal/config/config.go | 603 |
1 files changed, 7 insertions, 596 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index c8aea435c..2197f5aa4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,62 +11,13 @@ import ( "strings" "github.com/spf13/viper" - "github.com/sst/opencode/internal/llm/models" ) -// MCPType defines the type of MCP (Model Control Protocol) server. -type MCPType string - -// Supported MCP types -const ( - MCPStdio MCPType = "stdio" - MCPSse MCPType = "sse" -) - -// MCPServer defines the configuration for a Model Control Protocol server. -type MCPServer struct { - Command string `json:"command"` - Env []string `json:"env"` - Args []string `json:"args"` - Type MCPType `json:"type"` - URL string `json:"url"` - Headers map[string]string `json:"headers"` -} - -type AgentName string - -const ( - AgentPrimary AgentName = "primary" - AgentTask AgentName = "task" - AgentTitle AgentName = "title" -) - -// Agent defines configuration for different LLM models and their token limits. -type Agent struct { - Model models.ModelID `json:"model"` - MaxTokens int64 `json:"maxTokens"` - ReasoningEffort string `json:"reasoningEffort"` // For openai models low,medium,heigh -} - -// Provider defines configuration for an LLM provider. -type Provider struct { - APIKey string `json:"apiKey"` - Disabled bool `json:"disabled"` -} - // Data defines storage configuration. type Data struct { Directory string `json:"directory,omitempty"` } -// LSPConfig defines configuration for Language Server Protocol integration. -type LSPConfig struct { - Disabled bool `json:"enabled"` - Command string `json:"command"` - Args []string `json:"args"` - Options any `json:"options"` -} - // TUIConfig defines the configuration for the Terminal User Interface. type TUIConfig struct { Theme string `json:"theme,omitempty"` @@ -81,17 +32,13 @@ type ShellConfig struct { // Config is the main configuration structure for the application. type Config struct { - Data Data `json:"data"` - WorkingDir string `json:"wd,omitempty"` - MCPServers map[string]MCPServer `json:"mcpServers,omitempty"` - Providers map[models.ModelProvider]Provider `json:"providers,omitempty"` - LSP map[string]LSPConfig `json:"lsp,omitempty"` - Agents map[AgentName]Agent `json:"agents,omitempty"` - Debug bool `json:"debug,omitempty"` - DebugLSP bool `json:"debugLSP,omitempty"` - ContextPaths []string `json:"contextPaths,omitempty"` - TUI TUIConfig `json:"tui"` - Shell ShellConfig `json:"shell,omitempty"` + Data Data `json:"data"` + WorkingDir string `json:"wd,omitempty"` + Debug bool `json:"debug,omitempty"` + DebugLSP bool `json:"debugLSP,omitempty"` + ContextPaths []string `json:"contextPaths,omitempty"` + TUI TUIConfig `json:"tui"` + Shell ShellConfig `json:"shell,omitempty"` } // Application constants @@ -132,9 +79,6 @@ func Load(workingDir string, debug bool) (*Config, error) { cfg = &Config{ WorkingDir: workingDir, - MCPServers: make(map[string]MCPServer), - Providers: make(map[models.ModelProvider]Provider), - LSP: make(map[string]LSPConfig), } configureViper() @@ -148,15 +92,11 @@ func Load(workingDir string, debug bool) (*Config, error) { // Load and merge local config mergeLocalConfig(workingDir) - setProviderDefaults() - // Apply configuration to the struct if err := viper.Unmarshal(cfg); err != nil { return cfg, fmt.Errorf("failed to unmarshal config: %w", err) } - applyDefaultValues() - defaultLevel := slog.LevelInfo if cfg.Debug { defaultLevel = slog.LevelDebug @@ -167,16 +107,6 @@ func Load(workingDir string, debug bool) (*Config, error) { if err := Validate(); err != nil { return cfg, fmt.Errorf("config validation failed: %w", err) } - - if cfg.Agents == nil { - cfg.Agents = make(map[AgentName]Agent) - } - - // Override the max tokens for title agent - cfg.Agents[AgentTitle] = Agent{ - Model: cfg.Agents[AgentTitle].Model, - MaxTokens: 80, - } return cfg, nil } @@ -206,156 +136,6 @@ func setDefaults(debug bool) { } } -// setProviderDefaults configures LLM provider defaults based on provider provided by -// environment variables and configuration file. -func setProviderDefaults() { - // Set all API keys we can find in the environment - if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" { - viper.SetDefault("providers.anthropic.apiKey", apiKey) - } - if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" { - viper.SetDefault("providers.openai.apiKey", apiKey) - } - if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" { - viper.SetDefault("providers.gemini.apiKey", apiKey) - } - if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" { - viper.SetDefault("providers.groq.apiKey", apiKey) - } - if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" { - viper.SetDefault("providers.openrouter.apiKey", apiKey) - } - if apiKey := os.Getenv("XAI_API_KEY"); apiKey != "" { - viper.SetDefault("providers.xai.apiKey", apiKey) - } - if apiKey := os.Getenv("AZURE_OPENAI_ENDPOINT"); apiKey != "" { - // api-key may be empty when using Entra ID credentials – that's okay - viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY")) - } - - // Use this order to set the default models - // 1. Anthropic - // 2. OpenAI - // 3. Google Gemini - // 4. Groq - // 5. OpenRouter - // 6. AWS Bedrock - // 7. Azure - // 8. Google Cloud VertexAI - - // Anthropic configuration - if key := viper.GetString("providers.anthropic.apiKey"); strings.TrimSpace(key) != "" { - viper.SetDefault("agents.primary.model", models.Claude4Sonnet) - viper.SetDefault("agents.task.model", models.Claude4Sonnet) - viper.SetDefault("agents.title.model", models.Claude4Sonnet) - return - } - - // OpenAI configuration - if key := viper.GetString("providers.openai.apiKey"); strings.TrimSpace(key) != "" { - viper.SetDefault("agents.primary.model", models.GPT41) - viper.SetDefault("agents.task.model", models.GPT41Mini) - viper.SetDefault("agents.title.model", models.GPT41Mini) - return - } - - // Google Gemini configuration - if key := viper.GetString("providers.gemini.apiKey"); strings.TrimSpace(key) != "" { - viper.SetDefault("agents.primary.model", models.Gemini25) - viper.SetDefault("agents.task.model", models.Gemini25Flash) - viper.SetDefault("agents.title.model", models.Gemini25Flash) - return - } - - // Groq configuration - if key := viper.GetString("providers.groq.apiKey"); strings.TrimSpace(key) != "" { - viper.SetDefault("agents.primary.model", models.QWENQwq) - viper.SetDefault("agents.task.model", models.QWENQwq) - viper.SetDefault("agents.title.model", models.QWENQwq) - return - } - - // OpenRouter configuration - if key := viper.GetString("providers.openrouter.apiKey"); strings.TrimSpace(key) != "" { - viper.SetDefault("agents.primary.model", models.OpenRouterClaude37Sonnet) - viper.SetDefault("agents.task.model", models.OpenRouterClaude37Sonnet) - viper.SetDefault("agents.title.model", models.OpenRouterClaude35Haiku) - return - } - - // XAI configuration - if key := viper.GetString("providers.xai.apiKey"); strings.TrimSpace(key) != "" { - viper.SetDefault("agents.primary.model", models.XAIGrok3Beta) - viper.SetDefault("agents.task.model", models.XAIGrok3Beta) - viper.SetDefault("agents.title.model", models.XAiGrok3MiniFastBeta) - return - } - - // AWS Bedrock configuration - if hasAWSCredentials() { - viper.SetDefault("agents.primary.model", models.BedrockClaude37Sonnet) - viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet) - viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet) - return - } - - // Azure OpenAI configuration - if os.Getenv("AZURE_OPENAI_ENDPOINT") != "" { - viper.SetDefault("agents.primary.model", models.AzureGPT41) - viper.SetDefault("agents.task.model", models.AzureGPT41Mini) - viper.SetDefault("agents.title.model", models.AzureGPT41Mini) - return - } - - // Google Cloud VertexAI configuration - if hasVertexAICredentials() { - viper.SetDefault("agents.coder.model", models.VertexAIGemini25) - viper.SetDefault("agents.summarizer.model", models.VertexAIGemini25) - viper.SetDefault("agents.task.model", models.VertexAIGemini25Flash) - viper.SetDefault("agents.title.model", models.VertexAIGemini25Flash) - return - } -} - -// hasAWSCredentials checks if AWS credentials are available in the environment. -func hasAWSCredentials() bool { - // Check for explicit AWS credentials - if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" { - return true - } - - // Check for AWS profile - if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" { - return true - } - - // Check for AWS region - if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" { - return true - } - - // Check if running on EC2 with instance profile - if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" || - os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" { - return true - } - - return false -} - -// hasVertexAICredentials checks if VertexAI credentials are available in the environment. -func hasVertexAICredentials() bool { - // Check for explicit VertexAI parameters - if os.Getenv("VERTEXAI_PROJECT") != "" && os.Getenv("VERTEXAI_LOCATION") != "" { - return true - } - // Check for Google Cloud project and location - if os.Getenv("GOOGLE_CLOUD_PROJECT") != "" && (os.Getenv("GOOGLE_CLOUD_REGION") != "" || os.Getenv("GOOGLE_CLOUD_LOCATION") != "") { - return true - } - return false -} - // readConfig handles the result of reading a configuration file. func readConfig(err error) error { if err == nil { @@ -383,346 +163,15 @@ func mergeLocalConfig(workingDir string) { } } -// applyDefaultValues sets default values for configuration fields that need processing. -func applyDefaultValues() { - // Set default MCP type if not specified - for k, v := range cfg.MCPServers { - if v.Type == "" { - v.Type = MCPStdio - cfg.MCPServers[k] = v - } - } -} - -// It validates model IDs and providers, ensuring they are supported. -func validateAgent(cfg *Config, name AgentName, agent Agent) error { - // Check if model exists - model, modelExists := models.SupportedModels[agent.Model] - if !modelExists { - slog.Warn("unsupported model configured, reverting to default", - "agent", name, - "configured_model", agent.Model) - - // Set default model based on available providers - if setDefaultModelForAgent(name) { - slog.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model) - } else { - return fmt.Errorf("no valid provider available for agent %s", name) - } - return nil - } - - // Check if provider for the model is configured - provider := model.Provider - providerCfg, providerExists := cfg.Providers[provider] - - if !providerExists { - // Provider not configured, check if we have environment variables - apiKey := getProviderAPIKey(provider) - if apiKey == "" { - slog.Warn("provider not configured for model, reverting to default", - "agent", name, - "model", agent.Model, - "provider", provider) - - // Set default model based on available providers - if setDefaultModelForAgent(name) { - slog.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model) - } else { - return fmt.Errorf("no valid provider available for agent %s", name) - } - } else { - // Add provider with API key from environment - cfg.Providers[provider] = Provider{ - APIKey: apiKey, - } - slog.Info("added provider from environment", "provider", provider) - } - } else if providerCfg.Disabled || providerCfg.APIKey == "" { - // Provider is disabled or has no API key - slog.Warn("provider is disabled or has no API key, reverting to default", - "agent", name, - "model", agent.Model, - "provider", provider) - - // Set default model based on available providers - if setDefaultModelForAgent(name) { - slog.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model) - } else { - return fmt.Errorf("no valid provider available for agent %s", name) - } - } - - // Validate max tokens - if agent.MaxTokens <= 0 { - slog.Warn("invalid max tokens, setting to default", - "agent", name, - "model", agent.Model, - "max_tokens", agent.MaxTokens) - - // Update the agent with default max tokens - updatedAgent := cfg.Agents[name] - if model.DefaultMaxTokens > 0 { - updatedAgent.MaxTokens = model.DefaultMaxTokens - } else { - updatedAgent.MaxTokens = MaxTokensFallbackDefault - } - cfg.Agents[name] = updatedAgent - } else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 { - // Ensure max tokens doesn't exceed half the context window (reasonable limit) - slog.Warn("max tokens exceeds half the context window, adjusting", - "agent", name, - "model", agent.Model, - "max_tokens", agent.MaxTokens, - "context_window", model.ContextWindow) - - // Update the agent with adjusted max tokens - updatedAgent := cfg.Agents[name] - updatedAgent.MaxTokens = model.ContextWindow / 2 - cfg.Agents[name] = updatedAgent - } - - // Validate reasoning effort for models that support reasoning - if model.CanReason && provider == models.ProviderOpenAI { - if agent.ReasoningEffort == "" { - // Set default reasoning effort for models that support it - slog.Info("setting default reasoning effort for model that supports reasoning", - "agent", name, - "model", agent.Model) - - // Update the agent with default reasoning effort - updatedAgent := cfg.Agents[name] - updatedAgent.ReasoningEffort = "medium" - cfg.Agents[name] = updatedAgent - } else { - // Check if reasoning effort is valid (low, medium, high) - effort := strings.ToLower(agent.ReasoningEffort) - if effort != "low" && effort != "medium" && effort != "high" { - slog.Warn("invalid reasoning effort, setting to medium", - "agent", name, - "model", agent.Model, - "reasoning_effort", agent.ReasoningEffort) - - // Update the agent with valid reasoning effort - updatedAgent := cfg.Agents[name] - updatedAgent.ReasoningEffort = "medium" - cfg.Agents[name] = updatedAgent - } - } - } else if !model.CanReason && agent.ReasoningEffort != "" { - // Model doesn't support reasoning but reasoning effort is set - slog.Warn("model doesn't support reasoning but reasoning effort is set, ignoring", - "agent", name, - "model", agent.Model, - "reasoning_effort", agent.ReasoningEffort) - - // Update the agent to remove reasoning effort - updatedAgent := cfg.Agents[name] - updatedAgent.ReasoningEffort = "" - cfg.Agents[name] = updatedAgent - } - - return nil -} - // Validate checks if the configuration is valid and applies defaults where needed. func Validate() error { if cfg == nil { return fmt.Errorf("config not loaded") } - // Validate agent models - for name, agent := range cfg.Agents { - if err := validateAgent(cfg, name, agent); err != nil { - return err - } - } - - // Validate providers - for provider, providerCfg := range cfg.Providers { - if providerCfg.APIKey == "" && !providerCfg.Disabled { - slog.Warn("provider has no API key, marking as disabled", "provider", provider) - providerCfg.Disabled = true - cfg.Providers[provider] = providerCfg - } - } - - // Validate LSP configurations - for language, lspConfig := range cfg.LSP { - if lspConfig.Command == "" && !lspConfig.Disabled { - slog.Warn("LSP configuration has no command, marking as disabled", "language", language) - lspConfig.Disabled = true - cfg.LSP[language] = lspConfig - } - } - return nil } -// getProviderAPIKey gets the API key for a provider from environment variables -func getProviderAPIKey(provider models.ModelProvider) string { - switch provider { - case models.ProviderAnthropic: - return os.Getenv("ANTHROPIC_API_KEY") - case models.ProviderOpenAI: - return os.Getenv("OPENAI_API_KEY") - case models.ProviderGemini: - return os.Getenv("GEMINI_API_KEY") - case models.ProviderGROQ: - return os.Getenv("GROQ_API_KEY") - case models.ProviderAzure: - return os.Getenv("AZURE_OPENAI_API_KEY") - case models.ProviderOpenRouter: - return os.Getenv("OPENROUTER_API_KEY") - case models.ProviderBedrock: - if hasAWSCredentials() { - return "aws-credentials-available" - } - case models.ProviderVertexAI: - if hasVertexAICredentials() { - return "vertex-ai-credentials-available" - } - } - return "" -} - -// setDefaultModelForAgent sets a default model for an agent based on available providers -func setDefaultModelForAgent(agent AgentName) bool { - // Check providers in order of preference - if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" { - maxTokens := int64(5000) - if agent == AgentTitle { - maxTokens = 80 - } - cfg.Agents[agent] = Agent{ - Model: models.Claude4Sonnet, - MaxTokens: maxTokens, - } - return true - } - - if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" { - var model models.ModelID - maxTokens := int64(5000) - reasoningEffort := "" - - switch agent { - case AgentTitle: - model = models.GPT41Mini - maxTokens = 80 - case AgentTask: - model = models.GPT41Mini - default: - model = models.GPT41 - } - - // Check if model supports reasoning - if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason { - reasoningEffort = "medium" - } - - cfg.Agents[agent] = Agent{ - Model: model, - MaxTokens: maxTokens, - ReasoningEffort: reasoningEffort, - } - return true - } - - if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" { - var model models.ModelID - maxTokens := int64(5000) - reasoningEffort := "" - - switch agent { - case AgentTitle: - model = models.OpenRouterClaude35Haiku - maxTokens = 80 - case AgentTask: - model = models.OpenRouterClaude37Sonnet - default: - model = models.OpenRouterClaude37Sonnet - } - - // Check if model supports reasoning - if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason { - reasoningEffort = "medium" - } - - cfg.Agents[agent] = Agent{ - Model: model, - MaxTokens: maxTokens, - ReasoningEffort: reasoningEffort, - } - return true - } - - if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" { - var model models.ModelID - maxTokens := int64(5000) - - if agent == AgentTitle { - model = models.Gemini25Flash - maxTokens = 80 - } else { - model = models.Gemini25 - } - - cfg.Agents[agent] = Agent{ - Model: model, - MaxTokens: maxTokens, - } - return true - } - - if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" { - maxTokens := int64(5000) - if agent == AgentTitle { - maxTokens = 80 - } - - cfg.Agents[agent] = Agent{ - Model: models.QWENQwq, - MaxTokens: maxTokens, - } - return true - } - - if hasAWSCredentials() { - maxTokens := int64(5000) - if agent == AgentTitle { - maxTokens = 80 - } - - cfg.Agents[agent] = Agent{ - Model: models.BedrockClaude37Sonnet, - MaxTokens: maxTokens, - ReasoningEffort: "medium", // Claude models support reasoning - } - return true - } - - if hasVertexAICredentials() { - var model models.ModelID - maxTokens := int64(5000) - - if agent == AgentTitle { - model = models.VertexAIGemini25Flash - maxTokens = 80 - } else { - model = models.VertexAIGemini25 - } - - cfg.Agents[agent] = Agent{ - Model: model, - MaxTokens: maxTokens, - } - return true - } - - return false -} - // Get returns the current configuration. // It's safe to call this function multiple times. func Get() *Config { @@ -801,44 +250,6 @@ func updateCfgFile(updateCfg func(config *Config)) error { return nil } -func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error { - if cfg == nil { - panic("config not loaded") - } - - existingAgentCfg := cfg.Agents[agentName] - - model, ok := models.SupportedModels[modelID] - if !ok { - return fmt.Errorf("model %s not supported", modelID) - } - - maxTokens := existingAgentCfg.MaxTokens - if model.DefaultMaxTokens > 0 { - maxTokens = model.DefaultMaxTokens - } - - newAgentCfg := Agent{ - Model: modelID, - MaxTokens: maxTokens, - ReasoningEffort: existingAgentCfg.ReasoningEffort, - } - cfg.Agents[agentName] = newAgentCfg - - if err := validateAgent(cfg, agentName, newAgentCfg); err != nil { - // revert config update on failure - cfg.Agents[agentName] = existingAgentCfg - return fmt.Errorf("failed to update agent model: %w", err) - } - - return updateCfgFile(func(config *Config) { - if config.Agents == nil { - config.Agents = make(map[AgentName]Agent) - } - config.Agents[agentName] = newAgentCfg - }) -} - // UpdateTheme updates the theme in the configuration and writes it to the config file. func UpdateTheme(themeName string) error { if cfg == nil { |
