diff --git a/pkg/config/config.go b/pkg/config/config.go index 2b88f7b11..ffda5e6bd 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "os" + "reflect" + "strconv" "strings" "sync/atomic" @@ -505,30 +507,30 @@ type VoiceConfig struct { } type ProvidersConfig struct { - Anthropic ProviderConfig `json:"anthropic" envPrefix:"PICOCLAW_PROVIDERS_ANTHROPIC_"` - OpenAI OpenAIProviderConfig `json:"openai" envPrefix:"PICOCLAW_PROVIDERS_OPENAI_"` - LiteLLM ProviderConfig `json:"litellm" envPrefix:"PICOCLAW_PROVIDERS_LITELLM_"` - OpenRouter ProviderConfig `json:"openrouter" envPrefix:"PICOCLAW_PROVIDERS_OPENROUTER_"` - Groq ProviderConfig `json:"groq" envPrefix:"PICOCLAW_PROVIDERS_GROQ_"` - Zhipu ProviderConfig `json:"zhipu" envPrefix:"PICOCLAW_PROVIDERS_ZHIPU_"` - VLLM ProviderConfig `json:"vllm" envPrefix:"PICOCLAW_PROVIDERS_VLLM_"` - Gemini ProviderConfig `json:"gemini" envPrefix:"PICOCLAW_PROVIDERS_GEMINI_"` - Nvidia ProviderConfig `json:"nvidia" envPrefix:"PICOCLAW_PROVIDERS_NVIDIA_"` - Ollama ProviderConfig `json:"ollama" envPrefix:"PICOCLAW_PROVIDERS_OLLAMA_"` - Moonshot ProviderConfig `json:"moonshot" envPrefix:"PICOCLAW_PROVIDERS_MOONSHOT_"` - ShengSuanYun ProviderConfig `json:"shengsuanyun" envPrefix:"PICOCLAW_PROVIDERS_SHENGSUANYUN_"` - DeepSeek ProviderConfig `json:"deepseek" envPrefix:"PICOCLAW_PROVIDERS_DEEPSEEK_"` - Cerebras ProviderConfig `json:"cerebras" envPrefix:"PICOCLAW_PROVIDERS_CEREBRAS_"` - Vivgrid ProviderConfig `json:"vivgrid" envPrefix:"PICOCLAW_PROVIDERS_VIVGRID_"` - VolcEngine ProviderConfig `json:"volcengine" envPrefix:"PICOCLAW_PROVIDERS_VOLCENGINE_"` - GitHubCopilot ProviderConfig `json:"github_copilot" envPrefix:"PICOCLAW_PROVIDERS_GITHUB_COPILOT_"` - Antigravity ProviderConfig `json:"antigravity" envPrefix:"PICOCLAW_PROVIDERS_ANTIGRAVITY_"` - Qwen ProviderConfig `json:"qwen" envPrefix:"PICOCLAW_PROVIDERS_QWEN_"` - Mistral ProviderConfig `json:"mistral" envPrefix:"PICOCLAW_PROVIDERS_MISTRAL_"` - Avian ProviderConfig `json:"avian" envPrefix:"PICOCLAW_PROVIDERS_AVIAN_"` - Minimax ProviderConfig `json:"minimax" envPrefix:"PICOCLAW_PROVIDERS_MINIMAX_"` - LongCat ProviderConfig `json:"longcat" envPrefix:"PICOCLAW_PROVIDERS_LONGCAT_"` - ModelScope ProviderConfig `json:"modelscope" envPrefix:"PICOCLAW_PROVIDERS_MODELSCOPE_"` + Anthropic ProviderConfig `json:"anthropic"` + OpenAI OpenAIProviderConfig `json:"openai"` + LiteLLM ProviderConfig `json:"litellm"` + OpenRouter ProviderConfig `json:"openrouter"` + Groq ProviderConfig `json:"groq"` + Zhipu ProviderConfig `json:"zhipu"` + VLLM ProviderConfig `json:"vllm"` + Gemini ProviderConfig `json:"gemini"` + Nvidia ProviderConfig `json:"nvidia"` + Ollama ProviderConfig `json:"ollama"` + Moonshot ProviderConfig `json:"moonshot"` + ShengSuanYun ProviderConfig `json:"shengsuanyun"` + DeepSeek ProviderConfig `json:"deepseek"` + Cerebras ProviderConfig `json:"cerebras"` + Vivgrid ProviderConfig `json:"vivgrid"` + VolcEngine ProviderConfig `json:"volcengine"` + GitHubCopilot ProviderConfig `json:"github_copilot"` + Antigravity ProviderConfig `json:"antigravity"` + Qwen ProviderConfig `json:"qwen"` + Mistral ProviderConfig `json:"mistral"` + Avian ProviderConfig `json:"avian"` + Minimax ProviderConfig `json:"minimax"` + LongCat ProviderConfig `json:"longcat"` + ModelScope ProviderConfig `json:"modelscope"` } // IsEmpty checks if all provider configs are empty (no API keys or API bases set) @@ -571,17 +573,17 @@ func (p ProvidersConfig) MarshalJSON() ([]byte, error) { } type ProviderConfig struct { - APIKey string `json:"api_key" env:"API_KEY"` - APIBase string `json:"api_base" env:"API_BASE"` - Proxy string `json:"proxy,omitempty" env:"PROXY"` - RequestTimeout int `json:"request_timeout,omitempty" env:"REQUEST_TIMEOUT"` - AuthMethod string `json:"auth_method,omitempty" env:"AUTH_METHOD"` - ConnectMode string `json:"connect_mode,omitempty" env:"CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc` + APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"` + APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"` + Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"` + RequestTimeout int `json:"request_timeout,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_REQUEST_TIMEOUT"` + AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"` + ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc` } type OpenAIProviderConfig struct { ProviderConfig - WebSearch bool `json:"web_search" env:"WEB_SEARCH"` + WebSearch bool `json:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"` } // ModelConfig represents a model-centric provider configuration. @@ -840,6 +842,7 @@ func LoadConfig(path string) (*Config, error) { if err := env.Parse(cfg); err != nil { return nil, err } + cfg.applyProviderEnvOverrides() // Migrate legacy channel config fields to new unified structures cfg.migrateChannelConfigs() @@ -857,6 +860,68 @@ func LoadConfig(path string) (*Config, error) { return cfg, nil } +func (c *Config) applyProviderEnvOverrides() { + providersValue := reflect.ValueOf(&c.Providers).Elem() + providersType := providersValue.Type() + + for i := 0; i < providersValue.NumField(); i++ { + fieldValue := providersValue.Field(i) + fieldType := providersType.Field(i) + jsonName := strings.Split(fieldType.Tag.Get("json"), ",")[0] + if jsonName == "" { + continue + } + prefix := "PICOCLAW_PROVIDERS_" + strings.ToUpper(jsonName) + "_" + applyProviderConfigEnv(fieldValue, prefix) + } +} + +func applyProviderConfigEnv(fieldValue reflect.Value, prefix string) { + if fieldValue.Kind() != reflect.Struct { + return + } + + if nested := fieldValue.FieldByName("ProviderConfig"); nested.IsValid() { + applyProviderConfigFields(nested, prefix) + } else { + applyProviderConfigFields(fieldValue, prefix) + } + + if webSearch := fieldValue.FieldByName("WebSearch"); webSearch.IsValid() && webSearch.CanSet() { + if raw, ok := os.LookupEnv(prefix + "WEB_SEARCH"); ok { + if parsed, err := strconv.ParseBool(raw); err == nil { + webSearch.SetBool(parsed) + } + } + } +} + +func applyProviderConfigFields(fieldValue reflect.Value, prefix string) { + setStringFieldFromEnv(fieldValue, "APIKey", prefix+"API_KEY") + setStringFieldFromEnv(fieldValue, "APIBase", prefix+"API_BASE") + setStringFieldFromEnv(fieldValue, "Proxy", prefix+"PROXY") + setStringFieldFromEnv(fieldValue, "AuthMethod", prefix+"AUTH_METHOD") + setStringFieldFromEnv(fieldValue, "ConnectMode", prefix+"CONNECT_MODE") + + if requestTimeout := fieldValue.FieldByName("RequestTimeout"); requestTimeout.IsValid() && requestTimeout.CanSet() { + if raw, ok := os.LookupEnv(prefix + "REQUEST_TIMEOUT"); ok { + if parsed, err := strconv.Atoi(raw); err == nil { + requestTimeout.SetInt(int64(parsed)) + } + } + } +} + +func setStringFieldFromEnv(fieldValue reflect.Value, fieldName, envName string) { + target := fieldValue.FieldByName(fieldName) + if !target.IsValid() || !target.CanSet() { + return + } + if raw, ok := os.LookupEnv(envName); ok { + target.SetString(raw) + } +} + func (c *Config) migrateChannelConfigs() { // Discord: mention_only -> group_trigger.mention_only if c.Channels.Discord.MentionOnly && !c.Channels.Discord.GroupTrigger.MentionOnly {