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
9 changes: 5 additions & 4 deletions core/providers/anthropic/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -682,9 +682,10 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
for i < len(messages) && messages[i].Role == schemas.ChatMessageRoleTool {
toolMsg := messages[i]
if toolMsg.ChatToolMessage != nil && toolMsg.ChatToolMessage.ToolCallID != nil {
sanitizedToolUseID := providerUtils.SanitizeAnthropicToolUseID(*toolMsg.ChatToolMessage.ToolCallID)
toolResult := AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolResult,
ToolUseID: toolMsg.ChatToolMessage.ToolCallID,
ToolUseID: &sanitizedToolUseID,
}

// Convert tool result content
Expand Down Expand Up @@ -783,7 +784,7 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
for _, toolCall := range msg.ChatAssistantMessage.ToolCalls {
toolUse := AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolUse,
ID: toolCall.ID,
ID: providerUtils.SanitizeAnthropicToolUseIDPtr(toolCall.ID),
Name: toolCall.Function.Name,
}

Expand Down Expand Up @@ -1180,7 +1181,7 @@ func ToAnthropicChatResponse(bifrostResp *schemas.BifrostChatResponse) *Anthropi

content = append(content, AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolUse,
ID: toolCall.ID,
ID: providerUtils.SanitizeAnthropicToolUseIDPtr(toolCall.ID),
Name: toolCall.Function.Name,
Input: inputRaw,
})
Expand Down Expand Up @@ -1592,7 +1593,7 @@ func ToAnthropicChatStreamResponse(bifrostResp *schemas.BifrostChatResponse) str
streamResp.Index = &choice.Index
streamResp.ContentBlock = &AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolUse,
ID: toolCall.ID,
ID: providerUtils.SanitizeAnthropicToolUseIDPtr(toolCall.ID),
Name: toolCall.Function.Name,
}
} else if toolCall.Function.Arguments != "" {
Expand Down
54 changes: 30 additions & 24 deletions core/providers/anthropic/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -2184,8 +2184,8 @@ func ToAnthropicResponsesStreamResponse(ctx *schemas.BifrostContext, bifrostResp
// Note: Computer tool calls should not be converted to thinking blocks
contentBlock := &AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolUse,
ID: bifrostResp.Item.ID, // The tool use ID
Name: schemas.Ptr(string(AnthropicToolNameComputer)), // "computer"
ID: providerUtils.SanitizeAnthropicToolUseIDPtr(bifrostResp.Item.ID), // The tool use ID
Name: schemas.Ptr(string(AnthropicToolNameComputer)), // "computer"
}

// Always start with empty input for streaming compatibility
Expand All @@ -2203,8 +2203,8 @@ func ToAnthropicResponsesStreamResponse(ctx *schemas.BifrostContext, bifrostResp
// Build the content_block as server_tool_use
contentBlock := &AnthropicContentBlock{
Type: AnthropicContentBlockTypeServerToolUse,
ID: bifrostResp.Item.ID, // The tool use ID
Name: schemas.Ptr(string(AnthropicToolNameWebSearch)), // "web_search"
ID: providerUtils.SanitizeAnthropicToolUseIDPtr(bifrostResp.Item.ID), // The tool use ID
Name: schemas.Ptr(string(AnthropicToolNameWebSearch)), // "web_search"
}

// Deliver the query whole in content_block_start (no input_json_delta),
Expand All @@ -2224,7 +2224,7 @@ func ToAnthropicResponsesStreamResponse(ctx *schemas.BifrostContext, bifrostResp
if tm := bifrostResp.Item.ResponsesToolMessage; tm != nil && tm.Caller != nil {
contentBlock.Caller = &AnthropicToolCaller{
Type: AnthropicToolCallerType(tm.Caller.Type),
ToolID: tm.Caller.ToolID,
ToolID: providerUtils.SanitizeAnthropicToolUseIDPtr(tm.Caller.ToolID),
}
}

Expand All @@ -2240,6 +2240,7 @@ func ToAnthropicResponsesStreamResponse(ctx *schemas.BifrostContext, bifrostResp
if bifrostResp.Item.ResponsesToolMessage != nil && bifrostResp.Item.ResponsesToolMessage.CallID != nil {
toolUseID = bifrostResp.Item.ResponsesToolMessage.CallID
}
toolUseID = providerUtils.SanitizeAnthropicToolUseIDPtr(toolUseID)
streamResp.ContentBlock = &AnthropicContentBlock{
Type: AnthropicContentBlockTypeServerToolUse,
ID: toolUseID,
Expand Down Expand Up @@ -2271,7 +2272,7 @@ func ToAnthropicResponsesStreamResponse(ctx *schemas.BifrostContext, bifrostResp
addedState.codeExecToolNameByItem[reverseStreamItemKey(bifrostResp)] = toolName
streamResp.ContentBlock = &AnthropicContentBlock{
Type: AnthropicContentBlockTypeServerToolUse,
ID: toolUseID,
ID: providerUtils.SanitizeAnthropicToolUseIDPtr(toolUseID),
Name: schemas.Ptr(toolName),
Input: json.RawMessage("{}"),
}
Expand Down Expand Up @@ -2361,7 +2362,7 @@ func ToAnthropicResponsesStreamResponse(ctx *schemas.BifrostContext, bifrostResp
} else {
contentBlock.Type = AnthropicContentBlockTypeToolUse
if bifrostResp.Item.ResponsesToolMessage != nil {
contentBlock.ID = bifrostResp.Item.ResponsesToolMessage.CallID
contentBlock.ID = providerUtils.SanitizeAnthropicToolUseIDPtr(bifrostResp.Item.ResponsesToolMessage.CallID)
contentBlock.Name = bifrostResp.Item.ResponsesToolMessage.Name
// Always start with empty input for streaming compatibility
contentBlock.Input = json.RawMessage("{}")
Expand All @@ -2383,7 +2384,7 @@ func ToAnthropicResponsesStreamResponse(ctx *schemas.BifrostContext, bifrostResp
case schemas.ResponsesMessageTypeMCPCall:
contentBlock.Type = AnthropicContentBlockTypeMCPToolUse
if bifrostResp.Item.ResponsesToolMessage != nil {
contentBlock.ID = bifrostResp.Item.ID
contentBlock.ID = providerUtils.SanitizeAnthropicToolUseIDPtr(bifrostResp.Item.ID)
contentBlock.Name = bifrostResp.Item.ResponsesToolMessage.Name
if bifrostResp.Item.ResponsesToolMessage.ResponsesMCPToolCall != nil {
contentBlock.ServerName = &bifrostResp.Item.ResponsesToolMessage.ResponsesMCPToolCall.ServerLabel
Expand Down Expand Up @@ -2639,14 +2640,14 @@ func ToAnthropicResponsesStreamResponse(ctx *schemas.BifrostContext, bifrostResp

resultBlock := &AnthropicContentBlock{
Type: AnthropicContentBlockTypeWebSearchToolResult,
ToolUseID: bifrostResp.Item.ID, // Link to the server_tool_use block
ToolUseID: providerUtils.SanitizeAnthropicToolUseIDPtr(bifrostResp.Item.ID), // Link to the server_tool_use block
Content: &AnthropicContent{ContentBlocks: resultContentBlocks},
}
// Carry the programmatic-tool-calling caller onto the result too.
if tm != nil && tm.Caller != nil {
resultBlock.Caller = &AnthropicToolCaller{
Type: AnthropicToolCallerType(tm.Caller.Type),
ToolID: tm.Caller.ToolID,
ToolID: providerUtils.SanitizeAnthropicToolUseIDPtr(tm.Caller.ToolID),
}
}
events = append(events,
Expand Down Expand Up @@ -2743,6 +2744,7 @@ func ToAnthropicResponsesStreamResponse(ctx *schemas.BifrostContext, bifrostResp
if bifrostResp.Item.ResponsesToolMessage != nil && bifrostResp.Item.ResponsesToolMessage.CallID != nil {
toolUseID = bifrostResp.Item.ResponsesToolMessage.CallID
}
toolUseID = providerUtils.SanitizeAnthropicToolUseIDPtr(toolUseID)

serverIdx := state.blockIndexFor(reverseStreamItemKey(bifrostResp))

Expand Down Expand Up @@ -5508,7 +5510,7 @@ func convertBifrostFunctionCallToAnthropicToolUse(ctx *schemas.BifrostContext, m
}

if msg.ResponsesToolMessage.CallID != nil {
toolUseBlock.ID = msg.ResponsesToolMessage.CallID
toolUseBlock.ID = providerUtils.SanitizeAnthropicToolUseIDPtr(msg.ResponsesToolMessage.CallID)
}
if msg.ResponsesToolMessage.Name != nil {
toolUseBlock.Name = msg.ResponsesToolMessage.Name
Expand Down Expand Up @@ -5546,7 +5548,7 @@ func convertBifrostFunctionCallOutputToAnthropicToolResultBlock(msg *schemas.Res
if msg.ResponsesToolMessage != nil {
toolResultBlock := AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolResult,
ToolUseID: msg.ResponsesToolMessage.CallID,
ToolUseID: providerUtils.SanitizeAnthropicToolUseIDPtr(msg.ResponsesToolMessage.CallID),
CacheControl: msg.CacheControl,
}

Expand Down Expand Up @@ -5602,7 +5604,7 @@ func convertBifrostComputerCallOutputToAnthropicToolResultBlock(msg *schemas.Res
if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.CallID != nil {
toolResultBlock := AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolResult,
ToolUseID: msg.ResponsesToolMessage.CallID,
ToolUseID: providerUtils.SanitizeAnthropicToolUseIDPtr(msg.ResponsesToolMessage.CallID),
}

// Handle output
Expand Down Expand Up @@ -5633,7 +5635,7 @@ func convertBifrostMCPCallOutputToAnthropicToolResultBlock(msg *schemas.Response
if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.CallID != nil {
toolResultBlock := AnthropicContentBlock{
Type: AnthropicContentBlockTypeMCPToolResult,
ToolUseID: msg.ResponsesToolMessage.CallID,
ToolUseID: providerUtils.SanitizeAnthropicToolUseIDPtr(msg.ResponsesToolMessage.CallID),
}

// Handle output
Expand Down Expand Up @@ -5688,7 +5690,7 @@ func convertBifrostComputerCallToAnthropicToolUse(msg *schemas.ResponsesMessage)
Name: schemas.Ptr(string(AnthropicToolNameComputer)),
}
if msg.ResponsesToolMessage.CallID != nil {
toolUseBlock.ID = msg.ResponsesToolMessage.CallID
toolUseBlock.ID = providerUtils.SanitizeAnthropicToolUseIDPtr(msg.ResponsesToolMessage.CallID)
}
if msg.ResponsesToolMessage.Name != nil {
toolUseBlock.Name = msg.ResponsesToolMessage.Name
Expand All @@ -5714,7 +5716,7 @@ func convertBifrostMCPCallToAnthropicToolUse(msg *schemas.ResponsesMessage) *Ant
}

if msg.ID != nil {
toolUseBlock.ID = msg.ID
toolUseBlock.ID = providerUtils.SanitizeAnthropicToolUseIDPtr(msg.ID)
}
toolUseBlock.Name = msg.ResponsesToolMessage.Name

Expand All @@ -5741,7 +5743,7 @@ func convertBifrostMCPCallToAnthropicToolUse(msg *schemas.ResponsesMessage) *Ant
func convertBifrostMCPCallOutputToAnthropicMessage(msg *schemas.ResponsesMessage) *AnthropicMessage {
toolResultBlock := AnthropicContentBlock{
Type: AnthropicContentBlockTypeMCPToolResult,
ID: msg.ResponsesToolMessage.CallID,
ID: providerUtils.SanitizeAnthropicToolUseIDPtr(msg.ResponsesToolMessage.CallID),
}

if msg.ResponsesToolMessage.Output != nil {
Expand All @@ -5764,7 +5766,7 @@ func convertBifrostMCPApprovalToAnthropicToolUse(msg *schemas.ResponsesMessage)
}

if msg.ID != nil {
toolUseBlock.ID = msg.ID
toolUseBlock.ID = providerUtils.SanitizeAnthropicToolUseIDPtr(msg.ID)
}
toolUseBlock.Name = msg.ResponsesToolMessage.Name

Expand Down Expand Up @@ -5800,7 +5802,7 @@ func convertBifrostWebSearchCallToAnthropicBlocks(msg *schemas.ResponsesMessage)
// sandbox) must be re-emitted on both the server_tool_use and the result block.
var caller *AnthropicToolCaller
if c := msg.ResponsesToolMessage.Caller; c != nil {
caller = &AnthropicToolCaller{Type: AnthropicToolCallerType(c.Type), ToolID: c.ToolID}
caller = &AnthropicToolCaller{Type: AnthropicToolCallerType(c.Type), ToolID: providerUtils.SanitizeAnthropicToolUseIDPtr(c.ToolID)}
}

// 1. Create server_tool_use block for the web search
Expand All @@ -5811,7 +5813,7 @@ func convertBifrostWebSearchCallToAnthropicBlocks(msg *schemas.ResponsesMessage)
}

if msg.ID != nil {
serverToolUseBlock.ID = msg.ID
serverToolUseBlock.ID = providerUtils.SanitizeAnthropicToolUseIDPtr(msg.ID)
}

// Extract the query from the action
Expand Down Expand Up @@ -5852,6 +5854,7 @@ func convertBifrostWebSearchCallToAnthropicBlocks(msg *schemas.ResponsesMessage)
} else {
toolUseID = msg.ID
}
toolUseID = providerUtils.SanitizeAnthropicToolUseIDPtr(toolUseID)
webSearchResultBlock := AnthropicContentBlock{
Type: AnthropicContentBlockTypeWebSearchToolResult,
ToolUseID: toolUseID,
Expand Down Expand Up @@ -5949,10 +5952,11 @@ func convertBifrostWebFetchCallToAnthropicBlocks(msg *schemas.ResponsesMessage)
} else {
toolUseID = msg.ID
}
toolUseID = providerUtils.SanitizeAnthropicToolUseIDPtr(toolUseID)

var caller *AnthropicToolCaller
if tm.Caller != nil {
caller = &AnthropicToolCaller{Type: AnthropicToolCallerType(tm.Caller.Type), ToolID: tm.Caller.ToolID}
caller = &AnthropicToolCaller{Type: AnthropicToolCallerType(tm.Caller.Type), ToolID: providerUtils.SanitizeAnthropicToolUseIDPtr(tm.Caller.ToolID)}
}

serverToolUseBlock := AnthropicContentBlock{
Expand Down Expand Up @@ -6032,6 +6036,7 @@ func convertBifrostAdvisorCallToAnthropicBlocks(msg *schemas.ResponsesMessage) [
} else {
toolUseID = msg.ID
}
toolUseID = providerUtils.SanitizeAnthropicToolUseIDPtr(toolUseID)

// 1. server_tool_use block — advisor input is always empty.
serverToolUseBlock := AnthropicContentBlock{
Expand Down Expand Up @@ -6277,6 +6282,7 @@ func convertBifrostCodeExecCallToAnthropicBlocks(msg *schemas.ResponsesMessage)
} else {
toolUseID = msg.ID
}
toolUseID = providerUtils.SanitizeAnthropicToolUseIDPtr(toolUseID)

toolName := cec.ToolName
if toolName == "" {
Expand Down Expand Up @@ -6304,7 +6310,7 @@ func convertBifrostCodeExecCallToAnthropicBlocks(msg *schemas.ResponsesMessage)
if cec.Caller != nil {
serverToolUse.Caller = &AnthropicToolCaller{
Type: AnthropicToolCallerType(cec.Caller.Type),
ToolID: cec.Caller.ToolID,
ToolID: providerUtils.SanitizeAnthropicToolUseIDPtr(cec.Caller.ToolID),
}
}

Expand Down Expand Up @@ -6380,7 +6386,7 @@ func convertBifrostCodeExecCallToAnthropicBlocks(msg *schemas.ResponsesMessage)
if cec.Caller != nil {
resultBlock.Caller = &AnthropicToolCaller{
Type: AnthropicToolCallerType(cec.Caller.Type),
ToolID: cec.Caller.ToolID,
ToolID: providerUtils.SanitizeAnthropicToolUseIDPtr(cec.Caller.ToolID),
}
}

Expand Down Expand Up @@ -6418,7 +6424,7 @@ func convertBifrostComputerCallOutputToAnthropicMessage(msg *schemas.ResponsesMe
if msg.ResponsesToolMessage != nil {
toolResultBlock := AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolResult,
ToolUseID: msg.ResponsesToolMessage.CallID,
ToolUseID: providerUtils.SanitizeAnthropicToolUseIDPtr(msg.ResponsesToolMessage.CallID),
}

if msg.ResponsesToolMessage.Output != nil {
Expand Down
52 changes: 52 additions & 0 deletions core/providers/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"time"

"github.com/bytedance/sonic"
"github.com/cespare/xxhash/v2"
"github.com/maximhq/bifrost/core/network"
"github.com/maximhq/bifrost/core/schemas"
"github.com/tidwall/gjson"
Expand All @@ -40,6 +41,57 @@ import (
// "<baseID>_ts_<signature>".
const ThoughtSignatureSeparator = "_ts_"

// anthropicUnsafeToolUseIDCharRegex matches any character outside Anthropic's
// required tool_use/tool_result id charset (^[a-zA-Z0-9_-]+$).
var anthropicUnsafeToolUseIDCharRegex = regexp.MustCompile(`[^A-Za-z0-9_-]+`)

// maxSanitizedAnthropicToolUseIDLen bounds the sanitized id length, matching the
// 64-char cap this codebase already applies to tool identifiers elsewhere (e.g.
// OpenAI's call_id, Bedrock's tool-name aliasing) so a long, non-conforming
// upstream id can't sanitize into something Anthropic still rejects for length.
const maxSanitizedAnthropicToolUseIDLen = 64

// SanitizeAnthropicToolUseID rewrites a tool_use/tool_result id to satisfy Anthropic's
// ^[a-zA-Z0-9_-]+$ requirement. Some upstream providers (e.g. Kimi/Gemini-compatible
// backends) emit ids containing ':' or '.', which Anthropic's API rejects with a 400
// when such a conversation is replayed through the Anthropic provider. The mapping is
// deterministic (hash of the original id) so a tool_use id and its matching tool_result
// id always sanitize to the same value within a request, matching the alias pattern
// used for Bedrock tool names (see bedrockAliasToolName).
func SanitizeAnthropicToolUseID(id string) string {
// The empty string doesn't match Anthropic's pattern either (it requires at
// least one character), so it needs the same hash-based rewrite as ids with
// disallowed characters rather than being passed through unchanged.
if id != "" && !anthropicUnsafeToolUseIDCharRegex.MatchString(id) {
return id
}
// Use the full 64-bit hash (not a 32-bit truncation) to keep collisions
// between distinct ids astronomically unlikely, since two tool_use blocks
// sharing an id would make Anthropic's replies ambiguous or rejected.
hash := fmt.Sprintf("%016x", xxhash.Sum64String(id))
semantic := strings.Trim(anthropicUnsafeToolUseIDCharRegex.ReplaceAllString(id, "_"), "_")
if semantic == "" {
return hash
}
if maxSemanticLen := maxSanitizedAnthropicToolUseIDLen - len(hash) - 1; len(semantic) > maxSemanticLen {
semantic = strings.Trim(semantic[:maxSemanticLen], "_")
}
if semantic == "" {
return hash
}
return hash + "_" + semantic
}

// SanitizeAnthropicToolUseIDPtr is SanitizeAnthropicToolUseID for an optional id.
// Returns nil unchanged.
func SanitizeAnthropicToolUseIDPtr(id *string) *string {
if id == nil {
return nil
}
sanitized := SanitizeAnthropicToolUseID(*id)
return &sanitized
}

// StripThoughtSignature returns the base tool-call ID without any embedded provider
// reasoning signature. It is deterministic, so a tool call and its matching output strip
// to the same ID. Providers that cannot use the signature (e.g. OpenAI, which caps call_id
Expand Down
Loading