package anthropic import ( "context" "testing" "github.com/maximhq/bifrost/core/schemas" ) // --- isCompactionItem tests --- func TestIsCompactionItem(t *testing.T) { t.Parallel() tests := []struct { name string item *schemas.ResponsesMessage expected bool }{ { name: "nil item", item: nil, expected: false, }, { name: "nil type", item: &schemas.ResponsesMessage{ Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ {Type: schemas.ResponsesOutputMessageContentTypeCompaction}, }, }, }, expected: false, }, { name: "message type with compaction content block", item: &schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ { Type: schemas.ResponsesOutputMessageContentTypeCompaction, ResponsesOutputMessageContentCompaction: &schemas.ResponsesOutputMessageContentCompaction{ Summary: "Summary of conversation", }, }, }, }, }, expected: true, }, { name: "message type with text content block", item: &schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ { Type: schemas.ResponsesOutputMessageContentTypeText, Text: schemas.Ptr("Hello"), }, }, }, }, expected: false, }, { name: "function call type", item: &schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall), }, expected: false, }, { name: "message type with nil content", item: &schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Content: nil, }, expected: false, }, { name: "message type with empty content blocks", item: &schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{}, }, }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isCompactionItem(tt.item) if result != tt.expected { t.Errorf("isCompactionItem() = %v, want %v", result, tt.expected) } }) } } // --- Streaming: Anthropic → Bifrost (inbound) --- func TestToBifrostResponsesStream_CompactionContentBlockStart(t *testing.T) { t.Parallel() state := &AnthropicResponsesStreamState{ ContentIndexToOutputIndex: make(map[int]int), ContentIndexToBlockType: make(map[int]AnthropicContentBlockType), ToolArgumentBuffers: make(map[int]string), MCPCallOutputIndices: make(map[int]bool), ItemIDs: make(map[int]string), OutputItems: make(map[int]*schemas.ResponsesMessage), ReasoningSignatures: make(map[int]string), TextContentIndices: make(map[int]bool), ReasoningContentIndices: make(map[int]bool), CompactionContentIndices: make(map[int]*schemas.CacheControl), CurrentOutputIndex: 0, CreatedAt: 1234567890, HasEmittedCreated: true, HasEmittedInProgress: true, } // content_block_start with compaction type should return nil (defers to delta) chunk := &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeContentBlockStart, Index: schemas.Ptr(0), ContentBlock: &AnthropicContentBlock{ Type: AnthropicContentBlockTypeCompaction, CacheControl: &schemas.CacheControl{ Type: "ephemeral", }, }, } responses, err, isLast := chunk.ToBifrostResponsesStream(context.Background(), 0, state) if err != nil { t.Fatalf("unexpected error: %v", err) } if isLast { t.Error("should not be last chunk") } if len(responses) != 0 { t.Errorf("expected 0 responses for compaction content_block_start, got %d", len(responses)) } // Verify state was tracked if _, exists := state.CompactionContentIndices[0]; !exists { t.Error("expected compaction to be tracked in CompactionContentIndices") } if blockType, exists := state.ContentIndexToBlockType[0]; !exists || blockType != AnthropicContentBlockTypeCompaction { t.Error("expected compaction block type tracked in ContentIndexToBlockType") } } func TestToBifrostResponsesStream_CompactionDelta(t *testing.T) { t.Parallel() state := &AnthropicResponsesStreamState{ ContentIndexToOutputIndex: map[int]int{0: 0}, ContentIndexToBlockType: map[int]AnthropicContentBlockType{0: AnthropicContentBlockTypeCompaction}, ToolArgumentBuffers: make(map[int]string), MCPCallOutputIndices: make(map[int]bool), ItemIDs: map[int]string{0: "cmp_0"}, OutputItems: make(map[int]*schemas.ResponsesMessage), ReasoningSignatures: make(map[int]string), TextContentIndices: make(map[int]bool), ReasoningContentIndices: make(map[int]bool), CompactionContentIndices: map[int]*schemas.CacheControl{0: {Type: "ephemeral"}}, CurrentOutputIndex: 1, CreatedAt: 1234567890, HasEmittedCreated: true, HasEmittedInProgress: true, } summary := "The user asked about building a website. We discussed HTML, CSS, and JavaScript." chunk := &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeContentBlockDelta, Index: schemas.Ptr(0), Delta: &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeCompaction, Content: &summary, }, } responses, err, isLast := chunk.ToBifrostResponsesStream(context.Background(), 0, state) if err != nil { t.Fatalf("unexpected error: %v", err) } if isLast { t.Error("should not be last chunk") } // Should emit output_item.added and output_item.done if len(responses) != 2 { t.Fatalf("expected 2 responses for compaction delta, got %d", len(responses)) } // First: output_item.added added := responses[0] if added.Type != schemas.ResponsesStreamResponseTypeOutputItemAdded { t.Errorf("first response type = %v, want %v", added.Type, schemas.ResponsesStreamResponseTypeOutputItemAdded) } if added.Item == nil || added.Item.Content == nil || len(added.Item.Content.ContentBlocks) == 0 { t.Fatal("output_item.added should have content blocks") } block := added.Item.Content.ContentBlocks[0] if block.Type != schemas.ResponsesOutputMessageContentTypeCompaction { t.Errorf("content block type = %v, want compaction", block.Type) } if block.ResponsesOutputMessageContentCompaction == nil { t.Fatal("expected compaction content to be non-nil") } if block.ResponsesOutputMessageContentCompaction.Summary != summary { t.Errorf("summary = %q, want %q", block.ResponsesOutputMessageContentCompaction.Summary, summary) } // Cache control should be preserved from content_block_start if block.CacheControl == nil || block.CacheControl.Type != "ephemeral" { t.Error("expected cache control to be preserved") } // Second: output_item.done done := responses[1] if done.Type != schemas.ResponsesStreamResponseTypeOutputItemDone { t.Errorf("second response type = %v, want %v", done.Type, schemas.ResponsesStreamResponseTypeOutputItemDone) } } func TestToBifrostResponsesStream_CompactionContentBlockStop(t *testing.T) { t.Parallel() state := &AnthropicResponsesStreamState{ ContentIndexToOutputIndex: map[int]int{0: 0}, ContentIndexToBlockType: map[int]AnthropicContentBlockType{0: AnthropicContentBlockTypeCompaction}, ToolArgumentBuffers: make(map[int]string), MCPCallOutputIndices: make(map[int]bool), ItemIDs: map[int]string{0: "cmp_0"}, OutputItems: make(map[int]*schemas.ResponsesMessage), ReasoningSignatures: make(map[int]string), TextContentIndices: make(map[int]bool), ReasoningContentIndices: make(map[int]bool), CompactionContentIndices: make(map[int]*schemas.CacheControl), CurrentOutputIndex: 1, CreatedAt: 1234567890, HasEmittedCreated: true, HasEmittedInProgress: true, } chunk := &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeContentBlockStop, Index: schemas.Ptr(0), } responses, err, isLast := chunk.ToBifrostResponsesStream(context.Background(), 0, state) if err != nil { t.Fatalf("unexpected error: %v", err) } if isLast { t.Error("should not be last chunk") } // content_block_stop for compaction should return nil (done was already emitted with delta) if len(responses) != 0 { t.Errorf("expected 0 responses for compaction content_block_stop, got %d", len(responses)) } } // --- Streaming: Bifrost → Anthropic (outbound, non-passthrough) --- func TestToAnthropicResponsesStreamResponse_CompactionOutputItemAdded(t *testing.T) { t.Parallel() ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() summary := "Summary of the conversation about building a website" bifrostResp := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, OutputIndex: schemas.Ptr(0), Item: &schemas.ResponsesMessage{ ID: schemas.Ptr("cmp_test123"), Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Status: schemas.Ptr("completed"), Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant), Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ { Type: schemas.ResponsesOutputMessageContentTypeCompaction, ResponsesOutputMessageContentCompaction: &schemas.ResponsesOutputMessageContentCompaction{ Summary: summary, }, CacheControl: &schemas.CacheControl{Type: "ephemeral"}, }, }, }, }, } events := ToAnthropicResponsesStreamResponse(ctx, bifrostResp) // Should emit: content_block_start (compaction) + content_block_delta (compaction_delta) if len(events) < 2 { t.Fatalf("expected at least 2 events, got %d", len(events)) } // Event 1: content_block_start start := events[0] if start.Type != AnthropicStreamEventTypeContentBlockStart { t.Errorf("event[0] type = %v, want content_block_start", start.Type) } if start.ContentBlock == nil { t.Fatal("content_block_start should have ContentBlock") } if start.ContentBlock.Type != AnthropicContentBlockTypeCompaction { t.Errorf("ContentBlock.Type = %v, want compaction", start.ContentBlock.Type) } if start.ContentBlock.CacheControl == nil || start.ContentBlock.CacheControl.Type != "ephemeral" { t.Error("expected cache control to be preserved on content_block_start") } // Event 2: content_block_delta with compaction_delta delta := events[1] if delta.Type != AnthropicStreamEventTypeContentBlockDelta { t.Errorf("event[1] type = %v, want content_block_delta", delta.Type) } if delta.Delta == nil { t.Fatal("content_block_delta should have Delta") } if delta.Delta.Type != AnthropicStreamDeltaTypeCompaction { t.Errorf("Delta.Type = %v, want compaction_delta", delta.Delta.Type) } if delta.Delta.Content == nil || *delta.Delta.Content != summary { t.Errorf("Delta.Content = %v, want %q", delta.Delta.Content, summary) } } func TestToAnthropicResponsesStreamResponse_CompactionOutputItemDone(t *testing.T) { t.Parallel() ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() bifrostResp := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputItemDone, OutputIndex: schemas.Ptr(0), ItemID: schemas.Ptr("cmp_test123"), Item: &schemas.ResponsesMessage{ ID: schemas.Ptr("cmp_test123"), Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Status: schemas.Ptr("completed"), Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant), Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ { Type: schemas.ResponsesOutputMessageContentTypeCompaction, ResponsesOutputMessageContentCompaction: &schemas.ResponsesOutputMessageContentCompaction{ Summary: "Summary text", }, }, }, }, }, } events := ToAnthropicResponsesStreamResponse(ctx, bifrostResp) // Should emit content_block_stop if len(events) != 1 { t.Fatalf("expected 1 event for output_item.done, got %d", len(events)) } stop := events[0] if stop.Type != AnthropicStreamEventTypeContentBlockStop { t.Errorf("event type = %v, want content_block_stop", stop.Type) } } func TestToAnthropicResponsesStreamResponse_TextOutputItemAdded_NotAffectedByCompactionCheck(t *testing.T) { t.Parallel() ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() // Regular text message should still emit content_block_start with type=text bifrostResp := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, OutputIndex: schemas.Ptr(0), Item: &schemas.ResponsesMessage{ ID: schemas.Ptr("msg_test123"), Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Status: schemas.Ptr("in_progress"), Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant), Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ { Type: schemas.ResponsesOutputMessageContentTypeText, Text: schemas.Ptr(""), }, }, }, }, } events := ToAnthropicResponsesStreamResponse(ctx, bifrostResp) if len(events) == 0 { t.Fatal("expected at least 1 event") } start := events[0] if start.Type != AnthropicStreamEventTypeContentBlockStart { t.Errorf("event type = %v, want content_block_start", start.Type) } if start.ContentBlock == nil { t.Fatal("expected ContentBlock to be non-nil") } if start.ContentBlock.Type != AnthropicContentBlockTypeText { t.Errorf("ContentBlock.Type = %v, want text", start.ContentBlock.Type) } } // --- Non-Streaming: stop_reason mapping --- func TestToBifrostResponsesResponse_PreservesStopReason(t *testing.T) { t.Parallel() tests := []struct { name string stopReason AnthropicStopReason expectedStopReason string }{ { name: "compaction stop reason", stopReason: AnthropicStopReasonCompaction, expectedStopReason: "compaction", }, { name: "end_turn stop reason", stopReason: AnthropicStopReasonEndTurn, expectedStopReason: "end_turn", }, { name: "tool_use stop reason", stopReason: AnthropicStopReasonToolUse, expectedStopReason: "tool_use", }, { name: "max_tokens stop reason", stopReason: AnthropicStopReasonMaxTokens, expectedStopReason: "max_tokens", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() resp := &AnthropicMessageResponse{ ID: "msg_test", Type: "message", Role: "assistant", Model: "claude-sonnet-4-6", StopReason: tt.stopReason, Content: []AnthropicContentBlock{ {Type: AnthropicContentBlockTypeText, Text: schemas.Ptr("Hello")}, }, } bifrostResp := resp.ToBifrostResponsesResponse(ctx) if bifrostResp.StopReason == nil { t.Fatal("expected StopReason to be non-nil") } if *bifrostResp.StopReason != tt.expectedStopReason { t.Errorf("StopReason = %q, want %q", *bifrostResp.StopReason, tt.expectedStopReason) } }) } } func TestToBifrostResponsesResponse_EmptyStopReason(t *testing.T) { t.Parallel() ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() resp := &AnthropicMessageResponse{ ID: "msg_test", Type: "message", Role: "assistant", Model: "claude-sonnet-4-6", Content: []AnthropicContentBlock{}, } bifrostResp := resp.ToBifrostResponsesResponse(ctx) if bifrostResp.StopReason != nil { t.Errorf("expected nil StopReason for empty stop_reason, got %q", *bifrostResp.StopReason) } } func TestToAnthropicResponsesResponse_StopReasonFromBifrost(t *testing.T) { t.Parallel() tests := []struct { name string stopReason *string contentBlocks []schemas.ResponsesMessage expectedReason AnthropicStopReason }{ { name: "compaction stop reason from bifrost", stopReason: schemas.Ptr("compaction"), expectedReason: AnthropicStopReasonCompaction, }, { name: "end_turn mapped from stop", stopReason: schemas.Ptr("stop"), expectedReason: AnthropicStopReasonEndTurn, }, { name: "tool_use mapped from tool_calls", stopReason: schemas.Ptr("tool_calls"), expectedReason: AnthropicStopReasonToolUse, }, { name: "nil stop_reason defaults to end_turn", stopReason: nil, expectedReason: AnthropicStopReasonEndTurn, }, { name: "nil stop_reason with tool_use content defaults to tool_use", stopReason: nil, contentBlocks: []schemas.ResponsesMessage{ { Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall), ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: schemas.Ptr("call_123"), Name: schemas.Ptr("my_tool"), }, }, }, expectedReason: AnthropicStopReasonToolUse, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() bifrostResp := &schemas.BifrostResponsesResponse{ ID: schemas.Ptr("resp_test"), Model: "claude-sonnet-4-6", StopReason: tt.stopReason, Output: tt.contentBlocks, } result := ToAnthropicResponsesResponse(ctx, bifrostResp) if result.StopReason != tt.expectedReason { t.Errorf("StopReason = %v, want %v", result.StopReason, tt.expectedReason) } }) } } // --- Non-Streaming: compaction content block round-trip --- func TestCompactionContentBlock_NonStreamingRoundTrip(t *testing.T) { t.Parallel() ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() summary := "The user requested help building a web scraper using Python with BeautifulSoup." // Simulate Anthropic response with compaction block anthropicResp := &AnthropicMessageResponse{ ID: "msg_compaction_test", Type: "message", Role: "assistant", Model: "claude-opus-4-6", StopReason: AnthropicStopReasonCompaction, Content: []AnthropicContentBlock{ { Type: AnthropicContentBlockTypeCompaction, Content: &AnthropicContent{ ContentStr: &summary, }, CacheControl: &schemas.CacheControl{Type: "ephemeral"}, }, }, } // Step 1: Anthropic → Bifrost bifrostResp := anthropicResp.ToBifrostResponsesResponse(ctx) if bifrostResp.StopReason == nil || *bifrostResp.StopReason != "compaction" { t.Fatalf("expected stop_reason='compaction', got %v", bifrostResp.StopReason) } if len(bifrostResp.Output) == 0 { t.Fatal("expected at least one output message") } // Find the compaction block var foundCompaction bool for _, msg := range bifrostResp.Output { if msg.Content != nil { for _, block := range msg.Content.ContentBlocks { if block.Type == schemas.ResponsesOutputMessageContentTypeCompaction { foundCompaction = true if block.ResponsesOutputMessageContentCompaction == nil { t.Fatal("expected compaction content to be non-nil") } if block.ResponsesOutputMessageContentCompaction.Summary != summary { t.Errorf("summary = %q, want %q", block.ResponsesOutputMessageContentCompaction.Summary, summary) } } } } } if !foundCompaction { t.Error("compaction block not found in Bifrost output") } // Step 2: Bifrost → Anthropic result := ToAnthropicResponsesResponse(ctx, bifrostResp) if result.StopReason != AnthropicStopReasonCompaction { t.Errorf("result StopReason = %v, want compaction", result.StopReason) } // Find compaction content block in result var foundResultCompaction bool for _, block := range result.Content { if block.Type == AnthropicContentBlockTypeCompaction { foundResultCompaction = true if block.Content == nil || block.Content.ContentStr == nil { t.Fatal("expected compaction content string") } if *block.Content.ContentStr != summary { t.Errorf("result summary = %q, want %q", *block.Content.ContentStr, summary) } if block.CacheControl == nil || block.CacheControl.Type != "ephemeral" { t.Error("expected cache control to be preserved") } } } if !foundResultCompaction { t.Error("compaction block not found in Anthropic result") } } // --- Streaming: compaction stop_reason in response.completed --- func TestToAnthropicResponsesStreamResponse_CompletedWithCompactionStopReason(t *testing.T) { t.Parallel() ctx, cancel := schemas.NewBifrostContextWithCancel(nil) defer cancel() bifrostResp := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeCompleted, Response: &schemas.BifrostResponsesResponse{ ID: schemas.Ptr("resp_test"), Model: "claude-opus-4-6", StopReason: schemas.Ptr("compaction"), Usage: &schemas.ResponsesResponseUsage{ InputTokens: 1000, OutputTokens: 500, TotalTokens: 1500, }, }, } events := ToAnthropicResponsesStreamResponse(ctx, bifrostResp) // Should emit message_delta + message_stop if len(events) != 2 { t.Fatalf("expected 2 events for response.completed, got %d", len(events)) } // message_delta should have stop_reason=compaction messageDelta := events[0] if messageDelta.Type != AnthropicStreamEventTypeMessageDelta { t.Errorf("event[0] type = %v, want message_delta", messageDelta.Type) } if messageDelta.Delta == nil || messageDelta.Delta.StopReason == nil { t.Fatal("expected Delta.StopReason in message_delta") } if *messageDelta.Delta.StopReason != AnthropicStopReasonCompaction { t.Errorf("StopReason = %v, want compaction", *messageDelta.Delta.StopReason) } // message_stop messageStop := events[1] if messageStop.Type != AnthropicStreamEventTypeMessageStop { t.Errorf("event[1] type = %v, want message_stop", messageStop.Type) } }