1// Package config manages application configuration from various sources.
2package config
3
4import (
5 "fmt"
6 "log/slog"
7 "os"
8 "strings"
9
10 "github.com/kujtimiihoxha/opencode/internal/llm/models"
11 "github.com/kujtimiihoxha/opencode/internal/logging"
12 "github.com/spf13/viper"
13)
14
15// MCPType defines the type of MCP (Model Control Protocol) server.
16type MCPType string
17
18// Supported MCP types
19const (
20 MCPStdio MCPType = "stdio"
21 MCPSse MCPType = "sse"
22)
23
24// MCPServer defines the configuration for a Model Control Protocol server.
25type MCPServer struct {
26 Command string `json:"command"`
27 Env []string `json:"env"`
28 Args []string `json:"args"`
29 Type MCPType `json:"type"`
30 URL string `json:"url"`
31 Headers map[string]string `json:"headers"`
32}
33
34type AgentName string
35
36const (
37 AgentCoder AgentName = "coder"
38 AgentTask AgentName = "task"
39 AgentTitle AgentName = "title"
40)
41
42// Agent defines configuration for different LLM models and their token limits.
43type Agent struct {
44 Model models.ModelID `json:"model"`
45 MaxTokens int64 `json:"maxTokens"`
46 ReasoningEffort string `json:"reasoningEffort"` // For openai models low,medium,heigh
47}
48
49// Provider defines configuration for an LLM provider.
50type Provider struct {
51 APIKey string `json:"apiKey"`
52 Disabled bool `json:"disabled"`
53}
54
55// Data defines storage configuration.
56type Data struct {
57 Directory string `json:"directory"`
58}
59
60// LSPConfig defines configuration for Language Server Protocol integration.
61type LSPConfig struct {
62 Disabled bool `json:"enabled"`
63 Command string `json:"command"`
64 Args []string `json:"args"`
65 Options any `json:"options"`
66}
67
68// Config is the main configuration structure for the application.
69type Config struct {
70 Data Data `json:"data"`
71 WorkingDir string `json:"wd,omitempty"`
72 MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
73 Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
74 LSP map[string]LSPConfig `json:"lsp,omitempty"`
75 Agents map[AgentName]Agent `json:"agents"`
76 Debug bool `json:"debug,omitempty"`
77 DebugLSP bool `json:"debugLSP,omitempty"`
78}
79
80// Application constants
81const (
82 defaultDataDirectory = ".opencode"
83 defaultLogLevel = "info"
84 appName = "opencode"
85)
86
87// Global configuration instance
88var cfg *Config
89
90// Load initializes the configuration from environment variables and config files.
91// If debug is true, debug mode is enabled and log level is set to debug.
92// It returns an error if configuration loading fails.
93func Load(workingDir string, debug bool) (*Config, error) {
94 if cfg != nil {
95 return cfg, nil
96 }
97
98 cfg = &Config{
99 WorkingDir: workingDir,
100 MCPServers: make(map[string]MCPServer),
101 Providers: make(map[models.ModelProvider]Provider),
102 LSP: make(map[string]LSPConfig),
103 }
104
105 configureViper()
106 setDefaults(debug)
107 setProviderDefaults()
108
109 // Read global config
110 if err := readConfig(viper.ReadInConfig()); err != nil {
111 return cfg, err
112 }
113
114 // Load and merge local config
115 mergeLocalConfig(workingDir)
116
117 // Apply configuration to the struct
118 if err := viper.Unmarshal(cfg); err != nil {
119 return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
120 }
121
122 applyDefaultValues()
123
124 defaultLevel := slog.LevelInfo
125 if cfg.Debug {
126 defaultLevel = slog.LevelDebug
127 }
128 // if we are in debug mode make the writer a file
129 if cfg.Debug {
130 loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
131
132 // if file does not exist create it
133 if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
134 if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil {
135 return cfg, fmt.Errorf("failed to create directory: %w", err)
136 }
137 if _, err := os.Create(loggingFile); err != nil {
138 return cfg, fmt.Errorf("failed to create log file: %w", err)
139 }
140 }
141
142 sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
143 if err != nil {
144 return cfg, fmt.Errorf("failed to open log file: %w", err)
145 }
146 // Configure logger
147 logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
148 Level: defaultLevel,
149 }))
150 slog.SetDefault(logger)
151 } else {
152 // Configure logger
153 logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
154 Level: defaultLevel,
155 }))
156 slog.SetDefault(logger)
157 }
158
159 if cfg.Agents == nil {
160 cfg.Agents = make(map[AgentName]Agent)
161 }
162
163 // Override the max tokens for title agent
164 cfg.Agents[AgentTitle] = Agent{
165 Model: cfg.Agents[AgentTitle].Model,
166 MaxTokens: 80,
167 }
168 return cfg, nil
169}
170
171// configureViper sets up viper's configuration paths and environment variables.
172func configureViper() {
173 viper.SetConfigName(fmt.Sprintf(".%s", appName))
174 viper.SetConfigType("json")
175 viper.AddConfigPath("$HOME")
176 viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
177 viper.SetEnvPrefix(strings.ToUpper(appName))
178 viper.AutomaticEnv()
179}
180
181// setDefaults configures default values for configuration options.
182func setDefaults(debug bool) {
183 viper.SetDefault("data.directory", defaultDataDirectory)
184
185 if debug {
186 viper.SetDefault("debug", true)
187 viper.Set("log.level", "debug")
188 } else {
189 viper.SetDefault("debug", false)
190 viper.SetDefault("log.level", defaultLogLevel)
191 }
192}
193
194// setProviderDefaults configures LLM provider defaults based on environment variables.
195// the default model priority is:
196// 1. Anthropic
197// 2. OpenAI
198// 3. Google Gemini
199// 4. AWS Bedrock
200func setProviderDefaults() {
201 // Groq configuration
202 if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
203 viper.SetDefault("providers.groq.apiKey", apiKey)
204 viper.SetDefault("agents.coder.model", models.QWENQwq)
205 viper.SetDefault("agents.task.model", models.QWENQwq)
206 viper.SetDefault("agents.title.model", models.QWENQwq)
207 }
208
209 // Google Gemini configuration
210 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
211 viper.SetDefault("providers.gemini.apiKey", apiKey)
212 viper.SetDefault("agents.coder.model", models.GRMINI20Flash)
213 viper.SetDefault("agents.task.model", models.GRMINI20Flash)
214 viper.SetDefault("agents.title.model", models.GRMINI20Flash)
215 }
216
217 // OpenAI configuration
218 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
219 viper.SetDefault("providers.openai.apiKey", apiKey)
220 viper.SetDefault("agents.coder.model", models.GPT4o)
221 viper.SetDefault("agents.task.model", models.GPT4o)
222 viper.SetDefault("agents.title.model", models.GPT4o)
223
224 }
225
226 // Anthropic configuration
227 if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
228 viper.SetDefault("providers.anthropic.apiKey", apiKey)
229 viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
230 viper.SetDefault("agents.task.model", models.Claude37Sonnet)
231 viper.SetDefault("agents.title.model", models.Claude37Sonnet)
232 }
233
234 if hasAWSCredentials() {
235 viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
236 viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet)
237 viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
238 }
239}
240
241// hasAWSCredentials checks if AWS credentials are available in the environment.
242func hasAWSCredentials() bool {
243 // Check for explicit AWS credentials
244 if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
245 return true
246 }
247
248 // Check for AWS profile
249 if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
250 return true
251 }
252
253 // Check for AWS region
254 if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
255 return true
256 }
257
258 // Check if running on EC2 with instance profile
259 if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
260 os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
261 return true
262 }
263
264 return false
265}
266
267// readConfig handles the result of reading a configuration file.
268func readConfig(err error) error {
269 if err == nil {
270 return nil
271 }
272
273 // It's okay if the config file doesn't exist
274 if _, ok := err.(viper.ConfigFileNotFoundError); ok {
275 return nil
276 }
277
278 return fmt.Errorf("failed to read config: %w", err)
279}
280
281// mergeLocalConfig loads and merges configuration from the local directory.
282func mergeLocalConfig(workingDir string) {
283 local := viper.New()
284 local.SetConfigName(fmt.Sprintf(".%s", appName))
285 local.SetConfigType("json")
286 local.AddConfigPath(workingDir)
287
288 // Merge local config if it exists
289 if err := local.ReadInConfig(); err == nil {
290 viper.MergeConfigMap(local.AllSettings())
291 }
292}
293
294// applyDefaultValues sets default values for configuration fields that need processing.
295func applyDefaultValues() {
296 // Set default MCP type if not specified
297 for k, v := range cfg.MCPServers {
298 if v.Type == "" {
299 v.Type = MCPStdio
300 cfg.MCPServers[k] = v
301 }
302 }
303}
304
305// Get returns the current configuration.
306// It's safe to call this function multiple times.
307func Get() *Config {
308 return cfg
309}
310
311// WorkingDirectory returns the current working directory from the configuration.
312func WorkingDirectory() string {
313 if cfg == nil {
314 panic("config not loaded")
315 }
316 return cfg.WorkingDir
317}