diff options
| author | Kujtim Hoxha <[email protected]> | 2025-04-21 14:29:03 +0200 |
|---|---|---|
| committer | Kujtim Hoxha <[email protected]> | 2025-04-21 14:29:28 +0200 |
| commit | a8d5787e8ef561037f73b669128f46ae1b1e8553 (patch) | |
| tree | b606adbc85069d60cc518c74508a7ecbc87c051b /internal | |
| parent | 9ae6af8856ca6a13d575ec6a8989a5f6ee4297b1 (diff) | |
| download | opencode-a8d5787e8ef561037f73b669128f46ae1b1e8553.tar.gz opencode-a8d5787e8ef561037f73b669128f46ae1b1e8553.zip | |
config validation
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config/config.go | 276 | ||||
| -rw-r--r-- | internal/llm/agent/agent.go | 2 | ||||
| -rw-r--r-- | internal/llm/tools/edit.go | 27 | ||||
| -rw-r--r-- | internal/llm/tools/write.go | 11 | ||||
| -rw-r--r-- | internal/permission/permission.go | 10 | ||||
| -rw-r--r-- | internal/tui/tui.go | 4 | ||||
| -rw-r--r-- | internal/version/version.go | 2 |
7 files changed, 315 insertions, 17 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index 2dbbcc9ca..13c7d1328 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -120,13 +120,11 @@ func Load(workingDir string, debug bool) (*Config, error) { } applyDefaultValues() - defaultLevel := slog.LevelInfo if cfg.Debug { defaultLevel = slog.LevelDebug } - // if we are in debug mode make the writer a file - if cfg.Debug { + if os.Getenv("OPENCODE_DEV_DEBUG") == "true" { loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log") // if file does not exist create it @@ -156,6 +154,11 @@ func Load(workingDir string, debug bool) (*Config, error) { slog.SetDefault(logger) } + // Validate configuration + if err := Validate(); err != nil { + return cfg, fmt.Errorf("config validation failed: %w", err) + } + if cfg.Agents == nil { cfg.Agents = make(map[AgentName]Agent) } @@ -302,6 +305,273 @@ func applyDefaultValues() { } } +// Validate checks if the configuration is valid and applies defaults where needed. +// It validates model IDs and providers, ensuring they are supported. +func Validate() error { + if cfg == nil { + return fmt.Errorf("config not loaded") + } + + // Validate agent models + for name, agent := range cfg.Agents { + // Check if model exists + model, modelExists := models.SupportedModels[agent.Model] + if !modelExists { + logging.Warn("unsupported model configured, reverting to default", + "agent", name, + "configured_model", agent.Model) + + // Set default model based on available providers + if setDefaultModelForAgent(name) { + logging.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) + } + continue + } + + // 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 == "" { + logging.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) { + logging.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, + } + logging.Info("added provider from environment", "provider", provider) + } + } else if providerCfg.Disabled || providerCfg.APIKey == "" { + // Provider is disabled or has no API key + logging.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) { + logging.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 { + logging.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 = 4096 // Fallback default + } + 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) + logging.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 + logging.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" { + logging.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 + logging.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 + } + } + + // Validate providers + for provider, providerCfg := range cfg.Providers { + if providerCfg.APIKey == "" && !providerCfg.Disabled { + logging.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 { + logging.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.ProviderBedrock: + if hasAWSCredentials() { + return "aws-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.Claude37Sonnet, + 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("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 + } + + return false +} + // Get returns the current configuration. // It's safe to call this function multiple times. func Get() *Config { diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index ae5bcb231..6c5808eab 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -471,7 +471,7 @@ func createAgentProvider(agentName config.AgentName) (provider.Provider, error) provider.WithReasoningEffort(agentConfig.ReasoningEffort), ), ) - } else if model.Provider == models.ProviderAnthropic && model.CanReason { + } else if model.Provider == models.ProviderAnthropic && model.CanReason && agentName == config.AgentCoder { opts = append( opts, provider.WithAnthropicOptions( diff --git a/internal/llm/tools/edit.go b/internal/llm/tools/edit.go index b7b813ca7..23c44399b 100644 --- a/internal/llm/tools/edit.go +++ b/internal/llm/tools/edit.go @@ -196,11 +196,16 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string) content, filePath, ) + rootDir := config.WorkingDirectory() + permissionPath := filepath.Dir(filePath) + if strings.HasPrefix(filePath, rootDir) { + permissionPath = rootDir + } p := e.permissions.Request( permission.CreatePermissionRequest{ - Path: filepath.Dir(filePath), + Path: permissionPath, ToolName: EditToolName, - Action: "create", + Action: "write", Description: fmt.Sprintf("Create file %s", filePath), Params: EditPermissionsParams{ FilePath: filePath, @@ -301,11 +306,16 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string filePath, ) + rootDir := config.WorkingDirectory() + permissionPath := filepath.Dir(filePath) + if strings.HasPrefix(filePath, rootDir) { + permissionPath = rootDir + } p := e.permissions.Request( permission.CreatePermissionRequest{ - Path: filepath.Dir(filePath), + Path: permissionPath, ToolName: EditToolName, - Action: "delete", + Action: "write", Description: fmt.Sprintf("Delete content from file %s", filePath), Params: EditPermissionsParams{ FilePath: filePath, @@ -415,11 +425,16 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS newContent, filePath, ) + rootDir := config.WorkingDirectory() + permissionPath := filepath.Dir(filePath) + if strings.HasPrefix(filePath, rootDir) { + permissionPath = rootDir + } p := e.permissions.Request( permission.CreatePermissionRequest{ - Path: filepath.Dir(filePath), + Path: permissionPath, ToolName: EditToolName, - Action: "replace", + Action: "write", Description: fmt.Sprintf("Replace content in file %s", filePath), Params: EditPermissionsParams{ FilePath: filePath, diff --git a/internal/llm/tools/write.go b/internal/llm/tools/write.go index 2b3fa3dd0..3a94b47b6 100644 --- a/internal/llm/tools/write.go +++ b/internal/llm/tools/write.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" "github.com/kujtimiihoxha/opencode/internal/config" @@ -159,11 +160,17 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error params.Content, filePath, ) + + rootDir := config.WorkingDirectory() + permissionPath := filepath.Dir(filePath) + if strings.HasPrefix(filePath, rootDir) { + permissionPath = rootDir + } p := w.permissions.Request( permission.CreatePermissionRequest{ - Path: filePath, + Path: permissionPath, ToolName: WriteToolName, - Action: "create", + Action: "write", Description: fmt.Sprintf("Create file %s", filePath), Params: WritePermissionsParams{ FilePath: filePath, diff --git a/internal/permission/permission.go b/internal/permission/permission.go index 4cb379dea..06f69a33d 100644 --- a/internal/permission/permission.go +++ b/internal/permission/permission.go @@ -2,10 +2,12 @@ package permission import ( "errors" + "path/filepath" "sync" "time" "github.com/google/uuid" + "github.com/kujtimiihoxha/opencode/internal/config" "github.com/kujtimiihoxha/opencode/internal/pubsub" ) @@ -67,9 +69,13 @@ func (s *permissionService) Deny(permission PermissionRequest) { } func (s *permissionService) Request(opts CreatePermissionRequest) bool { + dir := filepath.Dir(opts.Path) + if dir == "." { + dir = config.WorkingDirectory() + } permission := PermissionRequest{ ID: uuid.New().String(), - Path: opts.Path, + Path: dir, ToolName: opts.ToolName, Description: opts.Description, Action: opts.Action, @@ -77,7 +83,7 @@ func (s *permissionService) Request(opts CreatePermissionRequest) bool { } for _, p := range s.sessionPermissions { - if p.ToolName == permission.ToolName && p.Action == permission.Action { + if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path { return true } } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index dec43f7c0..392b9ec41 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -57,8 +57,8 @@ var returnKey = key.NewBinding( ) var logsKeyReturnKey = key.NewBinding( - key.WithKeys("backspace"), - key.WithHelp("backspace", "go back"), + key.WithKeys("backspace", "q"), + key.WithHelp("backspace/q", "go back"), ) type appModel struct { diff --git a/internal/version/version.go b/internal/version/version.go index 54c576f6c..1e19bea38 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -5,7 +5,7 @@ import "runtime/debug" // Build-time parameters set via -ldflags var Version = "unknown" -// A user may install pug using `go install github.com/leg100/pug@latest` +// A user may install pug using `go install github.com/kujtimiihoxha/opencode@latest`. // without -ldflags, in which case the version above is unset. As a workaround // we use the embedded build version that *is* set when using `go install` (and // is only set for `go install` and not for `go build`). |
