config.go

  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}
 47
 48// Provider defines configuration for an LLM provider.
 49type Provider struct {
 50	APIKey   string `json:"apiKey"`
 51	Disabled bool   `json:"disabled"`
 52}
 53
 54// Data defines storage configuration.
 55type Data struct {
 56	Directory string `json:"directory"`
 57}
 58
 59// LSPConfig defines configuration for Language Server Protocol integration.
 60type LSPConfig struct {
 61	Disabled bool     `json:"enabled"`
 62	Command  string   `json:"command"`
 63	Args     []string `json:"args"`
 64	Options  any      `json:"options"`
 65}
 66
 67// Config is the main configuration structure for the application.
 68type Config struct {
 69	Data       Data                              `json:"data"`
 70	WorkingDir string                            `json:"wd,omitempty"`
 71	MCPServers map[string]MCPServer              `json:"mcpServers,omitempty"`
 72	Providers  map[models.ModelProvider]Provider `json:"providers,omitempty"`
 73	LSP        map[string]LSPConfig              `json:"lsp,omitempty"`
 74	Agents     map[AgentName]Agent               `json:"agents"`
 75	Debug      bool                              `json:"debug,omitempty"`
 76	DebugLSP   bool                              `json:"debugLSP,omitempty"`
 77}
 78
 79// Application constants
 80const (
 81	defaultDataDirectory = ".opencode"
 82	defaultLogLevel      = "info"
 83	defaultMaxTokens     = int64(5000)
 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.coder.maxTokens", defaultMaxTokens)
206		viper.SetDefault("agents.task.model", models.QWENQwq)
207		viper.SetDefault("agents.task.maxTokens", defaultMaxTokens)
208		viper.SetDefault("agents.title.model", models.QWENQwq)
209	}
210
211	// Google Gemini configuration
212	if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
213		viper.SetDefault("providers.gemini.apiKey", apiKey)
214		viper.SetDefault("agents.coder.model", models.GRMINI20Flash)
215		viper.SetDefault("agents.coder.maxTokens", defaultMaxTokens)
216		viper.SetDefault("agents.task.model", models.GRMINI20Flash)
217		viper.SetDefault("agents.task.maxTokens", defaultMaxTokens)
218		viper.SetDefault("agents.title.model", models.GRMINI20Flash)
219	}
220
221	// OpenAI configuration
222	if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
223		viper.SetDefault("providers.openai.apiKey", apiKey)
224		viper.SetDefault("agents.coder.model", models.GPT4o)
225		viper.SetDefault("agents.coder.maxTokens", defaultMaxTokens)
226		viper.SetDefault("agents.task.model", models.GPT4o)
227		viper.SetDefault("agents.task.maxTokens", defaultMaxTokens)
228		viper.SetDefault("agents.title.model", models.GPT4o)
229
230	}
231
232	// Anthropic configuration
233	if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
234		viper.SetDefault("providers.anthropic.apiKey", apiKey)
235		viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
236		viper.SetDefault("agents.coder.maxTokens", defaultMaxTokens)
237		viper.SetDefault("agents.task.model", models.Claude37Sonnet)
238		viper.SetDefault("agents.task.maxTokens", defaultMaxTokens)
239		viper.SetDefault("agents.title.model", models.Claude37Sonnet)
240	}
241
242	if hasAWSCredentials() {
243		viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
244		viper.SetDefault("agents.coder.maxTokens", defaultMaxTokens)
245		viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet)
246		viper.SetDefault("agents.task.maxTokens", defaultMaxTokens)
247		viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
248	}
249}
250
251// hasAWSCredentials checks if AWS credentials are available in the environment.
252func hasAWSCredentials() bool {
253	// Check for explicit AWS credentials
254	if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
255		return true
256	}
257
258	// Check for AWS profile
259	if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
260		return true
261	}
262
263	// Check for AWS region
264	if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
265		return true
266	}
267
268	// Check if running on EC2 with instance profile
269	if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
270		os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
271		return true
272	}
273
274	return false
275}
276
277// readConfig handles the result of reading a configuration file.
278func readConfig(err error) error {
279	if err == nil {
280		return nil
281	}
282
283	// It's okay if the config file doesn't exist
284	if _, ok := err.(viper.ConfigFileNotFoundError); ok {
285		return nil
286	}
287
288	return fmt.Errorf("failed to read config: %w", err)
289}
290
291// mergeLocalConfig loads and merges configuration from the local directory.
292func mergeLocalConfig(workingDir string) {
293	local := viper.New()
294	local.SetConfigName(fmt.Sprintf(".%s", appName))
295	local.SetConfigType("json")
296	local.AddConfigPath(workingDir)
297
298	// Merge local config if it exists
299	if err := local.ReadInConfig(); err == nil {
300		viper.MergeConfigMap(local.AllSettings())
301	}
302}
303
304// applyDefaultValues sets default values for configuration fields that need processing.
305func applyDefaultValues() {
306	// Set default MCP type if not specified
307	for k, v := range cfg.MCPServers {
308		if v.Type == "" {
309			v.Type = MCPStdio
310			cfg.MCPServers[k] = v
311		}
312	}
313}
314
315// Get returns the current configuration.
316// It's safe to call this function multiple times.
317func Get() *Config {
318	return cfg
319}
320
321// WorkingDirectory returns the current working directory from the configuration.
322func WorkingDirectory() string {
323	if cfg == nil {
324		panic("config not loaded")
325	}
326	return cfg.WorkingDir
327}