fix: check for missing api_base field to avoid default value pollution

fix: remove default ModelList from default config. add it back only when necessary
This commit is contained in:
zenix.huang 2026-02-21 12:57:26 +09:00
parent 7cbfa89a96
commit 80f17964e3
4 changed files with 233 additions and 156 deletions

View file

@ -32,6 +32,7 @@ func onboard() {
} }
cfg := config.DefaultConfig() cfg := config.DefaultConfig()
cfg.ModelList = config.DefaultModelList() // Populate default model list
if err := config.SaveConfig(configPath, cfg); err != nil { if err := config.SaveConfig(configPath, cfg); err != nil {
fmt.Printf("Error saving config: %v\n", err) fmt.Printf("Error saving config: %v\n", err)
os.Exit(1) os.Exit(1)

View file

@ -503,6 +503,19 @@ func LoadConfig(path string) (*Config, error) {
return nil, err return nil, err
} }
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
if _, hasModelList := raw["model_list"]; !hasModelList {
// only set the default model list if the config file doesn't have the old providers config either
if _, hasProviders := raw["providers"]; !hasProviders {
cfg.ModelList = DefaultModelList()
}
// otherwise, ConvertProvidersToModelList(cfg) will handle the default model list based on the providers config
}
if err := env.Parse(cfg); err != nil { if err := env.Parse(cfg); err != nil {
return nil, err return nil, err
} }

View file

@ -364,7 +364,11 @@ func TestDefaultConfig_OpenAIWebSearchEnabled(t *testing.T) {
func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) { func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
configPath := filepath.Join(dir, "config.json") configPath := filepath.Join(dir, "config.json")
if err := os.WriteFile(configPath, []byte(`{"providers":{"openai":{"api_base":""}}}`), 0o600); err != nil { if err := os.WriteFile(
configPath,
[]byte(`{"providers":{"openai":{"api_base":""}},"model_list":[{"model_name":"gpt-5","model":"openai/gpt-5"}]}`),
0o600,
); err != nil {
t.Fatalf("WriteFile() error: %v", err) t.Fatalf("WriteFile() error: %v", err)
} }
@ -380,7 +384,13 @@ func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) { func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
configPath := filepath.Join(dir, "config.json") configPath := filepath.Join(dir, "config.json")
if err := os.WriteFile(configPath, []byte(`{"providers":{"openai":{"web_search":false}}}`), 0o600); err != nil { if err := os.WriteFile(
configPath,
[]byte(
`{"providers":{"openai":{"web_search":false}},"model_list":[{"model_name":"gpt-5","model":"openai/gpt-5"}]}`,
),
0o600,
); err != nil {
t.Fatalf("WriteFile() error: %v", err) t.Fatalf("WriteFile() error: %v", err)
} }
@ -392,3 +402,56 @@ func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) {
t.Fatal("OpenAI codex web search should be false when disabled in config file") t.Fatal("OpenAI codex web search should be false when disabled in config file")
} }
} }
func TestLoadConfig_NoErrorsWhenModelListMissing(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.json")
data := `{
"providers": {
"zhipu": {
"api_key": "abc"
}
}
}`
if err := os.WriteFile(configPath, []byte(data), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
_, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("expected LoadConfig to pass even when model_list is missing, got: %v", err)
}
}
func TestLoadConfig_AllowsModelListWithoutAPIBaseField(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "config.json")
data := `{
"model_list": [
{
"model_name": "gpt-5.3-codex",
"model": "openai/gpt-5.3-codex",
"api_key": ""
}
]
}`
if err := os.WriteFile(configPath, []byte(data), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() should allow model_list entries without explicit api_base field, got error: %v", err)
}
if len(cfg.ModelList) != 1 {
t.Fatalf("expected one model_list entry, got %d", len(cfg.ModelList))
}
if cfg.ModelList[0].APIBase != "" {
t.Fatalf("expected api_base to remain empty when omitted, got %q", cfg.ModelList[0].APIBase)
}
}

View file

@ -5,122 +5,15 @@
package config package config
// DefaultConfig returns the default configuration for PicoClaw. func DefaultModelList() []ModelConfig {
func DefaultConfig() *Config { return []ModelConfig{
return &Config{ // OpenAI - https://platform.openai.com/api-keys
Agents: AgentsConfig{ {
Defaults: AgentDefaults{ ModelName: "gpt-5.2",
Workspace: "~/.picoclaw/workspace", Model: "openai/gpt-5.2",
RestrictToWorkspace: true, APIBase: "https://api.openai.com/v1",
Provider: "", APIKey: "",
Model: "glm-4.7",
MaxTokens: 8192,
Temperature: nil, // nil means use provider default
MaxToolIterations: 20,
}, },
},
Bindings: []AgentBinding{},
Session: SessionConfig{
DMScope: "main",
},
Channels: ChannelsConfig{
WhatsApp: WhatsAppConfig{
Enabled: false,
BridgeURL: "ws://localhost:3001",
AllowFrom: FlexibleStringSlice{},
},
Telegram: TelegramConfig{
Enabled: false,
Token: "",
AllowFrom: FlexibleStringSlice{},
},
Feishu: FeishuConfig{
Enabled: false,
AppID: "",
AppSecret: "",
EncryptKey: "",
VerificationToken: "",
AllowFrom: FlexibleStringSlice{},
},
Discord: DiscordConfig{
Enabled: false,
Token: "",
AllowFrom: FlexibleStringSlice{},
MentionOnly: false,
},
MaixCam: MaixCamConfig{
Enabled: false,
Host: "0.0.0.0",
Port: 18790,
AllowFrom: FlexibleStringSlice{},
},
QQ: QQConfig{
Enabled: false,
AppID: "",
AppSecret: "",
AllowFrom: FlexibleStringSlice{},
},
DingTalk: DingTalkConfig{
Enabled: false,
ClientID: "",
ClientSecret: "",
AllowFrom: FlexibleStringSlice{},
},
Slack: SlackConfig{
Enabled: false,
BotToken: "",
AppToken: "",
AllowFrom: FlexibleStringSlice{},
},
LINE: LINEConfig{
Enabled: false,
ChannelSecret: "",
ChannelAccessToken: "",
WebhookHost: "0.0.0.0",
WebhookPort: 18791,
WebhookPath: "/webhook/line",
AllowFrom: FlexibleStringSlice{},
},
OneBot: OneBotConfig{
Enabled: false,
WSUrl: "ws://127.0.0.1:3001",
AccessToken: "",
ReconnectInterval: 5,
GroupTriggerPrefix: []string{},
AllowFrom: FlexibleStringSlice{},
},
WeCom: WeComConfig{
Enabled: false,
Token: "",
EncodingAESKey: "",
WebhookURL: "",
WebhookHost: "0.0.0.0",
WebhookPort: 18793,
WebhookPath: "/webhook/wecom",
AllowFrom: FlexibleStringSlice{},
ReplyTimeout: 5,
},
WeComApp: WeComAppConfig{
Enabled: false,
CorpID: "",
CorpSecret: "",
AgentID: 0,
Token: "",
EncodingAESKey: "",
WebhookHost: "0.0.0.0",
WebhookPort: 18792,
WebhookPath: "/webhook/wecom-app",
AllowFrom: FlexibleStringSlice{},
ReplyTimeout: 5,
},
},
Providers: ProvidersConfig{
OpenAI: OpenAIProviderConfig{WebSearch: true},
},
ModelList: []ModelConfig{
// ============================================
// Add your API key to the model you want to use
// ============================================
// Zhipu AI (智谱) - https://open.bigmodel.cn/usercenter/apikeys // Zhipu AI (智谱) - https://open.bigmodel.cn/usercenter/apikeys
{ {
@ -130,14 +23,6 @@ func DefaultConfig() *Config {
APIKey: "", APIKey: "",
}, },
// OpenAI - https://platform.openai.com/api-keys
{
ModelName: "gpt-5.2",
Model: "openai/gpt-5.2",
APIBase: "https://api.openai.com/v1",
APIKey: "",
},
// Anthropic Claude - https://console.anthropic.com/settings/keys // Anthropic Claude - https://console.anthropic.com/settings/keys
{ {
ModelName: "claude-sonnet-4.6", ModelName: "claude-sonnet-4.6",
@ -270,7 +155,122 @@ func DefaultConfig() *Config {
APIBase: "http://localhost:8000/v1", APIBase: "http://localhost:8000/v1",
APIKey: "", APIKey: "",
}, },
}
}
// DefaultConfig returns the default configuration for PicoClaw.
func DefaultConfig() *Config {
return &Config{
Agents: AgentsConfig{
Defaults: AgentDefaults{
Workspace: "~/.picoclaw/workspace",
RestrictToWorkspace: true,
Provider: "",
Model: "glm-4.7",
MaxTokens: 8192,
Temperature: nil, // nil means use provider default
MaxToolIterations: 20,
}, },
},
Bindings: []AgentBinding{},
Session: SessionConfig{
DMScope: "main",
},
Channels: ChannelsConfig{
WhatsApp: WhatsAppConfig{
Enabled: false,
BridgeURL: "ws://localhost:3001",
AllowFrom: FlexibleStringSlice{},
},
Telegram: TelegramConfig{
Enabled: false,
Token: "",
AllowFrom: FlexibleStringSlice{},
},
Feishu: FeishuConfig{
Enabled: false,
AppID: "",
AppSecret: "",
EncryptKey: "",
VerificationToken: "",
AllowFrom: FlexibleStringSlice{},
},
Discord: DiscordConfig{
Enabled: false,
Token: "",
AllowFrom: FlexibleStringSlice{},
MentionOnly: false,
},
MaixCam: MaixCamConfig{
Enabled: false,
Host: "0.0.0.0",
Port: 18790,
AllowFrom: FlexibleStringSlice{},
},
QQ: QQConfig{
Enabled: false,
AppID: "",
AppSecret: "",
AllowFrom: FlexibleStringSlice{},
},
DingTalk: DingTalkConfig{
Enabled: false,
ClientID: "",
ClientSecret: "",
AllowFrom: FlexibleStringSlice{},
},
Slack: SlackConfig{
Enabled: false,
BotToken: "",
AppToken: "",
AllowFrom: FlexibleStringSlice{},
},
LINE: LINEConfig{
Enabled: false,
ChannelSecret: "",
ChannelAccessToken: "",
WebhookHost: "0.0.0.0",
WebhookPort: 18791,
WebhookPath: "/webhook/line",
AllowFrom: FlexibleStringSlice{},
},
OneBot: OneBotConfig{
Enabled: false,
WSUrl: "ws://127.0.0.1:3001",
AccessToken: "",
ReconnectInterval: 5,
GroupTriggerPrefix: []string{},
AllowFrom: FlexibleStringSlice{},
},
WeCom: WeComConfig{
Enabled: false,
Token: "",
EncodingAESKey: "",
WebhookURL: "",
WebhookHost: "0.0.0.0",
WebhookPort: 18793,
WebhookPath: "/webhook/wecom",
AllowFrom: FlexibleStringSlice{},
ReplyTimeout: 5,
},
WeComApp: WeComAppConfig{
Enabled: false,
CorpID: "",
CorpSecret: "",
AgentID: 0,
Token: "",
EncodingAESKey: "",
WebhookHost: "0.0.0.0",
WebhookPort: 18792,
WebhookPath: "/webhook/wecom-app",
AllowFrom: FlexibleStringSlice{},
ReplyTimeout: 5,
},
},
Providers: ProvidersConfig{
OpenAI: OpenAIProviderConfig{WebSearch: true},
},
ModelList: []ModelConfig{},
Gateway: GatewayConfig{ Gateway: GatewayConfig{
Host: "127.0.0.1", Host: "127.0.0.1",
Port: 18790, Port: 18790,