// PicoClaw - Ultra-lightweight personal AI agent // License: MIT // // Copyright (c) 2026 PicoClaw contributors package config import ( "encoding/json" "slices" "strings" ) type migratable interface { Migrate() (*Config, error) } // buildModelWithProtocol constructs a model string with protocol prefix. // If the model already contains a "/" (indicating it has a protocol prefix), it is returned as-is. // Otherwise, the protocol prefix is added. func buildModelWithProtocol(protocol, model string) string { if strings.Contains(model, "/") { // Model already has a protocol prefix, return as-is return model } return protocol + "/" + model } // v0ConvertProvidersToModelList converts the old providersConfigV0 to a slice of ModelConfig. // This enables backward compatibility with existing configurations. // It preserves the user's configured model from agents.defaults.model when possible. func v0ConvertProvidersToModelList(cfg *configV0) []modelConfigV0 { if cfg == nil { return nil } // providerMigrationConfig defines how to migrate a provider from old config to new format. type providerMigrationConfig struct { // providerNames are the possible names used in agents.defaults.provider providerNames []string // protocol is the protocol prefix for the model field protocol string // buildConfig creates the ModelConfig from ProviderConfig buildConfig func(p providersConfigV0) (modelConfigV0, bool) } // Get user's configured provider and model userProvider := strings.ToLower(cfg.Agents.Defaults.Provider) userModel := cfg.Agents.Defaults.GetModelName() p := cfg.Providers var result []modelConfigV0 // Track if we've applied the legacy model name fix (only for first provider) legacyModelNameApplied := false // Define migration rules for each provider migrations := []providerMigrationConfig{ { providerNames: []string{"openai", "gpt"}, protocol: "openai", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.OpenAI.APIKey == "" && p.OpenAI.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "openai", Model: "openai/gpt-5.4", APIKey: p.OpenAI.APIKey, APIBase: p.OpenAI.APIBase, Proxy: p.OpenAI.Proxy, RequestTimeout: p.OpenAI.RequestTimeout, AuthMethod: p.OpenAI.AuthMethod, }, true }, }, { providerNames: []string{"anthropic", "claude"}, protocol: "anthropic", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Anthropic.APIKey == "" && p.Anthropic.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "anthropic", Model: "anthropic/claude-sonnet-4.6", APIKey: p.Anthropic.APIKey, APIBase: p.Anthropic.APIBase, Proxy: p.Anthropic.Proxy, RequestTimeout: p.Anthropic.RequestTimeout, AuthMethod: p.Anthropic.AuthMethod, }, true }, }, { providerNames: []string{"litellm"}, protocol: "litellm", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.LiteLLM.APIKey == "" && p.LiteLLM.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "litellm", Model: "litellm/auto", APIKey: p.LiteLLM.APIKey, APIBase: p.LiteLLM.APIBase, Proxy: p.LiteLLM.Proxy, RequestTimeout: p.LiteLLM.RequestTimeout, }, true }, }, { providerNames: []string{"openrouter"}, protocol: "openrouter", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.OpenRouter.APIKey == "" && p.OpenRouter.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "openrouter", Model: "openrouter/auto", APIKey: p.OpenRouter.APIKey, APIBase: p.OpenRouter.APIBase, Proxy: p.OpenRouter.Proxy, RequestTimeout: p.OpenRouter.RequestTimeout, }, true }, }, { providerNames: []string{"groq"}, protocol: "groq", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Groq.APIKey == "" && p.Groq.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "groq", Model: "groq/llama-3.1-70b-versatile", APIKey: p.Groq.APIKey, APIBase: p.Groq.APIBase, Proxy: p.Groq.Proxy, RequestTimeout: p.Groq.RequestTimeout, }, true }, }, { providerNames: []string{"zhipu", "glm"}, protocol: "zhipu", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Zhipu.APIKey == "" && p.Zhipu.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "zhipu", Model: "zhipu/glm-4", APIKey: p.Zhipu.APIKey, APIBase: p.Zhipu.APIBase, Proxy: p.Zhipu.Proxy, RequestTimeout: p.Zhipu.RequestTimeout, }, true }, }, { providerNames: []string{"vllm"}, protocol: "vllm", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.VLLM.APIKey == "" && p.VLLM.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "vllm", Model: "vllm/auto", APIKey: p.VLLM.APIKey, APIBase: p.VLLM.APIBase, Proxy: p.VLLM.Proxy, RequestTimeout: p.VLLM.RequestTimeout, }, true }, }, { providerNames: []string{"gemini", "google"}, protocol: "gemini", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Gemini.APIKey == "" && p.Gemini.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "gemini", Model: "gemini/gemini-pro", APIKey: p.Gemini.APIKey, APIBase: p.Gemini.APIBase, Proxy: p.Gemini.Proxy, RequestTimeout: p.Gemini.RequestTimeout, }, true }, }, { providerNames: []string{"nvidia"}, protocol: "nvidia", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Nvidia.APIKey == "" && p.Nvidia.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "nvidia", Model: "nvidia/meta/llama-3.1-8b-instruct", APIKey: p.Nvidia.APIKey, APIBase: p.Nvidia.APIBase, Proxy: p.Nvidia.Proxy, RequestTimeout: p.Nvidia.RequestTimeout, }, true }, }, { providerNames: []string{"ollama"}, protocol: "ollama", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Ollama.APIKey == "" && p.Ollama.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "ollama", Model: "ollama/llama3", APIKey: p.Ollama.APIKey, APIBase: p.Ollama.APIBase, Proxy: p.Ollama.Proxy, RequestTimeout: p.Ollama.RequestTimeout, }, true }, }, { providerNames: []string{"moonshot", "kimi"}, protocol: "moonshot", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Moonshot.APIKey == "" && p.Moonshot.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "moonshot", Model: "moonshot/kimi", APIKey: p.Moonshot.APIKey, APIBase: p.Moonshot.APIBase, Proxy: p.Moonshot.Proxy, RequestTimeout: p.Moonshot.RequestTimeout, }, true }, }, { providerNames: []string{"shengsuanyun"}, protocol: "shengsuanyun", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.ShengSuanYun.APIKey == "" && p.ShengSuanYun.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "shengsuanyun", Model: "shengsuanyun/auto", APIKey: p.ShengSuanYun.APIKey, APIBase: p.ShengSuanYun.APIBase, Proxy: p.ShengSuanYun.Proxy, RequestTimeout: p.ShengSuanYun.RequestTimeout, }, true }, }, { providerNames: []string{"deepseek"}, protocol: "deepseek", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.DeepSeek.APIKey == "" && p.DeepSeek.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "deepseek", Model: "deepseek/deepseek-chat", APIKey: p.DeepSeek.APIKey, APIBase: p.DeepSeek.APIBase, Proxy: p.DeepSeek.Proxy, RequestTimeout: p.DeepSeek.RequestTimeout, }, true }, }, { providerNames: []string{"cerebras"}, protocol: "cerebras", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Cerebras.APIKey == "" && p.Cerebras.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "cerebras", Model: "cerebras/llama-3.3-70b", APIKey: p.Cerebras.APIKey, APIBase: p.Cerebras.APIBase, Proxy: p.Cerebras.Proxy, RequestTimeout: p.Cerebras.RequestTimeout, }, true }, }, { providerNames: []string{"vivgrid"}, protocol: "vivgrid", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Vivgrid.APIKey == "" && p.Vivgrid.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "vivgrid", Model: "vivgrid/auto", APIKey: p.Vivgrid.APIKey, APIBase: p.Vivgrid.APIBase, Proxy: p.Vivgrid.Proxy, RequestTimeout: p.Vivgrid.RequestTimeout, }, true }, }, { providerNames: []string{"volcengine", "doubao"}, protocol: "volcengine", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.VolcEngine.APIKey == "" && p.VolcEngine.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "volcengine", Model: "volcengine/doubao-pro", APIKey: p.VolcEngine.APIKey, APIBase: p.VolcEngine.APIBase, Proxy: p.VolcEngine.Proxy, RequestTimeout: p.VolcEngine.RequestTimeout, }, true }, }, { providerNames: []string{"github_copilot", "copilot"}, protocol: "github-copilot", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.GitHubCopilot.APIKey == "" && p.GitHubCopilot.APIBase == "" && p.GitHubCopilot.ConnectMode == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "github-copilot", Model: "github-copilot/gpt-5.4", APIBase: p.GitHubCopilot.APIBase, ConnectMode: p.GitHubCopilot.ConnectMode, }, true }, }, { providerNames: []string{"antigravity"}, protocol: "antigravity", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Antigravity.APIKey == "" && p.Antigravity.AuthMethod == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "antigravity", Model: "antigravity/gemini-2.0-flash", APIKey: p.Antigravity.APIKey, AuthMethod: p.Antigravity.AuthMethod, }, true }, }, { providerNames: []string{"qwen", "tongyi"}, protocol: "qwen", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Qwen.APIKey == "" && p.Qwen.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "qwen", Model: "qwen/qwen-max", APIKey: p.Qwen.APIKey, APIBase: p.Qwen.APIBase, Proxy: p.Qwen.Proxy, RequestTimeout: p.Qwen.RequestTimeout, }, true }, }, { providerNames: []string{"mistral"}, protocol: "mistral", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Mistral.APIKey == "" && p.Mistral.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "mistral", Model: "mistral/mistral-small-latest", APIKey: p.Mistral.APIKey, APIBase: p.Mistral.APIBase, Proxy: p.Mistral.Proxy, RequestTimeout: p.Mistral.RequestTimeout, }, true }, }, { providerNames: []string{"avian"}, protocol: "avian", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.Avian.APIKey == "" && p.Avian.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "avian", Model: "avian/deepseek/deepseek-v3.2", APIKey: p.Avian.APIKey, APIBase: p.Avian.APIBase, Proxy: p.Avian.Proxy, RequestTimeout: p.Avian.RequestTimeout, }, true }, }, { providerNames: []string{"longcat"}, protocol: "longcat", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.LongCat.APIKey == "" && p.LongCat.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "longcat", Model: "longcat/LongCat-Flash-Thinking", APIKey: p.LongCat.APIKey, APIBase: p.LongCat.APIBase, Proxy: p.LongCat.Proxy, RequestTimeout: p.LongCat.RequestTimeout, }, true }, }, { providerNames: []string{"modelscope"}, protocol: "modelscope", buildConfig: func(p providersConfigV0) (modelConfigV0, bool) { if p.ModelScope.APIKey == "" && p.ModelScope.APIBase == "" { return modelConfigV0{}, false } return modelConfigV0{ ModelName: "modelscope", Model: "modelscope/Qwen/Qwen3-235B-A22B-Instruct-2507", APIKey: p.ModelScope.APIKey, APIBase: p.ModelScope.APIBase, Proxy: p.ModelScope.Proxy, RequestTimeout: p.ModelScope.RequestTimeout, }, true }, }, } // Process each provider migration for _, m := range migrations { mc, ok := m.buildConfig(p) if !ok { continue } // Check if this is the user's configured provider if slices.Contains(m.providerNames, userProvider) && userModel != "" { // Use the user's configured model instead of default mc.Model = buildModelWithProtocol(m.protocol, userModel) } else if userProvider == "" && userModel != "" && !legacyModelNameApplied { // Legacy config: no explicit provider field but model is specified // Use userModel as ModelName for the FIRST provider so GetModelConfig(model) can find it // This maintains backward compatibility with old configs that relied on implicit provider selection mc.ModelName = userModel mc.Model = buildModelWithProtocol(m.protocol, userModel) legacyModelNameApplied = true } result = append(result, mc) } return result } // loadConfigV0 loads a legacy config (no version field) func loadConfigV0(data []byte) (migratable, error) { var v0 configV0 if err := json.Unmarshal(data, &v0); err != nil { return nil, err } v0.migrateChannelConfigs() // Auto-migrate: if only legacy providers config exists, convert to model_list if len(v0.ModelList) == 0 && !v0.Providers.IsEmpty() { newModelList := v0ConvertProvidersToModelList(&v0) // Convert []ModelConfig to []modelConfigV0 v0.ModelList = make([]modelConfigV0, len(newModelList)) for i, m := range newModelList { v0.ModelList[i] = modelConfigV0{ ModelName: m.ModelName, Model: m.Model, APIBase: m.APIBase, Proxy: m.Proxy, Fallbacks: m.Fallbacks, AuthMethod: m.AuthMethod, ConnectMode: m.ConnectMode, Workspace: m.Workspace, RPM: m.RPM, MaxTokensField: m.MaxTokensField, RequestTimeout: m.RequestTimeout, ThinkingLevel: m.ThinkingLevel, APIKey: m.APIKey, APIKeys: m.APIKeys, } } } return &v0, nil } // loadConfigV1 loads a version 1 config (current schema) func loadConfig(data []byte) (*Config, error) { cfg := DefaultConfig() // Pre-scan the JSON to check how many model_list entries the user provided. // Go's JSON decoder reuses existing slice backing-array elements rather than // zero-initializing them, so fields absent from the user's JSON (e.g. api_base) // would silently inherit values from the DefaultConfig template at the same // index position. We only reset cfg.ModelList when the user actually provides // entries; when count is 0 we keep DefaultConfig's built-in list as fallback. var tmp Config if err := json.Unmarshal(data, &tmp); err != nil { return nil, err } if len(tmp.ModelList) > 0 { cfg.ModelList = nil } if err := json.Unmarshal(data, cfg); err != nil { return nil, err } return cfg, nil }