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"]) }