Skip to content
Open
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: 2 additions & 1 deletion core/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
- fix: add support for AdditionalProperties structures (both boolean and object types)
- fix: improve thought signature handling in gemini for function calls
- fix: enhance citations structure to support multiple citation types
- fix: anthropic streaming events through integration
- fix: anthropic streaming events through integration
- feat: added support for code execution tool for openai, anthropic and gemini
289 changes: 222 additions & 67 deletions core/providers/anthropic/responses.go

Large diffs are not rendered by default.

66 changes: 45 additions & 21 deletions core/providers/anthropic/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,23 +158,27 @@ type AnthropicMessage struct {
type AnthropicContent struct {
ContentStr *string
ContentBlocks []AnthropicContentBlock
ContentBlock *AnthropicContentBlock // For "bash_code_execution_tool_result"
}

// MarshalJSON implements custom JSON marshalling for AnthropicContent.
// It marshals either ContentStr or ContentBlocks directly without wrapping.
func (mc AnthropicContent) MarshalJSON() ([]byte, error) {
// Validation: ensure only one field is set at a time
if mc.ContentStr != nil && mc.ContentBlocks != nil {
return nil, fmt.Errorf("both ContentStr and ContentBlocks are set; only one should be non-nil")
if mc.ContentStr != nil && mc.ContentBlocks != nil && mc.ContentBlock != nil {
return nil, fmt.Errorf("both ContentStr, ContentBlocks and ContentBlock are set; only one should be non-nil")
}

if mc.ContentStr != nil {
return sonic.Marshal(*mc.ContentStr)
}
if mc.ContentBlocks != nil {
if mc.ContentBlock != nil && mc.ContentBlocks == nil {
return sonic.Marshal(*mc.ContentBlock)
}
if mc.ContentBlocks != nil && mc.ContentBlock == nil {
return sonic.Marshal(mc.ContentBlocks)
}
// If both are nil, return null
// If all are nil, return null
return sonic.Marshal(nil)
Comment on lines 166 to 182
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

MarshalJSON validation logic is incorrect.

The validation at line 168 checks if all three fields (ContentStr, ContentBlocks, and ContentBlock) are non-nil simultaneously. However, the intended logic should check if more than one field is set (any two), not if all three are set. The current condition only triggers when all three are set, which is unlikely.

Additionally, the marshal logic at lines 175-180 has potential issues:

  • Line 175 checks mc.ContentBlock != nil && mc.ContentBlocks == nil
  • Line 178 checks mc.ContentBlocks != nil && mc.ContentBlock == nil

But what happens if both ContentBlock and ContentBlocks are set? The first condition fails, the second condition fails, and it falls through to return nil at line 182, silently losing data.

Proposed fix
 func (mc AnthropicContent) MarshalJSON() ([]byte, error) {
-	// Validation: ensure only one field is set at a time
-	if mc.ContentStr != nil && mc.ContentBlocks != nil && mc.ContentBlock != nil {
-		return nil, fmt.Errorf("both ContentStr, ContentBlocks and ContentBlock are set; only one should be non-nil")
+	// Count how many fields are set
+	setCount := 0
+	if mc.ContentStr != nil {
+		setCount++
 	}
+	if mc.ContentBlocks != nil {
+		setCount++
+	}
+	if mc.ContentBlock != nil {
+		setCount++
+	}
+	if setCount > 1 {
+		return nil, fmt.Errorf("multiple content fields are set; only one of ContentStr, ContentBlocks, or ContentBlock should be non-nil")
+	}
 
 	if mc.ContentStr != nil {
 		return sonic.Marshal(*mc.ContentStr)
 	}
-	if mc.ContentBlock != nil && mc.ContentBlocks == nil {
+	if mc.ContentBlock != nil {
 		return sonic.Marshal(*mc.ContentBlock)
 	}
-	if mc.ContentBlocks != nil && mc.ContentBlock == nil {
+	if mc.ContentBlocks != nil {
 		return sonic.Marshal(mc.ContentBlocks)
 	}
 	// If all are nil, return null
 	return sonic.Marshal(nil)
 }
🤖 Prompt for AI Agents
In `@core/providers/anthropic/types.go` around lines 166 - 182, The MarshalJSON
validation incorrectly only errors when all three fields are set; change it to
count non-nil fields (ContentStr, ContentBlock, ContentBlocks) and return an
error if more than one is non-nil. After that, simplify the marshaling branches
to explicitly handle each non-nil case (if ContentStr != nil -> marshal
*ContentStr; else if ContentBlock != nil -> marshal *ContentBlock; else if
ContentBlocks != nil -> marshal ContentBlocks) and otherwise marshal nil; keep
references to AnthropicContent.MarshalJSON, ContentStr, ContentBlock, and
ContentBlocks so the logic enforces mutual exclusivity and never silently falls
through.

}

Expand All @@ -188,31 +192,40 @@ func (mc *AnthropicContent) UnmarshalJSON(data []byte) error {
return nil
}

// Try to unmarshal as a direct ContentBlock
var contentBlock AnthropicContentBlock
if err := sonic.Unmarshal(data, &contentBlock); err == nil {
mc.ContentBlock = &contentBlock
return nil
}

// Try to unmarshal as a direct array of ContentBlock
var arrayContent []AnthropicContentBlock
if err := sonic.Unmarshal(data, &arrayContent); err == nil {
mc.ContentBlocks = arrayContent
return nil
}

return fmt.Errorf("content field is neither a string nor an array of ContentBlock")
return fmt.Errorf("content field is neither a string, ContentBlock nor an array of ContentBlock")
}
Comment on lines +195 to 210
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

UnmarshalJSON order may cause incorrect parsing.

The unmarshaling logic tries to parse as AnthropicContentBlock (single object) before trying to parse as an array. However, since AnthropicContentBlock has many optional fields with omitempty, any valid JSON object (including array elements) might successfully unmarshal into an AnthropicContentBlock with mostly nil fields.

This could cause arrays of content blocks to be incorrectly parsed as a single ContentBlock if the first element happens to unmarshal successfully.

Consider checking for array first, or adding a discriminator check (e.g., verify the Type field is set and valid for single block scenarios like bash_code_execution_tool_result).

Proposed fix - reorder to try array first
 func (mc *AnthropicContent) UnmarshalJSON(data []byte) error {
 	// First, try to unmarshal as a direct string
 	var stringContent string
 	if err := sonic.Unmarshal(data, &stringContent); err == nil {
 		mc.ContentStr = &stringContent
 		return nil
 	}

-	// Try to unmarshal as a direct ContentBlock
-	var contentBlock AnthropicContentBlock
-	if err := sonic.Unmarshal(data, &contentBlock); err == nil {
-		mc.ContentBlock = &contentBlock
-		return nil
-	}
-
 	// Try to unmarshal as a direct array of ContentBlock
 	var arrayContent []AnthropicContentBlock
 	if err := sonic.Unmarshal(data, &arrayContent); err == nil {
 		mc.ContentBlocks = arrayContent
 		return nil
 	}

+	// Try to unmarshal as a direct ContentBlock (single object, not array)
+	var contentBlock AnthropicContentBlock
+	if err := sonic.Unmarshal(data, &contentBlock); err == nil {
+		// Only accept as single ContentBlock if Type is set (to avoid false positives)
+		if contentBlock.Type != "" {
+			mc.ContentBlock = &contentBlock
+			return nil
+		}
+	}
+
 	return fmt.Errorf("content field is neither a string, ContentBlock nor an array of ContentBlock")
 }


type AnthropicContentBlockType string

const (
AnthropicContentBlockTypeText AnthropicContentBlockType = "text"
AnthropicContentBlockTypeImage AnthropicContentBlockType = "image"
AnthropicContentBlockTypeDocument AnthropicContentBlockType = "document"
AnthropicContentBlockTypeToolUse AnthropicContentBlockType = "tool_use"
AnthropicContentBlockTypeServerToolUse AnthropicContentBlockType = "server_tool_use"
AnthropicContentBlockTypeToolResult AnthropicContentBlockType = "tool_result"
AnthropicContentBlockTypeWebSearchToolResult AnthropicContentBlockType = "web_search_tool_result"
AnthropicContentBlockTypeWebSearchResult AnthropicContentBlockType = "web_search_result"
AnthropicContentBlockTypeMCPToolUse AnthropicContentBlockType = "mcp_tool_use"
AnthropicContentBlockTypeMCPToolResult AnthropicContentBlockType = "mcp_tool_result"
AnthropicContentBlockTypeThinking AnthropicContentBlockType = "thinking"
AnthropicContentBlockTypeRedactedThinking AnthropicContentBlockType = "redacted_thinking"
AnthropicContentBlockTypeText AnthropicContentBlockType = "text"
AnthropicContentBlockTypeImage AnthropicContentBlockType = "image"
AnthropicContentBlockTypeDocument AnthropicContentBlockType = "document"
AnthropicContentBlockTypeToolUse AnthropicContentBlockType = "tool_use"
AnthropicContentBlockTypeServerToolUse AnthropicContentBlockType = "server_tool_use"
AnthropicContentBlockTypeToolResult AnthropicContentBlockType = "tool_result"
AnthropicContentBlockTypeWebSearchToolResult AnthropicContentBlockType = "web_search_tool_result"
AnthropicContentBlockTypeWebSearchResult AnthropicContentBlockType = "web_search_result"
AnthropicContentBlockTypeMCPToolUse AnthropicContentBlockType = "mcp_tool_use"
AnthropicContentBlockTypeMCPToolResult AnthropicContentBlockType = "mcp_tool_result"
AnthropicContentBlockTypeBashCodeExecutionToolResult AnthropicContentBlockType = "bash_code_execution_tool_result"
AnthropicContentBlockTypeBashCodeExecutionResult AnthropicContentBlockType = "bash_code_execution_result"
AnthropicContentBlockTypeThinking AnthropicContentBlockType = "thinking"
AnthropicContentBlockTypeRedactedThinking AnthropicContentBlockType = "redacted_thinking"
)

// AnthropicContentBlock represents content in Anthropic message format
Expand All @@ -236,6 +249,9 @@ type AnthropicContentBlock struct {
URL *string `json:"url,omitempty"` // For web_search_result content
EncryptedContent *string `json:"encrypted_content,omitempty"` // For web_search_result content
PageAge *string `json:"page_age,omitempty"` // For web_search_result content
StdOut *string `json:"stdout,omitempty"` // For bash_code_execution_result content
StdErr *string `json:"stderr,omitempty"` // For bash_code_execution_result content
ReturnCode *int `json:"return_code,omitempty"` // For bash_code_execution_result content
}

// AnthropicSource represents image or document source in Anthropic format
Expand Down Expand Up @@ -360,10 +376,12 @@ const (
type AnthropicToolName string

const (
AnthropicToolNameComputer AnthropicToolName = "computer"
AnthropicToolNameWebSearch AnthropicToolName = "web_search"
AnthropicToolNameBash AnthropicToolName = "bash"
AnthropicToolNameTextEditor AnthropicToolName = "str_replace_based_edit_tool"
AnthropicToolNameComputer AnthropicToolName = "computer"
AnthropicToolNameWebSearch AnthropicToolName = "web_search"
AnthropicToolNameBash AnthropicToolName = "bash"
AnthropicToolNameTextEditor AnthropicToolName = "str_replace_based_edit_tool"
AnthropicToolNameBashCodeExecution AnthropicToolName = "bash_code_execution"
AnthropicToolNameCodeExecution AnthropicToolName = "code_execution"
)

type AnthropicToolComputerUse struct {
Expand Down Expand Up @@ -451,6 +469,7 @@ type AnthropicMessageResponse struct {
Model string `json:"model"`
StopReason AnthropicStopReason `json:"stop_reason,omitempty"`
StopSequence *string `json:"stop_sequence,omitempty"`
Container *AnthropicContainer `json:"container,omitempty"`
Usage *AnthropicUsage `json:"usage,omitempty"`
}

Expand All @@ -475,6 +494,11 @@ type AnthropicUsage struct {
OutputTokens int `json:"output_tokens"`
}

type AnthropicContainer struct {
ID string `json:"id"`
ExpiresAt string `json:"expires_at"` // ISO 8601 timestamp when the container expires (sent by Anthropic)
}

type AnthropicUsageCacheCreation struct {
Ephemeral5mInputTokens int `json:"ephemeral_5m_input_tokens"`
Ephemeral1hInputTokens int `json:"ephemeral_1h_input_tokens"`
Expand Down
34 changes: 25 additions & 9 deletions core/providers/gemini/count_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,32 @@ func (resp *GeminiCountTokensResponse) ToBifrostCountTokensResponse(model string
inputTokens := 0
inputDetails := &schemas.ResponsesResponseInputTokens{}

for _, m := range resp.PromptTokensDetails {
if m == nil {
continue
}
inputTokens += int(m.TokenCount)
mod := strings.ToLower(m.Modality)
// handle audio modality
if strings.Contains(mod, "audio") {
inputDetails.AudioTokens += int(m.TokenCount)
// Convert PromptTokensDetails to ModalityTokenCount
if len(resp.PromptTokensDetails) > 0 {
modalityDetails := make([]schemas.ModalityTokenCount, 0, len(resp.PromptTokensDetails))
for _, m := range resp.PromptTokensDetails {
if m == nil {
continue
}
inputTokens += int(m.TokenCount)
mod := strings.ToLower(m.Modality)

// Add to modality token count
modalityDetails = append(modalityDetails, schemas.ModalityTokenCount{
Modality: m.Modality,
TokenCount: int(m.TokenCount),
})

// Also populate specific fields for common modalities
if strings.Contains(mod, "audio") {
inputDetails.AudioTokens += int(m.TokenCount)
} else if strings.Contains(mod, "text") {
inputDetails.TextTokens += int(m.TokenCount)
} else if strings.Contains(mod, "image") {
inputDetails.ImageTokens += int(m.TokenCount)
}
}
inputDetails.ModalityTokenCount = modalityDetails
}

// Set cached tokens from top-level field if present
Expand Down
Loading