Files
bifrost/core/providers/anthropic/chat_test.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

753 lines
24 KiB
Go

package anthropic
import (
"encoding/json"
"strings"
"testing"
"github.com/maximhq/bifrost/core/schemas"
)
func TestToAnthropicChatRequest_PreservesPropertyOrder(t *testing.T) {
params := &schemas.ToolFunctionParameters{
Type: "object",
Properties: schemas.NewOrderedMapFromPairs(
schemas.KV("chain_of_thought", schemas.NewOrderedMapFromPairs(
schemas.KV("type", "string"),
schemas.KV("description", "Reasoning steps"),
)),
schemas.KV("answer", schemas.NewOrderedMapFromPairs(
schemas.KV("type", "string"),
schemas.KV("description", "The answer"),
)),
schemas.KV("citations", schemas.NewOrderedMapFromPairs(
schemas.KV("type", "array"),
)),
schemas.KV("is_unanswered", schemas.NewOrderedMapFromPairs(
schemas.KV("type", "boolean"),
)),
),
Required: []string{"answer", "is_unanswered"},
}
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-sonnet-4-20250514",
Input: []schemas.ChatMessage{{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")},
}},
Params: &schemas.ChatParameters{
Tools: []schemas.ChatTool{{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: "AnswerResponseModel",
Description: schemas.Ptr("Extract answer"),
Parameters: params,
},
}},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result, err := ToAnthropicChatRequest(ctx, bifrostReq)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result.Tools) == 0 {
t.Fatal("expected at least one tool")
}
inputSchema := result.Tools[0].InputSchema
if inputSchema == nil {
t.Fatal("expected InputSchema to be non-nil")
}
// CoT: property order preserved
keys := inputSchema.Properties.Keys()
expected := []string{"chain_of_thought", "answer", "citations", "is_unanswered"}
if len(keys) != len(expected) {
t.Fatalf("expected %d properties, got %d: %v", len(expected), len(keys), keys)
}
for i, k := range expected {
if keys[i] != k {
t.Errorf("property %d: expected %q, got %q (full order: %v)", i, k, keys[i], keys)
}
}
}
func TestToAnthropicChatRequest_CachingDeterminism(t *testing.T) {
makeReq := func(props *schemas.OrderedMap) *schemas.BifrostChatRequest {
return &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-sonnet-4-20250514",
Input: []schemas.ChatMessage{{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: new("test")},
}},
Params: &schemas.ChatParameters{
Tools: []schemas.ChatTool{{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: "test",
Parameters: &schemas.ToolFunctionParameters{
Type: "object",
Properties: props,
},
},
}},
},
}
}
// Version A: type before description
propsA := schemas.NewOrderedMapFromPairs(
schemas.KV("reasoning", schemas.NewOrderedMapFromPairs(
schemas.KV("type", "string"),
schemas.KV("description", "Step by step"),
)),
schemas.KV("answer", schemas.NewOrderedMapFromPairs(
schemas.KV("type", "string"),
schemas.KV("description", "Final answer"),
)),
)
// Version B: description before type
propsB := schemas.NewOrderedMapFromPairs(
schemas.KV("reasoning", schemas.NewOrderedMapFromPairs(
schemas.KV("description", "Step by step"),
schemas.KV("type", "string"),
)),
schemas.KV("answer", schemas.NewOrderedMapFromPairs(
schemas.KV("description", "Final answer"),
schemas.KV("type", "string"),
)),
)
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
resultA, err := ToAnthropicChatRequest(ctx, makeReq(propsA))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
resultB, err := ToAnthropicChatRequest(ctx, makeReq(propsB))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
jsonA, err := schemas.Marshal(resultA.Tools[0].InputSchema)
if err != nil {
t.Fatalf("failed to marshal params A: %v", err)
}
jsonB, err := schemas.Marshal(resultB.Tools[0].InputSchema)
if err != nil {
t.Fatalf("failed to marshal params B: %v", err)
}
if string(jsonA) != string(jsonB) {
t.Errorf("caching broken: same schema produced different JSON\nA: %s\nB: %s", jsonA, jsonB)
}
}
func TestToAnthropicChatRequest_NestedProperties_Preserved(t *testing.T) {
params := &schemas.ToolFunctionParameters{
Type: "object",
Properties: schemas.NewOrderedMapFromPairs(
schemas.KV("output", schemas.NewOrderedMapFromPairs(
schemas.KV("type", "object"),
schemas.KV("properties", schemas.NewOrderedMapFromPairs(
schemas.KV("verdict", schemas.NewOrderedMapFromPairs(schemas.KV("type", "string"))),
schemas.KV("score", schemas.NewOrderedMapFromPairs(schemas.KV("type", "number"))),
schemas.KV("explanation", schemas.NewOrderedMapFromPairs(schemas.KV("type", "string"))),
)),
)),
schemas.KV("reasoning", schemas.NewOrderedMapFromPairs(schemas.KV("type", "string"))),
),
}
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-sonnet-4-20250514",
Input: []schemas.ChatMessage{{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")},
}},
Params: &schemas.ChatParameters{
Tools: []schemas.ChatTool{{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: "nested_tool",
Parameters: params,
},
}},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result, err := ToAnthropicChatRequest(ctx, bifrostReq)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result.Tools) == 0 {
t.Fatal("expected at least one tool")
}
inputSchema := result.Tools[0].InputSchema
// CoT: top-level property order preserved
keys := inputSchema.Properties.Keys()
if len(keys) != 2 || keys[0] != "output" || keys[1] != "reasoning" {
t.Errorf("expected top-level property order [output, reasoning], got %v", keys)
}
// CoT: nested property order preserved
output, ok := inputSchema.Properties.Get("output")
if !ok {
t.Fatal("expected output property")
}
outputOM, ok := output.(*schemas.OrderedMap)
if !ok {
t.Fatalf("expected output to be *schemas.OrderedMap, got %T", output)
}
nestedProps, ok := outputOM.Get("properties")
if !ok {
t.Fatal("expected nested properties in output")
}
nestedPropsOM, ok := nestedProps.(*schemas.OrderedMap)
if !ok {
t.Fatalf("expected nested properties to be *schemas.OrderedMap, got %T", nestedProps)
}
nestedKeys := nestedPropsOM.Keys()
if len(nestedKeys) != 3 || nestedKeys[0] != "verdict" || nestedKeys[1] != "score" || nestedKeys[2] != "explanation" {
t.Errorf("expected nested property order [verdict, score, explanation], got %v", nestedKeys)
}
}
// TestToAnthropicChatRequest_ToolInputKeyOrderPreservation verifies that tool_use input
// arguments preserve the client's original key ordering after conversion to Anthropic format.
// This is critical for prompt caching, which relies on exact byte-for-byte prefix matching.
// The test uses multiple parallel tool calls in a single assistant message — each with
// a different key ordering — matching real-world Claude Code usage patterns.
func TestToAnthropicChatRequest_ToolInputKeyOrderPreservation(t *testing.T) {
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-sonnet-4-20250514",
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")},
},
{
// Multiple parallel tool calls with different key orderings per block
Role: schemas.ChatMessageRoleAssistant,
ChatAssistantMessage: &schemas.ChatAssistantMessage{
ToolCalls: []schemas.ChatAssistantMessageToolCall{
{
Index: 0,
Type: schemas.Ptr("function"),
ID: schemas.Ptr("toolu_vrtx_013t7gabfKz98BKpdwrnS6LP"),
Function: schemas.ChatAssistantMessageToolCallFunction{
Name: schemas.Ptr("bash"),
Arguments: `{"description":"Find references to auth_injector quickly","timeout":30000,"command":"grep -r \"auth_injector\" . --include=\"Makefile\" -l 2>/dev/null"}`,
},
},
{
Index: 1,
Type: schemas.Ptr("function"),
ID: schemas.Ptr("toolu_vrtx_01K2kr3wi7M4RriLgE7Kq3vJ"),
Function: schemas.ChatAssistantMessageToolCallFunction{
Name: schemas.Ptr("bash"),
Arguments: `{"command":"git diff main...HEAD --stat","description":"Show diff of commits in branch"}`,
},
},
{
Index: 2,
Type: schemas.Ptr("function"),
ID: schemas.Ptr("toolu_vrtx_01D1mMkcvpfqGrEhkcxUQpGc"),
Function: schemas.ChatAssistantMessageToolCallFunction{
Name: schemas.Ptr("bash"),
Arguments: `{"command":"git log main..HEAD --format=\"%H %s\" | head -20","description":"Show detailed commits in branch"}`,
},
},
},
},
},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result, err := ToAnthropicChatRequest(ctx, bifrostReq)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Collect all tool_use content blocks
var toolUseBlocks []AnthropicContentBlock
for _, msg := range result.Messages {
for _, block := range msg.Content.ContentBlocks {
if block.Type == AnthropicContentBlockTypeToolUse {
toolUseBlocks = append(toolUseBlocks, block)
}
}
}
if len(toolUseBlocks) != 3 {
t.Fatalf("expected 3 tool_use blocks, got %d", len(toolUseBlocks))
}
// Block 0: keys should be description, timeout, command (NOT alphabetical)
json0, _ := json.Marshal(toolUseBlocks[0].Input)
s0 := string(json0)
descIdx0 := strings.Index(s0, `"description"`)
timeIdx0 := strings.Index(s0, `"timeout"`)
cmdIdx0 := strings.Index(s0, `"command"`)
if descIdx0 < 0 || timeIdx0 < 0 || cmdIdx0 < 0 {
t.Fatalf("block 0: missing expected key(s) in: %s", s0)
}
if !(descIdx0 < timeIdx0 && timeIdx0 < cmdIdx0) {
t.Errorf("block 0: key order not preserved, expected description < timeout < command in: %s", s0)
}
// Block 1: keys should be command, description (NOT alphabetical)
json1, _ := json.Marshal(toolUseBlocks[1].Input)
s1 := string(json1)
cmdIdx1 := strings.Index(s1, `"command"`)
descIdx1 := strings.Index(s1, `"description"`)
if cmdIdx1 < 0 || descIdx1 < 0 {
t.Fatalf("block 1: missing expected key(s) in: %s", s1)
}
if !(cmdIdx1 < descIdx1) {
t.Errorf("block 1: key order not preserved, expected command < description in: %s", s1)
}
// Block 2: keys should be command, description (same as block 1)
json2, _ := json.Marshal(toolUseBlocks[2].Input)
s2 := string(json2)
cmdIdx2 := strings.Index(s2, `"command"`)
descIdx2 := strings.Index(s2, `"description"`)
if cmdIdx2 < 0 || descIdx2 < 0 {
t.Fatalf("block 2: missing expected key(s) in: %s", s2)
}
if !(cmdIdx2 < descIdx2) {
t.Errorf("block 2: key order not preserved, expected command < description in: %s", s2)
}
}
func TestToBifrostChatResponse_MultipleTextBlocksWithThinking(t *testing.T) {
thinkingText := "Let me reason step by step about this problem."
textBlock1 := "The answer is 42."
textBlock2 := "Here is why that is the case."
signature := "sig_abc123"
response := &AnthropicMessageResponse{
ID: "msg_test123",
Type: "message",
Role: "assistant",
Model: "claude-opus-4-6-20250514",
Content: []AnthropicContentBlock{
{
Type: AnthropicContentBlockTypeThinking,
Thinking: &thinkingText,
Signature: &signature,
},
{
Type: AnthropicContentBlockTypeText,
Text: &textBlock1,
},
{
Type: AnthropicContentBlockTypeText,
Text: &textBlock2,
},
},
StopReason: "end_turn",
Usage: &AnthropicUsage{
InputTokens: 100,
OutputTokens: 50,
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result := response.ToBifrostChatResponse(ctx)
if result == nil {
t.Fatal("expected non-nil result")
}
// With multiple text blocks, ToBifrostChatResponse preserves them as ContentBlocks
// (only a single text block collapses to ContentStr — see chat.go:812-815).
// Thinking flows through ReasoningDetails below, not ContentStr.
choice := result.Choices[0]
msg := choice.ChatNonStreamResponseChoice.Message
if msg.Content.ContentStr != nil {
t.Errorf("expected ContentStr to be nil with multiple text blocks, got %q", *msg.Content.ContentStr)
}
if len(msg.Content.ContentBlocks) != 2 {
t.Fatalf("expected 2 content blocks (one per text block), got %d", len(msg.Content.ContentBlocks))
}
if msg.Content.ContentBlocks[0].Text == nil || *msg.Content.ContentBlocks[0].Text != textBlock1 {
t.Errorf("block 0 text mismatch: got %v, want %q", msg.Content.ContentBlocks[0].Text, textBlock1)
}
if msg.Content.ContentBlocks[1].Text == nil || *msg.Content.ContentBlocks[1].Text != textBlock2 {
t.Errorf("block 1 text mismatch: got %v, want %q", msg.Content.ContentBlocks[1].Text, textBlock2)
}
// Thinking is surfaced via ReasoningDetails with the signature preserved
// (see chat.go:798-807).
if msg.ChatAssistantMessage == nil {
t.Fatal("expected ChatAssistantMessage to be non-nil")
}
rd := msg.ChatAssistantMessage.ReasoningDetails
if len(rd) != 1 {
t.Fatalf("expected 1 reasoning details entry (the thinking block), got %d", len(rd))
}
if rd[0].Type != schemas.BifrostReasoningDetailsTypeText {
t.Errorf("expected reasoning detail type %s, got %s", schemas.BifrostReasoningDetailsTypeText, rd[0].Type)
}
if rd[0].Signature == nil || *rd[0].Signature != signature {
t.Error("expected thinking signature to be preserved on reasoning detail")
}
if rd[0].Text == nil || *rd[0].Text != thinkingText {
t.Errorf("expected reasoning text to match thinking text")
}
}
func TestToBifrostChatResponse_SingleTextBlockNoThinking(t *testing.T) {
// Verify existing behavior: single text block without thinking collapses to string
text := "Simple response"
response := &AnthropicMessageResponse{
ID: "msg_simple",
Type: "message",
Role: "assistant",
Model: "claude-sonnet-4-6-20250514",
Content: []AnthropicContentBlock{
{Type: AnthropicContentBlockTypeText, Text: &text},
},
StopReason: "end_turn",
Usage: &AnthropicUsage{InputTokens: 10, OutputTokens: 5},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result := response.ToBifrostChatResponse(ctx)
msg := result.Choices[0].ChatNonStreamResponseChoice.Message
if msg.Content.ContentStr == nil || *msg.Content.ContentStr != text {
t.Error("expected ContentStr to be the text")
}
if msg.Content.ContentBlocks != nil {
t.Error("expected ContentBlocks to be nil")
}
// No reasoning details for plain text
if msg.ChatAssistantMessage != nil && len(msg.ChatAssistantMessage.ReasoningDetails) > 0 {
t.Error("expected no reasoning details for single text block without thinking")
}
}
func TestToAnthropicChatRequest_BoundaryMismatchFallback(t *testing.T) {
// If content was modified by the client, boundaries won't match — fall back to single text block
signature := "sig_fallback"
modifiedContent := "The user edited this content"
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-opus-4-6-20250514",
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("Hi")},
},
{
Role: schemas.ChatMessageRoleAssistant,
Content: &schemas.ChatMessageContent{ContentStr: &modifiedContent},
ChatAssistantMessage: &schemas.ChatAssistantMessage{
ReasoningDetails: []schemas.ChatReasoningDetails{
{Index: 0, Type: schemas.BifrostReasoningDetailsTypeText, Text: &modifiedContent, Signature: &signature},
},
},
},
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("Continue")},
},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result, err := ToAnthropicChatRequest(ctx, bifrostReq)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var assistantMsg *AnthropicMessage
for i := range result.Messages {
if result.Messages[i].Role == "assistant" {
assistantMsg = &result.Messages[i]
break
}
}
if assistantMsg == nil {
t.Fatal("expected assistant message")
}
// Should have thinking block (from reasoning_details with signature) + single text fallback
blocks := assistantMsg.Content.ContentBlocks
// First block: thinking (from reasoning_details, text is nil since it was cleared)
// Plus: fallback single text block with the full modified content
foundText := false
for _, block := range blocks {
if block.Type == AnthropicContentBlockTypeText {
if block.Text != nil && *block.Text == modifiedContent {
foundText = true
}
}
}
if !foundText {
t.Error("expected fallback to single text block with full content")
}
}
func TestToAnthropicChatRequest_NormalFlowUnchanged(t *testing.T) {
// Verify that the normal multi-turn flow (reasoning_details with text + signature,
// no bifrost.content_blocks) produces the same output as before.
thinkingText := "I need to think about this carefully"
signature := "sig_normal"
responseText := "Here is my answer"
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-opus-4-6-20250514",
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("What is 2+2?")},
},
{
Role: schemas.ChatMessageRoleAssistant,
Content: &schemas.ChatMessageContent{ContentStr: &responseText},
ChatAssistantMessage: &schemas.ChatAssistantMessage{
ReasoningDetails: []schemas.ChatReasoningDetails{
{
Index: 0,
Type: schemas.BifrostReasoningDetailsTypeText,
Text: &thinkingText,
Signature: &signature,
},
},
},
},
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("Are you sure?")},
},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result, err := ToAnthropicChatRequest(ctx, bifrostReq)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var assistantMsg *AnthropicMessage
for i := range result.Messages {
if result.Messages[i].Role == "assistant" {
assistantMsg = &result.Messages[i]
break
}
}
if assistantMsg == nil {
t.Fatal("expected assistant message")
}
blocks := assistantMsg.Content.ContentBlocks
if len(blocks) != 2 {
t.Fatalf("expected 2 content blocks (thinking + text), got %d", len(blocks))
}
// Block 0: thinking with original text and signature
if blocks[0].Type != AnthropicContentBlockTypeThinking {
t.Errorf("block 0: expected thinking, got %s", blocks[0].Type)
}
if blocks[0].Thinking == nil || *blocks[0].Thinking != thinkingText {
t.Errorf("block 0: expected thinking text %q, got %v", thinkingText, blocks[0].Thinking)
}
if blocks[0].Signature == nil || *blocks[0].Signature != signature {
t.Errorf("block 0: expected signature %q, got %v", signature, blocks[0].Signature)
}
// Block 1: text with response
if blocks[1].Type != AnthropicContentBlockTypeText {
t.Errorf("block 1: expected text, got %s", blocks[1].Type)
}
if blocks[1].Text == nil || *blocks[1].Text != responseText {
t.Errorf("block 1: expected text %q, got %v", responseText, blocks[1].Text)
}
}
func TestToAnthropicChatRequest_Opus47_StripsTemperatureTopPTopK(t *testing.T) {
temp := 0.7
topP := 0.9
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-opus-4-7-20260401",
Input: []schemas.ChatMessage{
{Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("hi")}},
},
Params: &schemas.ChatParameters{
Temperature: &temp,
TopP: &topP,
ExtraParams: map[string]interface{}{"top_k": 40},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result, err := ToAnthropicChatRequest(ctx, bifrostReq)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Temperature != nil {
t.Errorf("expected Temperature to be nil for Opus 4.7, got %v", result.Temperature)
}
if result.TopP != nil {
t.Errorf("expected TopP to be nil for Opus 4.7, got %v", result.TopP)
}
if result.TopK != nil {
t.Errorf("expected TopK to be nil for Opus 4.7, got %v", result.TopK)
}
}
func TestToAnthropicChatRequest_NonOpus47_PreservesTemperature(t *testing.T) {
temp := 0.7
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-opus-4-6-20250514",
Input: []schemas.ChatMessage{
{Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("hi")}},
},
Params: &schemas.ChatParameters{
Temperature: &temp,
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result, err := ToAnthropicChatRequest(ctx, bifrostReq)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Temperature == nil || *result.Temperature != temp {
t.Errorf("expected Temperature %v, got %v", temp, result.Temperature)
}
}
func TestToAnthropicChatRequest_Opus47_ReasoningMaxTokens_AdaptiveOnly(t *testing.T) {
maxTok := 2048
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-opus-4-7-20260401",
Input: []schemas.ChatMessage{
{Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("think")}},
},
Params: &schemas.ChatParameters{
MaxCompletionTokens: schemas.Ptr(8192),
Reasoning: &schemas.ChatReasoning{MaxTokens: &maxTok},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result, err := ToAnthropicChatRequest(ctx, bifrostReq)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Thinking == nil {
t.Fatal("expected Thinking to be set")
}
if result.Thinking.Type != "adaptive" {
t.Errorf("expected thinking type 'adaptive' for Opus 4.7, got %q", result.Thinking.Type)
}
if result.Thinking.BudgetTokens != nil {
t.Errorf("expected BudgetTokens to be nil for Opus 4.7, got %v", result.Thinking.BudgetTokens)
}
}
func TestToAnthropicChatRequest_NonOpus47_ReasoningMaxTokens_EnabledWithBudget(t *testing.T) {
maxTok := 2048
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-opus-4-6-20250514",
Input: []schemas.ChatMessage{
{Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("think")}},
},
Params: &schemas.ChatParameters{
MaxCompletionTokens: schemas.Ptr(8192),
Reasoning: &schemas.ChatReasoning{MaxTokens: &maxTok},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result, err := ToAnthropicChatRequest(ctx, bifrostReq)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Thinking == nil {
t.Fatal("expected Thinking to be set")
}
if result.Thinking.Type != "enabled" {
t.Errorf("expected thinking type 'enabled' for Opus 4.6, got %q", result.Thinking.Type)
}
if result.Thinking.BudgetTokens == nil || *result.Thinking.BudgetTokens != maxTok {
t.Errorf("expected BudgetTokens %d, got %v", maxTok, result.Thinking.BudgetTokens)
}
}
func TestToAnthropicChatRequest_Opus47_ReasoningEffort_AdaptiveWithEffort(t *testing.T) {
effort := "high"
bifrostReq := &schemas.BifrostChatRequest{
Provider: schemas.Anthropic,
Model: "claude-opus-4-7-20260401",
Input: []schemas.ChatMessage{
{Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("think")}},
},
Params: &schemas.ChatParameters{
MaxCompletionTokens: schemas.Ptr(8192),
Reasoning: &schemas.ChatReasoning{Effort: &effort},
},
}
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
defer cancel()
result, err := ToAnthropicChatRequest(ctx, bifrostReq)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Thinking == nil {
t.Fatal("expected Thinking to be set")
}
if result.Thinking.Type != "adaptive" {
t.Errorf("expected thinking type 'adaptive' for Opus 4.7 effort-based, got %q", result.Thinking.Type)
}
if result.OutputConfig == nil || result.OutputConfig.Effort == nil {
t.Error("expected OutputConfig.Effort to be set for Opus 4.7 effort-based reasoning")
}
}