config.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. // Package config manages application configuration from various sources.
  2. package config
  3. import (
  4. "fmt"
  5. "log/slog"
  6. "os"
  7. "strings"
  8. "github.com/opencode-ai/opencode/internal/llm/models"
  9. "github.com/opencode-ai/opencode/internal/logging"
  10. "github.com/spf13/viper"
  11. )
  12. // MCPType defines the type of MCP (Model Control Protocol) server.
  13. type MCPType string
  14. // Supported MCP types
  15. const (
  16. MCPStdio MCPType = "stdio"
  17. MCPSse MCPType = "sse"
  18. )
  19. // MCPServer defines the configuration for a Model Control Protocol server.
  20. type MCPServer struct {
  21. Command string `json:"command"`
  22. Env []string `json:"env"`
  23. Args []string `json:"args"`
  24. Type MCPType `json:"type"`
  25. URL string `json:"url"`
  26. Headers map[string]string `json:"headers"`
  27. }
  28. type AgentName string
  29. const (
  30. AgentCoder AgentName = "coder"
  31. AgentTask AgentName = "task"
  32. AgentTitle AgentName = "title"
  33. )
  34. // Agent defines configuration for different LLM models and their token limits.
  35. type Agent struct {
  36. Model models.ModelID `json:"model"`
  37. MaxTokens int64 `json:"maxTokens"`
  38. ReasoningEffort string `json:"reasoningEffort"` // For openai models low,medium,heigh
  39. }
  40. // Provider defines configuration for an LLM provider.
  41. type Provider struct {
  42. APIKey string `json:"apiKey"`
  43. Disabled bool `json:"disabled"`
  44. }
  45. // Data defines storage configuration.
  46. type Data struct {
  47. Directory string `json:"directory"`
  48. }
  49. // LSPConfig defines configuration for Language Server Protocol integration.
  50. type LSPConfig struct {
  51. Disabled bool `json:"enabled"`
  52. Command string `json:"command"`
  53. Args []string `json:"args"`
  54. Options any `json:"options"`
  55. }
  56. // Config is the main configuration structure for the application.
  57. type Config struct {
  58. Data Data `json:"data"`
  59. WorkingDir string `json:"wd,omitempty"`
  60. MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
  61. Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
  62. LSP map[string]LSPConfig `json:"lsp,omitempty"`
  63. Agents map[AgentName]Agent `json:"agents"`
  64. Debug bool `json:"debug,omitempty"`
  65. DebugLSP bool `json:"debugLSP,omitempty"`
  66. }
  67. // Application constants
  68. const (
  69. defaultDataDirectory = ".opencode"
  70. defaultLogLevel = "info"
  71. appName = "opencode"
  72. )
  73. // Global configuration instance
  74. var cfg *Config
  75. // Load initializes the configuration from environment variables and config files.
  76. // If debug is true, debug mode is enabled and log level is set to debug.
  77. // It returns an error if configuration loading fails.
  78. func Load(workingDir string, debug bool) (*Config, error) {
  79. if cfg != nil {
  80. return cfg, nil
  81. }
  82. cfg = &Config{
  83. WorkingDir: workingDir,
  84. MCPServers: make(map[string]MCPServer),
  85. Providers: make(map[models.ModelProvider]Provider),
  86. LSP: make(map[string]LSPConfig),
  87. }
  88. configureViper()
  89. setDefaults(debug)
  90. setProviderDefaults()
  91. // Read global config
  92. if err := readConfig(viper.ReadInConfig()); err != nil {
  93. return cfg, err
  94. }
  95. // Load and merge local config
  96. mergeLocalConfig(workingDir)
  97. // Apply configuration to the struct
  98. if err := viper.Unmarshal(cfg); err != nil {
  99. return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
  100. }
  101. applyDefaultValues()
  102. defaultLevel := slog.LevelInfo
  103. if cfg.Debug {
  104. defaultLevel = slog.LevelDebug
  105. }
  106. if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
  107. loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
  108. // if file does not exist create it
  109. if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
  110. if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil {
  111. return cfg, fmt.Errorf("failed to create directory: %w", err)
  112. }
  113. if _, err := os.Create(loggingFile); err != nil {
  114. return cfg, fmt.Errorf("failed to create log file: %w", err)
  115. }
  116. }
  117. sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
  118. if err != nil {
  119. return cfg, fmt.Errorf("failed to open log file: %w", err)
  120. }
  121. // Configure logger
  122. logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
  123. Level: defaultLevel,
  124. }))
  125. slog.SetDefault(logger)
  126. } else {
  127. // Configure logger
  128. logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
  129. Level: defaultLevel,
  130. }))
  131. slog.SetDefault(logger)
  132. }
  133. // Validate configuration
  134. if err := Validate(); err != nil {
  135. return cfg, fmt.Errorf("config validation failed: %w", err)
  136. }
  137. if cfg.Agents == nil {
  138. cfg.Agents = make(map[AgentName]Agent)
  139. }
  140. // Override the max tokens for title agent
  141. cfg.Agents[AgentTitle] = Agent{
  142. Model: cfg.Agents[AgentTitle].Model,
  143. MaxTokens: 80,
  144. }
  145. return cfg, nil
  146. }
  147. // configureViper sets up viper's configuration paths and environment variables.
  148. func configureViper() {
  149. viper.SetConfigName(fmt.Sprintf(".%s", appName))
  150. viper.SetConfigType("json")
  151. viper.AddConfigPath("$HOME")
  152. viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
  153. viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
  154. viper.SetEnvPrefix(strings.ToUpper(appName))
  155. viper.AutomaticEnv()
  156. }
  157. // setDefaults configures default values for configuration options.
  158. func setDefaults(debug bool) {
  159. viper.SetDefault("data.directory", defaultDataDirectory)
  160. if debug {
  161. viper.SetDefault("debug", true)
  162. viper.Set("log.level", "debug")
  163. } else {
  164. viper.SetDefault("debug", false)
  165. viper.SetDefault("log.level", defaultLogLevel)
  166. }
  167. }
  168. // setProviderDefaults configures LLM provider defaults based on environment variables.
  169. // the default model priority is:
  170. // 1. Anthropic
  171. // 2. OpenAI
  172. // 3. Google Gemini
  173. // 4. Groq
  174. // 5. AWS Bedrock
  175. func setProviderDefaults() {
  176. // Anthropic configuration
  177. if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
  178. viper.SetDefault("providers.anthropic.apiKey", apiKey)
  179. viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
  180. viper.SetDefault("agents.task.model", models.Claude37Sonnet)
  181. viper.SetDefault("agents.title.model", models.Claude37Sonnet)
  182. return
  183. }
  184. // OpenAI configuration
  185. if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
  186. viper.SetDefault("providers.openai.apiKey", apiKey)
  187. viper.SetDefault("agents.coder.model", models.GPT41)
  188. viper.SetDefault("agents.task.model", models.GPT41Mini)
  189. viper.SetDefault("agents.title.model", models.GPT41Mini)
  190. return
  191. }
  192. // Google Gemini configuration
  193. if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
  194. viper.SetDefault("providers.gemini.apiKey", apiKey)
  195. viper.SetDefault("agents.coder.model", models.Gemini25)
  196. viper.SetDefault("agents.task.model", models.Gemini25Flash)
  197. viper.SetDefault("agents.title.model", models.Gemini25Flash)
  198. return
  199. }
  200. // Groq configuration
  201. if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
  202. viper.SetDefault("providers.groq.apiKey", apiKey)
  203. viper.SetDefault("agents.coder.model", models.QWENQwq)
  204. viper.SetDefault("agents.task.model", models.QWENQwq)
  205. viper.SetDefault("agents.title.model", models.QWENQwq)
  206. return
  207. }
  208. // AWS Bedrock configuration
  209. if hasAWSCredentials() {
  210. viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
  211. viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet)
  212. viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
  213. return
  214. }
  215. }
  216. // hasAWSCredentials checks if AWS credentials are available in the environment.
  217. func hasAWSCredentials() bool {
  218. // Check for explicit AWS credentials
  219. if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
  220. return true
  221. }
  222. // Check for AWS profile
  223. if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
  224. return true
  225. }
  226. // Check for AWS region
  227. if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
  228. return true
  229. }
  230. // Check if running on EC2 with instance profile
  231. if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
  232. os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
  233. return true
  234. }
  235. return false
  236. }
  237. // readConfig handles the result of reading a configuration file.
  238. func readConfig(err error) error {
  239. if err == nil {
  240. return nil
  241. }
  242. // It's okay if the config file doesn't exist
  243. if _, ok := err.(viper.ConfigFileNotFoundError); ok {
  244. return nil
  245. }
  246. return fmt.Errorf("failed to read config: %w", err)
  247. }
  248. // mergeLocalConfig loads and merges configuration from the local directory.
  249. func mergeLocalConfig(workingDir string) {
  250. local := viper.New()
  251. local.SetConfigName(fmt.Sprintf(".%s", appName))
  252. local.SetConfigType("json")
  253. local.AddConfigPath(workingDir)
  254. // Merge local config if it exists
  255. if err := local.ReadInConfig(); err == nil {
  256. viper.MergeConfigMap(local.AllSettings())
  257. }
  258. }
  259. // applyDefaultValues sets default values for configuration fields that need processing.
  260. func applyDefaultValues() {
  261. // Set default MCP type if not specified
  262. for k, v := range cfg.MCPServers {
  263. if v.Type == "" {
  264. v.Type = MCPStdio
  265. cfg.MCPServers[k] = v
  266. }
  267. }
  268. }
  269. // Validate checks if the configuration is valid and applies defaults where needed.
  270. // It validates model IDs and providers, ensuring they are supported.
  271. func Validate() error {
  272. if cfg == nil {
  273. return fmt.Errorf("config not loaded")
  274. }
  275. // Validate agent models
  276. for name, agent := range cfg.Agents {
  277. // Check if model exists
  278. model, modelExists := models.SupportedModels[agent.Model]
  279. if !modelExists {
  280. logging.Warn("unsupported model configured, reverting to default",
  281. "agent", name,
  282. "configured_model", agent.Model)
  283. // Set default model based on available providers
  284. if setDefaultModelForAgent(name) {
  285. logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
  286. } else {
  287. return fmt.Errorf("no valid provider available for agent %s", name)
  288. }
  289. continue
  290. }
  291. // Check if provider for the model is configured
  292. provider := model.Provider
  293. providerCfg, providerExists := cfg.Providers[provider]
  294. if !providerExists {
  295. // Provider not configured, check if we have environment variables
  296. apiKey := getProviderAPIKey(provider)
  297. if apiKey == "" {
  298. logging.Warn("provider not configured for model, reverting to default",
  299. "agent", name,
  300. "model", agent.Model,
  301. "provider", provider)
  302. // Set default model based on available providers
  303. if setDefaultModelForAgent(name) {
  304. logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
  305. } else {
  306. return fmt.Errorf("no valid provider available for agent %s", name)
  307. }
  308. } else {
  309. // Add provider with API key from environment
  310. cfg.Providers[provider] = Provider{
  311. APIKey: apiKey,
  312. }
  313. logging.Info("added provider from environment", "provider", provider)
  314. }
  315. } else if providerCfg.Disabled || providerCfg.APIKey == "" {
  316. // Provider is disabled or has no API key
  317. logging.Warn("provider is disabled or has no API key, reverting to default",
  318. "agent", name,
  319. "model", agent.Model,
  320. "provider", provider)
  321. // Set default model based on available providers
  322. if setDefaultModelForAgent(name) {
  323. logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
  324. } else {
  325. return fmt.Errorf("no valid provider available for agent %s", name)
  326. }
  327. }
  328. // Validate max tokens
  329. if agent.MaxTokens <= 0 {
  330. logging.Warn("invalid max tokens, setting to default",
  331. "agent", name,
  332. "model", agent.Model,
  333. "max_tokens", agent.MaxTokens)
  334. // Update the agent with default max tokens
  335. updatedAgent := cfg.Agents[name]
  336. if model.DefaultMaxTokens > 0 {
  337. updatedAgent.MaxTokens = model.DefaultMaxTokens
  338. } else {
  339. updatedAgent.MaxTokens = 4096 // Fallback default
  340. }
  341. cfg.Agents[name] = updatedAgent
  342. } else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
  343. // Ensure max tokens doesn't exceed half the context window (reasonable limit)
  344. logging.Warn("max tokens exceeds half the context window, adjusting",
  345. "agent", name,
  346. "model", agent.Model,
  347. "max_tokens", agent.MaxTokens,
  348. "context_window", model.ContextWindow)
  349. // Update the agent with adjusted max tokens
  350. updatedAgent := cfg.Agents[name]
  351. updatedAgent.MaxTokens = model.ContextWindow / 2
  352. cfg.Agents[name] = updatedAgent
  353. }
  354. // Validate reasoning effort for models that support reasoning
  355. if model.CanReason && provider == models.ProviderOpenAI {
  356. if agent.ReasoningEffort == "" {
  357. // Set default reasoning effort for models that support it
  358. logging.Info("setting default reasoning effort for model that supports reasoning",
  359. "agent", name,
  360. "model", agent.Model)
  361. // Update the agent with default reasoning effort
  362. updatedAgent := cfg.Agents[name]
  363. updatedAgent.ReasoningEffort = "medium"
  364. cfg.Agents[name] = updatedAgent
  365. } else {
  366. // Check if reasoning effort is valid (low, medium, high)
  367. effort := strings.ToLower(agent.ReasoningEffort)
  368. if effort != "low" && effort != "medium" && effort != "high" {
  369. logging.Warn("invalid reasoning effort, setting to medium",
  370. "agent", name,
  371. "model", agent.Model,
  372. "reasoning_effort", agent.ReasoningEffort)
  373. // Update the agent with valid reasoning effort
  374. updatedAgent := cfg.Agents[name]
  375. updatedAgent.ReasoningEffort = "medium"
  376. cfg.Agents[name] = updatedAgent
  377. }
  378. }
  379. } else if !model.CanReason && agent.ReasoningEffort != "" {
  380. // Model doesn't support reasoning but reasoning effort is set
  381. logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
  382. "agent", name,
  383. "model", agent.Model,
  384. "reasoning_effort", agent.ReasoningEffort)
  385. // Update the agent to remove reasoning effort
  386. updatedAgent := cfg.Agents[name]
  387. updatedAgent.ReasoningEffort = ""
  388. cfg.Agents[name] = updatedAgent
  389. }
  390. }
  391. // Validate providers
  392. for provider, providerCfg := range cfg.Providers {
  393. if providerCfg.APIKey == "" && !providerCfg.Disabled {
  394. logging.Warn("provider has no API key, marking as disabled", "provider", provider)
  395. providerCfg.Disabled = true
  396. cfg.Providers[provider] = providerCfg
  397. }
  398. }
  399. // Validate LSP configurations
  400. for language, lspConfig := range cfg.LSP {
  401. if lspConfig.Command == "" && !lspConfig.Disabled {
  402. logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
  403. lspConfig.Disabled = true
  404. cfg.LSP[language] = lspConfig
  405. }
  406. }
  407. return nil
  408. }
  409. // getProviderAPIKey gets the API key for a provider from environment variables
  410. func getProviderAPIKey(provider models.ModelProvider) string {
  411. switch provider {
  412. case models.ProviderAnthropic:
  413. return os.Getenv("ANTHROPIC_API_KEY")
  414. case models.ProviderOpenAI:
  415. return os.Getenv("OPENAI_API_KEY")
  416. case models.ProviderGemini:
  417. return os.Getenv("GEMINI_API_KEY")
  418. case models.ProviderGROQ:
  419. return os.Getenv("GROQ_API_KEY")
  420. case models.ProviderBedrock:
  421. if hasAWSCredentials() {
  422. return "aws-credentials-available"
  423. }
  424. }
  425. return ""
  426. }
  427. // setDefaultModelForAgent sets a default model for an agent based on available providers
  428. func setDefaultModelForAgent(agent AgentName) bool {
  429. // Check providers in order of preference
  430. if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
  431. maxTokens := int64(5000)
  432. if agent == AgentTitle {
  433. maxTokens = 80
  434. }
  435. cfg.Agents[agent] = Agent{
  436. Model: models.Claude37Sonnet,
  437. MaxTokens: maxTokens,
  438. }
  439. return true
  440. }
  441. if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
  442. var model models.ModelID
  443. maxTokens := int64(5000)
  444. reasoningEffort := ""
  445. switch agent {
  446. case AgentTitle:
  447. model = models.GPT41Mini
  448. maxTokens = 80
  449. case AgentTask:
  450. model = models.GPT41Mini
  451. default:
  452. model = models.GPT41
  453. }
  454. // Check if model supports reasoning
  455. if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
  456. reasoningEffort = "medium"
  457. }
  458. cfg.Agents[agent] = Agent{
  459. Model: model,
  460. MaxTokens: maxTokens,
  461. ReasoningEffort: reasoningEffort,
  462. }
  463. return true
  464. }
  465. if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
  466. var model models.ModelID
  467. maxTokens := int64(5000)
  468. if agent == AgentTitle {
  469. model = models.Gemini25Flash
  470. maxTokens = 80
  471. } else {
  472. model = models.Gemini25
  473. }
  474. cfg.Agents[agent] = Agent{
  475. Model: model,
  476. MaxTokens: maxTokens,
  477. }
  478. return true
  479. }
  480. if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
  481. maxTokens := int64(5000)
  482. if agent == AgentTitle {
  483. maxTokens = 80
  484. }
  485. cfg.Agents[agent] = Agent{
  486. Model: models.QWENQwq,
  487. MaxTokens: maxTokens,
  488. }
  489. return true
  490. }
  491. if hasAWSCredentials() {
  492. maxTokens := int64(5000)
  493. if agent == AgentTitle {
  494. maxTokens = 80
  495. }
  496. cfg.Agents[agent] = Agent{
  497. Model: models.BedrockClaude37Sonnet,
  498. MaxTokens: maxTokens,
  499. ReasoningEffort: "medium", // Claude models support reasoning
  500. }
  501. return true
  502. }
  503. return false
  504. }
  505. // Get returns the current configuration.
  506. // It's safe to call this function multiple times.
  507. func Get() *Config {
  508. return cfg
  509. }
  510. // WorkingDirectory returns the current working directory from the configuration.
  511. func WorkingDirectory() string {
  512. if cfg == nil {
  513. panic("config not loaded")
  514. }
  515. return cfg.WorkingDir
  516. }