refactor provider resolution with registry and add gateway install
This commit is contained in:
parent
d83fb6e081
commit
f660143ddd
8 changed files with 538 additions and 184 deletions
|
|
@ -12,7 +12,9 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
|
@ -179,7 +181,7 @@ func printHelp() {
|
|||
fmt.Println(" onboard Initialize picoclaw configuration and workspace")
|
||||
fmt.Println(" agent Interact with the agent directly")
|
||||
fmt.Println(" auth Manage authentication (login, logout, status)")
|
||||
fmt.Println(" gateway Start picoclaw gateway")
|
||||
fmt.Println(" gateway Start gateway or install gateway service")
|
||||
fmt.Println(" status Show picoclaw status")
|
||||
fmt.Println(" cron Manage scheduled tasks")
|
||||
fmt.Println(" migrate Migrate from OpenClaw to PicoClaw")
|
||||
|
|
@ -606,8 +608,19 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|||
}
|
||||
|
||||
func gatewayCmd() {
|
||||
// Check for --debug flag
|
||||
args := os.Args[2:]
|
||||
if len(args) > 0 {
|
||||
switch args[0] {
|
||||
case "install":
|
||||
gatewayInstallCmd()
|
||||
return
|
||||
case "--help", "-h", "help":
|
||||
gatewayHelp()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for --debug flag
|
||||
for _, arg := range args {
|
||||
if arg == "--debug" || arg == "-d" {
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
|
|
@ -734,6 +747,109 @@ func gatewayCmd() {
|
|||
fmt.Println("✓ Gateway stopped")
|
||||
}
|
||||
|
||||
func gatewayHelp() {
|
||||
fmt.Println("\nGateway commands:")
|
||||
fmt.Println(" picoclaw gateway Start gateway")
|
||||
fmt.Println(" picoclaw gateway install Install and start systemd service")
|
||||
fmt.Println(" picoclaw gateway --debug Start gateway with debug logging")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func gatewayInstallCmd() {
|
||||
if runtime.GOOS != "linux" {
|
||||
fmt.Println("Error: gateway install is only supported on Linux with systemd")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Println("Error: gateway install requires root privileges")
|
||||
fmt.Println("Run: sudo picoclaw gateway install")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("systemctl"); err != nil {
|
||||
fmt.Println("Error: systemctl not found")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
serviceUser, serviceHome, err := resolveGatewayServiceUser()
|
||||
if err != nil {
|
||||
fmt.Printf("Error resolving service user: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
fmt.Printf("Error resolving executable path: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if resolved, err := filepath.EvalSymlinks(execPath); err == nil {
|
||||
execPath = resolved
|
||||
}
|
||||
|
||||
unitContent := buildGatewayServiceUnit(serviceUser, serviceHome, execPath)
|
||||
servicePath := "/etc/systemd/system/picoclaw.service"
|
||||
if err := os.WriteFile(servicePath, []byte(unitContent), 0644); err != nil {
|
||||
fmt.Printf("Error writing service file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
|
||||
fmt.Printf("Error running systemctl daemon-reload: %v\n%s\n", err, strings.TrimSpace(string(out)))
|
||||
os.Exit(1)
|
||||
}
|
||||
if out, err := exec.Command("systemctl", "enable", "--now", "picoclaw").CombinedOutput(); err != nil {
|
||||
fmt.Printf("Error enabling/starting service: %v\n%s\n", err, strings.TrimSpace(string(out)))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Installed systemd service: /etc/systemd/system/picoclaw.service")
|
||||
fmt.Println("✓ Enabled and started: picoclaw")
|
||||
fmt.Println("Check status: systemctl status picoclaw --no-pager")
|
||||
fmt.Println("View logs: journalctl -u picoclaw -f")
|
||||
}
|
||||
|
||||
func resolveGatewayServiceUser() (string, string, error) {
|
||||
if sudoUser := strings.TrimSpace(os.Getenv("SUDO_USER")); sudoUser != "" {
|
||||
u, err := user.Lookup(sudoUser)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return u.Username, u.HomeDir, nil
|
||||
}
|
||||
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return u.Username, u.HomeDir, nil
|
||||
}
|
||||
|
||||
func buildGatewayServiceUnit(serviceUser, serviceHome, execPath string) string {
|
||||
return fmt.Sprintf(`[Unit]
|
||||
Description=PicoClaw Gateway
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=%s
|
||||
WorkingDirectory=%s
|
||||
ExecStart=%s gateway
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=HOME=%s
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=false
|
||||
ReadWritePaths=%s/.picoclaw
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, serviceUser, serviceHome, execPath, serviceHome, serviceHome)
|
||||
}
|
||||
|
||||
func statusCmd() {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@
|
|||
"moonshot": {
|
||||
"api_key": "sk-xxx",
|
||||
"api_base": ""
|
||||
},
|
||||
"zen": {
|
||||
"api_key": "YOUR_OPENCODE_API_KEY",
|
||||
"api_base": "https://opencode.ai/zen/v1"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ type ProvidersConfig struct {
|
|||
Gemini ProviderConfig `json:"gemini"`
|
||||
Nvidia ProviderConfig `json:"nvidia"`
|
||||
Moonshot ProviderConfig `json:"moonshot"`
|
||||
Zen ProviderConfig `json:"zen"`
|
||||
}
|
||||
|
||||
type ProviderConfig struct {
|
||||
|
|
@ -242,6 +243,7 @@ func DefaultConfig() *Config {
|
|||
Gemini: ProviderConfig{},
|
||||
Nvidia: ProviderConfig{},
|
||||
Moonshot: ProviderConfig{},
|
||||
Zen: ProviderConfig{},
|
||||
},
|
||||
Gateway: GatewayConfig{
|
||||
Host: "0.0.0.0",
|
||||
|
|
|
|||
|
|
@ -219,186 +219,5 @@ func createCodexAuthProvider() (LLMProvider, error) {
|
|||
}
|
||||
|
||||
func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||
model := cfg.Agents.Defaults.Model
|
||||
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 == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
||||
return createCodexAuthProvider()
|
||||
}
|
||||
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" {
|
||||
return createClaudeAuthProvider()
|
||||
}
|
||||
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 "claude-cli", "claudecode", "claude-code":
|
||||
workspace := cfg.Agents.Defaults.Workspace
|
||||
if workspace == "" {
|
||||
workspace = "."
|
||||
}
|
||||
return NewClaudeCliProvider(workspace), nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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" {
|
||||
return createClaudeAuthProvider()
|
||||
}
|
||||
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" {
|
||||
return createCodexAuthProvider()
|
||||
}
|
||||
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, "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 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), nil
|
||||
return defaultProviderRegistry.Create(cfg)
|
||||
}
|
||||
|
|
|
|||
188
pkg/providers/provider_registry.go
Normal file
188
pkg/providers/provider_registry.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
type providerCreator func(cfg *config.Config, model string) (LLMProvider, bool, error)
|
||||
|
||||
type providerRegistration struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
ModelPrefixes []string
|
||||
Creator providerCreator
|
||||
}
|
||||
|
||||
type providerRegistry struct {
|
||||
byName map[string]providerRegistration
|
||||
ordered []*providerRegistration
|
||||
}
|
||||
|
||||
func newProviderRegistry() *providerRegistry {
|
||||
return &providerRegistry{
|
||||
byName: make(map[string]providerRegistration),
|
||||
ordered: make([]*providerRegistration, 0, 16),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *providerRegistry) Register(reg providerRegistration) {
|
||||
normalized := reg
|
||||
normalized.Name = strings.ToLower(strings.TrimSpace(normalized.Name))
|
||||
if normalized.Name == "" {
|
||||
return
|
||||
}
|
||||
|
||||
r.byName[normalized.Name] = normalized
|
||||
for _, alias := range normalized.Aliases {
|
||||
a := strings.ToLower(strings.TrimSpace(alias))
|
||||
if a != "" {
|
||||
r.byName[a] = normalized
|
||||
}
|
||||
}
|
||||
|
||||
normalizedCopy := normalized
|
||||
r.ordered = append(r.ordered, &normalizedCopy)
|
||||
}
|
||||
|
||||
func (r *providerRegistry) Create(cfg *config.Config) (LLMProvider, error) {
|
||||
model := strings.TrimSpace(cfg.Agents.Defaults.Model)
|
||||
providerName := strings.ToLower(strings.TrimSpace(cfg.Agents.Defaults.Provider))
|
||||
|
||||
if providerName != "" {
|
||||
reg, ok := r.byName[providerName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown provider: %s", providerName)
|
||||
}
|
||||
provider, configured, err := reg.Creator(cfg, model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !configured {
|
||||
return nil, fmt.Errorf("provider '%s' is not configured", reg.Name)
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
modelLower := strings.ToLower(model)
|
||||
for _, reg := range r.ordered {
|
||||
if !matchesModelPrefix(modelLower, reg.ModelPrefixes) {
|
||||
continue
|
||||
}
|
||||
provider, configured, err := reg.Creator(cfg, model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if configured {
|
||||
return provider, nil
|
||||
}
|
||||
}
|
||||
|
||||
if provider, configured, err := openRouterCreator(cfg, model); err != nil {
|
||||
return nil, err
|
||||
} else if configured {
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no API key configured for model: %s", model)
|
||||
}
|
||||
|
||||
func matchesModelPrefix(model string, prefixes []string) bool {
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(model, strings.ToLower(prefix)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func createHTTPProviderFromConfig(pc config.ProviderConfig, defaultBase string, requireAPIKey bool, requireAPIBase bool) (LLMProvider, bool, error) {
|
||||
if requireAPIKey && pc.APIKey == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
if requireAPIBase && pc.APIBase == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
apiBase := pc.APIBase
|
||||
if apiBase == "" {
|
||||
apiBase = defaultBase
|
||||
}
|
||||
if apiBase == "" {
|
||||
return nil, false, fmt.Errorf("no API base configured for provider")
|
||||
}
|
||||
|
||||
return NewHTTPProvider(pc.APIKey, apiBase, pc.Proxy), true, nil
|
||||
}
|
||||
|
||||
func claudeCreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
pc := cfg.Providers.Anthropic
|
||||
if pc.AuthMethod == "oauth" || pc.AuthMethod == "token" {
|
||||
p, err := createClaudeAuthProvider()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return p, true, nil
|
||||
}
|
||||
return createHTTPProviderFromConfig(pc, "https://api.anthropic.com/v1", true, false)
|
||||
}
|
||||
|
||||
func openAICreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
pc := cfg.Providers.OpenAI
|
||||
if pc.AuthMethod == "oauth" || pc.AuthMethod == "token" {
|
||||
p, err := createCodexAuthProvider()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return p, true, nil
|
||||
}
|
||||
return createHTTPProviderFromConfig(pc, "https://api.openai.com/v1", true, false)
|
||||
}
|
||||
|
||||
func openRouterCreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
return createHTTPProviderFromConfig(cfg.Providers.OpenRouter, "https://openrouter.ai/api/v1", true, false)
|
||||
}
|
||||
|
||||
func groqCreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
return createHTTPProviderFromConfig(cfg.Providers.Groq, "https://api.groq.com/openai/v1", true, false)
|
||||
}
|
||||
|
||||
func zhipuCreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
return createHTTPProviderFromConfig(cfg.Providers.Zhipu, "https://open.bigmodel.cn/api/paas/v4", true, false)
|
||||
}
|
||||
|
||||
func geminiCreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
return createHTTPProviderFromConfig(cfg.Providers.Gemini, "https://generativelanguage.googleapis.com/v1beta", true, false)
|
||||
}
|
||||
|
||||
func nvidiaCreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
return createHTTPProviderFromConfig(cfg.Providers.Nvidia, "https://integrate.api.nvidia.com/v1", true, false)
|
||||
}
|
||||
|
||||
func moonshotCreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
return createHTTPProviderFromConfig(cfg.Providers.Moonshot, "https://api.moonshot.cn/v1", true, false)
|
||||
}
|
||||
|
||||
func vllmCreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
return createHTTPProviderFromConfig(cfg.Providers.VLLM, "", false, true)
|
||||
}
|
||||
|
||||
func zenCreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
return createHTTPProviderFromConfig(cfg.Providers.Zen, "https://opencode.ai/zen/v1", true, false)
|
||||
}
|
||||
|
||||
func claudeCLICreator(cfg *config.Config, _ string) (LLMProvider, bool, error) {
|
||||
workspace := cfg.Agents.Defaults.Workspace
|
||||
if workspace == "" {
|
||||
workspace = "."
|
||||
}
|
||||
return NewClaudeCliProvider(workspace), true, nil
|
||||
}
|
||||
|
||||
var defaultProviderRegistry = newProviderRegistry()
|
||||
|
||||
func RegisterProvider(reg providerRegistration) {
|
||||
defaultProviderRegistry.Register(reg)
|
||||
}
|
||||
67
pkg/providers/provider_registry_defaults.go
Normal file
67
pkg/providers/provider_registry_defaults.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package providers
|
||||
|
||||
func init() {
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "moonshot",
|
||||
ModelPrefixes: []string{"moonshot/", "moonshot-", "kimi-"},
|
||||
Creator: moonshotCreator,
|
||||
})
|
||||
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "openrouter",
|
||||
ModelPrefixes: []string{"openrouter/", "anthropic/", "openai/", "meta-llama/", "deepseek/", "google/"},
|
||||
Creator: openRouterCreator,
|
||||
})
|
||||
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "anthropic",
|
||||
Aliases: []string{"claude"},
|
||||
ModelPrefixes: []string{"claude-", "anthropic/"},
|
||||
Creator: claudeCreator,
|
||||
})
|
||||
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "openai",
|
||||
Aliases: []string{"gpt"},
|
||||
ModelPrefixes: []string{"gpt-", "o1-", "o3-", "o4-", "chatgpt-", "openai/"},
|
||||
Creator: openAICreator,
|
||||
})
|
||||
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "gemini",
|
||||
Aliases: []string{"google"},
|
||||
ModelPrefixes: []string{"gemini-", "google/"},
|
||||
Creator: geminiCreator,
|
||||
})
|
||||
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "zhipu",
|
||||
Aliases: []string{"glm"},
|
||||
ModelPrefixes: []string{"glm-", "zhipu/", "zai-"},
|
||||
Creator: zhipuCreator,
|
||||
})
|
||||
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "groq",
|
||||
ModelPrefixes: []string{"groq/"},
|
||||
Creator: groqCreator,
|
||||
})
|
||||
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "nvidia",
|
||||
ModelPrefixes: []string{"nvidia/"},
|
||||
Creator: nvidiaCreator,
|
||||
})
|
||||
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "vllm",
|
||||
ModelPrefixes: []string{"vllm/"},
|
||||
Creator: vllmCreator,
|
||||
})
|
||||
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "claude-cli",
|
||||
Aliases: []string{"claudecode", "claude-code"},
|
||||
Creator: claudeCLICreator,
|
||||
})
|
||||
}
|
||||
149
pkg/providers/provider_registry_test.go
Normal file
149
pkg/providers/provider_registry_test.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
package providers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
func TestCreateProvider_ZenExplicit(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Provider = "zen"
|
||||
cfg.Agents.Defaults.Model = "claude-sonnet-4.5"
|
||||
cfg.Providers.Zen.APIKey = "zen-key"
|
||||
|
||||
provider, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(zen) error = %v", err)
|
||||
}
|
||||
|
||||
httpProvider, ok := provider.(*HTTPProvider)
|
||||
if !ok {
|
||||
t.Fatalf("CreateProvider(zen) returned %T, want *HTTPProvider", provider)
|
||||
}
|
||||
if httpProvider.apiBase != "https://opencode.ai/zen/v1" {
|
||||
t.Errorf("apiBase = %q, want %q", httpProvider.apiBase, "https://opencode.ai/zen/v1")
|
||||
}
|
||||
if httpProvider.apiKey != "zen-key" {
|
||||
t.Errorf("apiKey = %q, want %q", httpProvider.apiKey, "zen-key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_ZenByModelPrefix(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Model = "zen/kimi-k2.5-free"
|
||||
cfg.Providers.Zen.APIKey = "zen-key"
|
||||
|
||||
provider, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(zen/*) error = %v", err)
|
||||
}
|
||||
|
||||
httpProvider, ok := provider.(*HTTPProvider)
|
||||
if !ok {
|
||||
t.Fatalf("CreateProvider(zen/*) returned %T, want *HTTPProvider", provider)
|
||||
}
|
||||
if httpProvider.apiBase != "https://opencode.ai/zen/v1" {
|
||||
t.Errorf("apiBase = %q, want %q", httpProvider.apiBase, "https://opencode.ai/zen/v1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_OpencodePrefixDoesNotAutoSelectZen(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Model = "opencode/kimi-k2.5-free"
|
||||
cfg.Providers.Zen.APIKey = "zen-key"
|
||||
|
||||
_, err := CreateProvider(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("CreateProvider(opencode/*) expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_ExplicitProviderNotConfiguredFails(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Provider = "zen"
|
||||
cfg.Agents.Defaults.Model = "kimi-k2.5-free"
|
||||
|
||||
_, err := CreateProvider(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("CreateProvider(zen explicit without api key) expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "provider 'zen' is not configured") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_ExplicitUnknownProviderFails(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Provider = "unknown-provider"
|
||||
cfg.Agents.Defaults.Model = "gpt-4o"
|
||||
cfg.Providers.OpenRouter.APIKey = "or-key"
|
||||
|
||||
_, err := CreateProvider(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("CreateProvider(unknown provider) expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unknown provider: unknown-provider") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_MyGeminiProxyDoesNotAutoSelectGemini(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Model = "mygeminiproxy-v1"
|
||||
cfg.Providers.Gemini.APIKey = "gem-key"
|
||||
cfg.Providers.OpenRouter.APIKey = "or-key"
|
||||
|
||||
provider, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(ambiguous gemini model) error = %v", err)
|
||||
}
|
||||
|
||||
httpProvider, ok := provider.(*HTTPProvider)
|
||||
if !ok {
|
||||
t.Fatalf("CreateProvider(ambiguous gemini model) returned %T, want *HTTPProvider", provider)
|
||||
}
|
||||
if httpProvider.apiBase != "https://openrouter.ai/api/v1" {
|
||||
t.Errorf("apiBase = %q, want %q", httpProvider.apiBase, "https://openrouter.ai/api/v1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_FallbackOpenRouter(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Model = "custom/non-standard-model"
|
||||
cfg.Providers.OpenRouter.APIKey = "or-key"
|
||||
|
||||
provider, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(default fallback) error = %v", err)
|
||||
}
|
||||
|
||||
httpProvider, ok := provider.(*HTTPProvider)
|
||||
if !ok {
|
||||
t.Fatalf("CreateProvider(default fallback) returned %T, want *HTTPProvider", provider)
|
||||
}
|
||||
if httpProvider.apiBase != "https://openrouter.ai/api/v1" {
|
||||
t.Errorf("apiBase = %q, want %q", httpProvider.apiBase, "https://openrouter.ai/api/v1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_ModelPrefixAvoidsAmbiguousContains(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Model = "myclaudeproxy-v1"
|
||||
cfg.Providers.Anthropic.APIKey = "anthropic-key"
|
||||
cfg.Providers.OpenRouter.APIKey = "or-key"
|
||||
|
||||
provider, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(ambiguous-model) error = %v", err)
|
||||
}
|
||||
|
||||
httpProvider, ok := provider.(*HTTPProvider)
|
||||
if !ok {
|
||||
t.Fatalf("CreateProvider(ambiguous-model) returned %T, want *HTTPProvider", provider)
|
||||
}
|
||||
if httpProvider.apiBase != "https://openrouter.ai/api/v1" {
|
||||
t.Errorf("apiBase = %q, want %q", httpProvider.apiBase, "https://openrouter.ai/api/v1")
|
||||
}
|
||||
}
|
||||
9
pkg/providers/zen_provider.go
Normal file
9
pkg/providers/zen_provider.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package providers
|
||||
|
||||
func init() {
|
||||
RegisterProvider(providerRegistration{
|
||||
Name: "zen",
|
||||
ModelPrefixes: []string{"zen/"},
|
||||
Creator: zenCreator,
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue