- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
`chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
`rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
200 lines
6.4 KiB
Go
200 lines
6.4 KiB
Go
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 用两个 Anthropic 适配器验证
|
|
// 降级链:主步骤失败、备用步骤成功,证明该
|
|
// 适配器能在 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)
|
|
}
|
|
}
|