// PicoClaw - Ultra-lightweight personal AI agent // License: MIT // // Copyright (c) 2026 PicoClaw contributors package providers import ( "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/sipeed/picoclaw/pkg/config" ) func TestExtractProtocol(t *testing.T) { tests := []struct { name string model string wantProtocol string wantModelID string }{ { name: "openai with prefix", model: "openai/gpt-4o", wantProtocol: "openai", wantModelID: "gpt-4o", }, { name: "anthropic with prefix", model: "anthropic/claude-sonnet-4.6", wantProtocol: "anthropic", wantModelID: "claude-sonnet-4.6", }, { name: "no prefix - defaults to openai", model: "gpt-4o", wantProtocol: "openai", wantModelID: "gpt-4o", }, { name: "groq with prefix", model: "groq/llama-3.1-70b", wantProtocol: "groq", wantModelID: "llama-3.1-70b", }, { name: "empty string", model: "", wantProtocol: "openai", wantModelID: "", }, { name: "with whitespace", model: " openai/gpt-4 ", wantProtocol: "openai", wantModelID: "gpt-4", }, { name: "multiple slashes", model: "nvidia/meta/llama-3.1-8b", wantProtocol: "nvidia", wantModelID: "meta/llama-3.1-8b", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { protocol, modelID := ExtractProtocol(tt.model) if protocol != tt.wantProtocol { t.Errorf("ExtractProtocol(%q) protocol = %q, want %q", tt.model, protocol, tt.wantProtocol) } if modelID != tt.wantModelID { t.Errorf("ExtractProtocol(%q) modelID = %q, want %q", tt.model, modelID, tt.wantModelID) } }) } } func TestCreateProviderFromConfig_OpenAI(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-openai", Model: "openai/gpt-4o", APIKey: "test-key", APIBase: "https://api.example.com/v1", } provider, modelID, err := CreateProviderFromConfig(cfg) if err != nil { t.Fatalf("CreateProviderFromConfig() error = %v", err) } if provider == nil { t.Fatal("CreateProviderFromConfig() returned nil provider") } if modelID != "gpt-4o" { t.Errorf("modelID = %q, want %q", modelID, "gpt-4o") } } func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) { tests := []struct { name string protocol string }{ {"openai", "openai"}, {"groq", "groq"}, {"openrouter", "openrouter"}, {"cerebras", "cerebras"}, {"qwen", "qwen"}, {"vllm", "vllm"}, {"deepseek", "deepseek"}, {"ollama", "ollama"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-" + tt.protocol, Model: tt.protocol + "/test-model", APIKey: "test-key", } provider, _, err := CreateProviderFromConfig(cfg) if err != nil { t.Fatalf("CreateProviderFromConfig() error = %v", err) } // Verify we got an HTTPProvider for all these protocols if _, ok := provider.(*HTTPProvider); !ok { t.Fatalf("expected *HTTPProvider, got %T", provider) } }) } } func TestGetDefaultAPIBase_LiteLLM(t *testing.T) { if got := getDefaultAPIBase("litellm"); got != "http://localhost:4000/v1" { t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "litellm", got, "http://localhost:4000/v1") } } func TestCreateProviderFromConfig_LiteLLM(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-litellm", Model: "litellm/my-proxy-alias", APIKey: "test-key", APIBase: "http://localhost:4000/v1", } provider, modelID, err := CreateProviderFromConfig(cfg) if err != nil { t.Fatalf("CreateProviderFromConfig() error = %v", err) } if provider == nil { t.Fatal("CreateProviderFromConfig() returned nil provider") } if modelID != "my-proxy-alias" { t.Errorf("modelID = %q, want %q", modelID, "my-proxy-alias") } } func TestCreateProviderFromConfig_Anthropic(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-anthropic", Model: "anthropic/claude-sonnet-4.6", APIKey: "test-key", } provider, modelID, err := CreateProviderFromConfig(cfg) if err != nil { t.Fatalf("CreateProviderFromConfig() error = %v", err) } if provider == nil { t.Fatal("CreateProviderFromConfig() returned nil provider") } if modelID != "claude-sonnet-4.6" { t.Errorf("modelID = %q, want %q", modelID, "claude-sonnet-4.6") } } func TestCreateProviderFromConfig_Antigravity(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-antigravity", Model: "antigravity/gemini-2.0-flash", } provider, modelID, err := CreateProviderFromConfig(cfg) if err != nil { t.Fatalf("CreateProviderFromConfig() error = %v", err) } if provider == nil { t.Fatal("CreateProviderFromConfig() returned nil provider") } if modelID != "gemini-2.0-flash" { t.Errorf("modelID = %q, want %q", modelID, "gemini-2.0-flash") } } func TestCreateProviderFromConfig_ClaudeCLI(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-claude-cli", Model: "claude-cli/claude-sonnet-4.6", } provider, modelID, err := CreateProviderFromConfig(cfg) if err != nil { t.Fatalf("CreateProviderFromConfig() error = %v", err) } if provider == nil { t.Fatal("CreateProviderFromConfig() returned nil provider") } if modelID != "claude-sonnet-4.6" { t.Errorf("modelID = %q, want %q", modelID, "claude-sonnet-4.6") } } func TestCreateProviderFromConfig_CodexCLI(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-codex-cli", Model: "codex-cli/codex", } provider, modelID, err := CreateProviderFromConfig(cfg) if err != nil { t.Fatalf("CreateProviderFromConfig() error = %v", err) } if provider == nil { t.Fatal("CreateProviderFromConfig() returned nil provider") } if modelID != "codex" { t.Errorf("modelID = %q, want %q", modelID, "codex") } } func TestCreateProviderFromConfig_MissingAPIKey(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-no-key", Model: "openai/gpt-4o", } _, _, err := CreateProviderFromConfig(cfg) if err == nil { t.Fatal("CreateProviderFromConfig() expected error for missing API key") } } func TestCreateProviderFromConfig_UnknownProtocol(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-unknown", Model: "unknown-protocol/model", APIKey: "test-key", } _, _, err := CreateProviderFromConfig(cfg) if err == nil { t.Fatal("CreateProviderFromConfig() expected error for unknown protocol") } } func TestCreateProviderFromConfig_NilConfig(t *testing.T) { _, _, err := CreateProviderFromConfig(nil) if err == nil { t.Fatal("CreateProviderFromConfig(nil) expected error") } } func TestCreateProviderFromConfig_EmptyModel(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-empty", Model: "", } _, _, err := CreateProviderFromConfig(cfg) if err == nil { t.Fatal("CreateProviderFromConfig() expected error for empty model") } } func TestCreateProviderFromConfig_RequestTimeoutPropagation(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(1500 * time.Millisecond) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"},"finish_reason":"stop"}]}`)) })) defer server.Close() cfg := &config.ModelConfig{ ModelName: "test-timeout", Model: "openai/gpt-4o", APIBase: server.URL, RequestTimeout: 1, } provider, modelID, err := CreateProviderFromConfig(cfg) if err != nil { t.Fatalf("CreateProviderFromConfig() error = %v", err) } if modelID != "gpt-4o" { t.Fatalf("modelID = %q, want %q", modelID, "gpt-4o") } _, err = provider.Chat( t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, modelID, nil, ) if err == nil { t.Fatal("Chat() expected timeout error, got nil") } errMsg := err.Error() if !strings.Contains(errMsg, "context deadline exceeded") && !strings.Contains(errMsg, "Client.Timeout exceeded") { t.Fatalf("Chat() error = %q, want timeout-related error", errMsg) } }