fix: enable env var overrides for provider API keys and model_list entries (#836)
This commit is contained in:
parent
7e9b78bef5
commit
02a19bfb76
2 changed files with 99 additions and 25 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
|
|
@ -385,24 +386,24 @@ type DevicesConfig struct {
|
|||
}
|
||||
|
||||
type ProvidersConfig struct {
|
||||
Anthropic ProviderConfig `json:"anthropic"`
|
||||
OpenAI OpenAIProviderConfig `json:"openai"`
|
||||
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"`
|
||||
VolcEngine ProviderConfig `json:"volcengine"`
|
||||
GitHubCopilot ProviderConfig `json:"github_copilot"`
|
||||
Antigravity ProviderConfig `json:"antigravity"`
|
||||
Qwen ProviderConfig `json:"qwen"`
|
||||
Mistral ProviderConfig `json:"mistral"`
|
||||
Anthropic ProviderConfig `json:"anthropic" envPrefix:"PICOCLAW_PROVIDERS_ANTHROPIC_"`
|
||||
OpenAI OpenAIProviderConfig `json:"openai" envPrefix:"PICOCLAW_PROVIDERS_OPENAI_"`
|
||||
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_"`
|
||||
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_"`
|
||||
}
|
||||
|
||||
// IsEmpty checks if all provider configs are empty (no API keys or API bases set)
|
||||
|
|
@ -439,17 +440,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.
|
||||
|
|
@ -611,6 +612,20 @@ func LoadConfig(path string) (*Config, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Resolve environment variable overrides for model_list API keys.
|
||||
// Convention: PICOCLAW_MODEL_<UPPER_MODEL_NAME>_API_KEY
|
||||
modelNameReplacer := strings.NewReplacer("-", "_", ".", "_")
|
||||
for i := range cfg.ModelList {
|
||||
if cfg.ModelList[i].APIKey == "" && cfg.ModelList[i].ModelName != "" {
|
||||
envKey := "PICOCLAW_MODEL_" + strings.ToUpper(
|
||||
modelNameReplacer.Replace(cfg.ModelList[i].ModelName),
|
||||
) + "_API_KEY"
|
||||
if v := os.Getenv(envKey); v != "" {
|
||||
cfg.ModelList[i].APIKey = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate legacy channel config fields to new unified structures
|
||||
cfg.migrateChannelConfigs()
|
||||
|
||||
|
|
|
|||
|
|
@ -447,6 +447,65 @@ func TestDefaultConfig_SummarizationThresholds(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_ProviderEnvVars(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.json")
|
||||
configJSON := `{
|
||||
"providers": {
|
||||
"anthropic": {},
|
||||
"openai": {}
|
||||
},
|
||||
"model_list": [{"model_name":"test","model":"openai/gpt-5.2","api_key":"x"}]
|
||||
}`
|
||||
if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile error: %v", err)
|
||||
}
|
||||
|
||||
t.Setenv("PICOCLAW_PROVIDERS_ANTHROPIC_API_KEY", "ant-env-key")
|
||||
t.Setenv("PICOCLAW_PROVIDERS_OPENAI_API_KEY", "oai-env-key")
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Providers.Anthropic.APIKey != "ant-env-key" {
|
||||
t.Errorf("Anthropic.APIKey = %q, want %q", cfg.Providers.Anthropic.APIKey, "ant-env-key")
|
||||
}
|
||||
if cfg.Providers.OpenAI.APIKey != "oai-env-key" {
|
||||
t.Errorf("OpenAI.APIKey = %q, want %q", cfg.Providers.OpenAI.APIKey, "oai-env-key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_ModelListEnvVars(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.json")
|
||||
configJSON := `{
|
||||
"model_list": [
|
||||
{"model_name": "gpt-5.2", "model": "openai/gpt-5.2", "api_key": ""},
|
||||
{"model_name": "claude-sonnet-4.6", "model": "anthropic/claude-sonnet-4.6", "api_key": ""}
|
||||
]
|
||||
}`
|
||||
if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile error: %v", err)
|
||||
}
|
||||
|
||||
t.Setenv("PICOCLAW_MODEL_GPT_5_2_API_KEY", "gpt-env-key")
|
||||
t.Setenv("PICOCLAW_MODEL_CLAUDE_SONNET_4_6_API_KEY", "claude-env-key")
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.ModelList[0].APIKey != "gpt-env-key" {
|
||||
t.Errorf("ModelList[0].APIKey = %q, want %q", cfg.ModelList[0].APIKey, "gpt-env-key")
|
||||
}
|
||||
if cfg.ModelList[1].APIKey != "claude-env-key" {
|
||||
t.Errorf("ModelList[1].APIKey = %q, want %q", cfg.ModelList[1].APIKey, "claude-env-key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig_DMScope(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue