config.go 24 KB

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