package builtin import ( "encoding/json" "reflect" "strings" "testing" ) func TestRequestReasoning(t *testing.T) { t.Run("anthropic assistant thinking becomes reasoning part", func(t *testing.T) { req := anthropicMessagesRequest{ Model: "claude-3-5-sonnet", Messages: []anthropicMessageContent{{ Role: "assistant", Content: []any{ map[string]any{"type": "thinking", "thinking": "step by step"}, map[string]any{"type": "redacted_thinking", "data": "enc_1"}, map[string]any{"type": "text", "text": "hello"}, }, }}, } conv, err := normalizeAnthropicConversation(req) if err != nil { t.Fatalf("normalizeAnthropicConversation failed: %v", err) } if len(conv.Turns) != 1 || len(conv.Turns[0].Parts) != 3 { t.Fatalf("unexpected turns: %+v", conv.Turns) } if got := conv.Turns[0].Parts[0].Kind; got != "reasoning" { t.Fatalf("expected first part to be reasoning, got %q", got) } reasoning := reflect.ValueOf(conv.Turns[0].Parts[0]).FieldByName("Reasoning") if !reasoning.IsValid() || reasoning.IsNil() { t.Fatalf("expected reasoning payload on first part, got %+v", conv.Turns[0].Parts[0]) } if got := reasoning.Elem().FieldByName("Subtype"); !got.IsValid() || got.String() != "thinking" { t.Fatalf("expected thinking subtype, got %+v", reasoning.Interface()) } redacted := reflect.ValueOf(conv.Turns[0].Parts[1]).FieldByName("Reasoning") if !redacted.IsValid() || redacted.IsNil() { t.Fatalf("expected reasoning payload on second part, got %+v", conv.Turns[0].Parts[1]) } if got := redacted.Elem().FieldByName("Subtype"); !got.IsValid() || got.String() != "redacted_thinking" { t.Fatalf("expected redacted_thinking subtype, got %+v", redacted.Interface()) } }) t.Run("openai request flattens thinking into reasoning_content string only", func(t *testing.T) { conv := conversation{ Turns: []conversationTurn{{ Role: "assistant", Parts: []conversationPart{ newReasoningPart("thinking", "first thought", "", ""), newReasoningPart("redacted_thinking", "", "", "enc_blob"), newReasoningPart("thinking", "second thought", "sig_xyz", ""), {Kind: partKindText, Text: "final answer"}, }, }}, } raw, err := encodeOpenAIRequest("gpt-x", conv, false) if err != nil { t.Fatalf("encodeOpenAIRequest failed: %v", err) } body := string(raw) if strings.Contains(body, `"reasoning":[`) { t.Fatalf("openai request must not emit reasoning array, got: %s", body) } if strings.Contains(body, "redacted_thinking") || strings.Contains(body, "enc_blob") || strings.Contains(body, "sig_xyz") { t.Fatalf("openai request must drop redacted_thinking and signatures, got: %s", body) } if !strings.Contains(body, `"reasoning_content":"first thought\n\nsecond thought"`) { t.Fatalf("expected reasoning_content to join thinking texts with blank line, got: %s", body) } if !strings.Contains(body, `"final answer"`) { t.Fatalf("expected sibling text preserved, got: %s", body) } }) t.Run("gemini request drops reasoning but keeps sibling text and tool content", func(t *testing.T) { conv := conversation{ Turns: []conversationTurn{{ Role: "assistant", Parts: []conversationPart{ {Kind: "reasoning"}, {Kind: partKindText, Text: "public"}, {Kind: partKindToolCall, ToolCall: &conversationToolCall{ ID: "call_1", Name: "lookup", Arguments: json.RawMessage(`{"q":"go"}`), }}, }, }}, } raw, err := encodeGeminiRequest(conv) if err != nil { t.Fatalf("encodeGeminiRequest failed: %v", err) } body := string(raw) if !strings.Contains(body, `"public"`) || !strings.Contains(body, `"lookup"`) { t.Fatalf("unexpected gemini request body: %s", body) } if strings.Contains(body, `"reasoning"`) { t.Fatalf("expected gemini request to drop reasoning content, got %s", body) } }) }