350 lines
12 KiB
Go
350 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
|
||
|
|
}
|