feat(model): 双协议 LLM Provider(AnthropicChNativeAdapter /v1/messages + tool_use.input→Args 直接映射 + 流式重组)+ Provider 感知工厂/路由 + LLM_PROVIDER/anthropic 配置(OpenAI 兼容保留)
This commit is contained in:
1 parent
f9650185f1
commit
3632e04733
11 files changed
+1452
-18
No files matched your search
@@ -0,0 +1,199 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user