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 TestDecodeTools(t *testing.T) { body := []byte(`{ "model": "claude-3", "max_tokens": 1024, "messages": [{"role": "user", "content": "hi"}], "tools": [ {"name": "search", "description": "Search", "input_schema": {"type":"object"}}, {"name": "calc", "input_schema": {"type":"object"}} ] }`) req, err := decodeRequest(body) require.NoError(t, err) assert.Len(t, req.Tools, 2) assert.Equal(t, "search", req.Tools[0].Name) assert.Equal(t, "Search", req.Tools[0].Description) assert.Equal(t, "calc", req.Tools[1].Name) } func TestDecodeToolChoice(t *testing.T) { tests := []struct { name string jsonBody string wantType string wantName string }{ { "auto string", `{"model":"claude-3","max_tokens":1024,"messages":[{"role":"user","content":"hi"}],"tool_choice":"auto"}`, "auto", "", }, { "none string", `{"model":"claude-3","max_tokens":1024,"messages":[{"role":"user","content":"hi"}],"tool_choice":"none"}`, "none", "", }, { "any string", `{"model":"claude-3","max_tokens":1024,"messages":[{"role":"user","content":"hi"}],"tool_choice":"any"}`, "any", "", }, { "tool object", `{"model":"claude-3","max_tokens":1024,"messages":[{"role":"user","content":"hi"}],"tool_choice":{"type":"tool","name":"search"}}`, "tool", "search", }, { "auto object", `{"model":"claude-3","max_tokens":1024,"messages":[{"role":"user","content":"hi"}],"tool_choice":{"type":"auto"}}`, "auto", "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req, err := decodeRequest([]byte(tt.jsonBody)) require.NoError(t, err) require.NotNil(t, req.ToolChoice) assert.Equal(t, tt.wantType, req.ToolChoice.Type) assert.Equal(t, tt.wantName, req.ToolChoice.Name) }) } } func TestDecodeParameters_TopK(t *testing.T) { topK := 10 body := []byte(`{ "model": "claude-3", "max_tokens": 1024, "messages": [{"role": "user", "content": "hi"}], "top_k": 10, "stop_sequences": ["STOP"] }`) req, err := decodeRequest(body) require.NoError(t, err) require.NotNil(t, req.Parameters.TopK) assert.Equal(t, topK, *req.Parameters.TopK) assert.Equal(t, []string{"STOP"}, req.Parameters.StopSequences) } func TestDecodeRequest_MetadataUserID(t *testing.T) { body := []byte(`{ "model": "claude-3", "max_tokens": 1024, "messages": [{"role": "user", "content": "hi"}], "metadata": {"user_id": "user-123"} }`) req, err := decodeRequest(body) require.NoError(t, err) assert.Equal(t, "user-123", req.UserID) } func TestDecodeSystem_Empty(t *testing.T) { body := []byte(`{ "model": "claude-3", "max_tokens": 1024, "system": "", "messages": [{"role": "user", "content": "hi"}] }`) req, err := decodeRequest(body) require.NoError(t, err) assert.Nil(t, req.System) } func TestDecodeSystem_Nil(t *testing.T) { body := []byte(`{ "model": "claude-3", "max_tokens": 1024, "messages": [{"role": "user", "content": "hi"}] }`) req, err := decodeRequest(body) require.NoError(t, err) assert.Nil(t, req.System) } func TestDecodeThinking_WithEffort(t *testing.T) { body := []byte(`{ "model": "claude-3", "max_tokens": 1024, "messages": [{"role": "user", "content": "hi"}], "thinking": {"type": "enabled", "budget_tokens": 5000}, "output_config": {"effort": "high"} }`) req, err := decodeRequest(body) require.NoError(t, err) require.NotNil(t, req.Thinking) assert.Equal(t, "enabled", req.Thinking.Type) assert.Equal(t, "high", req.Thinking.Effort) } func TestDecodeOutputFormat_NilOutputConfig(t *testing.T) { body := []byte(`{ "model": "claude-3", "max_tokens": 1024, "messages": [{"role": "user", "content": "hi"}] }`) req, err := decodeRequest(body) require.NoError(t, err) assert.Nil(t, req.OutputFormat) } func TestDecodeMessage_UserWithOnlyToolResults(t *testing.T) { body := []byte(`{ "model": "claude-3", "max_tokens": 1024, "messages": [ {"role": "user", "content": "hi"}, {"role": "assistant", "content": [{"type": "tool_use", "id": "t1", "name": "fn", "input": {}}]}, { "role": "user", "content": [{"type": "tool_result", "tool_use_id": "t1", "content": "result"}] } ] }`) req, err := decodeRequest(body) require.NoError(t, err) lastMsg := req.Messages[len(req.Messages)-1] assert.Equal(t, canonical.RoleTool, lastMsg.Role) assert.Equal(t, "t1", lastMsg.Content[0].ToolUseID) } func TestDecodeContentBlocks_Nil(t *testing.T) { blocks := decodeContentBlocks(nil) assert.Len(t, blocks, 1) assert.Equal(t, "", blocks[0].Text) } func TestDecodeContentBlocks_String(t *testing.T) { blocks := decodeContentBlocks("hello") assert.Len(t, blocks, 1) assert.Equal(t, "hello", blocks[0].Text) } func TestParseTimestamp(t *testing.T) { tests := []struct { name string input string want int64 }{ {"valid RFC3339", "2024-01-15T00:00:00Z", time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC).Unix()}, {"empty", "", 0}, {"invalid", "not-a-date", 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, parseTimestamp(tt.input)) }) } } func TestEncodeToolChoice(t *testing.T) { tests := []struct { name string choice *canonical.ToolChoice want map[string]any }{ {"auto", canonical.NewToolChoiceAuto(), map[string]any{"type": "auto"}}, {"none", canonical.NewToolChoiceNone(), map[string]any{"type": "none"}}, {"any", canonical.NewToolChoiceAny(), map[string]any{"type": "any"}}, {"tool", canonical.NewToolChoiceNamed("search"), map[string]any{"type": "tool", "name": "search"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := encodeToolChoice(tt.choice) assert.Equal(t, tt.want["type"], result.(map[string]any)["type"]) assert.Equal(t, tt.want["name"], result.(map[string]any)["name"]) }) } } func TestEncodeThinkingConfig(t *testing.T) { budget := 5000 tests := []struct { name string cfg *canonical.ThinkingConfig want map[string]any }{ {"enabled", &canonical.ThinkingConfig{Type: "enabled", BudgetTokens: &budget}, map[string]any{"type": "enabled", "budget_tokens": float64(5000)}}, {"disabled", &canonical.ThinkingConfig{Type: "disabled"}, map[string]any{"type": "disabled"}}, {"adaptive", &canonical.ThinkingConfig{Type: "adaptive"}, map[string]any{"type": "adaptive"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := encodeThinkingConfig(tt.cfg) assert.Equal(t, tt.want["type"], result["type"]) }) } } func TestEncodeRequest_PublicFields(t *testing.T) { maxTokens := 1024 parallel := false req := &canonical.CanonicalRequest{ Model: "claude-3", Parameters: canonical.RequestParameters{MaxTokens: &maxTokens}, Messages: []canonical.CanonicalMessage{{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("hi")}}}, UserID: "user-123", ParallelToolUse: ¶llel, } 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)) assert.Equal(t, map[string]any{"user_id": "user-123"}, result["metadata"]) assert.Equal(t, true, result["disable_parallel_tool_use"]) } func TestEncodeRequest_DefaultMaxTokens(t *testing.T) { req := &canonical.CanonicalRequest{ Model: "claude-3", 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)) assert.Equal(t, float64(4096), result["max_tokens"]) } func TestEncodeRequest_TopK(t *testing.T) { maxTokens := 1024 topK := 10 req := &canonical.CanonicalRequest{ Model: "claude-3", Parameters: canonical.RequestParameters{MaxTokens: &maxTokens, TopK: &topK}, 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)) assert.Equal(t, float64(10), result["top_k"]) } func TestEncodeRequest_WithTools(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")}}}, Tools: []canonical.CanonicalTool{ {Name: "search", Description: "Search things", InputSchema: json.RawMessage(`{"type":"object"}`)}, }, ToolChoice: canonical.NewToolChoiceAuto(), } 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)) tools := result["tools"].([]any) assert.Len(t, tools, 1) tool := tools[0].(map[string]any) assert.Equal(t, "search", tool["name"]) assert.Equal(t, "Search things", tool["description"]) tc := result["tool_choice"].(map[string]any) assert.Equal(t, "auto", tc["type"]) } func TestEncodeRequest_ThinkingWithEffort(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: "enabled", Effort: "high"}, } 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) assert.Equal(t, "high", oc["effort"]) } func TestEncodeResponse_UsageWithCacheAndCreation(t *testing.T) { cacheRead := 30 cacheCreation := 10 sr := canonical.StopReasonEndTurn resp := &canonical.CanonicalResponse{ ID: "msg-1", Model: "claude-3", Content: []canonical.ContentBlock{canonical.NewTextBlock("ok")}, StopReason: &sr, Usage: canonical.CanonicalUsage{ InputTokens: 100, OutputTokens: 50, CacheReadTokens: &cacheRead, CacheCreationTokens: &cacheCreation, }, } 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) assert.Equal(t, float64(100), usage["input_tokens"]) assert.Equal(t, float64(30), usage["cache_read_input_tokens"]) assert.Equal(t, float64(10), usage["cache_creation_input_tokens"]) } func TestEncodeResponse_StopReasons(t *testing.T) { tests := []struct { name string stopReason canonical.StopReason want string }{ {"end_turn", canonical.StopReasonEndTurn, "end_turn"}, {"max_tokens", canonical.StopReasonMaxTokens, "max_tokens"}, {"tool_use", canonical.StopReasonToolUse, "tool_use"}, {"stop_sequence", canonical.StopReasonStopSequence, "stop_sequence"}, {"refusal", canonical.StopReasonRefusal, "refusal"}, {"content_filter→end_turn", canonical.StopReasonContentFilter, "end_turn"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sr := tt.stopReason resp := &canonical.CanonicalResponse{ ID: "r1", Model: "claude-3", Content: []canonical.ContentBlock{canonical.NewTextBlock("ok")}, 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, tt.want, result["stop_reason"]) }) } } func TestEncodeSystem_SystemBlocks(t *testing.T) { result := encodeSystem([]canonical.SystemBlock{{Text: "part1"}, {Text: "part2"}}) blocks, ok := result.([]map[string]any) require.True(t, ok) assert.Len(t, blocks, 2) assert.Equal(t, "part1", blocks[0]["text"]) } func TestEncodeModelInfoResponse(t *testing.T) { info := &canonical.CanonicalModelInfo{ ID: "claude-3-opus", Name: "Claude 3 Opus", Created: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC).Unix(), } body, err := encodeModelInfoResponse(info) require.NoError(t, err) var result map[string]any require.NoError(t, json.Unmarshal(body, &result)) assert.Equal(t, "claude-3-opus", result["id"]) assert.Equal(t, "Claude 3 Opus", result["display_name"]) } func TestDecodeModelInfoResponse(t *testing.T) { body := []byte(`{"id":"claude-3-opus","type":"model","display_name":"Claude 3 Opus","created_at":"2024-01-15T00:00:00Z"}`) info, err := decodeModelInfoResponse(body) require.NoError(t, err) assert.Equal(t, "claude-3-opus", info.ID) assert.Equal(t, "Claude 3 Opus", info.Name) assert.NotEqual(t, int64(0), info.Created) } func TestDecodeResponse_PauseTurn(t *testing.T) { body := []byte(`{ "id": "msg-1", "type": "message", "role": "assistant", "model": "claude-3", "content": [{"type": "text", "text": "ok"}], "stop_reason": "pause_turn", "usage": {"input_tokens": 1, "output_tokens": 1} }`) resp, err := decodeResponse(body) require.NoError(t, err) assert.Equal(t, canonical.StopReason("pause_turn"), *resp.StopReason) } func TestEncodeResponse_NoStopReason(t *testing.T) { resp := &canonical.CanonicalResponse{ ID: "msg-1", Model: "claude-3", Content: []canonical.ContentBlock{canonical.NewTextBlock("ok")}, } 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 TestDecodeRequest_MaxTokensZero(t *testing.T) { body := []byte(`{ "model": "claude-3", "max_tokens": 0, "messages": [{"role": "user", "content": "hi"}] }`) req, err := decodeRequest(body) require.NoError(t, err) assert.Nil(t, req.Parameters.MaxTokens) }