summaryrefslogtreecommitdiffhomepage
path: root/internal/config
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-05-29 15:18:47 -0500
committeradamdottv <[email protected]>2025-05-29 15:18:47 -0500
commit005d6e0bde9a42e2bebee7b712b0fe9a7be23499 (patch)
tree3e667d3237d99a95a123b8659c4f1a5c370b9e5e /internal/config
parent37c0c1f358cadbc918319500cd2b1b3fcbe41a9e (diff)
downloadopencode-005d6e0bde9a42e2bebee7b712b0fe9a7be23499.tar.gz
opencode-005d6e0bde9a42e2bebee7b712b0fe9a7be23499.zip
wip: refactoring tui
Diffstat (limited to 'internal/config')
-rw-r--r--internal/config/config.go603
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 {