refactor provider resolution with registry and add gateway install

This commit is contained in:
Henrique Costa 2026-02-12 16:33:19 -03:00
parent d83fb6e081
commit f660143ddd
8 changed files with 538 additions and 184 deletions

View file

@ -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 {

View file

@ -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": {

View file

@ -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",

View file

@ -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)
}

View 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)
}

View 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,
})
}

View 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")
}
}

View file

@ -0,0 +1,9 @@
package providers
func init() {
RegisterProvider(providerRegistration{
Name: "zen",
ModelPrefixes: []string{"zen/"},
Creator: zenCreator,
})
}