Refactor command handlers into separate files to improve code organization and maintainability. Each command (agent, auth, cron, gateway, migrate, onboard, skills, status) now has its own dedicated file. Restructure provider creation to support new model_list configuration system that enables zero-code addition of OpenAI-compatible providers. Move legacy provider logic to separate file for backward compatibility. Move configuration functions from config.go to separate files (defaults.go, migration.go) for better organization.
349 lines
12 KiB
Go
349 lines
12 KiB
Go
// PicoClaw - Ultra-lightweight personal AI agent
|
|
// License: MIT
|
|
//
|
|
// Copyright (c) 2026 PicoClaw contributors
|
|
|
|
package providers
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/auth"
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
)
|
|
|
|
// createClaudeAuthProvider creates a Claude provider using OAuth credentials.
|
|
func createClaudeAuthProvider() (LLMProvider, error) {
|
|
cred, err := auth.GetCredential("anthropic")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading auth credentials: %w", err)
|
|
}
|
|
if cred == nil {
|
|
return nil, fmt.Errorf("no credentials for anthropic. Run: picoclaw auth login --provider anthropic")
|
|
}
|
|
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
|
|
}
|
|
|
|
// createCodexAuthProvider creates a Codex provider using OAuth credentials.
|
|
func createCodexAuthProvider() (LLMProvider, error) {
|
|
cred, err := auth.GetCredential("openai")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading auth credentials: %w", err)
|
|
}
|
|
if cred == nil {
|
|
return nil, fmt.Errorf("no credentials for openai. Run: picoclaw auth login --provider openai")
|
|
}
|
|
return NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil
|
|
}
|
|
|
|
// CreateProvider creates a provider based on the configuration.
|
|
// It supports both the new model_list configuration and the legacy providers configuration.
|
|
// Returns the provider, the model ID to use, and any error.
|
|
func CreateProvider(cfg *config.Config) (LLMProvider, string, error) {
|
|
model := cfg.Agents.Defaults.Model
|
|
|
|
// First, try to use model_list configuration
|
|
if len(cfg.ModelList) > 0 {
|
|
// Try to get config by model name first
|
|
modelCfg, err := cfg.GetModelConfig(model)
|
|
if err == nil {
|
|
// Found in model_list, use factory to create provider
|
|
provider, modelID, err := CreateProviderFromConfig(modelCfg)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to create provider from model_list: %w", err)
|
|
}
|
|
return provider, modelID, nil
|
|
}
|
|
// Model not found in model_list, fall through to providers config
|
|
}
|
|
|
|
// Log deprecation warning if using old providers config
|
|
if cfg.HasProvidersConfig() && len(cfg.ModelList) == 0 {
|
|
fmt.Println("WARNING: providers config is deprecated, please migrate to model_list")
|
|
}
|
|
|
|
providerName := strings.ToLower(cfg.Agents.Defaults.Provider)
|
|
|
|
var apiKey, apiBase, proxy string
|
|
|
|
lowerModel := strings.ToLower(model)
|
|
|
|
// First, try to use explicitly configured provider
|
|
if providerName != "" {
|
|
switch providerName {
|
|
case "groq":
|
|
if cfg.Providers.Groq.APIKey != "" {
|
|
apiKey = cfg.Providers.Groq.APIKey
|
|
apiBase = cfg.Providers.Groq.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://api.groq.com/openai/v1"
|
|
}
|
|
}
|
|
case "openai", "gpt":
|
|
if cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != "" {
|
|
if cfg.Providers.OpenAI.AuthMethod == "codex-cli" {
|
|
return NewCodexProviderWithTokenSource("", "", CreateCodexCliTokenSource()), model, nil
|
|
}
|
|
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
|
provider, err := createCodexAuthProvider()
|
|
return provider, model, err
|
|
}
|
|
apiKey = cfg.Providers.OpenAI.APIKey
|
|
apiBase = cfg.Providers.OpenAI.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://api.openai.com/v1"
|
|
}
|
|
}
|
|
case "anthropic", "claude":
|
|
if cfg.Providers.Anthropic.APIKey != "" || cfg.Providers.Anthropic.AuthMethod != "" {
|
|
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" {
|
|
provider, err := createClaudeAuthProvider()
|
|
return provider, model, err
|
|
}
|
|
apiKey = cfg.Providers.Anthropic.APIKey
|
|
apiBase = cfg.Providers.Anthropic.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://api.anthropic.com/v1"
|
|
}
|
|
}
|
|
case "openrouter":
|
|
if cfg.Providers.OpenRouter.APIKey != "" {
|
|
apiKey = cfg.Providers.OpenRouter.APIKey
|
|
if cfg.Providers.OpenRouter.APIBase != "" {
|
|
apiBase = cfg.Providers.OpenRouter.APIBase
|
|
} else {
|
|
apiBase = "https://openrouter.ai/api/v1"
|
|
}
|
|
}
|
|
case "zhipu", "glm":
|
|
if cfg.Providers.Zhipu.APIKey != "" {
|
|
apiKey = cfg.Providers.Zhipu.APIKey
|
|
apiBase = cfg.Providers.Zhipu.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://open.bigmodel.cn/api/paas/v4"
|
|
}
|
|
}
|
|
case "gemini", "google":
|
|
if cfg.Providers.Gemini.APIKey != "" {
|
|
apiKey = cfg.Providers.Gemini.APIKey
|
|
apiBase = cfg.Providers.Gemini.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://generativelanguage.googleapis.com/v1beta"
|
|
}
|
|
}
|
|
case "vllm":
|
|
if cfg.Providers.VLLM.APIBase != "" {
|
|
apiKey = cfg.Providers.VLLM.APIKey
|
|
apiBase = cfg.Providers.VLLM.APIBase
|
|
}
|
|
case "shengsuanyun":
|
|
if cfg.Providers.ShengSuanYun.APIKey != "" {
|
|
apiKey = cfg.Providers.ShengSuanYun.APIKey
|
|
apiBase = cfg.Providers.ShengSuanYun.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://router.shengsuanyun.com/api/v1"
|
|
}
|
|
}
|
|
case "claude-cli", "claudecode", "claude-code":
|
|
workspace := cfg.WorkspacePath()
|
|
if workspace == "" {
|
|
workspace = "."
|
|
}
|
|
return NewClaudeCliProvider(workspace), model, nil
|
|
case "codex-cli", "codex-code":
|
|
workspace := cfg.WorkspacePath()
|
|
if workspace == "" {
|
|
workspace = "."
|
|
}
|
|
return NewCodexCliProvider(workspace), model, nil
|
|
case "cerebras":
|
|
if cfg.Providers.Cerebras.APIKey != "" {
|
|
apiKey = cfg.Providers.Cerebras.APIKey
|
|
apiBase = cfg.Providers.Cerebras.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://api.cerebras.ai/v1"
|
|
}
|
|
}
|
|
case "deepseek":
|
|
if cfg.Providers.DeepSeek.APIKey != "" {
|
|
apiKey = cfg.Providers.DeepSeek.APIKey
|
|
apiBase = cfg.Providers.DeepSeek.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://api.deepseek.com/v1"
|
|
}
|
|
if model != "deepseek-chat" && model != "deepseek-reasoner" {
|
|
model = "deepseek-chat"
|
|
}
|
|
}
|
|
case "qwen":
|
|
if cfg.Providers.Qwen.APIKey != "" {
|
|
apiKey = cfg.Providers.Qwen.APIKey
|
|
apiBase = cfg.Providers.Qwen.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
}
|
|
}
|
|
case "github_copilot", "copilot":
|
|
if cfg.Providers.GitHubCopilot.APIBase != "" {
|
|
apiBase = cfg.Providers.GitHubCopilot.APIBase
|
|
} else {
|
|
apiBase = "localhost:4321"
|
|
}
|
|
provider, err := NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model)
|
|
return provider, model, err
|
|
case "antigravity", "google-antigravity":
|
|
return NewAntigravityProvider(), model, nil
|
|
|
|
case "volcengine", "doubao":
|
|
if cfg.Providers.VolcEngine.APIKey != "" {
|
|
apiKey = cfg.Providers.VolcEngine.APIKey
|
|
apiBase = cfg.Providers.VolcEngine.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://ark.cn-beijing.volces.com/api/v3"
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Fallback: detect provider from model name
|
|
if apiKey == "" && apiBase == "" {
|
|
switch {
|
|
case (strings.Contains(lowerModel, "kimi") || strings.Contains(lowerModel, "moonshot") || strings.HasPrefix(model, "moonshot/")) && cfg.Providers.Moonshot.APIKey != "":
|
|
apiKey = cfg.Providers.Moonshot.APIKey
|
|
apiBase = cfg.Providers.Moonshot.APIBase
|
|
proxy = cfg.Providers.Moonshot.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "https://api.moonshot.cn/v1"
|
|
}
|
|
|
|
case strings.HasPrefix(model, "openrouter/") || strings.HasPrefix(model, "anthropic/") || strings.HasPrefix(model, "openai/") || strings.HasPrefix(model, "meta-llama/") || strings.HasPrefix(model, "deepseek/") || strings.HasPrefix(model, "google/"):
|
|
apiKey = cfg.Providers.OpenRouter.APIKey
|
|
proxy = cfg.Providers.OpenRouter.Proxy
|
|
if cfg.Providers.OpenRouter.APIBase != "" {
|
|
apiBase = cfg.Providers.OpenRouter.APIBase
|
|
} else {
|
|
apiBase = "https://openrouter.ai/api/v1"
|
|
}
|
|
|
|
case (strings.Contains(lowerModel, "claude") || strings.HasPrefix(model, "anthropic/")) && (cfg.Providers.Anthropic.APIKey != "" || cfg.Providers.Anthropic.AuthMethod != ""):
|
|
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" {
|
|
provider, err := createClaudeAuthProvider()
|
|
return provider, model, err
|
|
}
|
|
apiKey = cfg.Providers.Anthropic.APIKey
|
|
apiBase = cfg.Providers.Anthropic.APIBase
|
|
proxy = cfg.Providers.Anthropic.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "https://api.anthropic.com/v1"
|
|
}
|
|
|
|
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && (cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""):
|
|
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
|
provider, err := createCodexAuthProvider()
|
|
return provider, model, err
|
|
}
|
|
apiKey = cfg.Providers.OpenAI.APIKey
|
|
apiBase = cfg.Providers.OpenAI.APIBase
|
|
proxy = cfg.Providers.OpenAI.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "https://api.openai.com/v1"
|
|
}
|
|
|
|
case (strings.Contains(lowerModel, "gemini") || strings.HasPrefix(model, "google/")) && cfg.Providers.Gemini.APIKey != "":
|
|
apiKey = cfg.Providers.Gemini.APIKey
|
|
apiBase = cfg.Providers.Gemini.APIBase
|
|
proxy = cfg.Providers.Gemini.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "https://generativelanguage.googleapis.com/v1beta"
|
|
}
|
|
|
|
case (strings.Contains(lowerModel, "glm") || strings.Contains(lowerModel, "zhipu") || strings.Contains(lowerModel, "zai")) && cfg.Providers.Zhipu.APIKey != "":
|
|
apiKey = cfg.Providers.Zhipu.APIKey
|
|
apiBase = cfg.Providers.Zhipu.APIBase
|
|
proxy = cfg.Providers.Zhipu.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "https://open.bigmodel.cn/api/paas/v4"
|
|
}
|
|
|
|
case (strings.Contains(lowerModel, "groq") || strings.HasPrefix(model, "groq/")) && cfg.Providers.Groq.APIKey != "":
|
|
apiKey = cfg.Providers.Groq.APIKey
|
|
apiBase = cfg.Providers.Groq.APIBase
|
|
proxy = cfg.Providers.Groq.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "https://api.groq.com/openai/v1"
|
|
}
|
|
|
|
case (strings.Contains(lowerModel, "qwen") || strings.HasPrefix(model, "qwen/")) && cfg.Providers.Qwen.APIKey != "":
|
|
apiKey = cfg.Providers.Qwen.APIKey
|
|
apiBase = cfg.Providers.Qwen.APIBase
|
|
proxy = cfg.Providers.Qwen.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
}
|
|
|
|
case (strings.Contains(lowerModel, "nvidia") || strings.HasPrefix(model, "nvidia/")) && cfg.Providers.Nvidia.APIKey != "":
|
|
apiKey = cfg.Providers.Nvidia.APIKey
|
|
apiBase = cfg.Providers.Nvidia.APIBase
|
|
proxy = cfg.Providers.Nvidia.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "https://integrate.api.nvidia.com/v1"
|
|
}
|
|
case (strings.Contains(lowerModel, "cerebras") || strings.HasPrefix(model, "cerebras/")) && cfg.Providers.Cerebras.APIKey != "":
|
|
apiKey = cfg.Providers.Cerebras.APIKey
|
|
apiBase = cfg.Providers.Cerebras.APIBase
|
|
proxy = cfg.Providers.Cerebras.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "https://api.cerebras.ai/v1"
|
|
}
|
|
|
|
case (strings.Contains(lowerModel, "ollama") || strings.HasPrefix(model, "ollama/")) && cfg.Providers.Ollama.APIKey != "":
|
|
fmt.Println("Ollama provider selected based on model name prefix")
|
|
apiKey = cfg.Providers.Ollama.APIKey
|
|
apiBase = cfg.Providers.Ollama.APIBase
|
|
proxy = cfg.Providers.Ollama.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "http://localhost:11434/v1"
|
|
}
|
|
fmt.Println("Ollama apiBase:", apiBase)
|
|
|
|
case (strings.Contains(lowerModel, "doubao") || strings.HasPrefix(lowerModel, "doubao") || strings.Contains(lowerModel, "volcengine")) && cfg.Providers.VolcEngine.APIKey != "":
|
|
apiKey = cfg.Providers.VolcEngine.APIKey
|
|
apiBase = cfg.Providers.VolcEngine.APIBase
|
|
proxy = cfg.Providers.VolcEngine.Proxy
|
|
if apiBase == "" {
|
|
apiBase = "https://ark.cn-beijing.volces.com/api/v3"
|
|
}
|
|
|
|
case cfg.Providers.VLLM.APIBase != "":
|
|
apiKey = cfg.Providers.VLLM.APIKey
|
|
apiBase = cfg.Providers.VLLM.APIBase
|
|
proxy = cfg.Providers.VLLM.Proxy
|
|
|
|
default:
|
|
if cfg.Providers.OpenRouter.APIKey != "" {
|
|
apiKey = cfg.Providers.OpenRouter.APIKey
|
|
proxy = cfg.Providers.OpenRouter.Proxy
|
|
if cfg.Providers.OpenRouter.APIBase != "" {
|
|
apiBase = cfg.Providers.OpenRouter.APIBase
|
|
} else {
|
|
apiBase = "https://openrouter.ai/api/v1"
|
|
}
|
|
} else {
|
|
return nil, "", fmt.Errorf("no API key configured for model: %s", model)
|
|
}
|
|
}
|
|
}
|
|
|
|
if apiKey == "" && !strings.HasPrefix(model, "bedrock/") {
|
|
return nil, "", fmt.Errorf("no API key configured for provider (model: %s)", model)
|
|
}
|
|
|
|
if apiBase == "" {
|
|
return nil, "", fmt.Errorf("no API base configured for provider (model: %s)", model)
|
|
}
|
|
|
|
return NewHTTPProvider(apiKey, apiBase, proxy), model, nil
|
|
}
|