Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/internal/testutil/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type TestScenarios struct {
TranscriptionStream bool // Streaming speech-to-text functionality
Embedding bool // Embedding functionality
Reasoning bool // Reasoning/thinking functionality via Responses API
PromptCaching bool // Prompt caching functionality
ListModels bool // List available models functionality
BatchCreate bool // Batch API create functionality
BatchList bool // Batch API list functionality
Expand Down Expand Up @@ -605,6 +606,7 @@ var AllProviderConfigs = []ComprehensiveTestConfig{
ImageBase64: true,
MultipleImages: true,
CompleteEnd2End: true,
PromptCaching: true,
SpeechSynthesis: false, // Not supported
SpeechSynthesisStream: false, // Not supported
Transcription: false, // Not supported
Expand Down Expand Up @@ -638,6 +640,7 @@ var AllProviderConfigs = []ComprehensiveTestConfig{
ImageBase64: true,
MultipleImages: true,
CompleteEnd2End: true,
PromptCaching: true,
SpeechSynthesis: false, // Not supported
SpeechSynthesisStream: false, // Not supported
Transcription: false, // Not supported
Expand Down
23 changes: 16 additions & 7 deletions core/internal/testutil/prompt_caching.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ func GetPromptCachingTools() []schemas.ChatTool {
Required: []string{},
},
},
CacheControl: &schemas.CacheControl{
Type: schemas.CacheControlTypeEphemeral,
},
},
}
}
Expand All @@ -271,16 +274,14 @@ func GetPromptCachingTools() []schemas.ChatTool {
// by making multiple requests with the same long prefix and tools, and verifying
// that cached tokens increase in subsequent requests.
func RunPromptCachingTest(t *testing.T, client *bifrost.Bifrost, ctx context.Context, testConfig ComprehensiveTestConfig) {
// Only run for OpenAI provider as prompt caching is OpenAI-specific
if testConfig.Provider != schemas.OpenAI {
t.Logf("Prompt caching test skipped for provider %s (OpenAI-specific feature)", testConfig.Provider)
return
}

if !testConfig.Scenarios.SimpleChat {
t.Logf("Prompt caching test requires SimpleChat support")
return
}
if !testConfig.Scenarios.PromptCaching {
t.Logf("Prompt caching test not supported for provider %s", testConfig.Provider)
return
}

t.Run("PromptCaching", func(t *testing.T) {
if os.Getenv("SKIP_PARALLEL_TESTS") != "true" {
Expand All @@ -291,7 +292,15 @@ func RunPromptCachingTest(t *testing.T, client *bifrost.Bifrost, ctx context.Con
systemMessage := schemas.ChatMessage{
Role: schemas.ChatMessageRoleSystem,
Content: &schemas.ChatMessageContent{
ContentStr: bifrost.Ptr(longSharedPrefix),
ContentBlocks: []schemas.ChatContentBlock{
{
Type: schemas.ChatContentBlockTypeText,
Text: bifrost.Ptr(longSharedPrefix),
CacheControl: &schemas.CacheControl{
Type: schemas.CacheControlTypeEphemeral,
},
},
},
},
}

Expand Down
26 changes: 24 additions & 2 deletions core/internal/testutil/test_retry_framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,9 @@ func StreamingRetryConfig() TestRetryConfig {
},
OnRetry: func(attempt int, reason string, t *testing.T) {
// reason already contains ❌ prefix from retry logic
t.Logf("🔄 Retrying streaming test (attempt %d): %s", attempt, reason)
// attempt represents the current failed attempt number
// Log with attempt+1 to show the next attempt that will run
t.Logf("🔄 Retrying streaming test (attempt %d): %s", attempt+1, reason)
},
OnFinalFail: func(attempts int, finalErr error, t *testing.T) {
// finalErr already contains ❌ prefix from retry logic
Expand Down Expand Up @@ -2148,6 +2150,13 @@ func WithResponsesStreamValidationRetry(
for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
context.AttemptNumber = attempt

// Log attempt start (especially for retries)
if attempt > 1 {
t.Logf("🔄 Starting responses stream retry attempt %d/%d for %s", attempt, config.MaxAttempts, context.ScenarioName)
} else {
t.Logf("🔄 Starting responses stream test attempt %d/%d for %s", attempt, config.MaxAttempts, context.ScenarioName)
}

// Execute the operation to get the stream
responseChannel, err := operation()

Expand Down Expand Up @@ -2193,13 +2202,20 @@ func WithResponsesStreamValidationRetry(
}

if shouldRetry {
// Log the error and upcoming retry
if attempt > 1 {
t.Logf("❌ Responses stream request failed on attempt %d/%d for %s: %s", attempt, config.MaxAttempts, context.ScenarioName, retryReason)
}

if config.OnRetry != nil {
// Pass current failed attempt number
config.OnRetry(attempt, retryReason, t)
} else {
t.Logf("🔄 Retrying responses stream request (attempt %d/%d) for %s: %s", attempt+1, config.MaxAttempts, context.ScenarioName, retryReason)
}

delay := calculateRetryDelay(attempt-1, config.BaseDelay, config.MaxDelay)
t.Logf("⏳ Waiting %v before retry...", delay)
time.Sleep(delay)
continue
}
Expand All @@ -2225,12 +2241,17 @@ func WithResponsesStreamValidationRetry(
if responseChannel == nil {
if attempt < config.MaxAttempts {
retryReason := "❌ response channel is nil"
t.Logf("❌ Responses stream response channel is nil on attempt %d/%d for %s", attempt, config.MaxAttempts, context.ScenarioName)
if config.OnRetry != nil {
// Pass current failed attempt number
config.OnRetry(attempt, retryReason, t)
} else {
t.Logf("🔄 Retrying responses stream request (attempt %d/%d) for %s: %s", attempt+1, config.MaxAttempts, context.ScenarioName, retryReason)
}
delay := calculateRetryDelay(attempt-1, config.BaseDelay, config.MaxDelay)
t.Logf("⏳ Waiting %v before retry...", delay)
time.Sleep(delay)
continue
continue // CRITICAL: Must continue to retry, not return
}
return ResponsesStreamValidationResult{
Passed: false,
Expand Down Expand Up @@ -2293,6 +2314,7 @@ func WithResponsesStreamValidationRetry(
}

if config.OnRetry != nil {
// Pass current failed attempt number
config.OnRetry(attempt, retryReason, t)
} else {
t.Logf("🔄 Retrying responses stream validation (attempt %d/%d) for %s: %s", attempt+1, config.MaxAttempts, context.ScenarioName, retryReason)
Expand Down
6 changes: 4 additions & 2 deletions core/providers/anthropic/anthropic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ func TestAnthropic(t *testing.T) {
{Provider: schemas.Anthropic, Model: "claude-3-7-sonnet-20250219"},
{Provider: schemas.Anthropic, Model: "claude-sonnet-4-20250514"},
},
VisionModel: "claude-3-7-sonnet-20250219", // Same model supports vision
ReasoningModel: "claude-opus-4-5",
VisionModel: "claude-3-7-sonnet-20250219", // Same model supports vision
ReasoningModel: "claude-opus-4-5",
PromptCachingModel: "claude-sonnet-4-20250514",
Scenarios: testutil.TestScenarios{
TextCompletion: false, // Not supported
SimpleChat: true,
Expand All @@ -47,6 +48,7 @@ func TestAnthropic(t *testing.T) {
CompleteEnd2End: true,
Embedding: false,
Reasoning: true,
PromptCaching: true,
ListModels: true,
BatchCreate: true,
BatchList: true,
Expand Down
102 changes: 50 additions & 52 deletions core/providers/anthropic/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ func ToAnthropicChatRequest(bifrostReq *schemas.BifrostChatRequest) (*AnthropicM
}
}

if tool.CacheControl != nil {
anthropicTool.CacheControl = tool.CacheControl
}

tools = append(tools, anthropicTool)
}
anthropicReq.Tools = tools
Expand Down Expand Up @@ -143,8 +147,9 @@ func ToAnthropicChatRequest(bifrostReq *schemas.BifrostChatRequest) (*AnthropicM
for _, block := range msg.Content.ContentBlocks {
if block.Text != nil {
blocks = append(blocks, AnthropicContentBlock{
Type: "text",
Text: block.Text,
Type: AnthropicContentBlockTypeText,
Text: block.Text,
CacheControl: block.CacheControl,
})
}
}
Expand Down Expand Up @@ -177,8 +182,9 @@ func ToAnthropicChatRequest(bifrostReq *schemas.BifrostChatRequest) (*AnthropicM
for _, block := range toolMsg.Content.ContentBlocks {
if block.Text != nil {
blocks = append(blocks, AnthropicContentBlock{
Type: "text",
Text: block.Text,
Type: AnthropicContentBlockTypeText,
Text: block.Text,
CacheControl: block.CacheControl,
})
} else if block.ImageURLStruct != nil {
blocks = append(blocks, ConvertToAnthropicImageBlock(block))
Expand Down Expand Up @@ -233,8 +239,9 @@ func ToAnthropicChatRequest(bifrostReq *schemas.BifrostChatRequest) (*AnthropicM
for _, block := range msg.Content.ContentBlocks {
if block.Text != nil {
content = append(content, AnthropicContentBlock{
Type: AnthropicContentBlockTypeText,
Text: block.Text,
Type: AnthropicContentBlockTypeText,
Text: block.Text,
CacheControl: block.CacheControl,
})
} else if block.ImageURLStruct != nil {
content = append(content, ConvertToAnthropicImageBlock(block))
Expand Down Expand Up @@ -310,63 +317,54 @@ func (response *AnthropicMessageResponse) ToBifrostChatResponse() *schemas.Bifro

// Process content and tool calls
if response.Content != nil {
if len(response.Content) == 1 && response.Content[0].Type == AnthropicContentBlockTypeText {
contentStr = response.Content[0].Text
} else {
for _, c := range response.Content {
switch c.Type {
case AnthropicContentBlockTypeText:
if c.Text != nil {
contentBlocks = append(contentBlocks, schemas.ChatContentBlock{
Type: schemas.ChatContentBlockTypeText,
Text: c.Text,
})
for _, c := range response.Content {
switch c.Type {
case AnthropicContentBlockTypeText:
if c.Text != nil {
contentBlocks = append(contentBlocks, schemas.ChatContentBlock{
Type: schemas.ChatContentBlockTypeText,
Text: c.Text,
})
}
case AnthropicContentBlockTypeToolUse:
if c.ID != nil && c.Name != nil {
function := schemas.ChatAssistantMessageToolCallFunction{
Name: c.Name,
}
case AnthropicContentBlockTypeToolUse:
if c.ID != nil && c.Name != nil {
function := schemas.ChatAssistantMessageToolCallFunction{
Name: c.Name,
}

// Marshal the input to JSON string
if c.Input != nil {
args, err := json.Marshal(c.Input)
if err != nil {
function.Arguments = fmt.Sprintf("%v", c.Input)
} else {
function.Arguments = string(args)
}
// Marshal the input to JSON string
if c.Input != nil {
args, err := json.Marshal(c.Input)
if err != nil {
function.Arguments = fmt.Sprintf("%v", c.Input)
} else {
function.Arguments = "{}"
function.Arguments = string(args)
}

toolCalls = append(toolCalls, schemas.ChatAssistantMessageToolCall{
Index: uint16(len(toolCalls)),
Type: schemas.Ptr(string(schemas.ChatToolTypeFunction)),
ID: c.ID,
Function: function,
})
} else {
function.Arguments = "{}"
}
case AnthropicContentBlockTypeThinking:
reasoningDetails = append(reasoningDetails, schemas.ChatReasoningDetails{
Index: len(reasoningDetails),
Type: schemas.BifrostReasoningDetailsTypeText,
Text: c.Thinking,
Signature: c.Signature,

toolCalls = append(toolCalls, schemas.ChatAssistantMessageToolCall{
Index: uint16(len(toolCalls)),
Type: schemas.Ptr(string(schemas.ChatToolTypeFunction)),
ID: c.ID,
Function: function,
})
if c.Thinking != nil {
reasoningText += *c.Thinking + "\n"
}
}
case AnthropicContentBlockTypeThinking:
reasoningDetails = append(reasoningDetails, schemas.ChatReasoningDetails{
Index: len(reasoningDetails),
Type: schemas.BifrostReasoningDetailsTypeText,
Text: c.Thinking,
Signature: c.Signature,
})
if c.Thinking != nil {
reasoningText += *c.Thinking + "\n"
}
}
}
}

if len(contentBlocks) == 1 && contentBlocks[0].Type == schemas.ChatContentBlockTypeText {
contentStr = contentBlocks[0].Text
contentBlocks = nil
}

// Create a single choice with the collected content
// Create message content
messageContent := schemas.ChatMessageContent{
Expand Down
Loading