package model import ( "context" "errors" "io" "net/http" "net/http/httptest" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" ) func TestNewChatAdapter_Provider(t *testing.T) { t.Parallel() cases := []struct { name string provider string wantType any wantErr bool }{ {"openai", ProviderOpenAI, (*OpenAIChatAdapter)(nil), false}, {"empty defaults to openai", "", (*OpenAIChatAdapter)(nil), false}, {"anthropic", ProviderAnthropic, (*AnthropicChatAdapter)(nil), false}, {"unknown", "gemini", nil, true}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() ad, err := NewChatAdapter(config.LLMConfig{Provider: c.provider, Model: "m", AnthropicModel: "m"}) if c.wantErr { if !errors.Is(err, ErrUnknownProvider) { t.Fatalf("err = %v, want ErrUnknownProvider", err) } if ad != nil { t.Fatalf("adapter = %v, want nil on error", ad) } return } if err != nil { t.Fatalf("NewChatAdapter error: %v", err) } switch c.wantType.(type) { case *OpenAIChatAdapter: if _, ok := ad.(*OpenAIChatAdapter); !ok { t.Fatalf("adapter = %T, want *OpenAIChatAdapter", ad) } case *AnthropicChatAdapter: if _, ok := ad.(*AnthropicChatAdapter); !ok { t.Fatalf("adapter = %T, want *AnthropicChatAdapter", ad) } } }) } } func TestNewChatChain_Provider(t *testing.T) { t.Parallel() t.Run("openai primary and fallback", func(t *testing.T) { t.Parallel() chain, err := NewChatChain(config.LLMConfig{Provider: ProviderOpenAI, BaseURL: "http://x", Model: "m", Fallback: "fb"}) if err != nil { t.Fatalf("NewChatChain error: %v", err) } if len(chain.steps) != 2 { t.Fatalf("steps = %d, want 2", len(chain.steps)) } if chain.steps[0].Model != "m" || chain.steps[1].Model != "fb" { t.Errorf("step models = [%s %s], want [m fb]", chain.steps[0].Model, chain.steps[1].Model) } if _, ok := chain.steps[0].Adapter.(*OpenAIChatAdapter); !ok { t.Errorf("step[0] adapter = %T, want *OpenAIChatAdapter", chain.steps[0].Adapter) } }) t.Run("anthropic primary and fallback", func(t *testing.T) { t.Parallel() chain, err := NewChatChain(config.LLMConfig{ Provider: ProviderAnthropic, AnthropicBaseURL: "http://x", AnthropicModel: "claude-main", AnthropicFallback: "claude-fb", }) if err != nil { t.Fatalf("NewChatChain error: %v", err) } if len(chain.steps) != 2 { t.Fatalf("steps = %d, want 2", len(chain.steps)) } if chain.steps[0].Model != "claude-main" || chain.steps[1].Model != "claude-fb" { t.Errorf("step models = [%s %s], want [claude-main claude-fb]", chain.steps[0].Model, chain.steps[1].Model) } if _, ok := chain.steps[0].Adapter.(*AnthropicChatAdapter); !ok { t.Errorf("step[0] adapter = %T, want *AnthropicChatAdapter", chain.steps[0].Adapter) } }) t.Run("missing model returns ErrNoModel", func(t *testing.T) { t.Parallel() if _, err := NewChatChain(config.LLMConfig{Provider: ProviderOpenAI, BaseURL: "http://x"}); !errors.Is(err, ErrNoModel) { t.Fatalf("openai err = %v, want ErrNoModel", err) } if _, err := NewChatChain(config.LLMConfig{Provider: ProviderAnthropic, AnthropicBaseURL: "http://x"}); !errors.Is(err, ErrNoModel) { t.Fatalf("anthropic err = %v, want ErrNoModel", err) } }) t.Run("unknown provider returns ErrUnknownProvider", func(t *testing.T) { t.Parallel() if _, err := NewChatChain(config.LLMConfig{Provider: "gemini"}); !errors.Is(err, ErrUnknownProvider) { t.Fatalf("err = %v, want ErrUnknownProvider", err) } }) } func TestNewOpenAIChatChain_StillWorks(t *testing.T) { t.Parallel() chain, err := NewOpenAIChatChain(config.LLMConfig{BaseURL: "http://x", Model: "m", Fallback: "fb"}) if err != nil { t.Fatalf("NewOpenAIChatChain error: %v", err) } if len(chain.steps) != 2 { t.Fatalf("steps = %d, want 2", len(chain.steps)) } if _, ok := chain.steps[0].Adapter.(*OpenAIChatAdapter); !ok { t.Errorf("step[0] adapter = %T, want *OpenAIChatAdapter", chain.steps[0].Adapter) } } func TestDefaultModelForProvider(t *testing.T) { t.Parallel() if got := DefaultModelForProvider(config.LLMConfig{Provider: ProviderOpenAI, Model: "gpt"}); got != "gpt" { t.Errorf("openai = %q, want gpt", got) } if got := DefaultModelForProvider(config.LLMConfig{Provider: ProviderAnthropic, AnthropicModel: "claude"}); got != "claude" { t.Errorf("anthropic = %q, want claude", got) } } func TestRouter_RouteProvider(t *testing.T) { t.Parallel() r := NewRouter("default", WithDefaultProvider(ProviderAnthropic), WithRoleProvider(Role("engineer"), ProviderOpenAI), ) cases := []struct { role Role want string }{ {Role("engineer"), ProviderOpenAI}, {Role("user"), ProviderAnthropic}, {Role("assistant"), ProviderAnthropic}, } for _, c := range cases { c := c t.Run(string(c.role), func(t *testing.T) { t.Parallel() if got := r.RouteProvider(c.role); got != c.want { t.Fatalf("RouteProvider(%q) = %q, want %q", c.role, got, c.want) } }) } } // TestChain_AnthropicFallback exercises the fallback chain against two Anthropic // adapters: the primary step fails and the backup step succeeds, proving the // adapter composes cleanly inside a Chain. func TestChain_AnthropicFallback(t *testing.T) { t.Parallel() primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = io.WriteString(w, `{"error":"down"}`) })) t.Cleanup(primary.Close) backup := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = io.WriteString(w, `{"id":"msg_b","model":"backup","content":[{"type":"text","text":"Hello!"}],"usage":{"input_tokens":5,"output_tokens":2}}`) })) t.Cleanup(backup.Close) chain := NewChain( Step{Model: "main", Adapter: NewAnthropicChat(config.LLMConfig{AnthropicBaseURL: primary.URL, AnthropicModel: "main"})}, Step{Model: "backup", Adapter: NewAnthropicChat(config.LLMConfig{AnthropicBaseURL: backup.URL, AnthropicModel: "backup"})}, ) resp, err := chain.Chat(context.Background(), Request{Messages: []Message{TextMessage(RoleUser, "hi")}, Purpose: "chat"}, Options{}) if err != nil { t.Fatalf("Chain.Chat error: %v", err) } if resp.Content != "Hello!" { t.Errorf("resp.Content = %q, want Hello!", resp.Content) } if resp.Model != "backup" { t.Errorf("resp.Model = %q, want backup", resp.Model) } }