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/opencode-ai/opencode/internal/llm/models"
11 "github.com/opencode-ai/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 defaultLevel := slog.LevelInfo
124 if cfg.Debug {
125 defaultLevel = slog.LevelDebug
126 }
127 if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
128 loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
129
130 // if file does not exist create it
131 if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
132 if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil {
133 return cfg, fmt.Errorf("failed to create directory: %w", err)
134 }
135 if _, err := os.Create(loggingFile); err != nil {
136 return cfg, fmt.Errorf("failed to create log file: %w", err)
137 }
138 }
139
140 sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
141 if err != nil {
142 return cfg, fmt.Errorf("failed to open log file: %w", err)
143 }
144 // Configure logger
145 logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
146 Level: defaultLevel,
147 }))
148 slog.SetDefault(logger)
149 } else {
150 // Configure logger
151 logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
152 Level: defaultLevel,
153 }))
154 slog.SetDefault(logger)
155 }
156
157 // Validate configuration
158 if err := Validate(); err != nil {
159 return cfg, fmt.Errorf("config validation failed: %w", err)
160 }
161
162 if cfg.Agents == nil {
163 cfg.Agents = make(map[AgentName]Agent)
164 }
165
166 // Override the max tokens for title agent
167 cfg.Agents[AgentTitle] = Agent{
168 Model: cfg.Agents[AgentTitle].Model,
169 MaxTokens: 80,
170 }
171 return cfg, nil
172}
173
174// configureViper sets up viper's configuration paths and environment variables.
175func configureViper() {
176 viper.SetConfigName(fmt.Sprintf(".%s", appName))
177 viper.SetConfigType("json")
178 viper.AddConfigPath("$HOME")
179 viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
180 viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
181 viper.SetEnvPrefix(strings.ToUpper(appName))
182 viper.AutomaticEnv()
183}
184
185// setDefaults configures default values for configuration options.
186func setDefaults(debug bool) {
187 viper.SetDefault("data.directory", defaultDataDirectory)
188
189 if debug {
190 viper.SetDefault("debug", true)
191 viper.Set("log.level", "debug")
192 } else {
193 viper.SetDefault("debug", false)
194 viper.SetDefault("log.level", defaultLogLevel)
195 }
196}
197
198// setProviderDefaults configures LLM provider defaults based on environment variables.
199func setProviderDefaults() {
200 // Set all API keys we can find in the environment
201 if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
202 viper.SetDefault("providers.anthropic.apiKey", apiKey)
203 }
204 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
205 viper.SetDefault("providers.openai.apiKey", apiKey)
206 }
207 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
208 viper.SetDefault("providers.gemini.apiKey", apiKey)
209 }
210 if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
211 viper.SetDefault("providers.groq.apiKey", apiKey)
212 }
213
214 // Use this order to set the default models
215 // 1. Anthropic
216 // 2. OpenAI
217 // 3. Google Gemini
218 // 4. Groq
219 // 5. AWS Bedrock
220 // Anthropic configuration
221 if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
222 viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
223 viper.SetDefault("agents.task.model", models.Claude37Sonnet)
224 viper.SetDefault("agents.title.model", models.Claude37Sonnet)
225 return
226 }
227
228 // OpenAI configuration
229 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
230 viper.SetDefault("agents.coder.model", models.GPT41)
231 viper.SetDefault("agents.task.model", models.GPT41Mini)
232 viper.SetDefault("agents.title.model", models.GPT41Mini)
233 return
234 }
235
236 // Google Gemini configuration
237 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
238 viper.SetDefault("agents.coder.model", models.Gemini25)
239 viper.SetDefault("agents.task.model", models.Gemini25Flash)
240 viper.SetDefault("agents.title.model", models.Gemini25Flash)
241 return
242 }
243
244 // Groq configuration
245 if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
246 viper.SetDefault("agents.coder.model", models.QWENQwq)
247 viper.SetDefault("agents.task.model", models.QWENQwq)
248 viper.SetDefault("agents.title.model", models.QWENQwq)
249 return
250 }
251
252 // AWS Bedrock configuration
253 if hasAWSCredentials() {
254 viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
255 viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet)
256 viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
257 return
258 }
259}
260
261// hasAWSCredentials checks if AWS credentials are available in the environment.
262func hasAWSCredentials() bool {
263 // Check for explicit AWS credentials
264 if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
265 return true
266 }
267
268 // Check for AWS profile
269 if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
270 return true
271 }
272
273 // Check for AWS region
274 if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
275 return true
276 }
277
278 // Check if running on EC2 with instance profile
279 if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
280 os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
281 return true
282 }
283
284 return false
285}
286
287// readConfig handles the result of reading a configuration file.
288func readConfig(err error) error {
289 if err == nil {
290 return nil
291 }
292
293 // It's okay if the config file doesn't exist
294 if _, ok := err.(viper.ConfigFileNotFoundError); ok {
295 return nil
296 }
297
298 return fmt.Errorf("failed to read config: %w", err)
299}
300
301// mergeLocalConfig loads and merges configuration from the local directory.
302func mergeLocalConfig(workingDir string) {
303 local := viper.New()
304 local.SetConfigName(fmt.Sprintf(".%s", appName))
305 local.SetConfigType("json")
306 local.AddConfigPath(workingDir)
307
308 // Merge local config if it exists
309 if err := local.ReadInConfig(); err == nil {
310 viper.MergeConfigMap(local.AllSettings())
311 }
312}
313
314// applyDefaultValues sets default values for configuration fields that need processing.
315func applyDefaultValues() {
316 // Set default MCP type if not specified
317 for k, v := range cfg.MCPServers {
318 if v.Type == "" {
319 v.Type = MCPStdio
320 cfg.MCPServers[k] = v
321 }
322 }
323}
324
325// Validate checks if the configuration is valid and applies defaults where needed.
326// It validates model IDs and providers, ensuring they are supported.
327func Validate() error {
328 if cfg == nil {
329 return fmt.Errorf("config not loaded")
330 }
331
332 // Validate agent models
333 for name, agent := range cfg.Agents {
334 // Check if model exists
335 model, modelExists := models.SupportedModels[agent.Model]
336 if !modelExists {
337 logging.Warn("unsupported model configured, reverting to default",
338 "agent", name,
339 "configured_model", agent.Model)
340
341 // Set default model based on available providers
342 if setDefaultModelForAgent(name) {
343 logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
344 } else {
345 return fmt.Errorf("no valid provider available for agent %s", name)
346 }
347 continue
348 }
349
350 // Check if provider for the model is configured
351 provider := model.Provider
352 providerCfg, providerExists := cfg.Providers[provider]
353
354 if !providerExists {
355 // Provider not configured, check if we have environment variables
356 apiKey := getProviderAPIKey(provider)
357 if apiKey == "" {
358 logging.Warn("provider not configured for model, reverting to default",
359 "agent", name,
360 "model", agent.Model,
361 "provider", provider)
362
363 // Set default model based on available providers
364 if setDefaultModelForAgent(name) {
365 logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
366 } else {
367 return fmt.Errorf("no valid provider available for agent %s", name)
368 }
369 } else {
370 // Add provider with API key from environment
371 cfg.Providers[provider] = Provider{
372 APIKey: apiKey,
373 }
374 logging.Info("added provider from environment", "provider", provider)
375 }
376 } else if providerCfg.Disabled || providerCfg.APIKey == "" {
377 // Provider is disabled or has no API key
378 logging.Warn("provider is disabled or has no API key, reverting to default",
379 "agent", name,
380 "model", agent.Model,
381 "provider", provider)
382
383 // Set default model based on available providers
384 if setDefaultModelForAgent(name) {
385 logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
386 } else {
387 return fmt.Errorf("no valid provider available for agent %s", name)
388 }
389 }
390
391 // Validate max tokens
392 if agent.MaxTokens <= 0 {
393 logging.Warn("invalid max tokens, setting to default",
394 "agent", name,
395 "model", agent.Model,
396 "max_tokens", agent.MaxTokens)
397
398 // Update the agent with default max tokens
399 updatedAgent := cfg.Agents[name]
400 if model.DefaultMaxTokens > 0 {
401 updatedAgent.MaxTokens = model.DefaultMaxTokens
402 } else {
403 updatedAgent.MaxTokens = 4096 // Fallback default
404 }
405 cfg.Agents[name] = updatedAgent
406 } else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
407 // Ensure max tokens doesn't exceed half the context window (reasonable limit)
408 logging.Warn("max tokens exceeds half the context window, adjusting",
409 "agent", name,
410 "model", agent.Model,
411 "max_tokens", agent.MaxTokens,
412 "context_window", model.ContextWindow)
413
414 // Update the agent with adjusted max tokens
415 updatedAgent := cfg.Agents[name]
416 updatedAgent.MaxTokens = model.ContextWindow / 2
417 cfg.Agents[name] = updatedAgent
418 }
419
420 // Validate reasoning effort for models that support reasoning
421 if model.CanReason && provider == models.ProviderOpenAI {
422 if agent.ReasoningEffort == "" {
423 // Set default reasoning effort for models that support it
424 logging.Info("setting default reasoning effort for model that supports reasoning",
425 "agent", name,
426 "model", agent.Model)
427
428 // Update the agent with default reasoning effort
429 updatedAgent := cfg.Agents[name]
430 updatedAgent.ReasoningEffort = "medium"
431 cfg.Agents[name] = updatedAgent
432 } else {
433 // Check if reasoning effort is valid (low, medium, high)
434 effort := strings.ToLower(agent.ReasoningEffort)
435 if effort != "low" && effort != "medium" && effort != "high" {
436 logging.Warn("invalid reasoning effort, setting to medium",
437 "agent", name,
438 "model", agent.Model,
439 "reasoning_effort", agent.ReasoningEffort)
440
441 // Update the agent with valid reasoning effort
442 updatedAgent := cfg.Agents[name]
443 updatedAgent.ReasoningEffort = "medium"
444 cfg.Agents[name] = updatedAgent
445 }
446 }
447 } else if !model.CanReason && agent.ReasoningEffort != "" {
448 // Model doesn't support reasoning but reasoning effort is set
449 logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
450 "agent", name,
451 "model", agent.Model,
452 "reasoning_effort", agent.ReasoningEffort)
453
454 // Update the agent to remove reasoning effort
455 updatedAgent := cfg.Agents[name]
456 updatedAgent.ReasoningEffort = ""
457 cfg.Agents[name] = updatedAgent
458 }
459 }
460
461 // Validate providers
462 for provider, providerCfg := range cfg.Providers {
463 if providerCfg.APIKey == "" && !providerCfg.Disabled {
464 logging.Warn("provider has no API key, marking as disabled", "provider", provider)
465 providerCfg.Disabled = true
466 cfg.Providers[provider] = providerCfg
467 }
468 }
469
470 // Validate LSP configurations
471 for language, lspConfig := range cfg.LSP {
472 if lspConfig.Command == "" && !lspConfig.Disabled {
473 logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
474 lspConfig.Disabled = true
475 cfg.LSP[language] = lspConfig
476 }
477 }
478
479 return nil
480}
481
482// getProviderAPIKey gets the API key for a provider from environment variables
483func getProviderAPIKey(provider models.ModelProvider) string {
484 switch provider {
485 case models.ProviderAnthropic:
486 return os.Getenv("ANTHROPIC_API_KEY")
487 case models.ProviderOpenAI:
488 return os.Getenv("OPENAI_API_KEY")
489 case models.ProviderGemini:
490 return os.Getenv("GEMINI_API_KEY")
491 case models.ProviderGROQ:
492 return os.Getenv("GROQ_API_KEY")
493 case models.ProviderBedrock:
494 if hasAWSCredentials() {
495 return "aws-credentials-available"
496 }
497 }
498 return ""
499}
500
501// setDefaultModelForAgent sets a default model for an agent based on available providers
502func setDefaultModelForAgent(agent AgentName) bool {
503 // Check providers in order of preference
504 if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
505 maxTokens := int64(5000)
506 if agent == AgentTitle {
507 maxTokens = 80
508 }
509 cfg.Agents[agent] = Agent{
510 Model: models.Claude37Sonnet,
511 MaxTokens: maxTokens,
512 }
513 return true
514 }
515
516 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
517 var model models.ModelID
518 maxTokens := int64(5000)
519 reasoningEffort := ""
520
521 switch agent {
522 case AgentTitle:
523 model = models.GPT41Mini
524 maxTokens = 80
525 case AgentTask:
526 model = models.GPT41Mini
527 default:
528 model = models.GPT41
529 }
530
531 // Check if model supports reasoning
532 if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
533 reasoningEffort = "medium"
534 }
535
536 cfg.Agents[agent] = Agent{
537 Model: model,
538 MaxTokens: maxTokens,
539 ReasoningEffort: reasoningEffort,
540 }
541 return true
542 }
543
544 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
545 var model models.ModelID
546 maxTokens := int64(5000)
547
548 if agent == AgentTitle {
549 model = models.Gemini25Flash
550 maxTokens = 80
551 } else {
552 model = models.Gemini25
553 }
554
555 cfg.Agents[agent] = Agent{
556 Model: model,
557 MaxTokens: maxTokens,
558 }
559 return true
560 }
561
562 if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
563 maxTokens := int64(5000)
564 if agent == AgentTitle {
565 maxTokens = 80
566 }
567
568 cfg.Agents[agent] = Agent{
569 Model: models.QWENQwq,
570 MaxTokens: maxTokens,
571 }
572 return true
573 }
574
575 if hasAWSCredentials() {
576 maxTokens := int64(5000)
577 if agent == AgentTitle {
578 maxTokens = 80
579 }
580
581 cfg.Agents[agent] = Agent{
582 Model: models.BedrockClaude37Sonnet,
583 MaxTokens: maxTokens,
584 ReasoningEffort: "medium", // Claude models support reasoning
585 }
586 return true
587 }
588
589 return false
590}
591
592// Get returns the current configuration.
593// It's safe to call this function multiple times.
594func Get() *Config {
595 return cfg
596}
597
598// WorkingDirectory returns the current working directory from the configuration.
599func WorkingDirectory() string {
600 if cfg == nil {
601 panic("config not loaded")
602 }
603 return cfg.WorkingDir
604}