config.go 22 KB

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