mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
2276bd149e
Merge upstream/main into refactor/provider-protocol-122. Resolve http_provider.go conflict (keep thin delegate). Wire OpenAIProviderConfig.WebSearch through providerSelection and into CodexProvider for codex-auth and codex-cli-token paths.
361 lines
12 KiB
Go
361 lines
12 KiB
Go
package providers
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/auth"
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
)
|
|
|
|
const defaultAnthropicAPIBase = "https://api.anthropic.com/v1"
|
|
|
|
var getCredential = auth.GetCredential
|
|
|
|
type providerType int
|
|
|
|
const (
|
|
providerTypeHTTPCompat providerType = iota
|
|
providerTypeClaudeAuth
|
|
providerTypeCodexAuth
|
|
providerTypeCodexCLIToken
|
|
providerTypeClaudeCLI
|
|
providerTypeCodexCLI
|
|
providerTypeGitHubCopilot
|
|
)
|
|
|
|
type providerSelection struct {
|
|
providerType providerType
|
|
apiKey string
|
|
apiBase string
|
|
proxy string
|
|
model string
|
|
workspace string
|
|
connectMode string
|
|
enableWebSearch bool
|
|
}
|
|
|
|
func createClaudeAuthProvider(apiBase string) (LLMProvider, error) {
|
|
if apiBase == "" {
|
|
apiBase = defaultAnthropicAPIBase
|
|
}
|
|
cred, err := 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 NewClaudeProviderWithTokenSourceAndBaseURL(cred.AccessToken, createClaudeTokenSource(), apiBase), nil
|
|
}
|
|
|
|
func createCodexAuthProvider(enableWebSearch bool) (LLMProvider, error) {
|
|
cred, err := 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")
|
|
}
|
|
p := NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource())
|
|
p.enableWebSearch = enableWebSearch
|
|
return p, nil
|
|
}
|
|
|
|
func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
|
|
model := cfg.Agents.Defaults.Model
|
|
providerName := strings.ToLower(cfg.Agents.Defaults.Provider)
|
|
lowerModel := strings.ToLower(model)
|
|
|
|
sel := providerSelection{
|
|
providerType: providerTypeHTTPCompat,
|
|
model: model,
|
|
}
|
|
|
|
// First, prefer explicit provider configuration.
|
|
if providerName != "" {
|
|
switch providerName {
|
|
case "groq":
|
|
if cfg.Providers.Groq.APIKey != "" {
|
|
sel.apiKey = cfg.Providers.Groq.APIKey
|
|
sel.apiBase = cfg.Providers.Groq.APIBase
|
|
sel.proxy = cfg.Providers.Groq.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://api.groq.com/openai/v1"
|
|
}
|
|
}
|
|
case "openai", "gpt":
|
|
if cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != "" {
|
|
sel.enableWebSearch = cfg.Providers.OpenAI.WebSearch
|
|
if cfg.Providers.OpenAI.AuthMethod == "codex-cli" {
|
|
sel.providerType = providerTypeCodexCLIToken
|
|
return sel, nil
|
|
}
|
|
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
|
sel.providerType = providerTypeCodexAuth
|
|
return sel, nil
|
|
}
|
|
sel.apiKey = cfg.Providers.OpenAI.APIKey
|
|
sel.apiBase = cfg.Providers.OpenAI.APIBase
|
|
sel.proxy = cfg.Providers.OpenAI.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.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" {
|
|
sel.apiBase = cfg.Providers.Anthropic.APIBase
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = defaultAnthropicAPIBase
|
|
}
|
|
sel.providerType = providerTypeClaudeAuth
|
|
return sel, nil
|
|
}
|
|
sel.apiKey = cfg.Providers.Anthropic.APIKey
|
|
sel.apiBase = cfg.Providers.Anthropic.APIBase
|
|
sel.proxy = cfg.Providers.Anthropic.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = defaultAnthropicAPIBase
|
|
}
|
|
}
|
|
case "openrouter":
|
|
if cfg.Providers.OpenRouter.APIKey != "" {
|
|
sel.apiKey = cfg.Providers.OpenRouter.APIKey
|
|
sel.proxy = cfg.Providers.OpenRouter.Proxy
|
|
if cfg.Providers.OpenRouter.APIBase != "" {
|
|
sel.apiBase = cfg.Providers.OpenRouter.APIBase
|
|
} else {
|
|
sel.apiBase = "https://openrouter.ai/api/v1"
|
|
}
|
|
}
|
|
case "zhipu", "glm":
|
|
if cfg.Providers.Zhipu.APIKey != "" {
|
|
sel.apiKey = cfg.Providers.Zhipu.APIKey
|
|
sel.apiBase = cfg.Providers.Zhipu.APIBase
|
|
sel.proxy = cfg.Providers.Zhipu.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://open.bigmodel.cn/api/paas/v4"
|
|
}
|
|
}
|
|
case "gemini", "google":
|
|
if cfg.Providers.Gemini.APIKey != "" {
|
|
sel.apiKey = cfg.Providers.Gemini.APIKey
|
|
sel.apiBase = cfg.Providers.Gemini.APIBase
|
|
sel.proxy = cfg.Providers.Gemini.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://generativelanguage.googleapis.com/v1beta"
|
|
}
|
|
}
|
|
case "vllm":
|
|
if cfg.Providers.VLLM.APIBase != "" {
|
|
sel.apiKey = cfg.Providers.VLLM.APIKey
|
|
sel.apiBase = cfg.Providers.VLLM.APIBase
|
|
sel.proxy = cfg.Providers.VLLM.Proxy
|
|
}
|
|
case "shengsuanyun":
|
|
if cfg.Providers.ShengSuanYun.APIKey != "" {
|
|
sel.apiKey = cfg.Providers.ShengSuanYun.APIKey
|
|
sel.apiBase = cfg.Providers.ShengSuanYun.APIBase
|
|
sel.proxy = cfg.Providers.ShengSuanYun.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://router.shengsuanyun.com/api/v1"
|
|
}
|
|
}
|
|
case "nvidia":
|
|
if cfg.Providers.Nvidia.APIKey != "" {
|
|
sel.apiKey = cfg.Providers.Nvidia.APIKey
|
|
sel.apiBase = cfg.Providers.Nvidia.APIBase
|
|
sel.proxy = cfg.Providers.Nvidia.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://integrate.api.nvidia.com/v1"
|
|
}
|
|
}
|
|
case "claude-cli", "claude-code", "claudecode":
|
|
workspace := cfg.WorkspacePath()
|
|
if workspace == "" {
|
|
workspace = "."
|
|
}
|
|
sel.providerType = providerTypeClaudeCLI
|
|
sel.workspace = workspace
|
|
return sel, nil
|
|
case "codex-cli", "codex-code":
|
|
workspace := cfg.WorkspacePath()
|
|
if workspace == "" {
|
|
workspace = "."
|
|
}
|
|
sel.providerType = providerTypeCodexCLI
|
|
sel.workspace = workspace
|
|
return sel, nil
|
|
case "deepseek":
|
|
if cfg.Providers.DeepSeek.APIKey != "" {
|
|
sel.apiKey = cfg.Providers.DeepSeek.APIKey
|
|
sel.apiBase = cfg.Providers.DeepSeek.APIBase
|
|
sel.proxy = cfg.Providers.DeepSeek.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://api.deepseek.com/v1"
|
|
}
|
|
if model != "deepseek-chat" && model != "deepseek-reasoner" {
|
|
sel.model = "deepseek-chat"
|
|
}
|
|
}
|
|
case "github_copilot", "copilot":
|
|
sel.providerType = providerTypeGitHubCopilot
|
|
if cfg.Providers.GitHubCopilot.APIBase != "" {
|
|
sel.apiBase = cfg.Providers.GitHubCopilot.APIBase
|
|
} else {
|
|
sel.apiBase = "localhost:4321"
|
|
}
|
|
sel.connectMode = cfg.Providers.GitHubCopilot.ConnectMode
|
|
return sel, nil
|
|
}
|
|
}
|
|
|
|
// Fallback: infer provider from model and configured keys.
|
|
if sel.apiKey == "" && sel.apiBase == "" {
|
|
switch {
|
|
case (strings.Contains(lowerModel, "kimi") || strings.Contains(lowerModel, "moonshot") || strings.HasPrefix(model, "moonshot/")) && cfg.Providers.Moonshot.APIKey != "":
|
|
sel.apiKey = cfg.Providers.Moonshot.APIKey
|
|
sel.apiBase = cfg.Providers.Moonshot.APIBase
|
|
sel.proxy = cfg.Providers.Moonshot.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.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/"):
|
|
sel.apiKey = cfg.Providers.OpenRouter.APIKey
|
|
sel.proxy = cfg.Providers.OpenRouter.Proxy
|
|
if cfg.Providers.OpenRouter.APIBase != "" {
|
|
sel.apiBase = cfg.Providers.OpenRouter.APIBase
|
|
} else {
|
|
sel.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" {
|
|
sel.apiBase = cfg.Providers.Anthropic.APIBase
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = defaultAnthropicAPIBase
|
|
}
|
|
sel.providerType = providerTypeClaudeAuth
|
|
return sel, nil
|
|
}
|
|
sel.apiKey = cfg.Providers.Anthropic.APIKey
|
|
sel.apiBase = cfg.Providers.Anthropic.APIBase
|
|
sel.proxy = cfg.Providers.Anthropic.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = defaultAnthropicAPIBase
|
|
}
|
|
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) &&
|
|
(cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""):
|
|
sel.enableWebSearch = cfg.Providers.OpenAI.WebSearch
|
|
if cfg.Providers.OpenAI.AuthMethod == "codex-cli" {
|
|
sel.providerType = providerTypeCodexCLIToken
|
|
return sel, nil
|
|
}
|
|
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
|
sel.providerType = providerTypeCodexAuth
|
|
return sel, nil
|
|
}
|
|
sel.apiKey = cfg.Providers.OpenAI.APIKey
|
|
sel.apiBase = cfg.Providers.OpenAI.APIBase
|
|
sel.proxy = cfg.Providers.OpenAI.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://api.openai.com/v1"
|
|
}
|
|
case (strings.Contains(lowerModel, "gemini") || strings.HasPrefix(model, "google/")) && cfg.Providers.Gemini.APIKey != "":
|
|
sel.apiKey = cfg.Providers.Gemini.APIKey
|
|
sel.apiBase = cfg.Providers.Gemini.APIBase
|
|
sel.proxy = cfg.Providers.Gemini.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://generativelanguage.googleapis.com/v1beta"
|
|
}
|
|
case (strings.Contains(lowerModel, "glm") || strings.Contains(lowerModel, "zhipu") || strings.Contains(lowerModel, "zai")) && cfg.Providers.Zhipu.APIKey != "":
|
|
sel.apiKey = cfg.Providers.Zhipu.APIKey
|
|
sel.apiBase = cfg.Providers.Zhipu.APIBase
|
|
sel.proxy = cfg.Providers.Zhipu.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://open.bigmodel.cn/api/paas/v4"
|
|
}
|
|
case (strings.Contains(lowerModel, "groq") || strings.HasPrefix(model, "groq/")) && cfg.Providers.Groq.APIKey != "":
|
|
sel.apiKey = cfg.Providers.Groq.APIKey
|
|
sel.apiBase = cfg.Providers.Groq.APIBase
|
|
sel.proxy = cfg.Providers.Groq.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://api.groq.com/openai/v1"
|
|
}
|
|
case (strings.Contains(lowerModel, "nvidia") || strings.HasPrefix(model, "nvidia/")) && cfg.Providers.Nvidia.APIKey != "":
|
|
sel.apiKey = cfg.Providers.Nvidia.APIKey
|
|
sel.apiBase = cfg.Providers.Nvidia.APIBase
|
|
sel.proxy = cfg.Providers.Nvidia.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "https://integrate.api.nvidia.com/v1"
|
|
}
|
|
case (strings.Contains(lowerModel, "ollama") || strings.HasPrefix(model, "ollama/")) && cfg.Providers.Ollama.APIKey != "":
|
|
sel.apiKey = cfg.Providers.Ollama.APIKey
|
|
sel.apiBase = cfg.Providers.Ollama.APIBase
|
|
sel.proxy = cfg.Providers.Ollama.Proxy
|
|
if sel.apiBase == "" {
|
|
sel.apiBase = "http://localhost:11434/v1"
|
|
}
|
|
case cfg.Providers.VLLM.APIBase != "":
|
|
sel.apiKey = cfg.Providers.VLLM.APIKey
|
|
sel.apiBase = cfg.Providers.VLLM.APIBase
|
|
sel.proxy = cfg.Providers.VLLM.Proxy
|
|
default:
|
|
if cfg.Providers.OpenRouter.APIKey != "" {
|
|
sel.apiKey = cfg.Providers.OpenRouter.APIKey
|
|
sel.proxy = cfg.Providers.OpenRouter.Proxy
|
|
if cfg.Providers.OpenRouter.APIBase != "" {
|
|
sel.apiBase = cfg.Providers.OpenRouter.APIBase
|
|
} else {
|
|
sel.apiBase = "https://openrouter.ai/api/v1"
|
|
}
|
|
} else {
|
|
return providerSelection{}, fmt.Errorf("no API key configured for model: %s", model)
|
|
}
|
|
}
|
|
}
|
|
|
|
if sel.providerType == providerTypeHTTPCompat {
|
|
if sel.apiKey == "" && !strings.HasPrefix(model, "bedrock/") {
|
|
return providerSelection{}, fmt.Errorf("no API key configured for provider (model: %s)", model)
|
|
}
|
|
if sel.apiBase == "" {
|
|
return providerSelection{}, fmt.Errorf("no API base configured for provider (model: %s)", model)
|
|
}
|
|
}
|
|
|
|
return sel, nil
|
|
}
|
|
|
|
func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
|
sel, err := resolveProviderSelection(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch sel.providerType {
|
|
case providerTypeClaudeAuth:
|
|
return createClaudeAuthProvider(sel.apiBase)
|
|
case providerTypeCodexAuth:
|
|
return createCodexAuthProvider(sel.enableWebSearch)
|
|
case providerTypeCodexCLIToken:
|
|
c := NewCodexProviderWithTokenSource("", "", CreateCodexCliTokenSource())
|
|
c.enableWebSearch = sel.enableWebSearch
|
|
return c, nil
|
|
case providerTypeClaudeCLI:
|
|
return NewClaudeCliProvider(sel.workspace), nil
|
|
case providerTypeCodexCLI:
|
|
return NewCodexCliProvider(sel.workspace), nil
|
|
case providerTypeGitHubCopilot:
|
|
return NewGitHubCopilotProvider(sel.apiBase, sel.connectMode, sel.model)
|
|
default:
|
|
return NewHTTPProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
|
|
}
|
|
}
|