From 41a898b4211b95393e2003a44744ab7533a70e85 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E9=BE=99=200668001470?=
Date: Mon, 16 Mar 2026 10:36:05 +0800
Subject: [PATCH] fix(config): apply provider env overrides after parse
---
pkg/config/config.go | 127 ++++++++++++++++++++++++++++++++-----------
1 file changed, 96 insertions(+), 31 deletions(-)
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 {