Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit 5601466

Browse files
committed
cleanup config
1 parent d63d0c4 commit 5601466

File tree

2 files changed

+183
-564
lines changed

2 files changed

+183
-564
lines changed

internal/config/config.go

Lines changed: 183 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,200 +1,284 @@
1+
// Package config manages application configuration from various sources.
12
package config
23

34
import (
45
"fmt"
6+
"log/slog"
57
"os"
68
"strings"
79

810
"github.com/kujtimiihoxha/termai/internal/llm/models"
11+
"github.com/kujtimiihoxha/termai/internal/logging"
912
"github.com/spf13/viper"
1013
)
1114

15+
// MCPType defines the type of MCP (Model Control Protocol) server.
1216
type MCPType string
1317

18+
// Supported MCP types
1419
const (
1520
MCPStdio MCPType = "stdio"
1621
MCPSse MCPType = "sse"
1722
)
1823

24+
// MCPServer defines the configuration for a Model Control Protocol server.
1925
type MCPServer struct {
2026
Command string `json:"command"`
2127
Env []string `json:"env"`
2228
Args []string `json:"args"`
2329
Type MCPType `json:"type"`
2430
URL string `json:"url"`
2531
Headers map[string]string `json:"headers"`
26-
// TODO: add permissions configuration
27-
// TODO: add the ability to specify the tools to import
2832
}
2933

34+
// Model defines configuration for different LLM models and their token limits.
3035
type Model struct {
3136
Coder models.ModelID `json:"coder"`
3237
CoderMaxTokens int64 `json:"coderMaxTokens"`
33-
34-
Task models.ModelID `json:"task"`
35-
TaskMaxTokens int64 `json:"taskMaxTokens"`
36-
// TODO: Maybe support multiple models for different purposes
37-
}
38-
39-
type AnthropicConfig struct {
40-
DisableCache bool `json:"disableCache"`
41-
UseBedrock bool `json:"useBedrock"`
38+
Task models.ModelID `json:"task"`
39+
TaskMaxTokens int64 `json:"taskMaxTokens"`
4240
}
4341

42+
// Provider defines configuration for an LLM provider.
4443
type Provider struct {
45-
APIKey string `json:"apiKey"`
46-
Enabled bool `json:"enabled"`
44+
APIKey string `json:"apiKey"`
45+
Disabled bool `json:"disabled"`
4746
}
4847

48+
// Data defines storage configuration.
4949
type Data struct {
5050
Directory string `json:"directory"`
5151
}
5252

53-
type Log struct {
54-
Level string `json:"level"`
55-
}
56-
53+
// LSPConfig defines configuration for Language Server Protocol integration.
5754
type LSPConfig struct {
5855
Disabled bool `json:"enabled"`
5956
Command string `json:"command"`
6057
Args []string `json:"args"`
6158
Options any `json:"options"`
6259
}
6360

61+
// Config is the main configuration structure for the application.
6462
type Config struct {
65-
Data *Data `json:"data,omitempty"`
66-
Log *Log `json:"log,omitempty"`
63+
Data Data `json:"data"`
64+
WorkingDir string `json:"wd,omitempty"`
6765
MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
6866
Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
69-
70-
LSP map[string]LSPConfig `json:"lsp,omitempty"`
71-
72-
Model *Model `json:"model,omitempty"`
73-
74-
Debug bool `json:"debug,omitempty"`
67+
LSP map[string]LSPConfig `json:"lsp,omitempty"`
68+
Model Model `json:"model"`
69+
Debug bool `json:"debug,omitempty"`
7570
}
7671

77-
var cfg *Config
78-
72+
// Application constants
7973
const (
80-
defaultDataDirectory = ".termai"
74+
defaultDataDirectory = ".opencode"
8175
defaultLogLevel = "info"
8276
defaultMaxTokens = int64(5000)
83-
termai = "termai"
77+
appName = "opencode"
8478
)
8579

86-
func Load(debug bool) error {
80+
// Global configuration instance
81+
var cfg *Config
82+
83+
// Load initializes the configuration from environment variables and config files.
84+
// If debug is true, debug mode is enabled and log level is set to debug.
85+
// It returns an error if configuration loading fails.
86+
func Load(workingDir string, debug bool) error {
8787
if cfg != nil {
8888
return nil
8989
}
9090

91-
viper.SetConfigName(fmt.Sprintf(".%s", termai))
91+
cfg = &Config{
92+
WorkingDir: workingDir,
93+
MCPServers: make(map[string]MCPServer),
94+
Providers: make(map[models.ModelProvider]Provider),
95+
LSP: make(map[string]LSPConfig),
96+
}
97+
98+
configureViper()
99+
setDefaults(debug)
100+
setProviderDefaults()
101+
102+
// Read global config
103+
if err := readConfig(viper.ReadInConfig()); err != nil {
104+
return err
105+
}
106+
107+
// Load and merge local config
108+
mergeLocalConfig(workingDir)
109+
110+
// Apply configuration to the struct
111+
if err := viper.Unmarshal(cfg); err != nil {
112+
return err
113+
}
114+
115+
applyDefaultValues()
116+
117+
defaultLevel := slog.LevelInfo
118+
if cfg.Debug {
119+
defaultLevel = slog.LevelDebug
120+
}
121+
// Configure logger
122+
logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
123+
Level: defaultLevel,
124+
}))
125+
slog.SetDefault(logger)
126+
return nil
127+
}
128+
129+
// configureViper sets up viper's configuration paths and environment variables.
130+
func configureViper() {
131+
viper.SetConfigName(fmt.Sprintf(".%s", appName))
92132
viper.SetConfigType("json")
93133
viper.AddConfigPath("$HOME")
94-
viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", termai))
95-
viper.SetEnvPrefix(strings.ToUpper(termai))
134+
viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
135+
viper.SetEnvPrefix(strings.ToUpper(appName))
136+
viper.AutomaticEnv()
137+
}
96138

97-
// Add defaults
139+
// setDefaults configures default values for configuration options.
140+
func setDefaults(debug bool) {
98141
viper.SetDefault("data.directory", defaultDataDirectory)
142+
99143
if debug {
100144
viper.SetDefault("debug", true)
101145
viper.Set("log.level", "debug")
102146
} else {
103147
viper.SetDefault("debug", false)
104148
viper.SetDefault("log.level", defaultLogLevel)
105149
}
150+
}
151+
152+
// setProviderDefaults configures LLM provider defaults based on environment variables.
153+
// the default model priority is:
154+
// 1. Anthropic
155+
// 2. OpenAI
156+
// 3. Google Gemini
157+
// 4. AWS Bedrock
158+
func setProviderDefaults() {
159+
// Groq configuration
160+
if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
161+
viper.SetDefault("providers.groq.apiKey", apiKey)
162+
viper.SetDefault("model.coder", models.QWENQwq)
163+
viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
164+
viper.SetDefault("model.task", models.QWENQwq)
165+
viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
166+
}
167+
168+
// Google Gemini configuration
169+
if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
170+
viper.SetDefault("providers.gemini.apiKey", apiKey)
171+
viper.SetDefault("model.coder", models.GRMINI20Flash)
172+
viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
173+
viper.SetDefault("model.task", models.GRMINI20Flash)
174+
viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
175+
}
106176

107-
defaultModelSet := false
108-
if os.Getenv("ANTHROPIC_API_KEY") != "" {
109-
viper.SetDefault("providers.anthropic.apiKey", os.Getenv("ANTHROPIC_API_KEY"))
110-
viper.SetDefault("providers.anthropic.enabled", true)
177+
// OpenAI configuration
178+
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
179+
viper.SetDefault("providers.openai.apiKey", apiKey)
180+
viper.SetDefault("model.coder", models.GPT4o)
181+
viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
182+
viper.SetDefault("model.task", models.GPT4o)
183+
viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
184+
}
185+
186+
// Anthropic configuration
187+
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
188+
viper.SetDefault("providers.anthropic.apiKey", apiKey)
111189
viper.SetDefault("model.coder", models.Claude37Sonnet)
190+
viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
112191
viper.SetDefault("model.task", models.Claude37Sonnet)
113-
defaultModelSet = true
114-
}
115-
if os.Getenv("OPENAI_API_KEY") != "" {
116-
viper.SetDefault("providers.openai.apiKey", os.Getenv("OPENAI_API_KEY"))
117-
viper.SetDefault("providers.openai.enabled", true)
118-
if !defaultModelSet {
119-
viper.SetDefault("model.coder", models.GPT41)
120-
viper.SetDefault("model.task", models.GPT41)
121-
defaultModelSet = true
122-
}
192+
viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
123193
}
124-
if os.Getenv("GEMINI_API_KEY") != "" {
125-
viper.SetDefault("providers.gemini.apiKey", os.Getenv("GEMINI_API_KEY"))
126-
viper.SetDefault("providers.gemini.enabled", true)
127-
if !defaultModelSet {
128-
viper.SetDefault("model.coder", models.GRMINI20Flash)
129-
viper.SetDefault("model.task", models.GRMINI20Flash)
130-
defaultModelSet = true
131-
}
194+
195+
if hasAWSCredentials() {
196+
viper.SetDefault("model.coder", models.BedrockClaude37Sonnet)
197+
viper.SetDefault("model.coderMaxTokens", defaultMaxTokens)
198+
viper.SetDefault("model.task", models.BedrockClaude37Sonnet)
199+
viper.SetDefault("model.taskMaxTokens", defaultMaxTokens)
132200
}
133-
if os.Getenv("GROQ_API_KEY") != "" {
134-
viper.SetDefault("providers.groq.apiKey", os.Getenv("GROQ_API_KEY"))
135-
viper.SetDefault("providers.groq.enabled", true)
136-
if !defaultModelSet {
137-
viper.SetDefault("model.coder", models.QWENQwq)
138-
viper.SetDefault("model.task", models.QWENQwq)
139-
defaultModelSet = true
140-
}
201+
}
202+
203+
// hasAWSCredentials checks if AWS credentials are available in the environment.
204+
func hasAWSCredentials() bool {
205+
// Check for explicit AWS credentials
206+
if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
207+
return true
141208
}
142209

143-
viper.SetDefault("providers.bedrock.enabled", true)
144-
// TODO: add more providers
145-
cfg = &Config{}
210+
// Check for AWS profile
211+
if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
212+
return true
213+
}
146214

147-
err := viper.ReadInConfig()
148-
if err != nil {
149-
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
150-
return err
151-
}
215+
// Check for AWS region
216+
if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
217+
return true
152218
}
153-
local := viper.New()
154-
local.SetConfigName(fmt.Sprintf(".%s", termai))
155-
local.SetConfigType("json")
156-
local.AddConfigPath(".")
157-
// load local config, this will override the global config
158-
if err = local.ReadInConfig(); err == nil {
159-
viper.MergeConfigMap(local.AllSettings())
219+
220+
// Check if running on EC2 with instance profile
221+
if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
222+
os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
223+
return true
224+
}
225+
226+
return false
227+
}
228+
229+
// readConfig handles the result of reading a configuration file.
230+
func readConfig(err error) error {
231+
if err == nil {
232+
return nil
160233
}
161-
viper.Unmarshal(cfg)
162234

163-
if cfg.Model != nil && cfg.Model.CoderMaxTokens <= 0 {
164-
cfg.Model.CoderMaxTokens = defaultMaxTokens
235+
// It's okay if the config file doesn't exist
236+
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
237+
return nil
165238
}
166-
if cfg.Model != nil && cfg.Model.TaskMaxTokens <= 0 {
167-
cfg.Model.TaskMaxTokens = defaultMaxTokens
239+
240+
return err
241+
}
242+
243+
// mergeLocalConfig loads and merges configuration from the local directory.
244+
func mergeLocalConfig(workingDir string) {
245+
local := viper.New()
246+
local.SetConfigName(fmt.Sprintf(".%s", appName))
247+
local.SetConfigType("json")
248+
local.AddConfigPath(workingDir)
249+
250+
// Merge local config if it exists
251+
if err := local.ReadInConfig(); err == nil {
252+
viper.MergeConfigMap(local.AllSettings())
168253
}
254+
}
169255

170-
for _, v := range cfg.MCPServers {
256+
// applyDefaultValues sets default values for configuration fields that need processing.
257+
func applyDefaultValues() {
258+
// Set default MCP type if not specified
259+
for k, v := range cfg.MCPServers {
171260
if v.Type == "" {
172261
v.Type = MCPStdio
262+
cfg.MCPServers[k] = v
173263
}
174264
}
265+
}
175266

267+
// setWorkingDirectory stores the current working directory in the configuration.
268+
func setWorkingDirectory() {
176269
workdir, err := os.Getwd()
177-
if err != nil {
178-
return err
270+
if err == nil {
271+
viper.Set("wd", workdir)
179272
}
180-
viper.Set("wd", workdir)
181-
return nil
182273
}
183274

275+
// Get returns the current configuration.
276+
// It's safe to call this function multiple times.
184277
func Get() *Config {
185-
if cfg == nil {
186-
err := Load(false)
187-
if err != nil {
188-
panic(err)
189-
}
190-
}
191278
return cfg
192279
}
193280

281+
// WorkingDirectory returns the current working directory from the configuration.
194282
func WorkingDirectory() string {
195283
return viper.GetString("wd")
196284
}
197-
198-
func Write() error {
199-
return viper.WriteConfig()
200-
}

0 commit comments

Comments
 (0)