config.go 23 KB

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