2025-04-13 12:25:11 +02:00
|
|
|
|
// Package config manages application configuration from various sources.
|
2025-03-27 22:35:48 +01:00
|
|
|
|
package config
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2025-05-01 05:49:26 -05:00
|
|
|
|
"encoding/json"
|
2025-03-27 22:35:48 +01:00
|
|
|
|
"fmt"
|
2025-04-13 12:25:11 +02:00
|
|
|
|
"log/slog"
|
2025-03-27 22:35:48 +01:00
|
|
|
|
"os"
|
2025-05-01 05:49:26 -05:00
|
|
|
|
"path/filepath"
|
2025-03-27 22:35:48 +01:00
|
|
|
|
"strings"
|
|
|
|
|
|
|
2025-04-24 18:25:52 +02:00
|
|
|
|
"github.com/opencode-ai/opencode/internal/llm/models"
|
|
|
|
|
|
"github.com/opencode-ai/opencode/internal/logging"
|
2025-03-27 22:35:48 +01:00
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// MCPType defines the type of MCP (Model Control Protocol) server.
|
2025-03-27 22:35:48 +01:00
|
|
|
|
type MCPType string
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// Supported MCP types
|
2025-03-27 22:35:48 +01:00
|
|
|
|
const (
|
|
|
|
|
|
MCPStdio MCPType = "stdio"
|
|
|
|
|
|
MCPSse MCPType = "sse"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// MCPServer defines the configuration for a Model Control Protocol server.
|
2025-03-27 22:35:48 +01:00
|
|
|
|
type MCPServer struct {
|
|
|
|
|
|
Command string `json:"command"`
|
|
|
|
|
|
Env []string `json:"env"`
|
|
|
|
|
|
Args []string `json:"args"`
|
|
|
|
|
|
Type MCPType `json:"type"`
|
|
|
|
|
|
URL string `json:"url"`
|
|
|
|
|
|
Headers map[string]string `json:"headers"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 20:06:23 +02:00
|
|
|
|
type AgentName string
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
AgentCoder AgentName = "coder"
|
|
|
|
|
|
AgentTask AgentName = "task"
|
|
|
|
|
|
AgentTitle AgentName = "title"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Agent defines configuration for different LLM models and their token limits.
|
|
|
|
|
|
type Agent struct {
|
2025-04-18 20:17:38 +02:00
|
|
|
|
Model models.ModelID `json:"model"`
|
|
|
|
|
|
MaxTokens int64 `json:"maxTokens"`
|
|
|
|
|
|
ReasoningEffort string `json:"reasoningEffort"` // For openai models low,medium,heigh
|
2025-04-09 17:45:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// Provider defines configuration for an LLM provider.
|
2025-03-27 22:35:48 +01:00
|
|
|
|
type Provider struct {
|
2025-04-13 12:25:11 +02:00
|
|
|
|
APIKey string `json:"apiKey"`
|
|
|
|
|
|
Disabled bool `json:"disabled"`
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// Data defines storage configuration.
|
2025-03-27 22:35:48 +01:00
|
|
|
|
type Data struct {
|
|
|
|
|
|
Directory string `json:"directory"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// LSPConfig defines configuration for Language Server Protocol integration.
|
2025-04-03 15:20:15 +02:00
|
|
|
|
type LSPConfig struct {
|
|
|
|
|
|
Disabled bool `json:"enabled"`
|
|
|
|
|
|
Command string `json:"command"`
|
|
|
|
|
|
Args []string `json:"args"`
|
|
|
|
|
|
Options any `json:"options"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-01 05:49:26 -05:00
|
|
|
|
// TUIConfig defines the configuration for the Terminal User Interface.
|
|
|
|
|
|
type TUIConfig struct {
|
|
|
|
|
|
Theme string `json:"theme,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// Config is the main configuration structure for the application.
|
2025-03-27 22:35:48 +01:00
|
|
|
|
type Config struct {
|
2025-04-27 14:11:09 -04:00
|
|
|
|
Data Data `json:"data"`
|
|
|
|
|
|
WorkingDir string `json:"wd,omitempty"`
|
|
|
|
|
|
MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
|
|
|
|
|
|
Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
|
|
|
|
|
|
LSP map[string]LSPConfig `json:"lsp,omitempty"`
|
|
|
|
|
|
Agents map[AgentName]Agent `json:"agents"`
|
|
|
|
|
|
Debug bool `json:"debug,omitempty"`
|
|
|
|
|
|
DebugLSP bool `json:"debugLSP,omitempty"`
|
|
|
|
|
|
ContextPaths []string `json:"contextPaths,omitempty"`
|
2025-05-01 05:49:26 -05:00
|
|
|
|
TUI TUIConfig `json:"tui"`
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// Application constants
|
2025-03-27 22:35:48 +01:00
|
|
|
|
const (
|
2025-04-13 12:25:11 +02:00
|
|
|
|
defaultDataDirectory = ".opencode"
|
2025-03-27 22:35:48 +01:00
|
|
|
|
defaultLogLevel = "info"
|
2025-04-13 12:25:11 +02:00
|
|
|
|
appName = "opencode"
|
2025-04-28 12:25:06 -05:00
|
|
|
|
|
|
|
|
|
|
MaxTokensFallbackDefault = 4096
|
2025-03-27 22:35:48 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-04-27 14:11:09 -04:00
|
|
|
|
var defaultContextPaths = []string{
|
|
|
|
|
|
".github/copilot-instructions.md",
|
|
|
|
|
|
".cursorrules",
|
|
|
|
|
|
".cursor/rules/",
|
|
|
|
|
|
"CLAUDE.md",
|
|
|
|
|
|
"CLAUDE.local.md",
|
|
|
|
|
|
"opencode.md",
|
|
|
|
|
|
"opencode.local.md",
|
|
|
|
|
|
"OpenCode.md",
|
|
|
|
|
|
"OpenCode.local.md",
|
|
|
|
|
|
"OPENCODE.md",
|
|
|
|
|
|
"OPENCODE.local.md",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// Global configuration instance
|
|
|
|
|
|
var cfg *Config
|
|
|
|
|
|
|
|
|
|
|
|
// Load initializes the configuration from environment variables and config files.
|
|
|
|
|
|
// If debug is true, debug mode is enabled and log level is set to debug.
|
|
|
|
|
|
// It returns an error if configuration loading fails.
|
2025-04-13 13:17:17 +02:00
|
|
|
|
func Load(workingDir string, debug bool) (*Config, error) {
|
2025-03-27 22:35:48 +01:00
|
|
|
|
if cfg != nil {
|
2025-04-13 13:17:17 +02:00
|
|
|
|
return cfg, nil
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
cfg = &Config{
|
|
|
|
|
|
WorkingDir: workingDir,
|
|
|
|
|
|
MCPServers: make(map[string]MCPServer),
|
|
|
|
|
|
Providers: make(map[models.ModelProvider]Provider),
|
|
|
|
|
|
LSP: make(map[string]LSPConfig),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
configureViper()
|
|
|
|
|
|
setDefaults(debug)
|
|
|
|
|
|
|
|
|
|
|
|
// Read global config
|
|
|
|
|
|
if err := readConfig(viper.ReadInConfig()); err != nil {
|
2025-04-13 13:17:17 +02:00
|
|
|
|
return cfg, err
|
2025-04-13 12:25:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Load and merge local config
|
|
|
|
|
|
mergeLocalConfig(workingDir)
|
|
|
|
|
|
|
2025-05-01 06:22:48 -05:00
|
|
|
|
setProviderDefaults()
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// Apply configuration to the struct
|
|
|
|
|
|
if err := viper.Unmarshal(cfg); err != nil {
|
2025-04-13 13:17:17 +02:00
|
|
|
|
return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
|
2025-04-13 12:25:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
applyDefaultValues()
|
|
|
|
|
|
defaultLevel := slog.LevelInfo
|
|
|
|
|
|
if cfg.Debug {
|
|
|
|
|
|
defaultLevel = slog.LevelDebug
|
|
|
|
|
|
}
|
2025-04-21 14:29:03 +02:00
|
|
|
|
if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
|
2025-04-16 20:06:23 +02:00
|
|
|
|
loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
|
|
|
|
|
|
|
|
|
|
|
|
// if file does not exist create it
|
|
|
|
|
|
if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
|
|
|
|
|
|
if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil {
|
|
|
|
|
|
return cfg, fmt.Errorf("failed to create directory: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := os.Create(loggingFile); err != nil {
|
|
|
|
|
|
return cfg, fmt.Errorf("failed to create log file: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return cfg, fmt.Errorf("failed to open log file: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
// Configure logger
|
|
|
|
|
|
logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
|
|
|
|
|
|
Level: defaultLevel,
|
|
|
|
|
|
}))
|
|
|
|
|
|
slog.SetDefault(logger)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Configure logger
|
|
|
|
|
|
logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
|
|
|
|
|
|
Level: defaultLevel,
|
|
|
|
|
|
}))
|
|
|
|
|
|
slog.SetDefault(logger)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-21 14:29:03 +02:00
|
|
|
|
// Validate configuration
|
|
|
|
|
|
if err := Validate(); err != nil {
|
|
|
|
|
|
return cfg, fmt.Errorf("config validation failed: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-17 12:44:15 +02:00
|
|
|
|
if cfg.Agents == nil {
|
|
|
|
|
|
cfg.Agents = make(map[AgentName]Agent)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 20:06:23 +02:00
|
|
|
|
// Override the max tokens for title agent
|
|
|
|
|
|
cfg.Agents[AgentTitle] = Agent{
|
|
|
|
|
|
Model: cfg.Agents[AgentTitle].Model,
|
|
|
|
|
|
MaxTokens: 80,
|
|
|
|
|
|
}
|
2025-04-13 13:17:17 +02:00
|
|
|
|
return cfg, nil
|
2025-04-13 12:25:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// configureViper sets up viper's configuration paths and environment variables.
|
|
|
|
|
|
func configureViper() {
|
|
|
|
|
|
viper.SetConfigName(fmt.Sprintf(".%s", appName))
|
2025-03-27 22:35:48 +01:00
|
|
|
|
viper.SetConfigType("json")
|
|
|
|
|
|
viper.AddConfigPath("$HOME")
|
2025-04-13 12:25:11 +02:00
|
|
|
|
viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
|
2025-04-22 00:49:23 +05:30
|
|
|
|
viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
|
2025-04-13 12:25:11 +02:00
|
|
|
|
viper.SetEnvPrefix(strings.ToUpper(appName))
|
|
|
|
|
|
viper.AutomaticEnv()
|
|
|
|
|
|
}
|
2025-03-27 22:35:48 +01:00
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// setDefaults configures default values for configuration options.
|
|
|
|
|
|
func setDefaults(debug bool) {
|
2025-03-27 22:35:48 +01:00
|
|
|
|
viper.SetDefault("data.directory", defaultDataDirectory)
|
2025-04-27 14:11:09 -04:00
|
|
|
|
viper.SetDefault("contextPaths", defaultContextPaths)
|
2025-05-01 05:49:26 -05:00
|
|
|
|
viper.SetDefault("tui.theme", "opencode")
|
2025-04-13 12:25:11 +02:00
|
|
|
|
|
2025-03-27 22:35:48 +01:00
|
|
|
|
if debug {
|
2025-04-09 19:07:39 +02:00
|
|
|
|
viper.SetDefault("debug", true)
|
2025-03-27 22:35:48 +01:00
|
|
|
|
viper.Set("log.level", "debug")
|
|
|
|
|
|
} else {
|
2025-04-09 19:07:39 +02:00
|
|
|
|
viper.SetDefault("debug", false)
|
2025-03-27 22:35:48 +01:00
|
|
|
|
viper.SetDefault("log.level", defaultLogLevel)
|
|
|
|
|
|
}
|
2025-04-13 12:25:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-01 06:22:48 -05:00
|
|
|
|
// setProviderDefaults configures LLM provider defaults based on provider provided by
|
|
|
|
|
|
// environment variables and configuration file.
|
2025-04-13 12:25:11 +02:00
|
|
|
|
func setProviderDefaults() {
|
2025-04-27 14:41:31 +02:00
|
|
|
|
// Set all API keys we can find in the environment
|
2025-04-22 00:53:05 -04:00
|
|
|
|
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
|
|
|
|
|
|
viper.SetDefault("providers.anthropic.apiKey", apiKey)
|
2025-04-27 14:41:31 +02:00
|
|
|
|
}
|
|
|
|
|
|
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
|
|
|
|
|
|
viper.SetDefault("providers.openai.apiKey", apiKey)
|
|
|
|
|
|
}
|
|
|
|
|
|
if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
|
|
|
|
|
|
viper.SetDefault("providers.gemini.apiKey", apiKey)
|
|
|
|
|
|
}
|
|
|
|
|
|
if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
|
|
|
|
|
|
viper.SetDefault("providers.groq.apiKey", apiKey)
|
|
|
|
|
|
}
|
2025-04-30 03:50:57 -07:00
|
|
|
|
if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
|
|
|
|
|
|
viper.SetDefault("providers.openrouter.apiKey", apiKey)
|
|
|
|
|
|
}
|
2025-05-01 14:17:33 +02:00
|
|
|
|
if apiKey := os.Getenv("XAI_API_KEY"); apiKey != "" {
|
|
|
|
|
|
viper.SetDefault("providers.xai.apiKey", apiKey)
|
|
|
|
|
|
}
|
|
|
|
|
|
if apiKey := os.Getenv("AZURE_OPENAI_ENDPOINT"); apiKey != "" {
|
|
|
|
|
|
// api-key may be empty when using Entra ID credentials – that's okay
|
|
|
|
|
|
viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY"))
|
|
|
|
|
|
}
|
2025-04-27 14:41:31 +02:00
|
|
|
|
|
|
|
|
|
|
// Use this order to set the default models
|
|
|
|
|
|
// 1. Anthropic
|
|
|
|
|
|
// 2. OpenAI
|
|
|
|
|
|
// 3. Google Gemini
|
|
|
|
|
|
// 4. Groq
|
2025-05-01 06:22:48 -05:00
|
|
|
|
// 5. OpenRouter
|
|
|
|
|
|
// 6. AWS Bedrock
|
|
|
|
|
|
// 7. Azure
|
|
|
|
|
|
|
2025-04-27 14:41:31 +02:00
|
|
|
|
// Anthropic configuration
|
2025-05-05 09:40:58 +02:00
|
|
|
|
if key := viper.GetString("providers.anthropic.apiKey"); strings.TrimSpace(key) != "" {
|
2025-04-22 00:53:05 -04:00
|
|
|
|
viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
|
|
|
|
|
|
viper.SetDefault("agents.task.model", models.Claude37Sonnet)
|
|
|
|
|
|
viper.SetDefault("agents.title.model", models.Claude37Sonnet)
|
|
|
|
|
|
return
|
2025-04-13 12:25:11 +02:00
|
|
|
|
}
|
2025-03-27 22:35:48 +01:00
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// OpenAI configuration
|
2025-05-05 09:40:58 +02:00
|
|
|
|
if key := viper.GetString("providers.openai.apiKey"); strings.TrimSpace(key) != "" {
|
2025-04-19 15:15:29 +02:00
|
|
|
|
viper.SetDefault("agents.coder.model", models.GPT41)
|
|
|
|
|
|
viper.SetDefault("agents.task.model", models.GPT41Mini)
|
|
|
|
|
|
viper.SetDefault("agents.title.model", models.GPT41Mini)
|
2025-04-22 00:53:05 -04:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-04-16 20:06:23 +02:00
|
|
|
|
|
2025-04-22 00:53:05 -04:00
|
|
|
|
// Google Gemini configuration
|
2025-05-05 09:40:58 +02:00
|
|
|
|
if key := viper.GetString("providers.gemini.apiKey"); strings.TrimSpace(key) != "" {
|
2025-04-22 00:53:05 -04:00
|
|
|
|
viper.SetDefault("agents.coder.model", models.Gemini25)
|
|
|
|
|
|
viper.SetDefault("agents.task.model", models.Gemini25Flash)
|
|
|
|
|
|
viper.SetDefault("agents.title.model", models.Gemini25Flash)
|
|
|
|
|
|
return
|
2025-04-13 12:25:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-22 00:53:05 -04:00
|
|
|
|
// Groq configuration
|
2025-05-05 09:40:58 +02:00
|
|
|
|
if key := viper.GetString("providers.groq.apiKey"); strings.TrimSpace(key) != "" {
|
2025-04-22 00:53:05 -04:00
|
|
|
|
viper.SetDefault("agents.coder.model", models.QWENQwq)
|
|
|
|
|
|
viper.SetDefault("agents.task.model", models.QWENQwq)
|
|
|
|
|
|
viper.SetDefault("agents.title.model", models.QWENQwq)
|
|
|
|
|
|
return
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
2025-04-13 12:25:11 +02:00
|
|
|
|
|
2025-04-29 12:56:49 +01:00
|
|
|
|
// OpenRouter configuration
|
2025-05-05 09:40:58 +02:00
|
|
|
|
if key := viper.GetString("providers.openrouter.apiKey"); strings.TrimSpace(key) != "" {
|
2025-04-29 12:56:49 +01:00
|
|
|
|
viper.SetDefault("agents.coder.model", models.OpenRouterClaude37Sonnet)
|
|
|
|
|
|
viper.SetDefault("agents.task.model", models.OpenRouterClaude37Sonnet)
|
|
|
|
|
|
viper.SetDefault("agents.title.model", models.OpenRouterClaude35Haiku)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-05 09:40:58 +02:00
|
|
|
|
// XAI configuration
|
|
|
|
|
|
if key := viper.GetString("providers.xai.apiKey"); strings.TrimSpace(key) != "" {
|
2025-05-01 14:17:33 +02:00
|
|
|
|
viper.SetDefault("agents.coder.model", models.XAIGrok3Beta)
|
|
|
|
|
|
viper.SetDefault("agents.task.model", models.XAIGrok3Beta)
|
|
|
|
|
|
viper.SetDefault("agents.title.model", models.XAiGrok3MiniFastBeta)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-22 00:53:05 -04:00
|
|
|
|
// AWS Bedrock configuration
|
2025-04-13 12:25:11 +02:00
|
|
|
|
if hasAWSCredentials() {
|
2025-04-16 20:06:23 +02:00
|
|
|
|
viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
|
|
|
|
|
|
viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet)
|
|
|
|
|
|
viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
|
2025-04-22 00:53:05 -04:00
|
|
|
|
return
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
2025-04-28 10:42:57 -03:00
|
|
|
|
|
2025-05-05 09:40:58 +02:00
|
|
|
|
// Azure OpenAI configuration
|
2025-04-28 10:42:57 -03:00
|
|
|
|
if os.Getenv("AZURE_OPENAI_ENDPOINT") != "" {
|
|
|
|
|
|
viper.SetDefault("agents.coder.model", models.AzureGPT41)
|
|
|
|
|
|
viper.SetDefault("agents.task.model", models.AzureGPT41Mini)
|
|
|
|
|
|
viper.SetDefault("agents.title.model", models.AzureGPT41Mini)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-04-13 12:25:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// hasAWSCredentials checks if AWS credentials are available in the environment.
|
|
|
|
|
|
func hasAWSCredentials() bool {
|
|
|
|
|
|
// Check for explicit AWS credentials
|
|
|
|
|
|
if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
|
|
|
|
|
|
return true
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
2025-04-09 17:45:41 +02:00
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// Check for AWS profile
|
|
|
|
|
|
if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
2025-03-27 22:35:48 +01:00
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// Check for AWS region
|
|
|
|
|
|
if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
|
|
|
|
|
|
return true
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
2025-04-13 12:25:11 +02:00
|
|
|
|
|
|
|
|
|
|
// Check if running on EC2 with instance profile
|
|
|
|
|
|
if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
|
|
|
|
|
|
os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// readConfig handles the result of reading a configuration file.
|
|
|
|
|
|
func readConfig(err error) error {
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
return nil
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// It's okay if the config file doesn't exist
|
|
|
|
|
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
|
|
|
|
|
return nil
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
2025-04-13 12:25:11 +02:00
|
|
|
|
|
2025-04-13 13:17:17 +02:00
|
|
|
|
return fmt.Errorf("failed to read config: %w", err)
|
2025-04-13 12:25:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// mergeLocalConfig loads and merges configuration from the local directory.
|
|
|
|
|
|
func mergeLocalConfig(workingDir string) {
|
|
|
|
|
|
local := viper.New()
|
|
|
|
|
|
local.SetConfigName(fmt.Sprintf(".%s", appName))
|
|
|
|
|
|
local.SetConfigType("json")
|
|
|
|
|
|
local.AddConfigPath(workingDir)
|
|
|
|
|
|
|
|
|
|
|
|
// Merge local config if it exists
|
|
|
|
|
|
if err := local.ReadInConfig(); err == nil {
|
|
|
|
|
|
viper.MergeConfigMap(local.AllSettings())
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
2025-04-13 12:25:11 +02:00
|
|
|
|
}
|
2025-03-27 22:35:48 +01:00
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// applyDefaultValues sets default values for configuration fields that need processing.
|
|
|
|
|
|
func applyDefaultValues() {
|
|
|
|
|
|
// Set default MCP type if not specified
|
|
|
|
|
|
for k, v := range cfg.MCPServers {
|
2025-03-27 22:35:48 +01:00
|
|
|
|
if v.Type == "" {
|
|
|
|
|
|
v.Type = MCPStdio
|
2025-04-13 12:25:11 +02:00
|
|
|
|
cfg.MCPServers[k] = v
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-13 12:25:11 +02:00
|
|
|
|
}
|
2025-03-27 22:35:48 +01:00
|
|
|
|
|
2025-04-21 14:29:03 +02:00
|
|
|
|
// It validates model IDs and providers, ensuring they are supported.
|
2025-04-28 12:25:06 -05:00
|
|
|
|
func validateAgent(cfg *Config, name AgentName, agent Agent) error {
|
|
|
|
|
|
// Check if model exists
|
|
|
|
|
|
model, modelExists := models.SupportedModels[agent.Model]
|
|
|
|
|
|
if !modelExists {
|
|
|
|
|
|
logging.Warn("unsupported model configured, reverting to default",
|
|
|
|
|
|
"agent", name,
|
|
|
|
|
|
"configured_model", agent.Model)
|
|
|
|
|
|
|
|
|
|
|
|
// Set default model based on available providers
|
|
|
|
|
|
if setDefaultModelForAgent(name) {
|
|
|
|
|
|
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return fmt.Errorf("no valid provider available for agent %s", name)
|
2025-04-21 14:29:03 +02:00
|
|
|
|
}
|
2025-04-28 12:25:06 -05:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-04-21 14:29:03 +02:00
|
|
|
|
|
2025-04-28 12:25:06 -05:00
|
|
|
|
// Check if provider for the model is configured
|
|
|
|
|
|
provider := model.Provider
|
|
|
|
|
|
providerCfg, providerExists := cfg.Providers[provider]
|
2025-04-21 14:29:03 +02:00
|
|
|
|
|
2025-04-28 12:25:06 -05:00
|
|
|
|
if !providerExists {
|
|
|
|
|
|
// Provider not configured, check if we have environment variables
|
|
|
|
|
|
apiKey := getProviderAPIKey(provider)
|
|
|
|
|
|
if apiKey == "" {
|
|
|
|
|
|
logging.Warn("provider not configured for model, reverting to default",
|
2025-04-21 14:29:03 +02:00
|
|
|
|
"agent", name,
|
|
|
|
|
|
"model", agent.Model,
|
|
|
|
|
|
"provider", provider)
|
|
|
|
|
|
|
|
|
|
|
|
// Set default model based on available providers
|
|
|
|
|
|
if setDefaultModelForAgent(name) {
|
|
|
|
|
|
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return fmt.Errorf("no valid provider available for agent %s", name)
|
|
|
|
|
|
}
|
2025-04-28 12:25:06 -05:00
|
|
|
|
} else {
|
|
|
|
|
|
// Add provider with API key from environment
|
|
|
|
|
|
cfg.Providers[provider] = Provider{
|
|
|
|
|
|
APIKey: apiKey,
|
|
|
|
|
|
}
|
|
|
|
|
|
logging.Info("added provider from environment", "provider", provider)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if providerCfg.Disabled || providerCfg.APIKey == "" {
|
|
|
|
|
|
// Provider is disabled or has no API key
|
|
|
|
|
|
logging.Warn("provider is disabled or has no API key, reverting to default",
|
|
|
|
|
|
"agent", name,
|
|
|
|
|
|
"model", agent.Model,
|
|
|
|
|
|
"provider", provider)
|
|
|
|
|
|
|
|
|
|
|
|
// Set default model based on available providers
|
|
|
|
|
|
if setDefaultModelForAgent(name) {
|
|
|
|
|
|
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return fmt.Errorf("no valid provider available for agent %s", name)
|
2025-04-21 14:29:03 +02:00
|
|
|
|
}
|
2025-04-28 12:25:06 -05:00
|
|
|
|
}
|
2025-04-21 14:29:03 +02:00
|
|
|
|
|
2025-04-28 12:25:06 -05:00
|
|
|
|
// Validate max tokens
|
|
|
|
|
|
if agent.MaxTokens <= 0 {
|
|
|
|
|
|
logging.Warn("invalid max tokens, setting to default",
|
|
|
|
|
|
"agent", name,
|
|
|
|
|
|
"model", agent.Model,
|
|
|
|
|
|
"max_tokens", agent.MaxTokens)
|
2025-04-21 14:29:03 +02:00
|
|
|
|
|
2025-04-28 12:25:06 -05:00
|
|
|
|
// Update the agent with default max tokens
|
|
|
|
|
|
updatedAgent := cfg.Agents[name]
|
|
|
|
|
|
if model.DefaultMaxTokens > 0 {
|
|
|
|
|
|
updatedAgent.MaxTokens = model.DefaultMaxTokens
|
|
|
|
|
|
} else {
|
|
|
|
|
|
updatedAgent.MaxTokens = MaxTokensFallbackDefault
|
|
|
|
|
|
}
|
|
|
|
|
|
cfg.Agents[name] = updatedAgent
|
|
|
|
|
|
} else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
|
|
|
|
|
|
// Ensure max tokens doesn't exceed half the context window (reasonable limit)
|
|
|
|
|
|
logging.Warn("max tokens exceeds half the context window, adjusting",
|
|
|
|
|
|
"agent", name,
|
|
|
|
|
|
"model", agent.Model,
|
|
|
|
|
|
"max_tokens", agent.MaxTokens,
|
|
|
|
|
|
"context_window", model.ContextWindow)
|
|
|
|
|
|
|
|
|
|
|
|
// Update the agent with adjusted max tokens
|
|
|
|
|
|
updatedAgent := cfg.Agents[name]
|
|
|
|
|
|
updatedAgent.MaxTokens = model.ContextWindow / 2
|
|
|
|
|
|
cfg.Agents[name] = updatedAgent
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate reasoning effort for models that support reasoning
|
|
|
|
|
|
if model.CanReason && provider == models.ProviderOpenAI {
|
|
|
|
|
|
if agent.ReasoningEffort == "" {
|
|
|
|
|
|
// Set default reasoning effort for models that support it
|
|
|
|
|
|
logging.Info("setting default reasoning effort for model that supports reasoning",
|
2025-04-21 14:29:03 +02:00
|
|
|
|
"agent", name,
|
2025-04-28 12:25:06 -05:00
|
|
|
|
"model", agent.Model)
|
2025-04-21 14:29:03 +02:00
|
|
|
|
|
2025-04-28 12:25:06 -05:00
|
|
|
|
// Update the agent with default reasoning effort
|
2025-04-21 14:29:03 +02:00
|
|
|
|
updatedAgent := cfg.Agents[name]
|
2025-04-28 12:25:06 -05:00
|
|
|
|
updatedAgent.ReasoningEffort = "medium"
|
2025-04-21 14:29:03 +02:00
|
|
|
|
cfg.Agents[name] = updatedAgent
|
2025-04-28 12:25:06 -05:00
|
|
|
|
} else {
|
|
|
|
|
|
// Check if reasoning effort is valid (low, medium, high)
|
|
|
|
|
|
effort := strings.ToLower(agent.ReasoningEffort)
|
|
|
|
|
|
if effort != "low" && effort != "medium" && effort != "high" {
|
|
|
|
|
|
logging.Warn("invalid reasoning effort, setting to medium",
|
2025-04-21 14:29:03 +02:00
|
|
|
|
"agent", name,
|
2025-04-28 12:25:06 -05:00
|
|
|
|
"model", agent.Model,
|
|
|
|
|
|
"reasoning_effort", agent.ReasoningEffort)
|
2025-04-21 14:29:03 +02:00
|
|
|
|
|
2025-04-28 12:25:06 -05:00
|
|
|
|
// Update the agent with valid reasoning effort
|
2025-04-21 14:29:03 +02:00
|
|
|
|
updatedAgent := cfg.Agents[name]
|
|
|
|
|
|
updatedAgent.ReasoningEffort = "medium"
|
|
|
|
|
|
cfg.Agents[name] = updatedAgent
|
|
|
|
|
|
}
|
2025-04-28 12:25:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
} else if !model.CanReason && agent.ReasoningEffort != "" {
|
|
|
|
|
|
// Model doesn't support reasoning but reasoning effort is set
|
|
|
|
|
|
logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
|
|
|
|
|
|
"agent", name,
|
|
|
|
|
|
"model", agent.Model,
|
|
|
|
|
|
"reasoning_effort", agent.ReasoningEffort)
|
2025-04-21 14:29:03 +02:00
|
|
|
|
|
2025-04-28 12:25:06 -05:00
|
|
|
|
// Update the agent to remove reasoning effort
|
|
|
|
|
|
updatedAgent := cfg.Agents[name]
|
|
|
|
|
|
updatedAgent.ReasoningEffort = ""
|
|
|
|
|
|
cfg.Agents[name] = updatedAgent
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate checks if the configuration is valid and applies defaults where needed.
|
|
|
|
|
|
func Validate() error {
|
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
|
return fmt.Errorf("config not loaded")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate agent models
|
|
|
|
|
|
for name, agent := range cfg.Agents {
|
|
|
|
|
|
if err := validateAgent(cfg, name, agent); err != nil {
|
|
|
|
|
|
return err
|
2025-04-21 14:29:03 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate providers
|
|
|
|
|
|
for provider, providerCfg := range cfg.Providers {
|
|
|
|
|
|
if providerCfg.APIKey == "" && !providerCfg.Disabled {
|
|
|
|
|
|
logging.Warn("provider has no API key, marking as disabled", "provider", provider)
|
|
|
|
|
|
providerCfg.Disabled = true
|
|
|
|
|
|
cfg.Providers[provider] = providerCfg
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate LSP configurations
|
|
|
|
|
|
for language, lspConfig := range cfg.LSP {
|
|
|
|
|
|
if lspConfig.Command == "" && !lspConfig.Disabled {
|
|
|
|
|
|
logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
|
|
|
|
|
|
lspConfig.Disabled = true
|
|
|
|
|
|
cfg.LSP[language] = lspConfig
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getProviderAPIKey gets the API key for a provider from environment variables
|
|
|
|
|
|
func getProviderAPIKey(provider models.ModelProvider) string {
|
|
|
|
|
|
switch provider {
|
|
|
|
|
|
case models.ProviderAnthropic:
|
|
|
|
|
|
return os.Getenv("ANTHROPIC_API_KEY")
|
|
|
|
|
|
case models.ProviderOpenAI:
|
|
|
|
|
|
return os.Getenv("OPENAI_API_KEY")
|
|
|
|
|
|
case models.ProviderGemini:
|
|
|
|
|
|
return os.Getenv("GEMINI_API_KEY")
|
|
|
|
|
|
case models.ProviderGROQ:
|
|
|
|
|
|
return os.Getenv("GROQ_API_KEY")
|
2025-04-28 10:42:57 -03:00
|
|
|
|
case models.ProviderAzure:
|
|
|
|
|
|
return os.Getenv("AZURE_OPENAI_API_KEY")
|
2025-04-29 12:56:49 +01:00
|
|
|
|
case models.ProviderOpenRouter:
|
|
|
|
|
|
return os.Getenv("OPENROUTER_API_KEY")
|
2025-04-21 14:29:03 +02:00
|
|
|
|
case models.ProviderBedrock:
|
|
|
|
|
|
if hasAWSCredentials() {
|
|
|
|
|
|
return "aws-credentials-available"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// setDefaultModelForAgent sets a default model for an agent based on available providers
|
|
|
|
|
|
func setDefaultModelForAgent(agent AgentName) bool {
|
|
|
|
|
|
// Check providers in order of preference
|
|
|
|
|
|
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
|
|
|
|
|
|
maxTokens := int64(5000)
|
|
|
|
|
|
if agent == AgentTitle {
|
|
|
|
|
|
maxTokens = 80
|
|
|
|
|
|
}
|
|
|
|
|
|
cfg.Agents[agent] = Agent{
|
|
|
|
|
|
Model: models.Claude37Sonnet,
|
|
|
|
|
|
MaxTokens: maxTokens,
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
|
|
|
|
|
|
var model models.ModelID
|
|
|
|
|
|
maxTokens := int64(5000)
|
|
|
|
|
|
reasoningEffort := ""
|
|
|
|
|
|
|
|
|
|
|
|
switch agent {
|
|
|
|
|
|
case AgentTitle:
|
|
|
|
|
|
model = models.GPT41Mini
|
|
|
|
|
|
maxTokens = 80
|
|
|
|
|
|
case AgentTask:
|
|
|
|
|
|
model = models.GPT41Mini
|
|
|
|
|
|
default:
|
|
|
|
|
|
model = models.GPT41
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if model supports reasoning
|
|
|
|
|
|
if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
|
|
|
|
|
|
reasoningEffort = "medium"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-29 12:56:49 +01:00
|
|
|
|
cfg.Agents[agent] = Agent{
|
|
|
|
|
|
Model: model,
|
|
|
|
|
|
MaxTokens: maxTokens,
|
|
|
|
|
|
ReasoningEffort: reasoningEffort,
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
|
|
|
|
|
|
var model models.ModelID
|
|
|
|
|
|
maxTokens := int64(5000)
|
|
|
|
|
|
reasoningEffort := ""
|
|
|
|
|
|
|
|
|
|
|
|
switch agent {
|
|
|
|
|
|
case AgentTitle:
|
|
|
|
|
|
model = models.OpenRouterClaude35Haiku
|
|
|
|
|
|
maxTokens = 80
|
|
|
|
|
|
case AgentTask:
|
|
|
|
|
|
model = models.OpenRouterClaude37Sonnet
|
|
|
|
|
|
default:
|
|
|
|
|
|
model = models.OpenRouterClaude37Sonnet
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if model supports reasoning
|
|
|
|
|
|
if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
|
|
|
|
|
|
reasoningEffort = "medium"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-21 14:29:03 +02:00
|
|
|
|
cfg.Agents[agent] = Agent{
|
|
|
|
|
|
Model: model,
|
|
|
|
|
|
MaxTokens: maxTokens,
|
|
|
|
|
|
ReasoningEffort: reasoningEffort,
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
|
|
|
|
|
|
var model models.ModelID
|
|
|
|
|
|
maxTokens := int64(5000)
|
|
|
|
|
|
|
|
|
|
|
|
if agent == AgentTitle {
|
|
|
|
|
|
model = models.Gemini25Flash
|
|
|
|
|
|
maxTokens = 80
|
|
|
|
|
|
} else {
|
|
|
|
|
|
model = models.Gemini25
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cfg.Agents[agent] = Agent{
|
|
|
|
|
|
Model: model,
|
|
|
|
|
|
MaxTokens: maxTokens,
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
|
|
|
|
|
|
maxTokens := int64(5000)
|
|
|
|
|
|
if agent == AgentTitle {
|
|
|
|
|
|
maxTokens = 80
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cfg.Agents[agent] = Agent{
|
|
|
|
|
|
Model: models.QWENQwq,
|
|
|
|
|
|
MaxTokens: maxTokens,
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if hasAWSCredentials() {
|
|
|
|
|
|
maxTokens := int64(5000)
|
|
|
|
|
|
if agent == AgentTitle {
|
|
|
|
|
|
maxTokens = 80
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cfg.Agents[agent] = Agent{
|
|
|
|
|
|
Model: models.BedrockClaude37Sonnet,
|
|
|
|
|
|
MaxTokens: maxTokens,
|
|
|
|
|
|
ReasoningEffort: "medium", // Claude models support reasoning
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// Get returns the current configuration.
|
|
|
|
|
|
// It's safe to call this function multiple times.
|
2025-03-27 22:35:48 +01:00
|
|
|
|
func Get() *Config {
|
|
|
|
|
|
return cfg
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-13 12:25:11 +02:00
|
|
|
|
// WorkingDirectory returns the current working directory from the configuration.
|
2025-03-27 22:35:48 +01:00
|
|
|
|
func WorkingDirectory() string {
|
2025-04-15 11:58:01 +02:00
|
|
|
|
if cfg == nil {
|
|
|
|
|
|
panic("config not loaded")
|
|
|
|
|
|
}
|
|
|
|
|
|
return cfg.WorkingDir
|
2025-03-27 22:35:48 +01:00
|
|
|
|
}
|
2025-04-28 12:25:06 -05:00
|
|
|
|
|
|
|
|
|
|
func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error {
|
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
|
panic("config not loaded")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
existingAgentCfg := cfg.Agents[agentName]
|
|
|
|
|
|
|
|
|
|
|
|
model, ok := models.SupportedModels[modelID]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("model %s not supported", modelID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
maxTokens := existingAgentCfg.MaxTokens
|
|
|
|
|
|
if model.DefaultMaxTokens > 0 {
|
|
|
|
|
|
maxTokens = model.DefaultMaxTokens
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newAgentCfg := Agent{
|
|
|
|
|
|
Model: modelID,
|
|
|
|
|
|
MaxTokens: maxTokens,
|
|
|
|
|
|
ReasoningEffort: existingAgentCfg.ReasoningEffort,
|
|
|
|
|
|
}
|
|
|
|
|
|
cfg.Agents[agentName] = newAgentCfg
|
|
|
|
|
|
|
|
|
|
|
|
if err := validateAgent(cfg, agentName, newAgentCfg); err != nil {
|
|
|
|
|
|
// revert config update on failure
|
|
|
|
|
|
cfg.Agents[agentName] = existingAgentCfg
|
|
|
|
|
|
return fmt.Errorf("failed to update agent model: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-05-01 05:49:26 -05:00
|
|
|
|
|
|
|
|
|
|
// UpdateTheme updates the theme in the configuration and writes it to the config file.
|
|
|
|
|
|
func UpdateTheme(themeName string) error {
|
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
|
return fmt.Errorf("config not loaded")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update the in-memory config
|
|
|
|
|
|
cfg.TUI.Theme = themeName
|
|
|
|
|
|
|
|
|
|
|
|
// Get the config file path
|
|
|
|
|
|
configFile := viper.ConfigFileUsed()
|
|
|
|
|
|
var configData []byte
|
|
|
|
|
|
if configFile == "" {
|
|
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to get home directory: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName))
|
|
|
|
|
|
logging.Info("config file not found, creating new one", "path", configFile)
|
|
|
|
|
|
configData = []byte(`{}`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Read the existing config file
|
|
|
|
|
|
data, err := os.ReadFile(configFile)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to read config file: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
configData = data
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parse the JSON
|
|
|
|
|
|
var configMap map[string]interface{}
|
|
|
|
|
|
if err := json.Unmarshal(configData, &configMap); err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to parse config file: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update just the theme value
|
|
|
|
|
|
tuiConfig, ok := configMap["tui"].(map[string]interface{})
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
// TUI config doesn't exist yet, create it
|
|
|
|
|
|
configMap["tui"] = map[string]interface{}{"theme": themeName}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Update existing TUI config
|
|
|
|
|
|
tuiConfig["theme"] = themeName
|
|
|
|
|
|
configMap["tui"] = tuiConfig
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Write the updated config back to file
|
|
|
|
|
|
updatedData, err := json.MarshalIndent(configMap, "", " ")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to marshal config: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := os.WriteFile(configFile, updatedData, 0o644); err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to write config file: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|