diff --git a/cmd/picoclaw/cmd_onboard.go b/cmd/picoclaw/cmd_onboard.go index 1a9ebad61..6fe7ef8ff 100644 --- a/cmd/picoclaw/cmd_onboard.go +++ b/cmd/picoclaw/cmd_onboard.go @@ -32,6 +32,7 @@ func onboard() { } cfg := config.DefaultConfig() + cfg.ModelList = config.DefaultModelList() // Populate default model list if err := config.SaveConfig(configPath, cfg); err != nil { fmt.Printf("Error saving config: %v\n", err) os.Exit(1) diff --git a/pkg/config/config.go b/pkg/config/config.go index 2595398c7..98e784a73 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -503,6 +503,19 @@ func LoadConfig(path string) (*Config, error) { 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 { return nil, err } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f88c0269c..60b432de9 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -364,7 +364,11 @@ func TestDefaultConfig_OpenAIWebSearchEnabled(t *testing.T) { func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) { dir := t.TempDir() 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) } @@ -380,7 +384,13 @@ func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) { func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) { dir := t.TempDir() 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) } @@ -392,3 +402,56 @@ func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) { 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) + } +} diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index b96ee4d89..ae3db7d2d 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -5,6 +5,159 @@ package config +func DefaultModelList() []ModelConfig { + return []ModelConfig{ + // OpenAI - https://platform.openai.com/api-keys + { + ModelName: "gpt-5.2", + Model: "openai/gpt-5.2", + APIBase: "https://api.openai.com/v1", + APIKey: "", + }, + + // Zhipu AI (智谱) - https://open.bigmodel.cn/usercenter/apikeys + { + ModelName: "glm-4.7", + Model: "zhipu/glm-4.7", + APIBase: "https://open.bigmodel.cn/api/paas/v4", + APIKey: "", + }, + + // Anthropic Claude - https://console.anthropic.com/settings/keys + { + ModelName: "claude-sonnet-4.6", + Model: "anthropic/claude-sonnet-4.6", + APIBase: "https://api.anthropic.com/v1", + APIKey: "", + }, + + // DeepSeek - https://platform.deepseek.com/ + { + ModelName: "deepseek-chat", + Model: "deepseek/deepseek-chat", + APIBase: "https://api.deepseek.com/v1", + APIKey: "", + }, + + // Google Gemini - https://ai.google.dev/ + { + ModelName: "gemini-2.0-flash", + Model: "gemini/gemini-2.0-flash-exp", + APIBase: "https://generativelanguage.googleapis.com/v1beta", + APIKey: "", + }, + + // Qwen (通义千问) - https://dashscope.console.aliyun.com/apiKey + { + ModelName: "qwen-plus", + Model: "qwen/qwen-plus", + APIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1", + APIKey: "", + }, + + // Moonshot (月之暗面) - https://platform.moonshot.cn/console/api-keys + { + ModelName: "moonshot-v1-8k", + Model: "moonshot/moonshot-v1-8k", + APIBase: "https://api.moonshot.cn/v1", + APIKey: "", + }, + + // Groq - https://console.groq.com/keys + { + ModelName: "llama-3.3-70b", + Model: "groq/llama-3.3-70b-versatile", + APIBase: "https://api.groq.com/openai/v1", + APIKey: "", + }, + + // OpenRouter (100+ models) - https://openrouter.ai/keys + { + ModelName: "openrouter-auto", + Model: "openrouter/auto", + APIBase: "https://openrouter.ai/api/v1", + APIKey: "", + }, + { + ModelName: "openrouter-gpt-5.2", + Model: "openrouter/openai/gpt-5.2", + APIBase: "https://openrouter.ai/api/v1", + APIKey: "", + }, + + // NVIDIA - https://build.nvidia.com/ + { + ModelName: "nemotron-4-340b", + Model: "nvidia/nemotron-4-340b-instruct", + APIBase: "https://integrate.api.nvidia.com/v1", + APIKey: "", + }, + + // Cerebras - https://inference.cerebras.ai/ + { + ModelName: "cerebras-llama-3.3-70b", + Model: "cerebras/llama-3.3-70b", + APIBase: "https://api.cerebras.ai/v1", + APIKey: "", + }, + + // Volcengine (火山引擎) - https://console.volcengine.com/ark + { + ModelName: "doubao-pro", + Model: "volcengine/doubao-pro-32k", + APIBase: "https://ark.cn-beijing.volces.com/api/v3", + APIKey: "", + }, + + // ShengsuanYun (神算云) + { + ModelName: "deepseek-v3", + Model: "shengsuanyun/deepseek-v3", + APIBase: "https://api.shengsuanyun.com/v1", + APIKey: "", + }, + + // Antigravity (Google Cloud Code Assist) - OAuth only + { + ModelName: "gemini-flash", + Model: "antigravity/gemini-3-flash", + AuthMethod: "oauth", + }, + + // GitHub Copilot - https://github.com/settings/tokens + { + ModelName: "copilot-gpt-5.2", + Model: "github-copilot/gpt-5.2", + APIBase: "http://localhost:4321", + AuthMethod: "oauth", + }, + + // Ollama (local) - https://ollama.com + { + ModelName: "llama3", + Model: "ollama/llama3", + APIBase: "http://localhost:11434/v1", + APIKey: "ollama", + }, + + // Mistral AI - https://console.mistral.ai/api-keys + { + ModelName: "mistral-small", + Model: "mistral/mistral-small-latest", + APIBase: "https://api.mistral.ai/v1", + APIKey: "", + }, + + // VLLM (local) - http://localhost:8000 + { + ModelName: "local-model", + Model: "vllm/custom-model", + APIBase: "http://localhost:8000/v1", + APIKey: "", + }, + } +} + // DefaultConfig returns the default configuration for PicoClaw. func DefaultConfig() *Config { return &Config{ @@ -117,160 +270,7 @@ func DefaultConfig() *Config { 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 - { - ModelName: "glm-4.7", - Model: "zhipu/glm-4.7", - APIBase: "https://open.bigmodel.cn/api/paas/v4", - 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 - { - ModelName: "claude-sonnet-4.6", - Model: "anthropic/claude-sonnet-4.6", - APIBase: "https://api.anthropic.com/v1", - APIKey: "", - }, - - // DeepSeek - https://platform.deepseek.com/ - { - ModelName: "deepseek-chat", - Model: "deepseek/deepseek-chat", - APIBase: "https://api.deepseek.com/v1", - APIKey: "", - }, - - // Google Gemini - https://ai.google.dev/ - { - ModelName: "gemini-2.0-flash", - Model: "gemini/gemini-2.0-flash-exp", - APIBase: "https://generativelanguage.googleapis.com/v1beta", - APIKey: "", - }, - - // Qwen (通义千问) - https://dashscope.console.aliyun.com/apiKey - { - ModelName: "qwen-plus", - Model: "qwen/qwen-plus", - APIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1", - APIKey: "", - }, - - // Moonshot (月之暗面) - https://platform.moonshot.cn/console/api-keys - { - ModelName: "moonshot-v1-8k", - Model: "moonshot/moonshot-v1-8k", - APIBase: "https://api.moonshot.cn/v1", - APIKey: "", - }, - - // Groq - https://console.groq.com/keys - { - ModelName: "llama-3.3-70b", - Model: "groq/llama-3.3-70b-versatile", - APIBase: "https://api.groq.com/openai/v1", - APIKey: "", - }, - - // OpenRouter (100+ models) - https://openrouter.ai/keys - { - ModelName: "openrouter-auto", - Model: "openrouter/auto", - APIBase: "https://openrouter.ai/api/v1", - APIKey: "", - }, - { - ModelName: "openrouter-gpt-5.2", - Model: "openrouter/openai/gpt-5.2", - APIBase: "https://openrouter.ai/api/v1", - APIKey: "", - }, - - // NVIDIA - https://build.nvidia.com/ - { - ModelName: "nemotron-4-340b", - Model: "nvidia/nemotron-4-340b-instruct", - APIBase: "https://integrate.api.nvidia.com/v1", - APIKey: "", - }, - - // Cerebras - https://inference.cerebras.ai/ - { - ModelName: "cerebras-llama-3.3-70b", - Model: "cerebras/llama-3.3-70b", - APIBase: "https://api.cerebras.ai/v1", - APIKey: "", - }, - - // Volcengine (火山引擎) - https://console.volcengine.com/ark - { - ModelName: "doubao-pro", - Model: "volcengine/doubao-pro-32k", - APIBase: "https://ark.cn-beijing.volces.com/api/v3", - APIKey: "", - }, - - // ShengsuanYun (神算云) - { - ModelName: "deepseek-v3", - Model: "shengsuanyun/deepseek-v3", - APIBase: "https://api.shengsuanyun.com/v1", - APIKey: "", - }, - - // Antigravity (Google Cloud Code Assist) - OAuth only - { - ModelName: "gemini-flash", - Model: "antigravity/gemini-3-flash", - AuthMethod: "oauth", - }, - - // GitHub Copilot - https://github.com/settings/tokens - { - ModelName: "copilot-gpt-5.2", - Model: "github-copilot/gpt-5.2", - APIBase: "http://localhost:4321", - AuthMethod: "oauth", - }, - - // Ollama (local) - https://ollama.com - { - ModelName: "llama3", - Model: "ollama/llama3", - APIBase: "http://localhost:11434/v1", - APIKey: "ollama", - }, - - // Mistral AI - https://console.mistral.ai/api-keys - { - ModelName: "mistral-small", - Model: "mistral/mistral-small-latest", - APIBase: "https://api.mistral.ai/v1", - APIKey: "", - }, - - // VLLM (local) - http://localhost:8000 - { - ModelName: "local-model", - Model: "vllm/custom-model", - APIBase: "http://localhost:8000/v1", - APIKey: "", - }, - }, + ModelList: []ModelConfig{}, Gateway: GatewayConfig{ Host: "127.0.0.1", Port: 18790,