diff --git a/pkg/config/config.go b/pkg/config/config.go index 7a7edb489..e95d38902 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -505,29 +505,29 @@ type VoiceConfig struct { } type ProvidersConfig struct { - 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"` + 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_"` } // IsEmpty checks if all provider configs are empty (no API keys or API bases set) @@ -569,17 +569,17 @@ func (p ProvidersConfig) MarshalJSON() ([]byte, error) { } type ProviderConfig struct { - 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` + 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` } type OpenAIProviderConfig struct { ProviderConfig - WebSearch bool `json:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"` + WebSearch bool `json:"web_search" env:"WEB_SEARCH"` } // ModelConfig represents a model-centric provider configuration. diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ad89d6d2e..13d9c4ed8 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -460,6 +460,49 @@ func TestLoadConfig_WebToolsProxy(t *testing.T) { } } +func TestLoadConfig_ProviderEnvVarsOverrideFileValues(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.json") + configJSON := `{ + "providers": { + "gemini": { + "api_key": "from-file" + } + } +}` + if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil { + t.Fatalf("os.WriteFile() error: %v", err) + } + + t.Setenv("PICOCLAW_PROVIDERS_GEMINI_API_KEY", "from-env") + + cfg, err := LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig() error: %v", err) + } + if cfg.Providers.Gemini.APIKey != "from-env" { + t.Fatalf("Providers.Gemini.APIKey = %q, want %q", cfg.Providers.Gemini.APIKey, "from-env") + } +} + +func TestLoadConfig_OpenAIProviderEnvVarsUseNestedPrefix(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.json") + if err := os.WriteFile(configPath, []byte(`{}`), 0o600); err != nil { + t.Fatalf("os.WriteFile() error: %v", err) + } + + t.Setenv("PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH", "false") + + cfg, err := LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig() error: %v", err) + } + if cfg.Providers.OpenAI.WebSearch { + t.Fatal("Providers.OpenAI.WebSearch should be false when disabled via environment") + } +} + // TestDefaultConfig_DMScope verifies the default dm_scope value // TestDefaultConfig_SummarizationThresholds verifies summarization defaults func TestDefaultConfig_SummarizationThresholds(t *testing.T) {