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