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 ContextPaths []string `json:"contextPaths,omitempty"`
79}
80
81// Application constants
82const (
83 defaultDataDirectory = ".opencode"
84 defaultLogLevel = "info"
85 appName = "opencode"
86
87 MaxTokensFallbackDefault = 4096
88)
89
90var defaultContextPaths = []string{
91 ".github/copilot-instructions.md",
92 ".cursorrules",
93 ".cursor/rules/",
94 "CLAUDE.md",
95 "CLAUDE.local.md",
96 "opencode.md",
97 "opencode.local.md",
98 "OpenCode.md",
99 "OpenCode.local.md",
100 "OPENCODE.md",
101 "OPENCODE.local.md",
102}
103
104// Global configuration instance
105var cfg *Config
106
107// Load initializes the configuration from environment variables and config files.
108// If debug is true, debug mode is enabled and log level is set to debug.
109// It returns an error if configuration loading fails.
110func Load(workingDir string, debug bool) (*Config, error) {
111 if cfg != nil {
112 return cfg, nil
113 }
114
115 cfg = &Config{
116 WorkingDir: workingDir,
117 MCPServers: make(map[string]MCPServer),
118 Providers: make(map[models.ModelProvider]Provider),
119 LSP: make(map[string]LSPConfig),
120 }
121
122 configureViper()
123 setDefaults(debug)
124 setProviderDefaults()
125
126 // Read global config
127 if err := readConfig(viper.ReadInConfig()); err != nil {
128 return cfg, err
129 }
130
131 // Load and merge local config
132 mergeLocalConfig(workingDir)
133
134 // Apply configuration to the struct
135 if err := viper.Unmarshal(cfg); err != nil {
136 return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
137 }
138
139 applyDefaultValues()
140 defaultLevel := slog.LevelInfo
141 if cfg.Debug {
142 defaultLevel = slog.LevelDebug
143 }
144 if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
145 loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
146
147 // if file does not exist create it
148 if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
149 if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil {
150 return cfg, fmt.Errorf("failed to create directory: %w", err)
151 }
152 if _, err := os.Create(loggingFile); err != nil {
153 return cfg, fmt.Errorf("failed to create log file: %w", err)
154 }
155 }
156
157 sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
158 if err != nil {
159 return cfg, fmt.Errorf("failed to open log file: %w", err)
160 }
161 // Configure logger
162 logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
163 Level: defaultLevel,
164 }))
165 slog.SetDefault(logger)
166 } else {
167 // Configure logger
168 logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
169 Level: defaultLevel,
170 }))
171 slog.SetDefault(logger)
172 }
173
174 // Validate configuration
175 if err := Validate(); err != nil {
176 return cfg, fmt.Errorf("config validation failed: %w", err)
177 }
178
179 if cfg.Agents == nil {
180 cfg.Agents = make(map[AgentName]Agent)
181 }
182
183 // Override the max tokens for title agent
184 cfg.Agents[AgentTitle] = Agent{
185 Model: cfg.Agents[AgentTitle].Model,
186 MaxTokens: 80,
187 }
188 return cfg, nil
189}
190
191// configureViper sets up viper's configuration paths and environment variables.
192func configureViper() {
193 viper.SetConfigName(fmt.Sprintf(".%s", appName))
194 viper.SetConfigType("json")
195 viper.AddConfigPath("$HOME")
196 viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
197 viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
198 viper.SetEnvPrefix(strings.ToUpper(appName))
199 viper.AutomaticEnv()
200}
201
202// setDefaults configures default values for configuration options.
203func setDefaults(debug bool) {
204 viper.SetDefault("data.directory", defaultDataDirectory)
205 viper.SetDefault("contextPaths", defaultContextPaths)
206
207 if debug {
208 viper.SetDefault("debug", true)
209 viper.Set("log.level", "debug")
210 } else {
211 viper.SetDefault("debug", false)
212 viper.SetDefault("log.level", defaultLogLevel)
213 }
214}
215
216// setProviderDefaults configures LLM provider defaults based on environment variables.
217func setProviderDefaults() {
218 // Set all API keys we can find in the environment
219 if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
220 viper.SetDefault("providers.anthropic.apiKey", apiKey)
221 }
222 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
223 viper.SetDefault("providers.openai.apiKey", apiKey)
224 }
225 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
226 viper.SetDefault("providers.gemini.apiKey", apiKey)
227 }
228 if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
229 viper.SetDefault("providers.groq.apiKey", apiKey)
230 }
231
232 // Use this order to set the default models
233 // 1. Anthropic
234 // 2. OpenAI
235 // 3. Google Gemini
236 // 4. Groq
237 // 5. AWS Bedrock
238 // Anthropic configuration
239 if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
240 viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
241 viper.SetDefault("agents.task.model", models.Claude37Sonnet)
242 viper.SetDefault("agents.title.model", models.Claude37Sonnet)
243 return
244 }
245
246 // OpenAI configuration
247 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
248 viper.SetDefault("agents.coder.model", models.GPT41)
249 viper.SetDefault("agents.task.model", models.GPT41Mini)
250 viper.SetDefault("agents.title.model", models.GPT41Mini)
251 return
252 }
253
254 // Google Gemini configuration
255 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
256 viper.SetDefault("agents.coder.model", models.Gemini25)
257 viper.SetDefault("agents.task.model", models.Gemini25Flash)
258 viper.SetDefault("agents.title.model", models.Gemini25Flash)
259 return
260 }
261
262 // Groq configuration
263 if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
264 viper.SetDefault("agents.coder.model", models.QWENQwq)
265 viper.SetDefault("agents.task.model", models.QWENQwq)
266 viper.SetDefault("agents.title.model", models.QWENQwq)
267 return
268 }
269
270 // OpenRouter configuration
271 if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
272 viper.SetDefault("providers.openrouter.apiKey", apiKey)
273 viper.SetDefault("agents.coder.model", models.OpenRouterClaude37Sonnet)
274 viper.SetDefault("agents.task.model", models.OpenRouterClaude37Sonnet)
275 viper.SetDefault("agents.title.model", models.OpenRouterClaude35Haiku)
276 return
277 }
278
279 // AWS Bedrock configuration
280 if hasAWSCredentials() {
281 viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
282 viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet)
283 viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
284 return
285 }
286
287 if os.Getenv("AZURE_OPENAI_ENDPOINT") != "" {
288 // api-key may be empty when using Entra ID credentials – that's okay
289 viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY"))
290 viper.SetDefault("agents.coder.model", models.AzureGPT41)
291 viper.SetDefault("agents.task.model", models.AzureGPT41Mini)
292 viper.SetDefault("agents.title.model", models.AzureGPT41Mini)
293 return
294 }
295}
296
297// hasAWSCredentials checks if AWS credentials are available in the environment.
298func hasAWSCredentials() bool {
299 // Check for explicit AWS credentials
300 if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
301 return true
302 }
303
304 // Check for AWS profile
305 if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
306 return true
307 }
308
309 // Check for AWS region
310 if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
311 return true
312 }
313
314 // Check if running on EC2 with instance profile
315 if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
316 os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
317 return true
318 }
319
320 return false
321}
322
323// readConfig handles the result of reading a configuration file.
324func readConfig(err error) error {
325 if err == nil {
326 return nil
327 }
328
329 // It's okay if the config file doesn't exist
330 if _, ok := err.(viper.ConfigFileNotFoundError); ok {
331 return nil
332 }
333
334 return fmt.Errorf("failed to read config: %w", err)
335}
336
337// mergeLocalConfig loads and merges configuration from the local directory.
338func mergeLocalConfig(workingDir string) {
339 local := viper.New()
340 local.SetConfigName(fmt.Sprintf(".%s", appName))
341 local.SetConfigType("json")
342 local.AddConfigPath(workingDir)
343
344 // Merge local config if it exists
345 if err := local.ReadInConfig(); err == nil {
346 viper.MergeConfigMap(local.AllSettings())
347 }
348}
349
350// applyDefaultValues sets default values for configuration fields that need processing.
351func applyDefaultValues() {
352 // Set default MCP type if not specified
353 for k, v := range cfg.MCPServers {
354 if v.Type == "" {
355 v.Type = MCPStdio
356 cfg.MCPServers[k] = v
357 }
358 }
359}
360
361// It validates model IDs and providers, ensuring they are supported.
362func validateAgent(cfg *Config, name AgentName, agent Agent) error {
363 // Check if model exists
364 model, modelExists := models.SupportedModels[agent.Model]
365 if !modelExists {
366 logging.Warn("unsupported model configured, reverting to default",
367 "agent", name,
368 "configured_model", agent.Model)
369
370 // Set default model based on available providers
371 if setDefaultModelForAgent(name) {
372 logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
373 } else {
374 return fmt.Errorf("no valid provider available for agent %s", name)
375 }
376 return nil
377 }
378
379 // Check if provider for the model is configured
380 provider := model.Provider
381 providerCfg, providerExists := cfg.Providers[provider]
382
383 if !providerExists {
384 // Provider not configured, check if we have environment variables
385 apiKey := getProviderAPIKey(provider)
386 if apiKey == "" {
387 logging.Warn("provider not configured for model, reverting to default",
388 "agent", name,
389 "model", agent.Model,
390 "provider", provider)
391
392 // Set default model based on available providers
393 if setDefaultModelForAgent(name) {
394 logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
395 } else {
396 return fmt.Errorf("no valid provider available for agent %s", name)
397 }
398 } else {
399 // Add provider with API key from environment
400 cfg.Providers[provider] = Provider{
401 APIKey: apiKey,
402 }
403 logging.Info("added provider from environment", "provider", provider)
404 }
405 } else if providerCfg.Disabled || providerCfg.APIKey == "" {
406 // Provider is disabled or has no API key
407 logging.Warn("provider is disabled or has no API key, reverting to default",
408 "agent", name,
409 "model", agent.Model,
410 "provider", provider)
411
412 // Set default model based on available providers
413 if setDefaultModelForAgent(name) {
414 logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
415 } else {
416 return fmt.Errorf("no valid provider available for agent %s", name)
417 }
418 }
419
420 // Validate max tokens
421 if agent.MaxTokens <= 0 {
422 logging.Warn("invalid max tokens, setting to default",
423 "agent", name,
424 "model", agent.Model,
425 "max_tokens", agent.MaxTokens)
426
427 // Update the agent with default max tokens
428 updatedAgent := cfg.Agents[name]
429 if model.DefaultMaxTokens > 0 {
430 updatedAgent.MaxTokens = model.DefaultMaxTokens
431 } else {
432 updatedAgent.MaxTokens = MaxTokensFallbackDefault
433 }
434 cfg.Agents[name] = updatedAgent
435 } else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
436 // Ensure max tokens doesn't exceed half the context window (reasonable limit)
437 logging.Warn("max tokens exceeds half the context window, adjusting",
438 "agent", name,
439 "model", agent.Model,
440 "max_tokens", agent.MaxTokens,
441 "context_window", model.ContextWindow)
442
443 // Update the agent with adjusted max tokens
444 updatedAgent := cfg.Agents[name]
445 updatedAgent.MaxTokens = model.ContextWindow / 2
446 cfg.Agents[name] = updatedAgent
447 }
448
449 // Validate reasoning effort for models that support reasoning
450 if model.CanReason && provider == models.ProviderOpenAI {
451 if agent.ReasoningEffort == "" {
452 // Set default reasoning effort for models that support it
453 logging.Info("setting default reasoning effort for model that supports reasoning",
454 "agent", name,
455 "model", agent.Model)
456
457 // Update the agent with default reasoning effort
458 updatedAgent := cfg.Agents[name]
459 updatedAgent.ReasoningEffort = "medium"
460 cfg.Agents[name] = updatedAgent
461 } else {
462 // Check if reasoning effort is valid (low, medium, high)
463 effort := strings.ToLower(agent.ReasoningEffort)
464 if effort != "low" && effort != "medium" && effort != "high" {
465 logging.Warn("invalid reasoning effort, setting to medium",
466 "agent", name,
467 "model", agent.Model,
468 "reasoning_effort", agent.ReasoningEffort)
469
470 // Update the agent with valid reasoning effort
471 updatedAgent := cfg.Agents[name]
472 updatedAgent.ReasoningEffort = "medium"
473 cfg.Agents[name] = updatedAgent
474 }
475 }
476 } else if !model.CanReason && agent.ReasoningEffort != "" {
477 // Model doesn't support reasoning but reasoning effort is set
478 logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
479 "agent", name,
480 "model", agent.Model,
481 "reasoning_effort", agent.ReasoningEffort)
482
483 // Update the agent to remove reasoning effort
484 updatedAgent := cfg.Agents[name]
485 updatedAgent.ReasoningEffort = ""
486 cfg.Agents[name] = updatedAgent
487 }
488
489 return nil
490}
491
492// Validate checks if the configuration is valid and applies defaults where needed.
493func Validate() error {
494 if cfg == nil {
495 return fmt.Errorf("config not loaded")
496 }
497
498 // Validate agent models
499 for name, agent := range cfg.Agents {
500 if err := validateAgent(cfg, name, agent); err != nil {
501 return err
502 }
503 }
504
505 // Validate providers
506 for provider, providerCfg := range cfg.Providers {
507 if providerCfg.APIKey == "" && !providerCfg.Disabled {
508 logging.Warn("provider has no API key, marking as disabled", "provider", provider)
509 providerCfg.Disabled = true
510 cfg.Providers[provider] = providerCfg
511 }
512 }
513
514 // Validate LSP configurations
515 for language, lspConfig := range cfg.LSP {
516 if lspConfig.Command == "" && !lspConfig.Disabled {
517 logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
518 lspConfig.Disabled = true
519 cfg.LSP[language] = lspConfig
520 }
521 }
522
523 return nil
524}
525
526// getProviderAPIKey gets the API key for a provider from environment variables
527func getProviderAPIKey(provider models.ModelProvider) string {
528 switch provider {
529 case models.ProviderAnthropic:
530 return os.Getenv("ANTHROPIC_API_KEY")
531 case models.ProviderOpenAI:
532 return os.Getenv("OPENAI_API_KEY")
533 case models.ProviderGemini:
534 return os.Getenv("GEMINI_API_KEY")
535 case models.ProviderGROQ:
536 return os.Getenv("GROQ_API_KEY")
537 case models.ProviderAzure:
538 return os.Getenv("AZURE_OPENAI_API_KEY")
539 case models.ProviderOpenRouter:
540 return os.Getenv("OPENROUTER_API_KEY")
541 case models.ProviderBedrock:
542 if hasAWSCredentials() {
543 return "aws-credentials-available"
544 }
545 }
546 return ""
547}
548
549// setDefaultModelForAgent sets a default model for an agent based on available providers
550func setDefaultModelForAgent(agent AgentName) bool {
551 // Check providers in order of preference
552 if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
553 maxTokens := int64(5000)
554 if agent == AgentTitle {
555 maxTokens = 80
556 }
557 cfg.Agents[agent] = Agent{
558 Model: models.Claude37Sonnet,
559 MaxTokens: maxTokens,
560 }
561 return true
562 }
563
564 if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
565 var model models.ModelID
566 maxTokens := int64(5000)
567 reasoningEffort := ""
568
569 switch agent {
570 case AgentTitle:
571 model = models.GPT41Mini
572 maxTokens = 80
573 case AgentTask:
574 model = models.GPT41Mini
575 default:
576 model = models.GPT41
577 }
578
579 // Check if model supports reasoning
580 if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
581 reasoningEffort = "medium"
582 }
583
584 cfg.Agents[agent] = Agent{
585 Model: model,
586 MaxTokens: maxTokens,
587 ReasoningEffort: reasoningEffort,
588 }
589 return true
590 }
591
592 if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
593 var model models.ModelID
594 maxTokens := int64(5000)
595 reasoningEffort := ""
596
597 switch agent {
598 case AgentTitle:
599 model = models.OpenRouterClaude35Haiku
600 maxTokens = 80
601 case AgentTask:
602 model = models.OpenRouterClaude37Sonnet
603 default:
604 model = models.OpenRouterClaude37Sonnet
605 }
606
607 // Check if model supports reasoning
608 if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
609 reasoningEffort = "medium"
610 }
611
612 cfg.Agents[agent] = Agent{
613 Model: model,
614 MaxTokens: maxTokens,
615 ReasoningEffort: reasoningEffort,
616 }
617 return true
618 }
619
620 if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
621 var model models.ModelID
622 maxTokens := int64(5000)
623
624 if agent == AgentTitle {
625 model = models.Gemini25Flash
626 maxTokens = 80
627 } else {
628 model = models.Gemini25
629 }
630
631 cfg.Agents[agent] = Agent{
632 Model: model,
633 MaxTokens: maxTokens,
634 }
635 return true
636 }
637
638 if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
639 maxTokens := int64(5000)
640 if agent == AgentTitle {
641 maxTokens = 80
642 }
643
644 cfg.Agents[agent] = Agent{
645 Model: models.QWENQwq,
646 MaxTokens: maxTokens,
647 }
648 return true
649 }
650
651 if hasAWSCredentials() {
652 maxTokens := int64(5000)
653 if agent == AgentTitle {
654 maxTokens = 80
655 }
656
657 cfg.Agents[agent] = Agent{
658 Model: models.BedrockClaude37Sonnet,
659 MaxTokens: maxTokens,
660 ReasoningEffort: "medium", // Claude models support reasoning
661 }
662 return true
663 }
664
665 return false
666}
667
668// Get returns the current configuration.
669// It's safe to call this function multiple times.
670func Get() *Config {
671 return cfg
672}
673
674// WorkingDirectory returns the current working directory from the configuration.
675func WorkingDirectory() string {
676 if cfg == nil {
677 panic("config not loaded")
678 }
679 return cfg.WorkingDir
680}
681
682func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error {
683 if cfg == nil {
684 panic("config not loaded")
685 }
686
687 existingAgentCfg := cfg.Agents[agentName]
688
689 model, ok := models.SupportedModels[modelID]
690 if !ok {
691 return fmt.Errorf("model %s not supported", modelID)
692 }
693
694 maxTokens := existingAgentCfg.MaxTokens
695 if model.DefaultMaxTokens > 0 {
696 maxTokens = model.DefaultMaxTokens
697 }
698
699 newAgentCfg := Agent{
700 Model: modelID,
701 MaxTokens: maxTokens,
702 ReasoningEffort: existingAgentCfg.ReasoningEffort,
703 }
704 cfg.Agents[agentName] = newAgentCfg
705
706 if err := validateAgent(cfg, agentName, newAgentCfg); err != nil {
707 // revert config update on failure
708 cfg.Agents[agentName] = existingAgentCfg
709 return fmt.Errorf("failed to update agent model: %w", err)
710 }
711
712 return nil
713}