1
0
Files
nex/backend/internal/conversion/anthropic/encoder_test.go
lanyuanxiaoyao 1dac347d3b refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包
引入 Canonical Model 和 ProtocolAdapter 架构,支持 OpenAI/Anthropic 协议间
无缝转换,统一 ProxyHandler 替代分散的 OpenAI/Anthropic Handler,简化
ProviderClient 为协议无关的 HTTP 发送器,Provider 新增 protocol 字段。
2026-04-20 00:36:27 +08:00

351 lines
11 KiB
Go

package anthropic
import (
"encoding/json"
"testing"
"time"
"nex/backend/internal/conversion"
"nex/backend/internal/conversion/canonical"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEncodeRequest_Basic(t *testing.T) {
maxTokens := 1024
req := &canonical.CanonicalRequest{
Model: "claude-3",
Stream: true,
Parameters: canonical.RequestParameters{MaxTokens: &maxTokens},
Messages: []canonical.CanonicalMessage{
{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("hi")}},
},
}
provider := conversion.NewTargetProvider("", "key", "my-model")
body, err := encodeRequest(req, provider)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
assert.Equal(t, "my-model", result["model"])
assert.Equal(t, true, result["stream"])
assert.Equal(t, float64(1024), result["max_tokens"])
msgs := result["messages"].([]any)
assert.Len(t, msgs, 1)
}
func TestEncodeRequest_ToolMergeIntoUser(t *testing.T) {
maxTokens := 1024
req := &canonical.CanonicalRequest{
Model: "claude-3",
Parameters: canonical.RequestParameters{MaxTokens: &maxTokens},
Messages: []canonical.CanonicalMessage{
{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("查询")}},
{Role: canonical.RoleAssistant, Content: []canonical.ContentBlock{canonical.NewToolUseBlock("tool_1", "search", json.RawMessage(`{"q":"test"}`))}},
{Role: canonical.RoleTool, Content: []canonical.ContentBlock{canonical.NewToolResultBlock("tool_1", "结果", false)}},
},
}
provider := conversion.NewTargetProvider("", "key", "model")
body, err := encodeRequest(req, provider)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
msgs := result["messages"].([]any)
// tool 消息应被合并到相邻 user 消息
foundToolResult := false
for _, m := range msgs {
msgMap := m.(map[string]any)
if msgMap["role"] == "user" {
content, ok := msgMap["content"].([]any)
if ok {
for _, c := range content {
block := c.(map[string]any)
if block["type"] == "tool_result" {
foundToolResult = true
}
}
}
}
}
assert.True(t, foundToolResult)
}
func TestEncodeRequest_FirstUserGuarantee(t *testing.T) {
maxTokens := 1024
req := &canonical.CanonicalRequest{
Model: "claude-3",
Parameters: canonical.RequestParameters{MaxTokens: &maxTokens},
Messages: []canonical.CanonicalMessage{
{Role: canonical.RoleAssistant, Content: []canonical.ContentBlock{canonical.NewTextBlock("前置")}},
{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("hi")}},
},
}
provider := conversion.NewTargetProvider("", "key", "model")
body, err := encodeRequest(req, provider)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
msgs := result["messages"].([]any)
firstMsg := msgs[0].(map[string]any)
assert.Equal(t, "user", firstMsg["role"])
}
func TestEncodeRequest_ThinkingEnabled(t *testing.T) {
budget := 10000
maxTokens := 8096
req := &canonical.CanonicalRequest{
Model: "claude-3",
Parameters: canonical.RequestParameters{MaxTokens: &maxTokens},
Messages: []canonical.CanonicalMessage{{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("hi")}}},
Thinking: &canonical.ThinkingConfig{Type: "enabled", BudgetTokens: &budget},
}
provider := conversion.NewTargetProvider("", "key", "model")
body, err := encodeRequest(req, provider)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
thinking, ok := result["thinking"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "enabled", thinking["type"])
assert.Equal(t, float64(10000), thinking["budget_tokens"])
}
func TestEncodeResponse_Basic(t *testing.T) {
sr := canonical.StopReasonEndTurn
resp := &canonical.CanonicalResponse{
ID: "msg_1",
Model: "claude-3",
Content: []canonical.ContentBlock{canonical.NewTextBlock("你好")},
StopReason: &sr,
Usage: canonical.CanonicalUsage{InputTokens: 10, OutputTokens: 5},
}
body, err := encodeResponse(resp)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
assert.Equal(t, "msg_1", result["id"])
assert.Equal(t, "message", result["type"])
assert.Equal(t, "assistant", result["role"])
assert.Equal(t, "end_turn", result["stop_reason"])
content := result["content"].([]any)
assert.Len(t, content, 1)
block := content[0].(map[string]any)
assert.Equal(t, "text", block["type"])
assert.Equal(t, "你好", block["text"])
}
func TestEncodeModelsResponse(t *testing.T) {
ts := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC).Unix()
list := &canonical.CanonicalModelList{
Models: []canonical.CanonicalModel{
{ID: "claude-3-opus", Name: "Claude 3 Opus", Created: ts, OwnedBy: "anthropic"},
},
}
body, err := encodeModelsResponse(list)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
data := result["data"].([]any)
assert.Len(t, data, 1)
model := data[0].(map[string]any)
assert.Equal(t, "claude-3-opus", model["id"])
// created 应为 RFC3339 格式
createdAt, ok := model["created_at"].(string)
assert.True(t, ok)
assert.Contains(t, createdAt, "2024")
}
func TestEncodeRequest_ThinkingDisabled(t *testing.T) {
maxTokens := 1024
req := &canonical.CanonicalRequest{
Model: "claude-3",
Parameters: canonical.RequestParameters{MaxTokens: &maxTokens},
Messages: []canonical.CanonicalMessage{{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("hi")}}},
}
provider := conversion.NewTargetProvider("", "key", "model")
body, err := encodeRequest(req, provider)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
_, hasThinking := result["thinking"]
assert.False(t, hasThinking)
}
func TestEncodeRequest_ThinkingAdaptive(t *testing.T) {
maxTokens := 1024
req := &canonical.CanonicalRequest{
Model: "claude-3",
Parameters: canonical.RequestParameters{MaxTokens: &maxTokens},
Messages: []canonical.CanonicalMessage{{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("hi")}}},
Thinking: &canonical.ThinkingConfig{Type: "adaptive"},
}
provider := conversion.NewTargetProvider("", "key", "model")
body, err := encodeRequest(req, provider)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
thinking, ok := result["thinking"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "adaptive", thinking["type"])
}
func TestEncodeRequest_OutputFormat_JSONSchema(t *testing.T) {
maxTokens := 1024
schema := json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}}}`)
req := &canonical.CanonicalRequest{
Model: "claude-3",
Parameters: canonical.RequestParameters{MaxTokens: &maxTokens},
Messages: []canonical.CanonicalMessage{{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("hi")}}},
OutputFormat: &canonical.OutputFormat{
Type: "json_schema",
Schema: schema,
},
}
provider := conversion.NewTargetProvider("", "key", "model")
body, err := encodeRequest(req, provider)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
oc, ok := result["output_config"].(map[string]any)
require.True(t, ok)
format, ok := oc["format"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "json_schema", format["type"])
assert.NotNil(t, format["schema"])
}
func TestEncodeRequest_OutputFormat_JSON(t *testing.T) {
maxTokens := 1024
req := &canonical.CanonicalRequest{
Model: "claude-3",
Parameters: canonical.RequestParameters{MaxTokens: &maxTokens},
Messages: []canonical.CanonicalMessage{{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("hi")}}},
OutputFormat: &canonical.OutputFormat{
Type: "json_object",
},
}
provider := conversion.NewTargetProvider("", "key", "model")
body, err := encodeRequest(req, provider)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
oc, ok := result["output_config"].(map[string]any)
require.True(t, ok)
format, ok := oc["format"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "json_schema", format["type"])
schemaMap, ok := format["schema"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "object", schemaMap["type"])
}
func TestEncodeRequest_ConsecutiveRoleMerge(t *testing.T) {
maxTokens := 1024
req := &canonical.CanonicalRequest{
Model: "claude-3",
Parameters: canonical.RequestParameters{MaxTokens: &maxTokens},
Messages: []canonical.CanonicalMessage{
{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("A")}},
{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("B")}},
},
}
provider := conversion.NewTargetProvider("", "key", "model")
body, err := encodeRequest(req, provider)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
msgs := result["messages"].([]any)
assert.Len(t, msgs, 1)
userMsg := msgs[0].(map[string]any)
assert.Equal(t, "user", userMsg["role"])
content := userMsg["content"].([]any)
assert.Len(t, content, 2)
}
func TestEncodeResponse_ContentFilter(t *testing.T) {
sr := canonical.StopReasonContentFilter
resp := &canonical.CanonicalResponse{
ID: "msg-cf",
Model: "claude-3",
Content: []canonical.ContentBlock{canonical.NewTextBlock("hi")},
StopReason: &sr,
}
body, err := encodeResponse(resp)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
assert.Equal(t, "end_turn", result["stop_reason"])
}
func TestEncodeResponse_ReasoningTokens(t *testing.T) {
reasoning := 100
sr := canonical.StopReasonEndTurn
resp := &canonical.CanonicalResponse{
ID: "msg-rt",
Model: "claude-3",
Content: []canonical.ContentBlock{canonical.NewTextBlock("hi")},
StopReason: &sr,
Usage: canonical.CanonicalUsage{InputTokens: 10, OutputTokens: 5, ReasoningTokens: &reasoning},
}
body, err := encodeResponse(resp)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
usage := result["usage"].(map[string]any)
_, hasReasoning := usage["reasoning_tokens"]
assert.False(t, hasReasoning)
}
func TestEncodeResponse_ToolUse(t *testing.T) {
sr := canonical.StopReasonToolUse
input := json.RawMessage(`{"q":"test"}`)
resp := &canonical.CanonicalResponse{
ID: "msg-tool",
Model: "claude-3",
Content: []canonical.ContentBlock{canonical.NewToolUseBlock("tool_1", "search", input)},
StopReason: &sr,
}
body, err := encodeResponse(resp)
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(body, &result))
content := result["content"].([]any)
assert.Len(t, content, 1)
block := content[0].(map[string]any)
assert.Equal(t, "tool_use", block["type"])
assert.Equal(t, "tool_1", block["id"])
assert.Equal(t, "search", block["name"])
}