package anthropic import ( "context" "encoding/json" "fmt" "math" "strings" "sync" "time" "github.com/bytedance/sonic" "github.com/maximhq/bifrost/core/schemas" "github.com/tidwall/gjson" providerUtils "github.com/maximhq/bifrost/core/providers/utils" ) // AnthropicResponsesStreamState tracks state during streaming conversion for responses API type AnthropicResponsesStreamState struct { ChunkIndex *int // index of the chunk in the stream (reused for computer AND web search) AccumulatedJSON string // deltas of any event (reused for computer AND web search) // Computer tool accumulation ComputerToolID *string // Web search tool accumulation (minimal fields) WebSearchToolID *string // Tool ID of active web search WebSearchOutputIndex *int // Output index for this search WebSearchResult *AnthropicContentBlock // Result block when it arrives // Web fetch tool accumulation WebFetchToolID *string // Tool ID of active web fetch WebFetchOutputIndex *int // Output index for this fetch // OpenAI Responses API mapping state ContentIndexToOutputIndex map[int]int // Maps Anthropic content_index to OpenAI output_index ContentIndexToBlockType map[int]AnthropicContentBlockType // Tracks content block types ToolArgumentBuffers map[int]string // Maps output_index to accumulated tool argument JSON MCPCallOutputIndices map[int]bool // Tracks which output indices are MCP calls ItemIDs map[int]string // Maps output_index to item ID for stable IDs OutputItems map[int]*schemas.ResponsesMessage // Maps output_index to accumulated output item for response.completed ReasoningSignatures map[int]string // Maps output_index to reasoning signature TextContentIndices map[int]bool // Tracks which content indices are text blocks ReasoningContentIndices map[int]bool // Tracks which content indices are reasoning blocks CompactionContentIndices map[int]*schemas.CacheControl // Tracks pending compaction blocks with their cache control CurrentOutputIndex int // Current output index counter MessageID *string // Message ID from message_start Model *string // Model name from message_start StopReason *string // Stop reason for the message CreatedAt int // Timestamp for created_at consistency HasEmittedCreated bool // Whether we've emitted response.created HasEmittedInProgress bool // Whether we've emitted response.in_progress HasEmittedMessageDelta bool // Whether we've emitted message_delta (avoids duplicate from response.completed) StructuredOutputToolName string // Name of the structured output tool (if using tool-based SO for Vertex) StructuredOutputIndex *int // Output index of the structured output tool call } // anthropicResponsesStreamStatePool provides a pool for Anthropic responses stream state objects. var anthropicResponsesStreamStatePool = sync.Pool{ New: func() interface{} { return &AnthropicResponsesStreamState{ ContentIndexToOutputIndex: make(map[int]int), ToolArgumentBuffers: make(map[int]string), MCPCallOutputIndices: make(map[int]bool), ItemIDs: make(map[int]string), ReasoningSignatures: make(map[int]string), TextContentIndices: make(map[int]bool), ReasoningContentIndices: make(map[int]bool), CompactionContentIndices: make(map[int]*schemas.CacheControl), OutputItems: make(map[int]*schemas.ResponsesMessage), CurrentOutputIndex: 0, CreatedAt: int(time.Now().Unix()), HasEmittedCreated: false, HasEmittedInProgress: false, } }, } // anthropicToResponsesStreamState holds per-request state for the Bifrost→Anthropic // stream conversion direction. type anthropicToResponsesStreamState struct { // webSearchItemIDs tracks item IDs for WebSearch tools so their argument deltas // can be skipped and regenerated synthetically (with sanitization) at output_item.done. webSearchItemIDs map[string]bool } type anthropicToResponsesStreamStateKeyType struct{} var anthropicToResponsesStreamStateKey = anthropicToResponsesStreamStateKeyType{} // getOrCreateAnthropicToResponsesStreamState returns the per-request conversion state, // creating and storing it in ctx on first access. func getOrCreateAnthropicToResponsesStreamState(ctx *schemas.BifrostContext) *anthropicToResponsesStreamState { if v := ctx.Value(anthropicToResponsesStreamStateKey); v != nil { return v.(*anthropicToResponsesStreamState) } state := &anthropicToResponsesStreamState{} ctx.SetValue(anthropicToResponsesStreamStateKey, state) return state } // acquireAnthropicResponsesStreamState gets an Anthropic responses stream state from the pool. func acquireAnthropicResponsesStreamState() *AnthropicResponsesStreamState { state := anthropicResponsesStreamStatePool.Get().(*AnthropicResponsesStreamState) // Clear maps (they're already initialized from New or previous flush) // Only initialize if nil (shouldn't happen, but defensive) if state.ContentIndexToOutputIndex == nil { state.ContentIndexToOutputIndex = make(map[int]int) } else { clear(state.ContentIndexToOutputIndex) } if state.ContentIndexToBlockType == nil { state.ContentIndexToBlockType = make(map[int]AnthropicContentBlockType) } else { clear(state.ContentIndexToBlockType) } if state.ToolArgumentBuffers == nil { state.ToolArgumentBuffers = make(map[int]string) } else { clear(state.ToolArgumentBuffers) } if state.MCPCallOutputIndices == nil { state.MCPCallOutputIndices = make(map[int]bool) } else { clear(state.MCPCallOutputIndices) } if state.ItemIDs == nil { state.ItemIDs = make(map[int]string) } else { clear(state.ItemIDs) } if state.ReasoningSignatures == nil { state.ReasoningSignatures = make(map[int]string) } else { clear(state.ReasoningSignatures) } if state.TextContentIndices == nil { state.TextContentIndices = make(map[int]bool) } else { clear(state.TextContentIndices) } if state.ReasoningContentIndices == nil { state.ReasoningContentIndices = make(map[int]bool) } else { clear(state.ReasoningContentIndices) } if state.CompactionContentIndices == nil { state.CompactionContentIndices = make(map[int]*schemas.CacheControl) } else { clear(state.CompactionContentIndices) } if state.OutputItems == nil { state.OutputItems = make(map[int]*schemas.ResponsesMessage) } else { clear(state.OutputItems) } // Reset other fields state.ChunkIndex = nil state.AccumulatedJSON = "" state.ComputerToolID = nil state.WebSearchToolID = nil state.WebSearchOutputIndex = nil state.WebSearchResult = nil state.WebFetchToolID = nil state.WebFetchOutputIndex = nil state.CurrentOutputIndex = 0 state.MessageID = nil state.StopReason = nil state.Model = nil state.CreatedAt = int(time.Now().Unix()) state.HasEmittedCreated = false state.HasEmittedInProgress = false state.HasEmittedMessageDelta = false state.StructuredOutputToolName = "" state.StructuredOutputIndex = nil return state } // releaseAnthropicResponsesStreamState returns an Anthropic responses stream state to the pool. func releaseAnthropicResponsesStreamState(state *AnthropicResponsesStreamState) { if state != nil { state.flush() // Clean before returning to pool anthropicResponsesStreamStatePool.Put(state) } } // flush resets the state of the stream state to its initial values func (state *AnthropicResponsesStreamState) flush() { state.ChunkIndex = nil state.AccumulatedJSON = "" state.ComputerToolID = nil state.WebSearchToolID = nil state.WebSearchOutputIndex = nil state.WebSearchResult = nil state.WebFetchToolID = nil state.WebFetchOutputIndex = nil state.ContentIndexToOutputIndex = nil state.ContentIndexToBlockType = nil state.ToolArgumentBuffers = nil state.MCPCallOutputIndices = nil state.ItemIDs = nil state.ReasoningSignatures = nil state.TextContentIndices = nil state.ReasoningContentIndices = nil state.CompactionContentIndices = nil state.OutputItems = nil state.CurrentOutputIndex = 0 state.MessageID = nil state.StopReason = nil state.Model = nil state.CreatedAt = int(time.Now().Unix()) state.HasEmittedCreated = false state.HasEmittedInProgress = false state.HasEmittedMessageDelta = false state.StructuredOutputToolName = "" state.StructuredOutputIndex = nil } // isCompactionItem checks if a ResponsesMessage represents a compaction item // (a message with a compaction content block as its first content block) func isCompactionItem(item *schemas.ResponsesMessage) bool { return item != nil && item.Type != nil && *item.Type == schemas.ResponsesMessageTypeMessage && item.Content != nil && len(item.Content.ContentBlocks) > 0 && item.Content.ContentBlocks[0].Type == schemas.ResponsesOutputMessageContentTypeCompaction } // getOrCreateOutputIndex returns the output index for a given content index, creating a new one if needed func (state *AnthropicResponsesStreamState) getOrCreateOutputIndex(contentIndex *int) int { if contentIndex == nil { // If no content index, create a new output index outputIndex := state.CurrentOutputIndex state.CurrentOutputIndex++ return outputIndex } if outputIndex, exists := state.ContentIndexToOutputIndex[*contentIndex]; exists { return outputIndex } // Create new output index for this content index outputIndex := state.CurrentOutputIndex state.CurrentOutputIndex++ state.ContentIndexToOutputIndex[*contentIndex] = outputIndex return outputIndex } // ToBifrostResponsesStream converts an Anthropic stream event to a Bifrost Responses Stream response // It maintains state via the state for handling multi-chunk conversions like computer tools // Returns a slice of responses to support cases where a single event produces multiple responses func (chunk *AnthropicStreamEvent) ToBifrostResponsesStream(ctx context.Context, sequenceNumber int, state *AnthropicResponsesStreamState) ([]*schemas.BifrostResponsesStreamResponse, *schemas.BifrostError, bool) { switch chunk.Type { case AnthropicStreamEventTypeMessageStart: // Message start - emit response.created and response.in_progress (OpenAI-style lifecycle) if chunk.Message != nil { state.MessageID = &chunk.Message.ID state.Model = &chunk.Message.Model // Use the state's CreatedAt for consistency if state.CreatedAt == 0 { state.CreatedAt = int(time.Now().Unix()) } var responses []*schemas.BifrostResponsesStreamResponse // Emit response.created if !state.HasEmittedCreated { response := &schemas.BifrostResponsesResponse{ ID: state.MessageID, CreatedAt: state.CreatedAt, } if state.Model != nil { response.Model = *state.Model } // Forward input usage from message_start so clients see cache metrics early if chunk.Message.Usage != nil { response.Usage = &schemas.ResponsesResponseUsage{ InputTokens: chunk.Message.Usage.InputTokens, OutputTokens: chunk.Message.Usage.OutputTokens, TotalTokens: chunk.Message.Usage.InputTokens + chunk.Message.Usage.OutputTokens, } if chunk.Message.Usage.CacheReadInputTokens > 0 || chunk.Message.Usage.CacheCreationInputTokens > 0 { response.Usage.InputTokensDetails = &schemas.ResponsesResponseInputTokens{ CachedReadTokens: chunk.Message.Usage.CacheReadInputTokens, CachedWriteTokens: chunk.Message.Usage.CacheCreationInputTokens, } // Bifrost convention: InputTokens includes cached tokens response.Usage.InputTokens += chunk.Message.Usage.CacheReadInputTokens + chunk.Message.Usage.CacheCreationInputTokens response.Usage.TotalTokens += chunk.Message.Usage.CacheReadInputTokens + chunk.Message.Usage.CacheCreationInputTokens } } responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeCreated, SequenceNumber: sequenceNumber, Response: response, }) state.HasEmittedCreated = true } // Emit response.in_progress if !state.HasEmittedInProgress { response := &schemas.BifrostResponsesResponse{ ID: state.MessageID, CreatedAt: state.CreatedAt, // Use same timestamp } responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeInProgress, SequenceNumber: sequenceNumber + len(responses), Response: response, }) state.HasEmittedInProgress = true } if len(responses) > 0 { return responses, nil, false } } case AnthropicStreamEventTypeContentBlockStart: // Content block start - emit output_item.added (OpenAI-style) if chunk.ContentBlock != nil && chunk.Index != nil { outputIndex := state.getOrCreateOutputIndex(chunk.Index) if chunk.ContentBlock.Type == AnthropicContentBlockTypeToolUse && chunk.ContentBlock.Name != nil && *chunk.ContentBlock.Name == string(AnthropicToolNameComputer) && chunk.ContentBlock.ID != nil { // Start accumulating computer tool state.ComputerToolID = chunk.ContentBlock.ID state.ChunkIndex = chunk.Index state.AccumulatedJSON = "" // Emit output_item.added for computer_call item := &schemas.ResponsesMessage{ ID: chunk.ContentBlock.ID, Type: schemas.Ptr(schemas.ResponsesMessageTypeComputerCall), ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: chunk.ContentBlock.ID, }, } return []*schemas.BifrostResponsesStreamResponse{{ Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }}, nil, false } // Handle web_search server_tool_use (query block) if chunk.ContentBlock.Type == AnthropicContentBlockTypeServerToolUse && chunk.ContentBlock.Name != nil && *chunk.ContentBlock.Name == string(AnthropicToolNameWebSearch) && chunk.ContentBlock.ID != nil { // Start accumulating web search query (reuse shared accumulation fields) state.ChunkIndex = chunk.Index state.AccumulatedJSON = "" state.WebSearchToolID = chunk.ContentBlock.ID // Store output index value (allocate new int to avoid pointer-to-local-variable issue) state.WebSearchOutputIndex = schemas.Ptr(outputIndex) // Store item ID state.ItemIDs[outputIndex] = *chunk.ContentBlock.ID // Emit output_item.added for web_search_call item := &schemas.ResponsesMessage{ ID: chunk.ContentBlock.ID, Type: schemas.Ptr(schemas.ResponsesMessageTypeWebSearchCall), Status: schemas.Ptr("in_progress"), ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: chunk.ContentBlock.ID, Action: &schemas.ResponsesToolMessageActionStruct{ ResponsesWebSearchToolCallAction: &schemas.ResponsesWebSearchToolCallAction{ Type: "search", }, }, }, } var responses []*schemas.BifrostResponsesStreamResponse // Emit output_item.added responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }) // Emit web_search_call.in_progress responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeWebSearchCallInProgress, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ItemID: chunk.ContentBlock.ID, }) // Emit web_search_call.searching responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeWebSearchCallSearching, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ItemID: chunk.ContentBlock.ID, }) return responses, nil, false } // Handle web_search_tool_result block (results arrive) if chunk.ContentBlock.Type == AnthropicContentBlockTypeWebSearchToolResult && chunk.ContentBlock.ToolUseID != nil { // Track that this content index is a web search result block if chunk.Index != nil { state.ContentIndexToBlockType[*chunk.Index] = AnthropicContentBlockTypeWebSearchToolResult } // Check if this matches our active web search if state.WebSearchToolID != nil && *state.WebSearchToolID == *chunk.ContentBlock.ToolUseID { // Store the result block (arrives complete with all sources) state.WebSearchResult = chunk.ContentBlock if chunk.Index != nil { delete(state.ContentIndexToBlockType, *chunk.Index) } // Emit web_search_call.completed return []*schemas.BifrostResponsesStreamResponse{{ Type: schemas.ResponsesStreamResponseTypeWebSearchCallCompleted, SequenceNumber: sequenceNumber, OutputIndex: state.WebSearchOutputIndex, ItemID: chunk.ContentBlock.ToolUseID, }}, nil, false } // If no matching tool ID, skip (shouldn't happen in normal flow) return nil, nil, false } // Handle web_fetch server_tool_use (fetch block) if chunk.ContentBlock.Type == AnthropicContentBlockTypeServerToolUse && chunk.ContentBlock.Name != nil && *chunk.ContentBlock.Name == string(AnthropicToolNameWebFetch) && chunk.ContentBlock.ID != nil { state.ChunkIndex = chunk.Index state.AccumulatedJSON = "" state.WebFetchToolID = chunk.ContentBlock.ID state.WebFetchOutputIndex = schemas.Ptr(outputIndex) state.ItemIDs[outputIndex] = *chunk.ContentBlock.ID item := &schemas.ResponsesMessage{ ID: chunk.ContentBlock.ID, Type: schemas.Ptr(schemas.ResponsesMessageTypeWebFetchCall), Status: schemas.Ptr("in_progress"), ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: chunk.ContentBlock.ID, }, } var responses []*schemas.BifrostResponsesStreamResponse responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }) responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeWebFetchCallInProgress, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ItemID: chunk.ContentBlock.ID, }) responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeWebFetchCallFetching, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ItemID: chunk.ContentBlock.ID, }) return responses, nil, false } // Handle web_fetch_tool_result block if chunk.ContentBlock.Type == AnthropicContentBlockTypeWebFetchToolResult && chunk.ContentBlock.ToolUseID != nil { if chunk.Index != nil { state.ContentIndexToBlockType[*chunk.Index] = AnthropicContentBlockTypeWebFetchToolResult } if state.WebFetchToolID != nil && *state.WebFetchToolID == *chunk.ContentBlock.ToolUseID { if chunk.Index != nil { delete(state.ContentIndexToBlockType, *chunk.Index) } return []*schemas.BifrostResponsesStreamResponse{{ Type: schemas.ResponsesStreamResponseTypeWebFetchCallCompleted, SequenceNumber: sequenceNumber, OutputIndex: state.WebFetchOutputIndex, ItemID: chunk.ContentBlock.ToolUseID, }}, nil, false } return nil, nil, false } switch chunk.ContentBlock.Type { case AnthropicContentBlockTypeCompaction: // Compaction block - track it but don't emit yet (summary arrives in delta) itemID := fmt.Sprintf("cmp_%d", outputIndex) state.ItemIDs[outputIndex] = itemID // Store cache control for later use when delta arrives state.CompactionContentIndices[outputIndex] = chunk.ContentBlock.CacheControl // Track in ContentIndexToBlockType so content_block_stop skips generic done if chunk.Index != nil { state.ContentIndexToBlockType[*chunk.Index] = AnthropicContentBlockTypeCompaction } // Don't emit output_item.added yet - wait for the delta with actual summary return nil, nil, false case AnthropicContentBlockTypeText: // Text block - emit output_item.added with type "message" messageType := schemas.ResponsesMessageTypeMessage role := schemas.ResponsesInputMessageRoleAssistant // Generate stable ID for text item var itemID string if state.MessageID == nil { itemID = fmt.Sprintf("item_%d", outputIndex) } else { itemID = fmt.Sprintf("msg_%s_item_%d", *state.MessageID, outputIndex) } state.ItemIDs[outputIndex] = itemID item := &schemas.ResponsesMessage{ ID: schemas.Ptr(itemID), Status: schemas.Ptr("in_progress"), Type: &messageType, Role: &role, Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{}, // Empty blocks slice for mutation support }, } // Track that this content index is a text block if chunk.Index != nil { state.TextContentIndices[*chunk.Index] = true } var responses []*schemas.BifrostResponsesStreamResponse // Emit output_item.added responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }) // Emit content_part.added with empty output_text part emptyText := "" part := &schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesOutputMessageContentTypeText, Text: &emptyText, ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{ LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{}, Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{}, }, } responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeContentPartAdded, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, ItemID: &itemID, Part: part, }) return responses, nil, false case AnthropicContentBlockTypeToolUse: // Check if this is the structured output tool - if so, skip emitting tool call if state.StructuredOutputToolName != "" && chunk.ContentBlock.Name != nil && *chunk.ContentBlock.Name == state.StructuredOutputToolName { // Mark this output index for structured output handling state.StructuredOutputIndex = &outputIndex // Initialize argument buffer for accumulating the JSON state.ToolArgumentBuffers[outputIndex] = "" // Mark tool use blocks to prevent synthetic content_part.added events if chunk.Index != nil { state.TextContentIndices[*chunk.Index] = false } // Store item ID for this structured output if chunk.ContentBlock.ID != nil { state.ItemIDs[outputIndex] = *chunk.ContentBlock.ID } return nil, nil, false } // Function call starting - emit output_item.added with type "function_call" and status "in_progress" statusInProgress := "in_progress" itemID := "" if chunk.ContentBlock.ID != nil { itemID = *chunk.ContentBlock.ID state.ItemIDs[outputIndex] = itemID } item := &schemas.ResponsesMessage{ ID: chunk.ContentBlock.ID, Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall), Status: &statusInProgress, ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: chunk.ContentBlock.ID, Name: chunk.ContentBlock.Name, Arguments: schemas.Ptr(""), // Arguments will be filled by deltas }, } // Initialize argument buffer for this tool call state.ToolArgumentBuffers[outputIndex] = "" // Store a cloned copy so later mutations (e.g. setting Arguments/Status // to "completed") don't affect the already-emitted output_item.added event. clonedItem := *item clonedToolMsg := *item.ResponsesToolMessage clonedItem.ResponsesToolMessage = &clonedToolMsg state.OutputItems[outputIndex] = &clonedItem // Mark tool use blocks to prevent synthetic content_part.added events // This prevents extra content_block_stop events for tools like web_search if chunk.Index != nil { state.TextContentIndices[*chunk.Index] = false } return []*schemas.BifrostResponsesStreamResponse{{ Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }}, nil, false case AnthropicContentBlockTypeMCPToolUse: // MCP tool call starting - emit output_item.added itemID := "" if chunk.ContentBlock.ID != nil { itemID = *chunk.ContentBlock.ID state.ItemIDs[outputIndex] = itemID } item := &schemas.ResponsesMessage{ ID: chunk.ContentBlock.ID, Type: schemas.Ptr(schemas.ResponsesMessageTypeMCPCall), ResponsesToolMessage: &schemas.ResponsesToolMessage{ Name: chunk.ContentBlock.Name, Arguments: schemas.Ptr(""), // Arguments will be filled by deltas }, } // Set server name if present if chunk.ContentBlock.ServerName != nil { item.ResponsesToolMessage.ResponsesMCPToolCall = &schemas.ResponsesMCPToolCall{ ServerLabel: *chunk.ContentBlock.ServerName, } } // Initialize argument buffer for this MCP call and mark as MCP state.ToolArgumentBuffers[outputIndex] = "" state.MCPCallOutputIndices[outputIndex] = true // Mark MCP tool use blocks to prevent synthetic content_part.added events if chunk.Index != nil { state.TextContentIndices[*chunk.Index] = false } return []*schemas.BifrostResponsesStreamResponse{{ Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }}, nil, false case AnthropicContentBlockTypeThinking: // Thinking/reasoning block - emit output_item.added with type "reasoning" messageType := schemas.ResponsesMessageTypeReasoning role := schemas.ResponsesInputMessageRoleAssistant // Generate stable ID for reasoning item var itemID string if state.MessageID == nil { itemID = fmt.Sprintf("reasoning_%d", outputIndex) } else { itemID = fmt.Sprintf("msg_%s_reasoning_%d", *state.MessageID, outputIndex) } state.ItemIDs[outputIndex] = itemID // Initialize reasoning structure item := &schemas.ResponsesMessage{ ID: &itemID, Type: &messageType, Role: &role, ResponsesReasoning: &schemas.ResponsesReasoning{ Summary: []schemas.ResponsesReasoningSummary{}, }, } // Track that this content index is a reasoning block if chunk.Index != nil { state.ReasoningContentIndices[*chunk.Index] = true } var responses []*schemas.BifrostResponsesStreamResponse // Emit output_item.added responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }) // Emit content_part.added with empty reasoning_text part emptyText := "" part := &schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesOutputMessageContentTypeReasoning, Text: &emptyText, } // Preserve signature in the content part if present if chunk.ContentBlock.Signature != nil { part.Signature = chunk.ContentBlock.Signature } responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeContentPartAdded, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, ItemID: &itemID, Part: part, }) return responses, nil, false default: // Send down an empty response only when integration type is anthropic if ctx.Value(schemas.BifrostContextKeyIntegrationType) == "anthropic" { return []*schemas.BifrostResponsesStreamResponse{{ Type: "", SequenceNumber: sequenceNumber, }}, nil, false } return nil, nil, false } } case AnthropicStreamEventTypeContentBlockDelta: if chunk.Index != nil && chunk.Delta != nil { outputIndex := state.getOrCreateOutputIndex(chunk.Index) // Handle different delta types switch chunk.Delta.Type { case AnthropicStreamDeltaTypeCompaction: if chunk.Delta.Content != nil { // Compaction summary arrives - emit both output_item.added and output_item.done itemID := state.ItemIDs[outputIndex] messageType := schemas.ResponsesMessageTypeMessage role := schemas.ResponsesInputMessageRoleAssistant // Retrieve cache control stored from content_block_start cacheControl := state.CompactionContentIndices[outputIndex] item := &schemas.ResponsesMessage{ ID: &itemID, Status: schemas.Ptr("completed"), Type: &messageType, Role: &role, Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ { Type: schemas.ResponsesOutputMessageContentTypeCompaction, ResponsesOutputMessageContentCompaction: &schemas.ResponsesOutputMessageContentCompaction{ Summary: *chunk.Delta.Content, }, CacheControl: cacheControl, }, }, }, } // Emit both output_item.added (with summary) and output_item.done return []*schemas.BifrostResponsesStreamResponse{ { Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }, { Type: schemas.ResponsesStreamResponseTypeOutputItemDone, SequenceNumber: sequenceNumber + 1, OutputIndex: schemas.Ptr(outputIndex), ItemID: schemas.Ptr(itemID), Item: item, }, }, nil, false } case AnthropicStreamDeltaTypeText: if chunk.Delta.Text != nil && *chunk.Delta.Text != "" { // Text content delta - emit output_text.delta with item ID itemID := state.ItemIDs[outputIndex] response := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputTextDelta, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Delta: chunk.Delta.Text, } if itemID != "" { response.ItemID = &itemID } return []*schemas.BifrostResponsesStreamResponse{response}, nil, false } case AnthropicStreamDeltaTypeInputJSON: // Function call arguments delta if chunk.Delta.PartialJSON != nil { // Check if we're accumulating any tool (computer or web search) // Both use the shared ChunkIndex and AccumulatedJSON fields if state.ChunkIndex != nil && *state.ChunkIndex == *chunk.Index { // Accumulate the JSON and don't emit anything state.AccumulatedJSON += *chunk.Delta.PartialJSON return nil, nil, false } // Accumulate tool arguments in buffer if _, exists := state.ToolArgumentBuffers[outputIndex]; !exists { state.ToolArgumentBuffers[outputIndex] = "" } state.ToolArgumentBuffers[outputIndex] += *chunk.Delta.PartialJSON // Check if this is the structured output tool - if so, just accumulate without emitting if state.StructuredOutputIndex != nil && *state.StructuredOutputIndex == outputIndex { // This is the structured output tool - accumulate without emitting delta events return nil, nil, false } // Emit appropriate delta type based on whether this is an MCP call var deltaType schemas.ResponsesStreamResponseType if state.MCPCallOutputIndices[outputIndex] { deltaType = schemas.ResponsesStreamResponseTypeMCPCallArgumentsDelta } else { deltaType = schemas.ResponsesStreamResponseTypeFunctionCallArgumentsDelta } itemID := state.ItemIDs[outputIndex] response := &schemas.BifrostResponsesStreamResponse{ Type: deltaType, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Delta: chunk.Delta.PartialJSON, } if itemID != "" { response.ItemID = &itemID } return []*schemas.BifrostResponsesStreamResponse{response}, nil, false } case AnthropicStreamDeltaTypeThinking: // Reasoning/thinking content delta if chunk.Delta.Thinking != nil && *chunk.Delta.Thinking != "" { itemID := state.ItemIDs[outputIndex] response := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDelta, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Delta: chunk.Delta.Thinking, } if itemID != "" { response.ItemID = &itemID } return []*schemas.BifrostResponsesStreamResponse{response}, nil, false } case AnthropicStreamDeltaTypeSignature: // Handle signature verification for thinking content // Store the signature in state for the reasoning item if chunk.Delta.Signature != nil && *chunk.Delta.Signature != "" { state.ReasoningSignatures[outputIndex] = *chunk.Delta.Signature // Emit signature_delta event using the signature field itemID := state.ItemIDs[outputIndex] response := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDelta, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Signature: chunk.Delta.Signature, // Use signature field instead of delta } if itemID != "" { response.ItemID = &itemID } return []*schemas.BifrostResponsesStreamResponse{response}, nil, false } return nil, nil, false case AnthropicStreamDeltaTypeCitations: // Handle citations delta - convert Anthropic citation to OpenAI annotation if chunk.Delta.Citation != nil { // For streaming, we don't compute indices yet (pass empty string) annotation := convertAnthropicCitationToAnnotation(*chunk.Delta.Citation, "") // Emit output_text.annotation.added event itemID := state.ItemIDs[outputIndex] response := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputTextAnnotationAdded, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Annotation: &annotation, } if itemID != "" { response.ItemID = &itemID } return []*schemas.BifrostResponsesStreamResponse{response}, nil, false } return nil, nil, false } } case AnthropicStreamEventTypeContentBlockStop: // Content block is complete - emit output_item.done (OpenAI-style) if chunk.Index != nil { outputIndex := state.getOrCreateOutputIndex(chunk.Index) // Check if this is the end of a tool accumulation (computer or web search query) if state.ChunkIndex != nil && *state.ChunkIndex == *chunk.Index { // Computer tool completion if state.ComputerToolID != nil { // Parse accumulated JSON and convert to OpenAI format var inputMap map[string]interface{} var action *schemas.ResponsesComputerToolCallAction if state.AccumulatedJSON != "" { if err := sonic.Unmarshal([]byte(state.AccumulatedJSON), &inputMap); err == nil { action = convertAnthropicToResponsesComputerAction(inputMap) } } // Create computer_call item with action statusCompleted := "completed" item := &schemas.ResponsesMessage{ ID: state.ComputerToolID, Type: schemas.Ptr(schemas.ResponsesMessageTypeComputerCall), Status: &statusCompleted, ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: state.ComputerToolID, ResponsesComputerToolCall: &schemas.ResponsesComputerToolCall{ PendingSafetyChecks: []schemas.ResponsesComputerToolCallPendingSafetyCheck{}, }, }, } // Add action if we successfully parsed it if action != nil { item.ResponsesToolMessage.Action = &schemas.ResponsesToolMessageActionStruct{ ResponsesComputerToolCallAction: action, } } // Clear computer tool state state.ComputerToolID = nil state.ChunkIndex = nil state.AccumulatedJSON = "" // Return output_item.done return []*schemas.BifrostResponsesStreamResponse{ { Type: schemas.ResponsesStreamResponseTypeOutputItemDone, SequenceNumber: sequenceNumber, OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }, }, nil, false } // Web search query block ended (don't emit output_item.done yet - wait for result) if state.WebSearchToolID != nil { // Clear ChunkIndex (done accumulating query) // Keep WebSearchToolID, WebSearchOutputIndex, and AccumulatedJSON (need them for final item) state.ChunkIndex = nil return nil, nil, false } } // Check if this is the end of a web_search_tool_result block if state.WebSearchResult != nil && state.WebSearchToolID != nil { // Parse the query from AccumulatedJSON var query string var queries []string if state.AccumulatedJSON != "" { if q := providerUtils.GetJSONField([]byte(state.AccumulatedJSON), "query"); q.Exists() && q.Type == gjson.String { query = q.Str queries = []string{q.Str} } } // Extract sources from the result block var sources []schemas.ResponsesWebSearchToolCallActionSearchSource if state.WebSearchResult.Content != nil && len(state.WebSearchResult.Content.ContentBlocks) > 0 { for _, resultBlock := range state.WebSearchResult.Content.ContentBlocks { if resultBlock.Type == AnthropicContentBlockTypeWebSearchResult && resultBlock.URL != nil { sources = append(sources, schemas.ResponsesWebSearchToolCallActionSearchSource{ Type: "url", URL: *resultBlock.URL, Title: resultBlock.Title, EncryptedContent: resultBlock.EncryptedContent, PageAge: resultBlock.PageAge, }) } } } // Create complete web_search_call item with action including query and sources statusCompleted := "completed" action := &schemas.ResponsesWebSearchToolCallAction{ Type: "search", Sources: sources, } // Only set query fields if query is not empty if query != "" { action.Query = &query action.Queries = queries } item := &schemas.ResponsesMessage{ ID: state.WebSearchToolID, Type: schemas.Ptr(schemas.ResponsesMessageTypeWebSearchCall), Status: &statusCompleted, ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: state.WebSearchToolID, Action: &schemas.ResponsesToolMessageActionStruct{ ResponsesWebSearchToolCallAction: action, }, }, } outputIdx := state.WebSearchOutputIndex // Clear all web search state state.WebSearchToolID = nil state.WebSearchOutputIndex = nil state.WebSearchResult = nil state.AccumulatedJSON = "" if chunk.Index != nil { delete(state.ContentIndexToBlockType, *chunk.Index) } // Return output_item.done for the web_search_call (not the result block) return []*schemas.BifrostResponsesStreamResponse{ { Type: schemas.ResponsesStreamResponseTypeOutputItemDone, SequenceNumber: sequenceNumber, OutputIndex: outputIdx, ContentIndex: chunk.Index, Item: item, }, }, nil, false } // Skip generic output_item.done if this is a web_search_tool_result or compaction block // (their handlers already emitted the proper done event) if chunk.Index != nil { if blockType, exists := state.ContentIndexToBlockType[*chunk.Index]; exists { if blockType == AnthropicContentBlockTypeWebSearchToolResult || blockType == AnthropicContentBlockTypeWebFetchToolResult { delete(state.ContentIndexToBlockType, *chunk.Index) return nil, nil, false } if blockType == AnthropicContentBlockTypeCompaction { // Clean up the tracking delete(state.ContentIndexToBlockType, *chunk.Index) return nil, nil, false } } } // Check if this is a text block - emit output_text.done and content_part.done var responses []*schemas.BifrostResponsesStreamResponse itemID := state.ItemIDs[outputIndex] // Check if this content index is a text block if chunk.Index != nil { if state.TextContentIndices[*chunk.Index] { // Emit output_text.done (without accumulated text, just the event) emptyText := "" textDoneResponse := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputTextDone, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Text: &emptyText, } if itemID != "" { textDoneResponse.ItemID = &itemID } responses = append(responses, textDoneResponse) // Emit content_part.done partDoneResponse := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeContentPartDone, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, } if itemID != "" { partDoneResponse.ItemID = &itemID } responses = append(responses, partDoneResponse) // Clear the text content index tracking delete(state.TextContentIndices, *chunk.Index) } // Check if this content index is a reasoning block if state.ReasoningContentIndices[*chunk.Index] { // Emit reasoning_summary_text.done (reasoning equivalent of output_text.done) emptyText := "" reasoningDoneResponse := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDone, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Text: &emptyText, } if itemID != "" { reasoningDoneResponse.ItemID = &itemID } responses = append(responses, reasoningDoneResponse) // Emit content_part.done for reasoning partDoneResponse := &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeContentPartDone, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, } if itemID != "" { partDoneResponse.ItemID = &itemID } responses = append(responses, partDoneResponse) // Clear the reasoning content index tracking delete(state.ReasoningContentIndices, *chunk.Index) } } // Check if this is a structured output tool call if accumulatedArgs, hasArgs := state.ToolArgumentBuffers[outputIndex]; hasArgs && state.StructuredOutputIndex != nil && *state.StructuredOutputIndex == outputIndex { // This was a structured output tool - emit as text message instead textContent := accumulatedArgs if textContent == "" { textContent = "{}" } // Create ContentBlocks with output_text type instead of ContentStr contentBlock := schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesOutputMessageContentTypeText, Text: &textContent, ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{ Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{}, LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{}, }, } item := &schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant), Status: schemas.Ptr("completed"), Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{contentBlock}, }, } if itemID != "" { item.ID = &itemID } // Emit output_item.added for the text message responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputItemAdded, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }) // Emit output_item.done responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputItemDone, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: item, }) // Clear the buffer and tracking delete(state.ToolArgumentBuffers, outputIndex) state.StructuredOutputIndex = nil return responses, nil, false } // Check if this is a tool call (function_call or MCP call) // If we have accumulated arguments, emit appropriate arguments.done first // Note: we check hasArgs only (not accumulatedArgs != "") to handle zero-arg tool calls if accumulatedArgs, hasArgs := state.ToolArgumentBuffers[outputIndex]; hasArgs { // Update the stored output item with the final arguments if storedItem, exists := state.OutputItems[outputIndex]; exists && storedItem.ResponsesToolMessage != nil { storedItem.ResponsesToolMessage.Arguments = &accumulatedArgs storedItem.Status = schemas.Ptr("completed") } // Emit appropriate arguments.done based on whether this is an MCP call var doneType schemas.ResponsesStreamResponseType if state.MCPCallOutputIndices[outputIndex] { doneType = schemas.ResponsesStreamResponseTypeMCPCallArgumentsDone } else { doneType = schemas.ResponsesStreamResponseTypeFunctionCallArgumentsDone } response := &schemas.BifrostResponsesStreamResponse{ Type: doneType, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Arguments: &accumulatedArgs, } if itemID != "" { response.ItemID = &itemID } responses = append(responses, response) // Clear the buffer and MCP tracking delete(state.ToolArgumentBuffers, outputIndex) delete(state.MCPCallOutputIndices, outputIndex) } // Emit output_item.done for all content blocks (text, tool, etc.) statusCompleted := "completed" doneItemID := state.ItemIDs[outputIndex] var doneItem *schemas.ResponsesMessage if storedItem, exists := state.OutputItems[outputIndex]; exists { copied := *storedItem if storedItem.ResponsesToolMessage != nil { toolMsgCopy := *storedItem.ResponsesToolMessage copied.ResponsesToolMessage = &toolMsgCopy } doneItem = &copied } else { doneItem = &schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: schemas.Ptr(schemas.ResponsesInputMessageRoleAssistant), Status: &statusCompleted, Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{}, }, } if doneItemID != "" { doneItem.ID = &doneItemID } } responses = append(responses, &schemas.BifrostResponsesStreamResponse{ Type: schemas.ResponsesStreamResponseTypeOutputItemDone, SequenceNumber: sequenceNumber + len(responses), OutputIndex: schemas.Ptr(outputIndex), ContentIndex: chunk.Index, Item: doneItem, }) return responses, nil, false } case AnthropicStreamEventTypeMessageDelta: if chunk.Delta.StopReason != nil { state.StopReason = schemas.Ptr(ConvertAnthropicFinishReasonToBifrost(*chunk.Delta.StopReason)) } // Check if integration type in ctx is anthropic if ctx.Value(schemas.BifrostContextKeyIntegrationType) == "anthropic" { // Convert usage from Anthropic format to Bifrost bifrostUsage := ConvertAnthropicUsageToBifrostUsage(chunk.Usage) // Convert stop reason if present var stopReason *string if chunk.Delta != nil && chunk.Delta.StopReason != nil { converted := ConvertAnthropicFinishReasonToBifrost(*chunk.Delta.StopReason) stopReason = &converted } // Create response object with usage and stop reason response := &schemas.BifrostResponsesResponse{ CreatedAt: state.CreatedAt, } if state.MessageID != nil { response.ID = state.MessageID } if state.Model != nil { response.Model = *state.Model } if stopReason != nil { response.StopReason = stopReason } if bifrostUsage != nil { response.Usage = bifrostUsage } // Mark that we already emitted a message_delta so response.completed // doesn't synthesize a duplicate one. state.HasEmittedMessageDelta = true return []*schemas.BifrostResponsesStreamResponse{{ Type: "message_delta", SequenceNumber: sequenceNumber, Response: response, }}, nil, false } // Message-level updates (like stop reason, usage, etc.) // Note: We don't emit output_item.done here because items are already closed // by content_block_stop. This event is informational only. return nil, nil, false case AnthropicStreamEventTypeMessageStop: // Message stop - emit response.completed (OpenAI-style) response := &schemas.BifrostResponsesResponse{ CreatedAt: state.CreatedAt, } if state.MessageID != nil { response.ID = state.MessageID } if state.Model != nil { response.Model = *state.Model } if state.StopReason != nil { response.StopReason = state.StopReason } // Populate the Output array from accumulated items for response.completed // This is needed for clients that check Output for function_call items if len(state.OutputItems) > 0 { // Sort by output index to maintain order response.Output = make([]schemas.ResponsesMessage, 0, len(state.OutputItems)) for i := 0; i < state.CurrentOutputIndex; i++ { if item, exists := state.OutputItems[i]; exists { response.Output = append(response.Output, *item) } } } return []*schemas.BifrostResponsesStreamResponse{{ Type: schemas.ResponsesStreamResponseTypeCompleted, SequenceNumber: sequenceNumber, Response: response, }}, nil, true // Indicate stream is complete case AnthropicStreamEventTypePing: return []*schemas.BifrostResponsesStreamResponse{{ Type: schemas.ResponsesStreamResponseTypePing, SequenceNumber: sequenceNumber, }}, nil, false case AnthropicStreamEventTypeError: if chunk.Error != nil { // Send error event bifrostErr := &schemas.BifrostError{ IsBifrostError: false, Error: &schemas.ErrorField{ Type: &chunk.Error.Type, Message: chunk.Error.Message, }, } return []*schemas.BifrostResponsesStreamResponse{{ Type: schemas.ResponsesStreamResponseTypeError, SequenceNumber: sequenceNumber, Message: &chunk.Error.Message, }}, bifrostErr, false } } return nil, nil, false } // ToAnthropicResponsesStreamResponse converts a Bifrost Responses stream response to Anthropic SSE string format func ToAnthropicResponsesStreamResponse(ctx *schemas.BifrostContext, bifrostResp *schemas.BifrostResponsesStreamResponse) []*AnthropicStreamEvent { if bifrostResp == nil { return nil } streamResp := &AnthropicStreamEvent{} // Map ResponsesStreamResponse types to Anthropic stream events switch bifrostResp.Type { case schemas.ResponsesStreamResponseTypeCreated: // Only convert response.created back to message_start (not response.in_progress to avoid duplicates) streamResp.Type = AnthropicStreamEventTypeMessageStart if bifrostResp.Response != nil { // Use actual usage if available (forwarded from upstream message_start), // otherwise fall back to zeros for non-Anthropic providers var messageUsage *AnthropicUsage if bifrostResp.Response.Usage != nil { messageUsage = ConvertBifrostUsageToAnthropicUsage(bifrostResp.Response.Usage) } else { messageUsage = &AnthropicUsage{ InputTokens: 0, OutputTokens: 0, CacheReadInputTokens: 0, CacheCreationInputTokens: 0, CacheCreation: AnthropicUsageCacheCreation{ Ephemeral5mInputTokens: 0, Ephemeral1hInputTokens: 0, }, } } streamMessage := &AnthropicMessageResponse{ Type: "message", Role: "assistant", Content: []AnthropicContentBlock{}, // Always empty array in message_start Usage: messageUsage, } if bifrostResp.Response.ID != nil { streamMessage.ID = *bifrostResp.Response.ID } // Prefer Response.Model, then ResolvedModelUsed, then OriginalModelRequested if bifrostResp.Response != nil && bifrostResp.Response.Model != "" { streamMessage.Model = bifrostResp.Response.Model } else if bifrostResp.ExtraFields.ResolvedModelUsed != "" { streamMessage.Model = bifrostResp.ExtraFields.ResolvedModelUsed } else if bifrostResp.ExtraFields.OriginalModelRequested != "" { streamMessage.Model = bifrostResp.ExtraFields.OriginalModelRequested } streamResp.Message = streamMessage } case schemas.ResponsesStreamResponseTypeInProgress: // Skip converting response.in_progress back to avoid duplicate message_start events // This is an OpenAI-style lifecycle event that doesn't map directly to Anthropic events return nil case schemas.ResponsesStreamResponseTypeOutputItemAdded: // Check if this is a computer tool call if bifrostResp.Item != nil && bifrostResp.Item.Type != nil && *bifrostResp.Item.Type == schemas.ResponsesMessageTypeComputerCall { // Computer tool - emit content_block_start streamResp.Type = AnthropicStreamEventTypeContentBlockStart if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } // Build the content_block as tool_use // 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" } // Always start with empty input for streaming compatibility contentBlock.Input = json.RawMessage("{}") streamResp.ContentBlock = contentBlock } else if bifrostResp.Item != nil && bifrostResp.Item.Type != nil && *bifrostResp.Item.Type == schemas.ResponsesMessageTypeWebSearchCall { // Web search call - emit content_block_start with server_tool_use streamResp.Type = AnthropicStreamEventTypeContentBlockStart if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } else if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } // 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" } // Start with empty input for streaming compatibility contentBlock.Input = json.RawMessage("{}") streamResp.ContentBlock = contentBlock } else { // Text or other content blocks - emit content_block_start streamResp.Type = AnthropicStreamEventTypeContentBlockStart // Use OutputIndex for global Anthropic indexing if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } // Build content_block based on item type if bifrostResp.Item != nil { contentBlock := &AnthropicContentBlock{} // Check if this is a compaction item (message with compaction content block) if isCompactionItem(bifrostResp.Item) { contentBlock.Type = AnthropicContentBlockTypeCompaction contentBlock.Content = &AnthropicContent{ContentStr: schemas.Ptr("")} if bifrostResp.Item.Content.ContentBlocks[0].CacheControl != nil { contentBlock.CacheControl = bifrostResp.Item.Content.ContentBlocks[0].CacheControl } } else if bifrostResp.Item.Type != nil { switch *bifrostResp.Item.Type { case schemas.ResponsesMessageTypeMessage: contentBlock.Type = AnthropicContentBlockTypeText contentBlock.Text = schemas.Ptr("") case schemas.ResponsesMessageTypeReasoning: contentBlock.Type = AnthropicContentBlockTypeThinking contentBlock.Thinking = schemas.Ptr("") contentBlock.Signature = schemas.Ptr("") // Preserve signature if present if bifrostResp.Item.ResponsesReasoning != nil && bifrostResp.Item.ResponsesReasoning.EncryptedContent != nil && *bifrostResp.Item.ResponsesReasoning.EncryptedContent != "" { contentBlock.Data = bifrostResp.Item.ResponsesReasoning.EncryptedContent // When signature is present but thinking content is empty, use redacted_thinking if contentBlock.Thinking != nil && *contentBlock.Thinking == "" { contentBlock.Type = AnthropicContentBlockTypeRedactedThinking } } case schemas.ResponsesMessageTypeFunctionCall: // Check if this item actually has reasoning content (misclassified) // When thinking is enabled, reasoning content might be incorrectly classified as FunctionCall if bifrostResp.Item.ResponsesReasoning != nil { // This is actually reasoning content, not a function call contentBlock.Type = AnthropicContentBlockTypeThinking contentBlock.Thinking = schemas.Ptr("") contentBlock.Signature = schemas.Ptr("") // Check if there's encrypted content for redacted_thinking if bifrostResp.Item.ResponsesReasoning.EncryptedContent != nil && *bifrostResp.Item.ResponsesReasoning.EncryptedContent != "" { contentBlock.Type = AnthropicContentBlockTypeRedactedThinking contentBlock.Data = bifrostResp.Item.ResponsesReasoning.EncryptedContent } } else { // Regular function call - check if ContentIndex is 0 and thinking might be enabled // If ContentIndex is 0, we need to check if there's reasoning content in the response contentIndex := 0 if bifrostResp.ContentIndex != nil { contentIndex = *bifrostResp.ContentIndex } isFirstBlock := contentIndex == 0 // Check if response has reasoning content (indicating thinking is enabled) hasReasoningInResponse := false if bifrostResp.Response != nil && bifrostResp.Response.Output != nil { for _, msg := range bifrostResp.Response.Output { if msg.Type != nil && *msg.Type == schemas.ResponsesMessageTypeReasoning { hasReasoningInResponse = true break } } } // When thinking is enabled and this is the first block, use thinking/redacted_thinking if isFirstBlock && hasReasoningInResponse { contentBlock.Type = AnthropicContentBlockTypeThinking contentBlock.Thinking = schemas.Ptr("") contentBlock.Signature = schemas.Ptr("") } else { contentBlock.Type = AnthropicContentBlockTypeToolUse if bifrostResp.Item.ResponsesToolMessage != nil { contentBlock.ID = bifrostResp.Item.ResponsesToolMessage.CallID contentBlock.Name = bifrostResp.Item.ResponsesToolMessage.Name // Always start with empty input for streaming compatibility contentBlock.Input = json.RawMessage("{}") // Track WebSearch tools so we can skip their argument deltas // and regenerate them synthetically (with sanitization) at output_item.done if bifrostResp.Item.ResponsesToolMessage.Name != nil && *bifrostResp.Item.ResponsesToolMessage.Name == "WebSearch" && bifrostResp.Item.ID != nil { streamState := getOrCreateAnthropicToResponsesStreamState(ctx) if streamState.webSearchItemIDs == nil { streamState.webSearchItemIDs = make(map[string]bool) } streamState.webSearchItemIDs[*bifrostResp.Item.ID] = true } } } } case schemas.ResponsesMessageTypeMCPCall: contentBlock.Type = AnthropicContentBlockTypeMCPToolUse if bifrostResp.Item.ResponsesToolMessage != nil { contentBlock.ID = bifrostResp.Item.ID contentBlock.Name = bifrostResp.Item.ResponsesToolMessage.Name if bifrostResp.Item.ResponsesToolMessage.ResponsesMCPToolCall != nil { contentBlock.ServerName = &bifrostResp.Item.ResponsesToolMessage.ResponsesMCPToolCall.ServerLabel } // Always start with empty input for streaming compatibility contentBlock.Input = json.RawMessage("{}") } } } if contentBlock.Type != "" { streamResp.ContentBlock = contentBlock } } } // Generate synthetic input_json_delta events for tool calls with arguments var events []*AnthropicStreamEvent events = append(events, streamResp) // Generate compaction_delta event for compaction items if isCompactionItem(bifrostResp.Item) { block := bifrostResp.Item.Content.ContentBlocks[0] if block.ResponsesOutputMessageContentCompaction != nil { var indexToUse *int if bifrostResp.OutputIndex != nil { indexToUse = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { indexToUse = bifrostResp.ContentIndex } events = append(events, &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeContentBlockDelta, Index: indexToUse, Delta: &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeCompaction, Content: &block.ResponsesOutputMessageContentCompaction.Summary, }, }) } } // Check if this is a tool call with arguments that need to be streamed if bifrostResp.Item != nil && bifrostResp.Item.ResponsesToolMessage != nil { var argumentsJSON string var shouldGenerateDeltas bool switch *bifrostResp.Item.Type { case schemas.ResponsesMessageTypeFunctionCall: if bifrostResp.Item.ResponsesToolMessage.Arguments != nil && *bifrostResp.Item.ResponsesToolMessage.Arguments != "" { argumentsJSON = *bifrostResp.Item.ResponsesToolMessage.Arguments shouldGenerateDeltas = true } case schemas.ResponsesMessageTypeMCPCall: if bifrostResp.Item.ResponsesToolMessage.Arguments != nil && *bifrostResp.Item.ResponsesToolMessage.Arguments != "" { argumentsJSON = *bifrostResp.Item.ResponsesToolMessage.Arguments shouldGenerateDeltas = true } case schemas.ResponsesMessageTypeComputerCall: if bifrostResp.Item.ResponsesToolMessage.Action != nil && bifrostResp.Item.ResponsesToolMessage.Action.ResponsesComputerToolCallAction != nil { actionInput := convertResponsesToAnthropicComputerAction(bifrostResp.Item.ResponsesToolMessage.Action.ResponsesComputerToolCallAction) if jsonBytes, err := providerUtils.MarshalSorted(actionInput); err == nil { argumentsJSON = string(jsonBytes) shouldGenerateDeltas = true } } } if shouldGenerateDeltas && argumentsJSON != "" { // Generate synthetic input_json_delta events by chunking the JSON var indexToUse *int if bifrostResp.OutputIndex != nil { indexToUse = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { indexToUse = bifrostResp.ContentIndex } deltaEvents := generateSyntheticInputJSONDeltas(argumentsJSON, indexToUse) events = append(events, deltaEvents...) } } return events case schemas.ResponsesStreamResponseTypeContentPartAdded: return nil case schemas.ResponsesStreamResponseTypeOutputTextDelta: streamResp.Type = AnthropicStreamEventTypeContentBlockDelta // Use OutputIndex instead of ContentIndex for global Anthropic indexing if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { // Fallback to ContentIndex if OutputIndex not available streamResp.Index = bifrostResp.ContentIndex } if bifrostResp.Delta != nil { streamResp.Delta = &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeText, Text: bifrostResp.Delta, } } case schemas.ResponsesStreamResponseTypeFunctionCallArgumentsDelta: // Skip WebSearch tool argument deltas - they will be sent synthetically in output_item.done if bifrostResp.ItemID != nil { streamState := getOrCreateAnthropicToResponsesStreamState(ctx) if streamState.webSearchItemIDs[*bifrostResp.ItemID] { return nil } } streamResp.Type = AnthropicStreamEventTypeContentBlockDelta // Use OutputIndex for global Anthropic indexing if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } if bifrostResp.Arguments != nil { streamResp.Delta = &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeInputJSON, PartialJSON: bifrostResp.Arguments, } } else if bifrostResp.Delta != nil { // Handle cases where Delta field is used instead of Arguments streamResp.Delta = &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeInputJSON, PartialJSON: bifrostResp.Delta, } } case schemas.ResponsesStreamResponseTypeReasoningSummaryTextDelta: streamResp.Type = AnthropicStreamEventTypeContentBlockDelta // Use OutputIndex for global Anthropic indexing if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } // Check if this is a signature delta or text delta if bifrostResp.Signature != nil { // This is a signature_delta streamResp.Delta = &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeSignature, Signature: bifrostResp.Signature, } } else if bifrostResp.Delta != nil { // This is a thinking_delta streamResp.Delta = &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeThinking, Thinking: bifrostResp.Delta, } } case schemas.ResponsesStreamResponseTypeOutputTextAnnotationAdded: // Convert OpenAI annotation to Anthropic citation if bifrostResp.Annotation != nil { streamResp.Type = AnthropicStreamEventTypeContentBlockDelta if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } citation := convertAnnotationToAnthropicCitation(*bifrostResp.Annotation) streamResp.Delta = &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeCitations, Citation: &citation, } } case schemas.ResponsesStreamResponseTypeContentPartDone: return nil case schemas.ResponsesStreamResponseTypeOutputItemDone: // Handle WebSearch tool completion with sanitization and synthetic delta generation if bifrostResp.Item != nil && bifrostResp.Item.Type != nil && *bifrostResp.Item.Type == schemas.ResponsesMessageTypeFunctionCall && bifrostResp.Item.ResponsesToolMessage != nil && bifrostResp.Item.ResponsesToolMessage.Name != nil && *bifrostResp.Item.ResponsesToolMessage.Name == "WebSearch" && bifrostResp.Item.ResponsesToolMessage.Arguments != nil { argumentsJSON := sanitizeWebSearchArguments(*bifrostResp.Item.ResponsesToolMessage.Arguments) bifrostResp.Item.ResponsesToolMessage.Arguments = &argumentsJSON // Generate synthetic input_json_delta events for the sanitized WebSearch arguments // This replaces the delta events that were skipped earlier var events []*AnthropicStreamEvent // Use OutputIndex for proper Anthropic indexing, fallback to ContentIndex var indexToUse *int if bifrostResp.OutputIndex != nil { indexToUse = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { indexToUse = bifrostResp.ContentIndex } deltaEvents := generateSyntheticInputJSONDeltas(argumentsJSON, indexToUse) events = append(events, deltaEvents...) // Add the content_block_stop event at the end stopEvent := &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeContentBlockStop, Index: indexToUse, } events = append(events, stopEvent) // Clean up the tracking for this WebSearch item if bifrostResp.Item.ID != nil { streamState := getOrCreateAnthropicToResponsesStreamState(ctx) delete(streamState.webSearchItemIDs, *bifrostResp.Item.ID) } return events } if bifrostResp.Item != nil && bifrostResp.Item.Type != nil && *bifrostResp.Item.Type == schemas.ResponsesMessageTypeComputerCall { // Computer tool complete - emit content_block_delta with the action, then stop // Note: We're sending the complete action JSON in one delta streamResp.Type = AnthropicStreamEventTypeContentBlockDelta // Use OutputIndex for global Anthropic indexing if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } // Convert the action to Anthropic format and marshal to JSON if bifrostResp.Item.ResponsesToolMessage != nil && bifrostResp.Item.ResponsesToolMessage.Action != nil && bifrostResp.Item.ResponsesToolMessage.Action.ResponsesComputerToolCallAction != nil { actionInput := convertResponsesToAnthropicComputerAction( bifrostResp.Item.ResponsesToolMessage.Action.ResponsesComputerToolCallAction, ) // Marshal the action to JSON string if jsonBytes, err := providerUtils.MarshalSorted(actionInput); err == nil { jsonStr := string(jsonBytes) streamResp.Delta = &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeInputJSON, PartialJSON: &jsonStr, } } } } else if bifrostResp.Item != nil && bifrostResp.Item.Type != nil && *bifrostResp.Item.Type == schemas.ResponsesMessageTypeWebSearchCall { // Web search call complete - generate synthetic input_json_delta events, then emit content_block_stop var events []*AnthropicStreamEvent // Extract query from web search action for synthetic delta generation var queryJSON string if bifrostResp.Item.ResponsesToolMessage != nil && bifrostResp.Item.ResponsesToolMessage.Action != nil && bifrostResp.Item.ResponsesToolMessage.Action.ResponsesWebSearchToolCallAction != nil && bifrostResp.Item.ResponsesToolMessage.Action.ResponsesWebSearchToolCallAction.Query != nil { // Create input map with query inputMap := map[string]interface{}{ "query": *bifrostResp.Item.ResponsesToolMessage.Action.ResponsesWebSearchToolCallAction.Query, } if jsonBytes, err := providerUtils.MarshalSorted(inputMap); err == nil { queryJSON = string(jsonBytes) } } // Generate synthetic input_json_delta events if we have a query if queryJSON != "" { var indexToUse *int if bifrostResp.OutputIndex != nil { indexToUse = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { indexToUse = bifrostResp.ContentIndex } deltaEvents := generateSyntheticInputJSONDeltas(queryJSON, indexToUse) events = append(events, deltaEvents...) } // 1. Emit content_block_stop for the query block (server_tool_use) stopEvent := &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeContentBlockStop, } if bifrostResp.ContentIndex != nil { stopEvent.Index = bifrostResp.ContentIndex } else if bifrostResp.OutputIndex != nil { stopEvent.Index = bifrostResp.OutputIndex } events = append(events, stopEvent) // 2. Extract sources and create web_search_tool_result block if sources exist if bifrostResp.Item.ResponsesToolMessage != nil && bifrostResp.Item.ResponsesToolMessage.Action != nil && bifrostResp.Item.ResponsesToolMessage.Action.ResponsesWebSearchToolCallAction != nil && len(bifrostResp.Item.ResponsesToolMessage.Action.ResponsesWebSearchToolCallAction.Sources) > 0 { // Calculate next index for result block var resultIndex *int if bifrostResp.OutputIndex != nil { nextIdx := *bifrostResp.OutputIndex + 1 resultIndex = &nextIdx } else if bifrostResp.ContentIndex != nil { nextIdx := *bifrostResp.ContentIndex + 1 resultIndex = &nextIdx } // Create content blocks for each source var resultContentBlocks []AnthropicContentBlock for _, source := range bifrostResp.Item.ResponsesToolMessage.Action.ResponsesWebSearchToolCallAction.Sources { block := AnthropicContentBlock{ Type: AnthropicContentBlockTypeWebSearchResult, URL: &source.URL, EncryptedContent: source.EncryptedContent, PageAge: source.PageAge, } if source.Title != nil { block.Title = source.Title } else if source.URL != "" { block.Title = schemas.Ptr(source.URL) } resultContentBlocks = append(resultContentBlocks, block) } // Emit content_block_start for web_search_tool_result resultStartEvent := &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeContentBlockStart, Index: resultIndex, ContentBlock: &AnthropicContentBlock{ Type: AnthropicContentBlockTypeWebSearchToolResult, ToolUseID: bifrostResp.Item.ID, // Link to the server_tool_use block Content: &AnthropicContent{ ContentBlocks: resultContentBlocks, }, }, } events = append(events, resultStartEvent) // Emit content_block_stop for the result block resultStopEvent := &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeContentBlockStop, Index: resultIndex, } events = append(events, resultStopEvent) } return events } else if bifrostResp.Item != nil && bifrostResp.Item.Type != nil && (*bifrostResp.Item.Type == schemas.ResponsesMessageTypeFunctionCall || *bifrostResp.Item.Type == schemas.ResponsesMessageTypeMCPCall) { // Function call or MCP call complete - just emit content_block_stop streamResp.Type = AnthropicStreamEventTypeContentBlockStop if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } else if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } } else { // For text blocks and other content blocks, emit content_block_stop streamResp.Type = AnthropicStreamEventTypeContentBlockStop // Use OutputIndex for global Anthropic indexing if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } } case schemas.ResponsesStreamResponseTypeWebSearchCallInProgress, schemas.ResponsesStreamResponseTypeWebSearchCallSearching, schemas.ResponsesStreamResponseTypeWebSearchCallCompleted: // Web search lifecycle events - these are OpenAI-style events that don't have Anthropic equivalents // Skip them to avoid cluttering the stream return nil case schemas.ResponsesStreamResponseTypePing: streamResp.Type = AnthropicStreamEventTypePing case schemas.ResponsesStreamResponseTypeCompleted: streamResp.Type = AnthropicStreamEventTypeMessageStop // If a message_delta was already emitted from the upstream event, only emit message_stop // to avoid sending a duplicate message_delta to the client. if alreadyEmitted, ok := ctx.Value(schemas.BifrostContextKeyHasEmittedMessageDelta).(bool); ok && alreadyEmitted { return []*AnthropicStreamEvent{streamResp} } anthropicContentDeltaEvent := &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeMessageDelta, Delta: &AnthropicStreamDelta{ StopReason: schemas.Ptr(AnthropicStopReasonEndTurn), StopSequence: schemas.Ptr(""), }, } // Convert usage from Bifrost to Anthropic if bifrostResp.Response != nil { anthropicContentDeltaEvent.Usage = ConvertBifrostUsageToAnthropicUsage(bifrostResp.Response.Usage) if bifrostResp.Response.StopReason != nil { anthropicContentDeltaEvent.Delta = &AnthropicStreamDelta{ StopReason: schemas.Ptr(ConvertBifrostFinishReasonToAnthropic(*bifrostResp.Response.StopReason)), StopSequence: nil, } } } return []*AnthropicStreamEvent{anthropicContentDeltaEvent, streamResp} case schemas.ResponsesStreamResponseTypeMCPCallArgumentsDelta: // MCP call arguments delta - convert to content_block_delta with input_json streamResp.Type = AnthropicStreamEventTypeContentBlockDelta // Use OutputIndex for global Anthropic indexing if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } if bifrostResp.Delta != nil { streamResp.Delta = &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeInputJSON, PartialJSON: bifrostResp.Delta, } } else if bifrostResp.Arguments != nil { // Handle cases where Arguments field is used instead of Delta streamResp.Delta = &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeInputJSON, PartialJSON: bifrostResp.Arguments, } } case schemas.ResponsesStreamResponseTypeMCPCallCompleted: // MCP call completed - emit content_block_stop streamResp.Type = AnthropicStreamEventTypeContentBlockStop // Use OutputIndex for global Anthropic indexing if bifrostResp.OutputIndex != nil { streamResp.Index = bifrostResp.OutputIndex } else if bifrostResp.ContentIndex != nil { streamResp.Index = bifrostResp.ContentIndex } case schemas.ResponsesStreamResponseTypeMCPCallFailed: // MCP call failed - emit error event streamResp.Type = AnthropicStreamEventTypeError errorMsg := "MCP call failed" if bifrostResp.Message != nil { errorMsg = *bifrostResp.Message } streamResp.Error = &AnthropicStreamError{ Type: "error", Message: errorMsg, } case "message_delta": // Check if integration type in ctx is anthropic if ctx.Value(schemas.BifrostContextKeyIntegrationType) == "anthropic" { streamResp.Type = AnthropicStreamEventTypeMessageDelta // Convert usage from Bifrost format to Anthropic format using common converter if bifrostResp.Response != nil { streamResp.Usage = ConvertBifrostUsageToAnthropicUsage(bifrostResp.Response.Usage) } // Convert stop reason from Bifrost format to Anthropic format if bifrostResp.Response != nil && bifrostResp.Response.StopReason != nil { streamResp.Delta = &AnthropicStreamDelta{ StopReason: schemas.Ptr(ConvertBifrostFinishReasonToAnthropic(*bifrostResp.Response.StopReason)), } } else if bifrostResp.Delta != nil { // Handle text delta if present streamResp.Delta = &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeText, Text: bifrostResp.Delta, } } } case schemas.ResponsesStreamResponseTypeError: streamResp.Type = AnthropicStreamEventTypeError if bifrostResp.Message != nil { streamResp.Error = &AnthropicStreamError{ Type: "error", Message: *bifrostResp.Message, } } default: // Unknown event type, return empty return nil } return []*AnthropicStreamEvent{streamResp} } // ToBifrostResponsesRequest converts an Anthropic message request to Bifrost format func (req *AnthropicMessageRequest) ToBifrostResponsesRequest(ctx *schemas.BifrostContext) *schemas.BifrostResponsesRequest { provider, model := schemas.ParseModelString(req.Model, providerUtils.CheckAndSetDefaultProvider(ctx, schemas.Anthropic)) bifrostReq := &schemas.BifrostResponsesRequest{ Provider: provider, Model: model, Fallbacks: schemas.ParseFallbacks(req.Fallbacks), } // Convert basic parameters params := &schemas.ResponsesParameters{ ExtraParams: make(map[string]interface{}), } if req.MaxTokens > 0 { params.MaxOutputTokens = &req.MaxTokens } if req.Temperature != nil { params.Temperature = req.Temperature } if req.TopP != nil { params.TopP = req.TopP } if req.Metadata != nil && req.Metadata.UserID != nil { params.User = req.Metadata.UserID } if req.ContextManagement != nil { params.ExtraParams["context_management"] = req.ContextManagement } if req.InferenceGeo != nil { params.ExtraParams["inference_geo"] = *req.InferenceGeo } if req.CacheControl != nil { params.ExtraParams["cache_control"] = req.CacheControl } if req.TopK != nil { params.ExtraParams["top_k"] = *req.TopK } if req.Speed != nil { params.ExtraParams["speed"] = *req.Speed } if req.StopSequences != nil { params.ExtraParams["stop"] = req.StopSequences } if req.OutputFormat != nil { params.Text = convertAnthropicOutputFormatToResponsesTextConfig(req.OutputFormat) } else if req.OutputConfig != nil && req.OutputConfig.Format != nil { // GA structured outputs - OutputConfig.Format has same structure as OutputFormat params.Text = convertAnthropicOutputFormatToResponsesTextConfig(req.OutputConfig.Format) } if req.OutputConfig != nil && req.OutputConfig.TaskBudget != nil { params.ExtraParams["task_budget"] = req.OutputConfig.TaskBudget } if req.Thinking != nil { if req.Thinking.Type == "enabled" || req.Thinking.Type == "adaptive" { var summary *string if summaryValue, ok := schemas.SafeExtractStringPointer(req.ExtraParams["reasoning_summary"]); ok { summary = summaryValue } // check if user agent in ctx is claude-cli if ctx != nil { if IsClaudeCodeRequest(ctx) { summary = schemas.Ptr("detailed") } } // If the request was sent with display:"omitted" if req.Thinking.Display != nil && *req.Thinking.Display == "omitted" { summary = schemas.Ptr("none") } if req.OutputConfig != nil && req.OutputConfig.Effort != nil { // Native effort present — map to Bifrost enum (e.g., "max" → "high") params.Reasoning = &schemas.ResponsesParametersReasoning{ Effort: schemas.Ptr(*req.OutputConfig.Effort), MaxTokens: req.Thinking.BudgetTokens, Summary: summary, } } else if req.Thinking.BudgetTokens != nil { // Fallback: convert budget_tokens to effort params.Reasoning = &schemas.ResponsesParametersReasoning{ Effort: schemas.Ptr(providerUtils.GetReasoningEffortFromBudgetTokens(*req.Thinking.BudgetTokens, MinimumReasoningMaxTokens, providerUtils.GetMaxOutputTokensOrDefault(req.Model, AnthropicDefaultMaxTokens))), MaxTokens: req.Thinking.BudgetTokens, Summary: summary, } } else { // Adaptive with no explicit effort — default to "high" params.Reasoning = &schemas.ResponsesParametersReasoning{ Effort: schemas.Ptr("high"), Summary: summary, } } } else { params.Reasoning = &schemas.ResponsesParametersReasoning{ Effort: schemas.Ptr("none"), } } } if include, ok := schemas.SafeExtractStringSlice(req.ExtraParams["include"]); ok { params.Include = include } // Add truncation parameter if computer tool is being used if provider == schemas.OpenAI && req.Tools != nil { for _, tool := range req.Tools { if tool.Type == nil { continue } switch *tool.Type { case AnthropicToolTypeComputer20250124, AnthropicToolTypeComputer20251124: params.Truncation = schemas.Ptr("auto") case AnthropicToolTypeWebSearch20250305, AnthropicToolTypeWebSearch20260209: params.Include = []string{"web_search_call.action.sources"} } } } bifrostReq.Params = params // Convert messages directly to ChatMessage format var bifrostMessages []schemas.ResponsesMessage // Convert regular messages using the new conversion method convertedMessages := ConvertAnthropicMessagesToBifrostMessages(ctx, req.Messages, req.System, false, provider == schemas.Bedrock) bifrostMessages = append(bifrostMessages, convertedMessages...) // Convert tools if present if req.Tools != nil { var bifrostTools []schemas.ResponsesTool for _, tool := range req.Tools { bifrostTool := convertAnthropicToolToBifrost(&tool) if bifrostTool != nil { applyAnthropicToolFlagsToResponsesTool(&tool, bifrostTool) bifrostTools = append(bifrostTools, *bifrostTool) } } if len(bifrostTools) > 0 { bifrostReq.Params.Tools = bifrostTools } } if req.MCPServers != nil { // Build a map of mcp_toolset entries from tools[] keyed by mcp_server_name. // Stores the full *AnthropicTool (not just *AnthropicMCPToolsetTool) so // top-level Anthropic tool flags (DeferLoading, AllowedCallers, // InputExamples, EagerInputStreaming) survive the mcp_servers merge path — // without this, mcp_toolset tools bypass applyAnthropicToolFlagsToResponsesTool // because convertAnthropicToolToBifrost skips them. toolsetByServer := make(map[string]*AnthropicTool) if req.Tools != nil { for i := range req.Tools { if req.Tools[i].MCPToolset != nil { toolsetByServer[req.Tools[i].MCPToolset.MCPServerName] = &req.Tools[i] } } } var bifrostMCPTools []schemas.ResponsesTool for _, mcpServer := range req.MCPServers { bifrostMCPTool := convertAnthropicMCPServerV2ToBifrostTool(&mcpServer) if bifrostMCPTool != nil { // Merge mcp_toolset configs (allowed tools) + Anthropic tool flags if present if toolWithFlags, ok := toolsetByServer[mcpServer.Name]; ok { applyMCPToolsetConfigToBifrostTool(bifrostMCPTool, toolWithFlags.MCPToolset) applyAnthropicToolFlagsToResponsesTool(toolWithFlags, bifrostMCPTool) } bifrostMCPTools = append(bifrostMCPTools, *bifrostMCPTool) } } if len(bifrostMCPTools) > 0 { bifrostReq.Params.Tools = append(bifrostReq.Params.Tools, bifrostMCPTools...) } } // Convert tool choice if present if req.ToolChoice != nil { bifrostToolChoice := convertAnthropicToolChoiceToBifrost(req.ToolChoice) if bifrostToolChoice != nil { bifrostReq.Params.ToolChoice = bifrostToolChoice } } // Set the converted messages if len(bifrostMessages) > 0 { bifrostReq.Input = bifrostMessages } return bifrostReq } // ToAnthropicResponsesRequest converts a BifrostRequest with Responses structure back to AnthropicMessageRequest func ToAnthropicResponsesRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.BifrostResponsesRequest) (*AnthropicMessageRequest, error) { if bifrostReq == nil { return nil, fmt.Errorf("bifrost request is nil") } anthropicReq := &AnthropicMessageRequest{ Model: bifrostReq.Model, MaxTokens: providerUtils.GetMaxOutputTokensOrDefault(bifrostReq.Model, AnthropicDefaultMaxTokens), } // Convert basic parameters if bifrostReq.Params != nil { if bifrostReq.Params.MaxOutputTokens != nil { anthropicReq.MaxTokens = *bifrostReq.Params.MaxOutputTokens } // Opus 4.7+ rejects temperature, top_p, and top_k with a 400 error. if !IsOpus47(bifrostReq.Model) { // Anthropic doesn't allow both temperature and top_p to be specified. // If both are present, prefer temperature (more commonly used). if bifrostReq.Params.Temperature != nil { anthropicReq.Temperature = bifrostReq.Params.Temperature } else if bifrostReq.Params.TopP != nil { anthropicReq.TopP = bifrostReq.Params.TopP } } if bifrostReq.Params.User != nil { anthropicReq.Metadata = &AnthropicMetaData{ UserID: bifrostReq.Params.User, } } if bifrostReq.Params.Text != nil { // Vertex doesn't support native structured outputs, so convert to tool if bifrostReq.Provider == schemas.Vertex { if bifrostReq.Params.Text.Format != nil { responseFormatTool := convertResponsesTextFormatToTool(ctx, bifrostReq.Params.Text) if responseFormatTool != nil { if anthropicReq.Tools == nil { anthropicReq.Tools = []AnthropicTool{} } anthropicReq.Tools = append(anthropicReq.Tools, *responseFormatTool) // Force the model to use this specific tool anthropicReq.ToolChoice = &AnthropicToolChoice{ Type: "tool", Name: responseFormatTool.Name, } } } } else { // Citations cannot be used together with Structured Outputs in anthropic. hasCitationsEnabled := false // loop over input messages and check if any message has citations enabled for _, message := range bifrostReq.Input { if message.Content == nil || message.Content.ContentBlocks == nil { continue } if message.Content.ContentBlocks != nil { for _, block := range message.Content.ContentBlocks { if block.Type == schemas.ResponsesInputMessageContentBlockTypeFile && block.Citations != nil && block.Citations.Enabled != nil && *block.Citations.Enabled { hasCitationsEnabled = true break } } } if hasCitationsEnabled { break } } if !hasCitationsEnabled { // Use GA structured outputs (output_config.format) instead of beta (output_format) outputFormat := convertResponsesTextConfigToAnthropicOutputFormat(bifrostReq.Params.Text) if outputFormat != nil { anthropicReq.OutputConfig = &AnthropicOutputConfig{ Format: outputFormat, } } } } } if bifrostReq.Params.Reasoning != nil { if bifrostReq.Params.Reasoning.MaxTokens != nil { if IsOpus47(bifrostReq.Model) { // Opus 4.7+: budget_tokens removed; adaptive thinking is the only thinking-on mode. anthropicReq.Thinking = &AnthropicThinking{Type: "adaptive"} } else { budgetTokens := *bifrostReq.Params.Reasoning.MaxTokens if *bifrostReq.Params.Reasoning.MaxTokens == -1 { // anthropic does not support dynamic reasoning budget like gemini // setting it to default max tokens budgetTokens = MinimumReasoningMaxTokens } if budgetTokens < MinimumReasoningMaxTokens { return nil, fmt.Errorf("reasoning.max_tokens must be >= %d for anthropic", MinimumReasoningMaxTokens) } anthropicReq.Thinking = &AnthropicThinking{ Type: "enabled", BudgetTokens: schemas.Ptr(budgetTokens), } } } else { if bifrostReq.Params.Reasoning.Effort != nil { if *bifrostReq.Params.Reasoning.Effort != "none" { effort := MapBifrostEffortToAnthropic(*bifrostReq.Params.Reasoning.Effort) if SupportsAdaptiveThinking(bifrostReq.Model) || IsOpus47(bifrostReq.Model) { // Opus 4.6+ and Opus 4.7+: adaptive thinking + native effort anthropicReq.Thinking = &AnthropicThinking{Type: "adaptive"} setEffortOnOutputConfig(anthropicReq, effort) } else if SupportsNativeEffort(bifrostReq.Model) { // Opus 4.5: native effort + budget_tokens thinking setEffortOnOutputConfig(anthropicReq, effort) budgetTokens, err := providerUtils.GetBudgetTokensFromReasoningEffort(effort, MinimumReasoningMaxTokens, anthropicReq.MaxTokens) if err != nil { return nil, err } anthropicReq.Thinking = &AnthropicThinking{ Type: "enabled", BudgetTokens: schemas.Ptr(budgetTokens), } } else { // Older models: budget_tokens only budgetTokens, err := providerUtils.GetBudgetTokensFromReasoningEffort(effort, MinimumReasoningMaxTokens, anthropicReq.MaxTokens) if err != nil { return nil, err } anthropicReq.Thinking = &AnthropicThinking{ Type: "enabled", BudgetTokens: schemas.Ptr(budgetTokens), } } } else { anthropicReq.Thinking = &AnthropicThinking{ Type: "disabled", } } } } if anthropicReq.Thinking != nil && anthropicReq.Thinking.Type != "disabled" { if bifrostReq.Params.Reasoning != nil && bifrostReq.Params.Reasoning.Summary != nil && *bifrostReq.Params.Reasoning.Summary == "none" { anthropicReq.Thinking.Display = schemas.Ptr("omitted") } else { // Default to "summarized" to preserve visible thinking output anthropicReq.Thinking.Display = schemas.Ptr("summarized") } } } // Convert service tier anthropicReq.ServiceTier = bifrostReq.Params.ServiceTier if bifrostReq.Params.ExtraParams != nil { anthropicReq.ExtraParams = make(map[string]interface{}, len(bifrostReq.Params.ExtraParams)) for k, v := range bifrostReq.Params.ExtraParams { anthropicReq.ExtraParams[k] = v } if cacheControlRaw, exists := anthropicReq.ExtraParams["cache_control"]; exists { parsed := false switch v := cacheControlRaw.(type) { case *schemas.CacheControl: anthropicReq.CacheControl = v parsed = true case schemas.CacheControl: anthropicReq.CacheControl = &v parsed = true default: if data, err := providerUtils.MarshalSorted(v); err == nil { var cc schemas.CacheControl if sonic.Unmarshal(data, &cc) == nil { anthropicReq.CacheControl = &cc parsed = true } } } if parsed { delete(anthropicReq.ExtraParams, "cache_control") } } topK, ok := schemas.SafeExtractIntPointer(bifrostReq.Params.ExtraParams["top_k"]) if ok { delete(anthropicReq.ExtraParams, "top_k") if !IsOpus47(bifrostReq.Model) { anthropicReq.TopK = topK } } if speed, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["speed"]); ok { delete(anthropicReq.ExtraParams, "speed") anthropicReq.Speed = speed } if stop, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["stop"]); ok { delete(anthropicReq.ExtraParams, "stop") anthropicReq.StopSequences = stop } if inferenceGeo, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["inference_geo"]); ok { delete(anthropicReq.ExtraParams, "inference_geo") anthropicReq.InferenceGeo = inferenceGeo } if cmVal := bifrostReq.Params.ExtraParams["context_management"]; cmVal != nil { if cm, ok := cmVal.(*ContextManagement); ok && cm != nil { delete(anthropicReq.ExtraParams, "context_management") anthropicReq.ContextManagement = cm } else if data, err := providerUtils.MarshalSorted(cmVal); err == nil { var cm ContextManagement if sonic.Unmarshal(data, &cm) == nil { delete(anthropicReq.ExtraParams, "context_management") anthropicReq.ContextManagement = &cm } } } if tbVal, exists := bifrostReq.Params.ExtraParams["task_budget"]; exists { // Always consume provider-specific key from passthrough extras. delete(anthropicReq.ExtraParams, "task_budget") var taskBudget *AnthropicTaskBudget switch v := tbVal.(type) { case *AnthropicTaskBudget: taskBudget = v case AnthropicTaskBudget: taskBudget = &v default: if data, err := providerUtils.MarshalSorted(v); err == nil { var tb AnthropicTaskBudget if sonic.Unmarshal(data, &tb) == nil { taskBudget = &tb } } } if taskBudget == nil { return nil, fmt.Errorf("invalid task_budget format for anthropic") } if anthropicReq.OutputConfig == nil { anthropicReq.OutputConfig = &AnthropicOutputConfig{} } anthropicReq.OutputConfig.TaskBudget = taskBudget } } // Convert tools if bifrostReq.Params.Tools != nil { anthropicTools, mcpServers := convertBifrostToolsToAnthropic(bifrostReq.Model, bifrostReq.Params.Tools, bifrostReq.Provider) if len(anthropicTools) > 0 { if anthropicReq.Tools == nil { anthropicReq.Tools = anthropicTools } else { anthropicReq.Tools = append(anthropicReq.Tools, anthropicTools...) } } if len(mcpServers) > 0 { anthropicReq.MCPServers = mcpServers } } // Convert tool choice if bifrostReq.Params.ToolChoice != nil { anthropicToolChoice := convertResponsesToolChoiceToAnthropic(bifrostReq.Params.ToolChoice) if anthropicToolChoice != nil { anthropicReq.ToolChoice = anthropicToolChoice } } } if bifrostReq.Input != nil { anthropicMessages, systemContent := ConvertBifrostMessagesToAnthropicMessages(ctx, bifrostReq.Input) // Set system message if present if systemContent != nil { anthropicReq.System = systemContent } else if bifrostReq.Params != nil && bifrostReq.Params.Instructions != nil && *bifrostReq.Params.Instructions != "" { // if no system content, check if instructions are present // system messages take precedence over instructions anthropicReq.System = &AnthropicContent{ ContentBlocks: []AnthropicContentBlock{ { Type: AnthropicContentBlockTypeText, Text: bifrostReq.Params.Instructions, }, }, } } // Set regular messages anthropicReq.Messages = anthropicMessages } return anthropicReq, nil } // ConvertAnthropicUsageToBifrostUsage converts Anthropic usage format to Bifrost usage format // Handles iterations recursively func ConvertAnthropicUsageToBifrostUsage(anthropicUsage *AnthropicUsage) *schemas.ResponsesResponseUsage { if anthropicUsage == nil { return nil } bifrostUsage := &schemas.ResponsesResponseUsage{ Type: anthropicUsage.Type, InputTokens: anthropicUsage.InputTokens, OutputTokens: anthropicUsage.OutputTokens, TotalTokens: anthropicUsage.InputTokens + anthropicUsage.OutputTokens, } // Handle cache read tokens if anthropicUsage.CacheReadInputTokens > 0 { if bifrostUsage.InputTokensDetails == nil { bifrostUsage.InputTokensDetails = &schemas.ResponsesResponseInputTokens{} } bifrostUsage.InputTokensDetails.CachedReadTokens = anthropicUsage.CacheReadInputTokens bifrostUsage.InputTokens = bifrostUsage.InputTokens + anthropicUsage.CacheReadInputTokens bifrostUsage.TotalTokens = bifrostUsage.TotalTokens + anthropicUsage.CacheReadInputTokens } // Handle cache creation tokens if anthropicUsage.CacheCreationInputTokens > 0 { if bifrostUsage.InputTokensDetails == nil { bifrostUsage.InputTokensDetails = &schemas.ResponsesResponseInputTokens{} } bifrostUsage.InputTokensDetails.CachedWriteTokens = anthropicUsage.CacheCreationInputTokens bifrostUsage.InputTokens = bifrostUsage.InputTokens + anthropicUsage.CacheCreationInputTokens bifrostUsage.TotalTokens = bifrostUsage.TotalTokens + anthropicUsage.CacheCreationInputTokens } // Propagate server tool use (web search) counts if anthropicUsage.ServerToolUse != nil && anthropicUsage.ServerToolUse.WebSearchRequests > 0 { if bifrostUsage.OutputTokensDetails == nil { bifrostUsage.OutputTokensDetails = &schemas.ResponsesResponseOutputTokens{} } bifrostUsage.OutputTokensDetails.NumSearchQueries = schemas.Ptr(anthropicUsage.ServerToolUse.WebSearchRequests) } // Recursively convert iterations if len(anthropicUsage.Iterations) > 0 { bifrostUsage.Iterations = make([]schemas.ResponsesResponseUsage, len(anthropicUsage.Iterations)) for i, iteration := range anthropicUsage.Iterations { if converted := ConvertAnthropicUsageToBifrostUsage(&iteration); converted != nil { bifrostUsage.Iterations[i] = *converted } } } return bifrostUsage } // ConvertBifrostUsageToAnthropicUsage converts Bifrost usage format to Anthropic usage format // Handles iterations recursively func ConvertBifrostUsageToAnthropicUsage(bifrostUsage *schemas.ResponsesResponseUsage) *AnthropicUsage { if bifrostUsage == nil { return nil } anthropicUsage := &AnthropicUsage{ Type: bifrostUsage.Type, InputTokens: bifrostUsage.InputTokens, OutputTokens: bifrostUsage.OutputTokens, } // Handle cache read tokens if bifrostUsage.InputTokensDetails != nil { if bifrostUsage.InputTokensDetails.CachedReadTokens > 0 { anthropicUsage.CacheReadInputTokens = bifrostUsage.InputTokensDetails.CachedReadTokens anthropicUsage.InputTokens = anthropicUsage.InputTokens - bifrostUsage.InputTokensDetails.CachedReadTokens } if bifrostUsage.InputTokensDetails.CachedWriteTokens > 0 { anthropicUsage.CacheCreationInputTokens = bifrostUsage.InputTokensDetails.CachedWriteTokens anthropicUsage.InputTokens = anthropicUsage.InputTokens - bifrostUsage.InputTokensDetails.CachedWriteTokens // Populate the cache_creation breakdown — default to ephemeral (5m) since // the Bifrost internal format doesn't distinguish TTL variants. anthropicUsage.CacheCreation = AnthropicUsageCacheCreation{ Ephemeral5mInputTokens: bifrostUsage.InputTokensDetails.CachedWriteTokens, } } } // Handle server tool use statistics (e.g., web search) if bifrostUsage.OutputTokensDetails != nil && bifrostUsage.OutputTokensDetails.NumSearchQueries != nil && *bifrostUsage.OutputTokensDetails.NumSearchQueries > 0 { anthropicUsage.ServerToolUse = &AnthropicServerToolUseUsage{ WebSearchRequests: *bifrostUsage.OutputTokensDetails.NumSearchQueries, } } // Recursively convert iterations if len(bifrostUsage.Iterations) > 0 { anthropicUsage.Iterations = make([]AnthropicUsage, len(bifrostUsage.Iterations)) for i, iteration := range bifrostUsage.Iterations { if converted := ConvertBifrostUsageToAnthropicUsage(&iteration); converted != nil { anthropicUsage.Iterations[i] = *converted } } } return anthropicUsage } // ToBifrostResponsesResponse converts an Anthropic response to BifrostResponse with Responses structure func (response *AnthropicMessageResponse) ToBifrostResponsesResponse(ctx *schemas.BifrostContext) *schemas.BifrostResponsesResponse { if response == nil { return nil } // Create the BifrostResponse with Responses structure bifrostResp := &schemas.BifrostResponsesResponse{ ID: schemas.Ptr(response.ID), CreatedAt: int(time.Now().Unix()), } // Convert usage information using common converter (handles iterations recursively) bifrostResp.Usage = ConvertAnthropicUsageToBifrostUsage(response.Usage) // Convert content to Responses output messages using the new conversion method if len(response.Content) > 0 { // Create a temporary message to use the conversion method tempMsg := AnthropicMessage{ Role: AnthropicMessageRoleAssistant, Content: AnthropicContent{ ContentBlocks: response.Content, }, } outputMessages := ConvertAnthropicMessagesToBifrostMessages(ctx, []AnthropicMessage{tempMsg}, nil, true, false) if len(outputMessages) > 0 { bifrostResp.Output = outputMessages } } bifrostResp.Model = response.Model // Preserve stop reason from Anthropic response if response.StopReason != "" { bifrostResp.StopReason = schemas.Ptr(string(response.StopReason)) } return bifrostResp } // ToAnthropicResponsesResponse converts a BifrostResponse with Responses structure back to AnthropicMessageResponse func ToAnthropicResponsesResponse(ctx *schemas.BifrostContext, bifrostResp *schemas.BifrostResponsesResponse) *AnthropicMessageResponse { anthropicResp := &AnthropicMessageResponse{ Type: "message", Role: "assistant", } if bifrostResp.ID != nil { anthropicResp.ID = *bifrostResp.ID } // Convert usage information using common converter (handles iterations recursively) anthropicResp.Usage = ConvertBifrostUsageToAnthropicUsage(bifrostResp.Usage) // Convert output messages to Anthropic content blocks using the new conversion method var contentBlocks []AnthropicContentBlock if bifrostResp.Output != nil { anthropicMessages, _ := ConvertBifrostMessagesToAnthropicMessages(ctx, bifrostResp.Output) // Extract content blocks from the converted messages for _, msg := range anthropicMessages { if msg.Content.ContentBlocks != nil { contentBlocks = append(contentBlocks, msg.Content.ContentBlocks...) } else if msg.Content.ContentStr != nil { contentBlocks = append(contentBlocks, AnthropicContentBlock{ Type: AnthropicContentBlockTypeText, Text: msg.Content.ContentStr, }) } } } if len(contentBlocks) > 0 { anthropicResp.Content = contentBlocks } else { anthropicResp.Content = []AnthropicContentBlock{} } // Map stop reason from Bifrost response if available, otherwise infer from content if bifrostResp.StopReason != nil { anthropicResp.StopReason = ConvertBifrostFinishReasonToAnthropic(*bifrostResp.StopReason) } else { anthropicResp.StopReason = AnthropicStopReasonEndTurn for _, block := range contentBlocks { if block.Type == AnthropicContentBlockTypeToolUse { anthropicResp.StopReason = AnthropicStopReasonToolUse break } } } anthropicResp.Model = bifrostResp.Model return anthropicResp } // ConvertAnthropicMessagesToBifrostMessages converts an array of Anthropic messages to Bifrost ResponsesMessage format func ConvertAnthropicMessagesToBifrostMessages(ctx *schemas.BifrostContext, anthropicMessages []AnthropicMessage, systemContent *AnthropicContent, isOutputMessage bool, keepToolsGrouped bool) []schemas.ResponsesMessage { var bifrostMessages []schemas.ResponsesMessage // Get structured output tool name from context if present var structuredOutputToolName string if ctx != nil { if toolName, ok := ctx.Value(schemas.BifrostContextKeyStructuredOutputToolName).(string); ok { structuredOutputToolName = toolName } } // Handle system message first if present if systemContent != nil { systemMessages := convertAnthropicSystemToBifrostMessages(systemContent) bifrostMessages = append(bifrostMessages, systemMessages...) } // Convert regular messages for _, msg := range anthropicMessages { var convertedMessages []schemas.ResponsesMessage if keepToolsGrouped { convertedMessages = convertSingleAnthropicMessageToBifrostMessagesGrouped(&msg, isOutputMessage, structuredOutputToolName) } else { convertedMessages = convertSingleAnthropicMessageToBifrostMessages(ctx, &msg, isOutputMessage, structuredOutputToolName) } bifrostMessages = append(bifrostMessages, convertedMessages...) } return bifrostMessages } // ConvertBifrostMessagesToAnthropicMessages converts an array of Bifrost ResponsesMessage to Anthropic message format // This is the main conversion method from Bifrost to Anthropic - handles all message types and returns messages + system content func ConvertBifrostMessagesToAnthropicMessages(ctx *schemas.BifrostContext, bifrostMessages []schemas.ResponsesMessage) ([]AnthropicMessage, *AnthropicContent) { var anthropicMessages []AnthropicMessage var systemContent *AnthropicContent var pendingToolCalls []AnthropicContentBlock var pendingToolResultBlocks []AnthropicContentBlock var pendingReasoningContentBlocks []AnthropicContentBlock var currentAssistantMessage *AnthropicMessage // Track tool call IDs for each assistant turn to properly match tool results // Each assistant turn that contains tool_use blocks should have its tool results // grouped in a corresponding user message type toolCallGroup struct { toolCallIDs map[string]bool // Set of tool call IDs in this group flushed bool // Whether the tool results for this group have been flushed } var toolCallGroups []toolCallGroup var currentToolCallIDs map[string]bool // IDs of tool calls in the current pending batch // Helper to flush pending tool result blocks into user messages // This now matches tool results to their corresponding tool call groups flushPendingToolResults := func() { if len(pendingToolResultBlocks) == 0 { return } // If there are no tool call groups, just flush all results together if len(toolCallGroups) == 0 { anthropicMessages = append(anthropicMessages, AnthropicMessage{ Role: AnthropicMessageRoleUser, Content: AnthropicContent{ ContentBlocks: pendingToolResultBlocks, }, }) pendingToolResultBlocks = nil return } // Group tool results by their corresponding tool call group // Each group should be flushed as a separate user message for i := range toolCallGroups { if toolCallGroups[i].flushed { continue } var groupResults []AnthropicContentBlock var remainingResults []AnthropicContentBlock for _, block := range pendingToolResultBlocks { if block.ToolUseID != nil && toolCallGroups[i].toolCallIDs[*block.ToolUseID] { groupResults = append(groupResults, block) } else { remainingResults = append(remainingResults, block) } } if len(groupResults) > 0 { anthropicMessages = append(anthropicMessages, AnthropicMessage{ Role: AnthropicMessageRoleUser, Content: AnthropicContent{ ContentBlocks: groupResults, }, }) toolCallGroups[i].flushed = true pendingToolResultBlocks = remainingResults } } // Flush any remaining tool results that didn't match any group if len(pendingToolResultBlocks) > 0 { anthropicMessages = append(anthropicMessages, AnthropicMessage{ Role: AnthropicMessageRoleUser, Content: AnthropicContent{ ContentBlocks: pendingToolResultBlocks, }, }) pendingToolResultBlocks = nil } } // Helper to flush pending tool calls with tool call ID tracking flushPendingToolCallsWithTracking := func() { if len(pendingToolCalls) > 0 && currentAssistantMessage != nil { // Copy the slice to avoid aliasing issues copied := make([]AnthropicContentBlock, len(pendingToolCalls)) copy(copied, pendingToolCalls) currentAssistantMessage.Content = AnthropicContent{ ContentBlocks: copied, } anthropicMessages = append(anthropicMessages, *currentAssistantMessage) // Record this tool call group for matching with tool results if len(currentToolCallIDs) > 0 { toolCallGroups = append(toolCallGroups, toolCallGroup{ toolCallIDs: currentToolCallIDs, flushed: false, }) currentToolCallIDs = nil } pendingToolCalls = nil currentAssistantMessage = nil } } for _, msg := range bifrostMessages { // Handle nil Type as regular message msgType := schemas.ResponsesMessageTypeMessage if msg.Type != nil { msgType = *msg.Type } switch msgType { case schemas.ResponsesMessageTypeMessage: // Flush any pending tool results before processing other message types flushPendingToolResults() // Flush any pending tool calls first (with tracking for tool call groups) flushPendingToolCallsWithTracking() // Handle system messages separately if msg.Role != nil && *msg.Role == schemas.ResponsesInputMessageRoleSystem { systemContent = convertBifrostMessageToAnthropicSystemContent(&msg) continue } // If there are pending reasoning blocks and this is a user message, // flush them into a separate assistant message first // (thinking blocks can only appear in assistant messages in Anthropic) if len(pendingReasoningContentBlocks) > 0 && (msg.Role == nil || *msg.Role == schemas.ResponsesInputMessageRoleUser) { // Copy the pending reasoning content blocks copied := make([]AnthropicContentBlock, len(pendingReasoningContentBlocks)) copy(copied, pendingReasoningContentBlocks) assistantReasoningMsg := AnthropicMessage{ Role: AnthropicMessageRoleAssistant, Content: AnthropicContent{ ContentBlocks: copied, }, } anthropicMessages = append(anthropicMessages, assistantReasoningMsg) pendingReasoningContentBlocks = nil } // Regular user/assistant message anthropicMsg := convertBifrostMessageToAnthropicMessage(&msg, &pendingReasoningContentBlocks) if anthropicMsg != nil { anthropicMessages = append(anthropicMessages, *anthropicMsg) } case schemas.ResponsesMessageTypeReasoning: // Flush any pending tool results before processing reasoning flushPendingToolResults() // Handle reasoning as thinking content reasoningBlocks := convertBifrostReasoningToAnthropicThinking(&msg) pendingReasoningContentBlocks = append(pendingReasoningContentBlocks, reasoningBlocks...) case schemas.ResponsesMessageTypeFunctionCall: // Flush any pending tool results before processing function calls flushPendingToolResults() // When thinking blocks exist, they MUST come first before tool_use blocks // If we have pending reasoning blocks, we need to prepend them to the assistant message if currentAssistantMessage == nil { currentAssistantMessage = &AnthropicMessage{ Role: AnthropicMessageRoleAssistant, } } // Prepend any pending reasoning blocks to ensure they come BEFORE tool_use blocks // This is required by Anthropic/Bedrock API: if an assistant message contains thinking blocks, // the first block must be thinking or redacted_thinking, NOT tool_use if len(pendingReasoningContentBlocks) > 0 { copied := make([]AnthropicContentBlock, len(pendingReasoningContentBlocks)) copy(copied, pendingReasoningContentBlocks) pendingToolCalls = append(copied, pendingToolCalls...) pendingReasoningContentBlocks = nil } toolUseBlock := convertBifrostFunctionCallToAnthropicToolUse(ctx, &msg) if toolUseBlock != nil { // If there was a previous assistant message (text only) that was just added, // and we have no pending tool calls yet, we should merge the tool call into it. // This handles the case where an assistant text message precedes tool calls. if len(pendingToolCalls) == 0 && len(anthropicMessages) > 0 { lastMsgIdx := len(anthropicMessages) - 1 lastMsg := &anthropicMessages[lastMsgIdx] // Check if the last message is an assistant message that could have text if lastMsg.Role == AnthropicMessageRoleAssistant { hasToolUse := false for _, block := range lastMsg.Content.ContentBlocks { if block.Type == AnthropicContentBlockTypeToolUse { hasToolUse = true break } } // If the last assistant message has no tool_use blocks, merge the tool call into it if !hasToolUse { // Copy existing content blocks and append the tool_use existingBlocks := lastMsg.Content.ContentBlocks existingBlocks = append(existingBlocks, *toolUseBlock) lastMsg.Content = AnthropicContent{ ContentBlocks: existingBlocks, } // Track the tool call ID if currentToolCallIDs == nil { currentToolCallIDs = make(map[string]bool) } if toolUseBlock.ID != nil { currentToolCallIDs[*toolUseBlock.ID] = true } // Use this message as the current one for subsequent tool calls pendingToolCalls = lastMsg.Content.ContentBlocks anthropicMessages = anthropicMessages[:lastMsgIdx] // Remove it, will be re-added on flush currentAssistantMessage = lastMsg continue } } } pendingToolCalls = append(pendingToolCalls, *toolUseBlock) // Track the tool call ID for matching with tool results if currentToolCallIDs == nil { currentToolCallIDs = make(map[string]bool) } if toolUseBlock.ID != nil { currentToolCallIDs[*toolUseBlock.ID] = true } } case schemas.ResponsesMessageTypeFunctionCallOutput: // Flush any pending tool calls first before processing tool results (with tracking) flushPendingToolCallsWithTracking() // Accumulate tool result blocks - they will be merged into a single user message // This is required because Anthropic/Bedrock expect all tool results for parallel // tool calls to be in the same user message, in the same order as the tool calls toolResultBlock := convertBifrostFunctionCallOutputToAnthropicToolResultBlock(&msg) if toolResultBlock != nil { pendingToolResultBlocks = append(pendingToolResultBlocks, *toolResultBlock) } case schemas.ResponsesMessageTypeItemReference: // Flush any pending tool results before processing item reference flushPendingToolResults() // Handle item reference as regular text message referenceMsg := convertBifrostItemReferenceToAnthropicMessage(&msg) if referenceMsg != nil { anthropicMessages = append(anthropicMessages, *referenceMsg) } case schemas.ResponsesMessageTypeComputerCall: // Flush any pending tool results before processing computer calls flushPendingToolResults() // Start accumulating computer tool calls for assistant message if currentAssistantMessage == nil { currentAssistantMessage = &AnthropicMessage{ Role: AnthropicMessageRoleAssistant, } } // Prepend any pending reasoning blocks to ensure they come BEFORE tool_use blocks if len(pendingReasoningContentBlocks) > 0 { copied := make([]AnthropicContentBlock, len(pendingReasoningContentBlocks)) copy(copied, pendingReasoningContentBlocks) pendingToolCalls = append(copied, pendingToolCalls...) pendingReasoningContentBlocks = nil } computerToolUseBlock := convertBifrostComputerCallToAnthropicToolUse(&msg) if computerToolUseBlock != nil { pendingToolCalls = append(pendingToolCalls, *computerToolUseBlock) // Track the tool call ID for matching with tool results if currentToolCallIDs == nil { currentToolCallIDs = make(map[string]bool) } if computerToolUseBlock.ID != nil { currentToolCallIDs[*computerToolUseBlock.ID] = true } } case schemas.ResponsesMessageTypeMCPCall: // Check if this is a tool use (from assistant) or tool result (from user) if msg.ResponsesToolMessage != nil { if msg.ResponsesToolMessage.Name != nil { // Flush any pending tool results before processing MCP calls flushPendingToolResults() // This is a tool use call (assistant calling a tool) if currentAssistantMessage == nil { currentAssistantMessage = &AnthropicMessage{ Role: AnthropicMessageRoleAssistant, } } // Prepend any pending reasoning blocks to ensure they come BEFORE tool_use blocks if len(pendingReasoningContentBlocks) > 0 { copied := make([]AnthropicContentBlock, len(pendingReasoningContentBlocks)) copy(copied, pendingReasoningContentBlocks) pendingToolCalls = append(copied, pendingToolCalls...) pendingReasoningContentBlocks = nil } mcpToolUseBlock := convertBifrostMCPCallToAnthropicToolUse(&msg) if mcpToolUseBlock != nil { pendingToolCalls = append(pendingToolCalls, *mcpToolUseBlock) // Track the tool call ID for matching with tool results if currentToolCallIDs == nil { currentToolCallIDs = make(map[string]bool) } if mcpToolUseBlock.ID != nil { currentToolCallIDs[*mcpToolUseBlock.ID] = true } } } else if msg.ResponsesToolMessage.CallID != nil { // This is a tool result (user providing result of tool execution) // Accumulate with other tool results mcpToolResultBlock := convertBifrostMCPCallOutputToAnthropicToolResultBlock(&msg) if mcpToolResultBlock != nil { pendingToolResultBlocks = append(pendingToolResultBlocks, *mcpToolResultBlock) } } } case schemas.ResponsesMessageTypeMCPApprovalRequest: // Flush any pending tool results before processing MCP approval requests flushPendingToolResults() // MCP approval request is OpenAI-specific for human-in-the-loop workflows // Convert to Anthropic's mcp_tool_use format (same as regular MCP calls) if currentAssistantMessage == nil { currentAssistantMessage = &AnthropicMessage{ Role: AnthropicMessageRoleAssistant, } } // Prepend any pending reasoning blocks to ensure they come BEFORE tool_use blocks if len(pendingReasoningContentBlocks) > 0 { copied := make([]AnthropicContentBlock, len(pendingReasoningContentBlocks)) copy(copied, pendingReasoningContentBlocks) pendingToolCalls = append(copied, pendingToolCalls...) pendingReasoningContentBlocks = nil } mcpApprovalBlock := convertBifrostMCPApprovalToAnthropicToolUse(&msg) if mcpApprovalBlock != nil { pendingToolCalls = append(pendingToolCalls, *mcpApprovalBlock) // Track the tool call ID for matching with tool results if currentToolCallIDs == nil { currentToolCallIDs = make(map[string]bool) } if mcpApprovalBlock.ID != nil { currentToolCallIDs[*mcpApprovalBlock.ID] = true } } case schemas.ResponsesMessageTypeWebSearchCall: // Flush any pending tool results before processing web search calls flushPendingToolResults() // Web search calls need special handling: create server_tool_use + web_search_tool_result blocks webSearchBlocks := convertBifrostWebSearchCallToAnthropicBlocks(&msg) if len(webSearchBlocks) > 0 { // For web search, we create both server_tool_use and web_search_tool_result // These should appear in an assistant message if currentAssistantMessage == nil { currentAssistantMessage = &AnthropicMessage{ Role: AnthropicMessageRoleAssistant, } } // Prepend any pending reasoning blocks to ensure they come BEFORE tool blocks if len(pendingReasoningContentBlocks) > 0 { copied := make([]AnthropicContentBlock, len(pendingReasoningContentBlocks)) copy(copied, pendingReasoningContentBlocks) pendingToolCalls = append(copied, pendingToolCalls...) pendingReasoningContentBlocks = nil } // Add the web search blocks (server_tool_use + web_search_tool_result) pendingToolCalls = append(pendingToolCalls, webSearchBlocks...) // Track the tool call ID for the server_tool_use block (first block) if len(webSearchBlocks) > 0 && webSearchBlocks[0].ID != nil { if currentToolCallIDs == nil { currentToolCallIDs = make(map[string]bool) } currentToolCallIDs[*webSearchBlocks[0].ID] = true } } case schemas.ResponsesMessageTypeWebFetchCall: flushPendingToolResults() if currentAssistantMessage == nil { currentAssistantMessage = &AnthropicMessage{ Role: AnthropicMessageRoleAssistant, } } if len(pendingReasoningContentBlocks) > 0 { copied := make([]AnthropicContentBlock, len(pendingReasoningContentBlocks)) copy(copied, pendingReasoningContentBlocks) pendingToolCalls = append(copied, pendingToolCalls...) pendingReasoningContentBlocks = nil } serverToolUseBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeServerToolUse, Name: schemas.Ptr(string(AnthropicToolNameWebFetch)), } if msg.ID != nil { serverToolUseBlock.ID = msg.ID } if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.Action != nil && msg.ResponsesToolMessage.Action.ResponsesWebFetchToolCallAction != nil { inputBytes, err := providerUtils.MarshalSorted(map[string]interface{}{ "url": msg.ResponsesToolMessage.Action.ResponsesWebFetchToolCallAction.URL, }) if err == nil { serverToolUseBlock.Input = json.RawMessage(inputBytes) } } pendingToolCalls = append(pendingToolCalls, serverToolUseBlock) if serverToolUseBlock.ID != nil { if currentToolCallIDs == nil { currentToolCallIDs = make(map[string]bool) } currentToolCallIDs[*serverToolUseBlock.ID] = true } // Handle other tool call types that are not natively supported by Anthropic case schemas.ResponsesMessageTypeFileSearchCall, schemas.ResponsesMessageTypeCodeInterpreterCall, schemas.ResponsesMessageTypeLocalShellCall, schemas.ResponsesMessageTypeCustomToolCall, schemas.ResponsesMessageTypeImageGenerationCall: // Flush any pending tool results before processing unsupported tool calls flushPendingToolResults() // Convert unsupported tool calls to regular text messages unsupportedToolMsg := convertBifrostUnsupportedToolCallToAnthropicMessage(&msg, msgType) if unsupportedToolMsg != nil { anthropicMessages = append(anthropicMessages, *unsupportedToolMsg) } case schemas.ResponsesMessageTypeComputerCallOutput: // Flush any pending tool calls first before processing tool results (with tracking) flushPendingToolCallsWithTracking() // Accumulate computer call output with other tool results computerResultBlock := convertBifrostComputerCallOutputToAnthropicToolResultBlock(&msg) if computerResultBlock != nil { pendingToolResultBlocks = append(pendingToolResultBlocks, *computerResultBlock) } case schemas.ResponsesMessageTypeLocalShellCallOutput, schemas.ResponsesMessageTypeCustomToolCallOutput: // Handle tool outputs as user messages toolOutputMsg := convertBifrostToolOutputToAnthropicMessage(&msg) if toolOutputMsg != nil { anthropicMessages = append(anthropicMessages, *toolOutputMsg) } default: // Skip unknown message types or log them for debugging continue } } // Flush any remaining pending tool results flushPendingToolResults() // Flush any remaining pending tool calls (with tracking) flushPendingToolCallsWithTracking() return anthropicMessages, systemContent } // Helper function to convert Anthropic system content to Bifrost messages func convertAnthropicSystemToBifrostMessages(systemContent *AnthropicContent) []schemas.ResponsesMessage { var bifrostMessages []schemas.ResponsesMessage if systemContent.ContentStr != nil && *systemContent.ContentStr != "" { bifrostMessages = append(bifrostMessages, schemas.ResponsesMessage{ Role: schemas.Ptr(schemas.ResponsesInputMessageRoleSystem), Content: &schemas.ResponsesMessageContent{ ContentStr: systemContent.ContentStr, }, }) } else if systemContent.ContentBlocks != nil { contentBlocks := []schemas.ResponsesMessageContentBlock{} for _, block := range systemContent.ContentBlocks { if block.Text != nil { // System messages will only have text content contentBlocks = append(contentBlocks, schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesInputMessageContentBlockTypeText, Text: block.Text, CacheControl: block.CacheControl, }) } } if len(contentBlocks) > 0 { bifrostMessages = append(bifrostMessages, schemas.ResponsesMessage{ Role: schemas.Ptr(schemas.ResponsesInputMessageRoleSystem), Content: &schemas.ResponsesMessageContent{ ContentBlocks: contentBlocks, }, }) } } return bifrostMessages } // Helper function to convert a single Anthropic message to Bifrost messages func convertSingleAnthropicMessageToBifrostMessages(ctx *schemas.BifrostContext, msg *AnthropicMessage, isOutputMessage bool, structuredOutputToolName string) []schemas.ResponsesMessage { // Determine if this message should use output types based on role // Assistant messages in conversation history should use output_text isOutput := isOutputMessage || msg.Role == AnthropicMessageRoleAssistant // Handle text content (simple case) if msg.Content.ContentStr != nil { roleVal := schemas.ResponsesMessageRoleType(msg.Role) return []schemas.ResponsesMessage{ { Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: &roleVal, Content: &schemas.ResponsesMessageContent{ ContentStr: msg.Content.ContentStr, }, }, } } // Handle content blocks if msg.Content.ContentBlocks != nil { roleVal := schemas.ResponsesMessageRoleType(msg.Role) return convertAnthropicContentBlocksToResponsesMessages(ctx, msg.Content.ContentBlocks, &roleVal, isOutput, structuredOutputToolName) } return []schemas.ResponsesMessage{} } // Helper function to convert a single Anthropic message to Bifrost messages, grouping text and tool calls // This keeps assistant messages with mixed text and tool_use blocks together func convertSingleAnthropicMessageToBifrostMessagesGrouped(msg *AnthropicMessage, isOutputMessage bool, structuredOutputToolName string) []schemas.ResponsesMessage { // Determine if this message should use output types based on role // Assistant messages in conversation history should use output_text isOutput := isOutputMessage || msg.Role == AnthropicMessageRoleAssistant // Handle text content (simple case) if msg.Content.ContentStr != nil { roleVal := schemas.ResponsesMessageRoleType(msg.Role) return []schemas.ResponsesMessage{ { Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: &roleVal, Content: &schemas.ResponsesMessageContent{ ContentStr: msg.Content.ContentStr, }, }, } } // Handle content blocks with grouping for text and tool calls if msg.Content.ContentBlocks != nil { roleVal := schemas.ResponsesMessageRoleType(msg.Role) return convertAnthropicContentBlocksToResponsesMessagesGrouped(msg.Content.ContentBlocks, &roleVal, isOutput) } return []schemas.ResponsesMessage{} } // Helper function to convert Anthropic content blocks to Bifrost ResponsesMessages, grouping text and tool_use blocks func convertAnthropicContentBlocksToResponsesMessagesGrouped(contentBlocks []AnthropicContentBlock, role *schemas.ResponsesMessageRoleType, isOutputMessage bool) []schemas.ResponsesMessage { var bifrostMessages []schemas.ResponsesMessage var accumulatedTextContent []schemas.ResponsesMessageContentBlock var pendingToolUseBlocks []*AnthropicContentBlock // Accumulate tool_use blocks // Process content blocks for _, block := range contentBlocks { switch block.Type { case AnthropicContentBlockTypeText: if block.Text != nil { if isOutputMessage { // For output messages, accumulate text blocks (don't emit immediately) accumulatedTextContent = append(accumulatedTextContent, schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesOutputMessageContentTypeText, Text: block.Text, ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{ LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{}, Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{}, }, }) } else { // For input messages, emit text immediately as separate message bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ { Type: schemas.ResponsesOutputMessageContentTypeText, Text: block.Text, CacheControl: block.CacheControl, ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{ LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{}, Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{}, }, }, }, }, } bifrostMessages = append(bifrostMessages, bifrostMsg) } } case AnthropicContentBlockTypeImage: // Don't emit accumulated text or tool_use blocks for images if block.Source != nil && block.Source.SourceObj != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{block.toBifrostResponsesImageBlock()}, }, } if isOutputMessage { bifrostMsg.ID = schemas.Ptr("msg_" + providerUtils.GetRandomString(50)) } bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeDocument: // Handle document blocks similar to images if block.Source != nil && block.Source.SourceObj != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{block.toBifrostResponsesDocumentBlock()}, }, } if isOutputMessage { bifrostMsg.ID = schemas.Ptr("msg_" + providerUtils.GetRandomString(50)) } bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeThinking: if block.Thinking != nil { bifrostMsg := schemas.ResponsesMessage{ ID: schemas.Ptr("rs_" + providerUtils.GetRandomString(50)), Type: schemas.Ptr(schemas.ResponsesMessageTypeReasoning), Role: role, Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ { Type: schemas.ResponsesOutputMessageContentTypeReasoning, Text: block.Thinking, Signature: block.Signature, }, }, }, } bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeRedactedThinking: // Handle redacted thinking (encrypted content) if block.Data != nil { bifrostMsg := schemas.ResponsesMessage{ ID: schemas.Ptr("rs_" + providerUtils.GetRandomString(50)), Type: schemas.Ptr(schemas.ResponsesMessageTypeReasoning), ResponsesReasoning: &schemas.ResponsesReasoning{ Summary: []schemas.ResponsesReasoningSummary{}, EncryptedContent: block.Data, }, } bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeToolUse: // Accumulate tool_use blocks to group them together if block.ID != nil && block.Name != nil { blockCopy := block pendingToolUseBlocks = append(pendingToolUseBlocks, &blockCopy) } case AnthropicContentBlockTypeToolResult: // Convert tool result to function call output message if block.ToolUseID != nil { if block.Content != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCallOutput), Status: schemas.Ptr("completed"), CacheControl: block.CacheControl, ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: block.ToolUseID, }, } // Initialize the nested struct before any writes bifrostMsg.ResponsesToolMessage.Output = &schemas.ResponsesToolMessageOutputStruct{} if block.Content.ContentStr != nil { bifrostMsg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr = block.Content.ContentStr } else if block.Content.ContentBlocks != nil { var toolMsgContentBlocks []schemas.ResponsesMessageContentBlock for _, contentBlock := range block.Content.ContentBlocks { switch contentBlock.Type { case AnthropicContentBlockTypeText: if contentBlock.Text != nil { var blockType schemas.ResponsesMessageContentBlockType if isOutputMessage { blockType = schemas.ResponsesOutputMessageContentTypeText } else { blockType = schemas.ResponsesInputMessageContentBlockTypeText } toolMsgContentBlocks = append(toolMsgContentBlocks, schemas.ResponsesMessageContentBlock{ Type: blockType, Text: contentBlock.Text, CacheControl: contentBlock.CacheControl, }) } case AnthropicContentBlockTypeImage: if contentBlock.Source != nil && contentBlock.Source.SourceObj != nil { toolMsgContentBlocks = append(toolMsgContentBlocks, contentBlock.toBifrostResponsesImageBlock()) } } } bifrostMsg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks = toolMsgContentBlocks } // Handle is_error from Anthropic if block.IsError != nil && *block.IsError { bifrostMsg.Status = schemas.Ptr("incomplete") } bifrostMessages = append(bifrostMessages, bifrostMsg) } } case AnthropicContentBlockTypeServerToolUse: // Accumulate server tool use blocks if block.ID != nil && block.Name != nil { blockCopy := block pendingToolUseBlocks = append(pendingToolUseBlocks, &blockCopy) } case AnthropicContentBlockTypeMCPToolUse: // Accumulate MCP tool use blocks if block.ID != nil && block.Name != nil { blockCopy := block pendingToolUseBlocks = append(pendingToolUseBlocks, &blockCopy) } case AnthropicContentBlockTypeMCPToolResult: // Handle MCP tool results directly without flushing other blocks // MCP results will be emitted as separate messages case AnthropicContentBlockTypeWebSearchResult: // Find the corresponding web_search_call by tool_use_id and attach sources if block.ToolUseID != nil { attachWebSearchSourcesToCall(bifrostMessages, *block.ToolUseID, block, true) } } } // Flush any remaining pending blocks if len(accumulatedTextContent) > 0 { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, } if isOutputMessage { bifrostMsg.ID = schemas.Ptr("msg_" + providerUtils.GetRandomString(50)) bifrostMsg.Content = &schemas.ResponsesMessageContent{ ContentBlocks: accumulatedTextContent, } bifrostMessages = append(bifrostMessages, bifrostMsg) } } // Emit any accumulated tool_use blocks as function_calls if len(pendingToolUseBlocks) > 0 { for _, toolBlock := range pendingToolUseBlocks { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall), Status: schemas.Ptr("completed"), CacheControl: toolBlock.CacheControl, ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: toolBlock.ID, Name: toolBlock.Name, }, } if isOutputMessage { bifrostMsg.ID = schemas.Ptr("fc_" + providerUtils.GetRandomString(50)) } // Check for computer tool use if toolBlock.Name != nil && *toolBlock.Name == string(AnthropicToolNameComputer) { bifrostMsg.Type = schemas.Ptr(schemas.ResponsesMessageTypeComputerCall) bifrostMsg.ResponsesToolMessage.Name = nil var inputMap map[string]interface{} if err := sonic.Unmarshal(toolBlock.Input, &inputMap); err == nil { bifrostMsg.ResponsesToolMessage.Action = &schemas.ResponsesToolMessageActionStruct{ ResponsesComputerToolCallAction: convertAnthropicToResponsesComputerAction(inputMap), } } } else if toolBlock.Name != nil && *toolBlock.Name == string(AnthropicToolNameWebSearch) { bifrostMsg.Type = schemas.Ptr(schemas.ResponsesMessageTypeWebSearchCall) bifrostMsg.ResponsesToolMessage.Name = nil if q := providerUtils.GetJSONField(toolBlock.Input, "query"); q.Exists() && q.Type == gjson.String { query := q.Str bifrostMsg.ResponsesToolMessage.Action = &schemas.ResponsesToolMessageActionStruct{ ResponsesWebSearchToolCallAction: &schemas.ResponsesWebSearchToolCallAction{ Type: "search", Query: schemas.Ptr(query), Queries: []string{query}, }, } } } else if toolBlock.Name != nil && *toolBlock.Name == string(AnthropicToolNameWebFetch) { bifrostMsg.Type = schemas.Ptr(schemas.ResponsesMessageTypeWebFetchCall) bifrostMsg.ResponsesToolMessage.Name = nil if u := providerUtils.GetJSONField(toolBlock.Input, "url"); u.Exists() && u.Type == gjson.String { bifrostMsg.ResponsesToolMessage.Action = &schemas.ResponsesToolMessageActionStruct{ ResponsesWebFetchToolCallAction: &schemas.ResponsesWebFetchToolCallAction{ URL: u.Str, }, } } } else { if len(toolBlock.Input) > 0 { bifrostMsg.ResponsesToolMessage.Arguments = schemas.Ptr(string(toolBlock.Input)) } } bifrostMessages = append(bifrostMessages, bifrostMsg) } } return bifrostMessages } // Helper function to convert Anthropic content blocks to Bifrost ResponsesMessages func convertAnthropicContentBlocksToResponsesMessages(ctx *schemas.BifrostContext, contentBlocks []AnthropicContentBlock, role *schemas.ResponsesMessageRoleType, isOutputMessage bool, structuredOutputToolName string) []schemas.ResponsesMessage { var bifrostMessages []schemas.ResponsesMessage var reasoningContentBlocks []schemas.ResponsesMessageContentBlock // Process content blocks for _, block := range contentBlocks { switch block.Type { case AnthropicContentBlockTypeCompaction: if block.Content != nil { var summaryText string if block.Content.ContentStr != nil { summaryText = *block.Content.ContentStr } bifrostMsg := schemas.ResponsesMessage{ ID: schemas.Ptr("cmp_" + providerUtils.GetRandomString(50)), Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, Status: schemas.Ptr("completed"), Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ { Type: schemas.ResponsesOutputMessageContentTypeCompaction, CacheControl: block.CacheControl, ResponsesOutputMessageContentCompaction: &schemas.ResponsesOutputMessageContentCompaction{ Summary: summaryText, }, }, }, }, } bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeText: if block.Text != nil { var bifrostMsg schemas.ResponsesMessage if isOutputMessage { // For output messages, use ContentBlocks with ResponsesOutputMessageContentTypeText contentBlock := schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesOutputMessageContentTypeText, Text: block.Text, CacheControl: block.CacheControl, ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{ LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{}, Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{}, }, } // Convert Anthropic citations to OpenAI annotations if block.Citations != nil && len(block.Citations.TextCitations) > 0 { annotations := make([]schemas.ResponsesOutputMessageContentTextAnnotation, len(block.Citations.TextCitations)) fullText := "" if block.Text != nil { fullText = *block.Text } for i, citation := range block.Citations.TextCitations { annotations[i] = convertAnthropicCitationToAnnotation(citation, fullText) } contentBlock.ResponsesOutputMessageContentText = &schemas.ResponsesOutputMessageContentText{ Annotations: annotations, } } bifrostMsg = schemas.ResponsesMessage{ ID: schemas.Ptr("msg_" + providerUtils.GetRandomString(50)), Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, Status: schemas.Ptr("completed"), Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{contentBlock}, }, } } else { // For input messages, use ContentStr bifrostMsg = schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{ { Type: schemas.ResponsesInputMessageContentBlockTypeText, Text: block.Text, CacheControl: block.CacheControl, }, }, }, } } bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeImage: if block.Source != nil && block.Source.SourceObj != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{block.toBifrostResponsesImageBlock()}, }, } if isOutputMessage { bifrostMsg.ID = schemas.Ptr("msg_" + providerUtils.GetRandomString(50)) } bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeDocument: if block.Source != nil && block.Source.SourceObj != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{block.toBifrostResponsesDocumentBlock()}, }, } if isOutputMessage { bifrostMsg.ID = schemas.Ptr("msg_" + providerUtils.GetRandomString(50)) } bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeThinking: if block.Thinking != nil { // Collect reasoning blocks to create a single reasoning message reasoningContentBlocks = append(reasoningContentBlocks, schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesOutputMessageContentTypeReasoning, Text: block.Thinking, Signature: block.Signature, }) } case AnthropicContentBlockTypeRedactedThinking: if block.Data != nil { bifrostMsg := schemas.ResponsesMessage{ ID: schemas.Ptr("rs_" + providerUtils.GetRandomString(50)), Type: schemas.Ptr(schemas.ResponsesMessageTypeReasoning), ResponsesReasoning: &schemas.ResponsesReasoning{ Summary: []schemas.ResponsesReasoningSummary{}, EncryptedContent: block.Data, }, } bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeToolUse: // Check if this is the structured output tool - if so, convert to text content if structuredOutputToolName != "" && block.Name != nil && *block.Name == structuredOutputToolName { // This is a structured output tool - convert to text message var jsonStr string if block.Input != nil { jsonStr = string(block.Input) } else { jsonStr = "{}" } contentBlock := schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesOutputMessageContentTypeText, Text: &jsonStr, ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{ LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{}, Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{}, }, } bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage), Role: role, Status: schemas.Ptr("completed"), Content: &schemas.ResponsesMessageContent{ ContentBlocks: []schemas.ResponsesMessageContentBlock{contentBlock}, }, } if isOutputMessage { bifrostMsg.ID = schemas.Ptr("msg_" + providerUtils.GetRandomString(50)) } bifrostMessages = append(bifrostMessages, bifrostMsg) } else { // Convert tool use to function call message if block.ID != nil && block.Name != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall), Status: schemas.Ptr("completed"), CacheControl: block.CacheControl, ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: block.ID, Name: block.Name, }, } if isOutputMessage { bifrostMsg.ID = schemas.Ptr("fc_" + providerUtils.GetRandomString(50)) } // here need to check for computer tool use if block.Name != nil && *block.Name == string(AnthropicToolNameComputer) { bifrostMsg.Type = schemas.Ptr(schemas.ResponsesMessageTypeComputerCall) bifrostMsg.ResponsesToolMessage.Name = nil var inputMap map[string]interface{} if err := sonic.Unmarshal(block.Input, &inputMap); err == nil { bifrostMsg.ResponsesToolMessage.Action = &schemas.ResponsesToolMessageActionStruct{ ResponsesComputerToolCallAction: convertAnthropicToResponsesComputerAction(inputMap), } } } else if len(block.Input) > 0 { bifrostMsg.ResponsesToolMessage.Arguments = schemas.Ptr(string(block.Input)) } bifrostMessages = append(bifrostMessages, bifrostMsg) } } case AnthropicContentBlockTypeToolResult: // Convert tool result to function call output message if block.ToolUseID != nil { if block.Content != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCallOutput), Status: schemas.Ptr("completed"), CacheControl: block.CacheControl, ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: block.ToolUseID, }, } // Initialize the nested struct before any writes bifrostMsg.ResponsesToolMessage.Output = &schemas.ResponsesToolMessageOutputStruct{} if block.Content.ContentStr != nil { bifrostMsg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr = block.Content.ContentStr } else if block.Content.ContentBlocks != nil { var toolMsgContentBlocks []schemas.ResponsesMessageContentBlock for _, contentBlock := range block.Content.ContentBlocks { switch contentBlock.Type { case AnthropicContentBlockTypeText: if contentBlock.Text != nil { var blockType schemas.ResponsesMessageContentBlockType if isOutputMessage { blockType = schemas.ResponsesOutputMessageContentTypeText } else { blockType = schemas.ResponsesInputMessageContentBlockTypeText } toolMsgContentBlocks = append(toolMsgContentBlocks, schemas.ResponsesMessageContentBlock{ Type: blockType, Text: contentBlock.Text, CacheControl: contentBlock.CacheControl, }) } case AnthropicContentBlockTypeImage: if contentBlock.Source != nil && contentBlock.Source.SourceObj != nil { toolMsgContentBlocks = append(toolMsgContentBlocks, contentBlock.toBifrostResponsesImageBlock()) } } } bifrostMsg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks = toolMsgContentBlocks } // Handle is_error from Anthropic if block.IsError != nil && *block.IsError { bifrostMsg.Status = schemas.Ptr("incomplete") } bifrostMessages = append(bifrostMessages, bifrostMsg) } } case AnthropicContentBlockTypeServerToolUse: // Check if it's a web_search tool if block.Name != nil && *block.Name == string(AnthropicToolNameWebSearch) { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeWebSearchCall), Status: schemas.Ptr("completed"), ResponsesToolMessage: &schemas.ResponsesToolMessage{}, } // Extract query from input if block.Input != nil { if q := providerUtils.GetJSONField(block.Input, "query"); q.Exists() && q.Type == gjson.String { query := q.Str bifrostMsg.ResponsesToolMessage.Action = &schemas.ResponsesToolMessageActionStruct{ ResponsesWebSearchToolCallAction: &schemas.ResponsesWebSearchToolCallAction{ Type: "search", Query: schemas.Ptr(query), Queries: []string{query}, // Anthropic uses single query }, } } } if isOutputMessage { bifrostMsg.ID = block.ID bifrostMessages = append(bifrostMessages, bifrostMsg) } } else if block.Name != nil && *block.Name == string(AnthropicToolNameWebFetch) { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeWebFetchCall), Status: schemas.Ptr("completed"), ResponsesToolMessage: &schemas.ResponsesToolMessage{}, } if block.Input != nil { if u := providerUtils.GetJSONField(block.Input, "url"); u.Exists() && u.Type == gjson.String { bifrostMsg.ResponsesToolMessage.Action = &schemas.ResponsesToolMessageActionStruct{ ResponsesWebFetchToolCallAction: &schemas.ResponsesWebFetchToolCallAction{ URL: u.Str, }, } } } if isOutputMessage { bifrostMsg.ID = block.ID bifrostMessages = append(bifrostMessages, bifrostMsg) } } case AnthropicContentBlockTypeWebSearchToolResult: // Find the corresponding web_search_call by tool_use_id if block.ToolUseID != nil { attachWebSearchSourcesToCall(bifrostMessages, *block.ToolUseID, block, true) } case AnthropicContentBlockTypeWebFetchToolResult: // Web fetch results are handled server-side by Anthropic, skip case AnthropicContentBlockTypeWebSearchToolResultError: // Handle web search errors — find matching web_search_call and mark as failed if block.ToolUseID != nil { for i := len(bifrostMessages) - 1; i >= 0; i-- { msg := &bifrostMessages[i] if msg.Type != nil && *msg.Type == schemas.ResponsesMessageTypeWebSearchCall && msg.ID != nil && *msg.ID == *block.ToolUseID { msg.Status = schemas.Ptr("failed") break } } } case AnthropicContentBlockTypeMCPToolUse: // Convert MCP tool use to MCP call (assistant's tool call) if block.ID != nil && block.Name != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMCPCall), ID: block.ID, ResponsesToolMessage: &schemas.ResponsesToolMessage{ Name: block.Name, }, } if len(block.Input) > 0 { bifrostMsg.ResponsesToolMessage.Arguments = schemas.Ptr(string(block.Input)) } if block.ServerName != nil { bifrostMsg.ResponsesToolMessage.ResponsesMCPToolCall = &schemas.ResponsesMCPToolCall{ ServerLabel: *block.ServerName, } } bifrostMessages = append(bifrostMessages, bifrostMsg) } case AnthropicContentBlockTypeMCPToolResult: // Convert MCP tool result to MCP call (user's tool result) if block.ToolUseID != nil { bifrostMsg := schemas.ResponsesMessage{ Type: schemas.Ptr(schemas.ResponsesMessageTypeMCPCall), Status: schemas.Ptr("completed"), ResponsesToolMessage: &schemas.ResponsesToolMessage{ CallID: block.ToolUseID, }, } if isOutputMessage { bifrostMsg.ID = schemas.Ptr("msg_" + providerUtils.GetRandomString(50)) } // Initialize the nested struct before any writes bifrostMsg.ResponsesToolMessage.Output = &schemas.ResponsesToolMessageOutputStruct{} if block.Content != nil { if block.Content.ContentStr != nil { bifrostMsg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr = block.Content.ContentStr } else if block.Content.ContentBlocks != nil { var toolMsgContentBlocks []schemas.ResponsesMessageContentBlock for _, contentBlock := range block.Content.ContentBlocks { if contentBlock.Type == AnthropicContentBlockTypeText { if contentBlock.Text != nil { var blockType schemas.ResponsesMessageContentBlockType if isOutputMessage { blockType = schemas.ResponsesOutputMessageContentTypeText } else { blockType = schemas.ResponsesInputMessageContentBlockTypeText } toolMsgContentBlocks = append(toolMsgContentBlocks, schemas.ResponsesMessageContentBlock{ Type: blockType, Text: contentBlock.Text, CacheControl: contentBlock.CacheControl, }) } } } bifrostMsg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks = toolMsgContentBlocks } } bifrostMessages = append(bifrostMessages, bifrostMsg) } default: // Handle other block types if needed } } // Handle reasoning blocks - prepend reasoning message if we collected any // This ensures reasoning comes before any text/tool blocks (Bedrock compatibility) if len(reasoningContentBlocks) > 0 { reasoningMessage := schemas.ResponsesMessage{ ID: schemas.Ptr("rs_" + providerUtils.GetRandomString(50)), Type: schemas.Ptr(schemas.ResponsesMessageTypeReasoning), ResponsesReasoning: &schemas.ResponsesReasoning{ Summary: []schemas.ResponsesReasoningSummary{}, }, Content: &schemas.ResponsesMessageContent{ ContentBlocks: reasoningContentBlocks, }, } // Prepend the reasoning message to the start of the messages list // This ensures reasoning comes before text/tool responses bifrostMessages = append([]schemas.ResponsesMessage{reasoningMessage}, bifrostMessages...) } return bifrostMessages } // Helper functions for converting individual Bifrost message types to Anthropic messages // convertBifrostMessageToAnthropicSystemContent converts a Bifrost system message to Anthropic system content func convertBifrostMessageToAnthropicSystemContent(msg *schemas.ResponsesMessage) *AnthropicContent { if msg.Content != nil { if msg.Content.ContentStr != nil { return &AnthropicContent{ ContentStr: msg.Content.ContentStr, } } else if msg.Content.ContentBlocks != nil { contentBlocks := convertBifrostContentBlocksToAnthropic(msg.Content.ContentBlocks) if len(contentBlocks) > 0 { return &AnthropicContent{ ContentBlocks: contentBlocks, } } } } return nil } // convertBifrostMessageToAnthropicMessage converts a regular Bifrost message to Anthropic message func convertBifrostMessageToAnthropicMessage(msg *schemas.ResponsesMessage, pendingReasoningContentBlocks *[]AnthropicContentBlock) *AnthropicMessage { anthropicMsg := AnthropicMessage{} // Set role if msg.Role != nil { switch *msg.Role { case schemas.ResponsesInputMessageRoleUser: anthropicMsg.Role = AnthropicMessageRoleUser case schemas.ResponsesInputMessageRoleAssistant: anthropicMsg.Role = AnthropicMessageRoleAssistant default: anthropicMsg.Role = AnthropicMessageRoleUser // Default fallback } } else { anthropicMsg.Role = AnthropicMessageRoleUser // Default fallback } // Add any pending reasoning content blocks to the message // Only add reasoning blocks to assistant messages (thinking blocks can only appear in assistant messages in Anthropic) if len(*pendingReasoningContentBlocks) > 0 && anthropicMsg.Role == AnthropicMessageRoleAssistant { // copy the pending reasoning content blocks copied := make([]AnthropicContentBlock, len(*pendingReasoningContentBlocks)) copy(copied, *pendingReasoningContentBlocks) contentBlocks := copied *pendingReasoningContentBlocks = nil // Add content blocks after pending reasoning content blocks are added if msg.Content != nil { if msg.Content.ContentStr != nil { contentBlocks = append(contentBlocks, AnthropicContentBlock{ Type: AnthropicContentBlockTypeText, Text: msg.Content.ContentStr, }) } else if msg.Content.ContentBlocks != nil { contentBlocks = append(contentBlocks, convertBifrostContentBlocksToAnthropic(msg.Content.ContentBlocks)...) } } anthropicMsg.Content = AnthropicContent{ ContentBlocks: contentBlocks, } } else { // Convert content if msg.Content != nil { if msg.Content.ContentStr != nil { anthropicMsg.Content = AnthropicContent{ ContentBlocks: []AnthropicContentBlock{{ Type: AnthropicContentBlockTypeText, Text: msg.Content.ContentStr, }}, } } else if msg.Content.ContentBlocks != nil { contentBlocks := convertBifrostContentBlocksToAnthropic(msg.Content.ContentBlocks) if len(contentBlocks) > 0 { anthropicMsg.Content = AnthropicContent{ ContentBlocks: contentBlocks, } } } } } return &anthropicMsg } // convertBifrostReasoningToAnthropicThinking converts a Bifrost reasoning message to Anthropic thinking blocks func convertBifrostReasoningToAnthropicThinking(msg *schemas.ResponsesMessage) []AnthropicContentBlock { var thinkingBlocks []AnthropicContentBlock if msg.Content != nil && msg.Content.ContentBlocks != nil { for _, block := range msg.Content.ContentBlocks { if block.Type == schemas.ResponsesOutputMessageContentTypeReasoning && block.Text != nil { thinkingBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeThinking, Thinking: block.Text, Signature: block.Signature, } thinkingBlocks = append(thinkingBlocks, thinkingBlock) } } } else if msg.ResponsesReasoning != nil { if msg.ResponsesReasoning.Summary != nil { for _, reasoningContent := range msg.ResponsesReasoning.Summary { thinkingBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeThinking, Thinking: &reasoningContent.Text, } thinkingBlocks = append(thinkingBlocks, thinkingBlock) } } else if msg.ResponsesReasoning.EncryptedContent != nil { thinkingBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeRedactedThinking, Data: msg.ResponsesReasoning.EncryptedContent, } thinkingBlocks = append(thinkingBlocks, thinkingBlock) } } return thinkingBlocks } // convertBifrostFunctionCallToAnthropicToolUse converts a Bifrost function call to Anthropic tool use func convertBifrostFunctionCallToAnthropicToolUse(ctx *schemas.BifrostContext, msg *schemas.ResponsesMessage) *AnthropicContentBlock { if msg.ResponsesToolMessage != nil { toolUseBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeToolUse, CacheControl: msg.CacheControl, } if msg.ResponsesToolMessage.CallID != nil { toolUseBlock.ID = msg.ResponsesToolMessage.CallID } if msg.ResponsesToolMessage.Name != nil { toolUseBlock.Name = msg.ResponsesToolMessage.Name } // Parse arguments as JSON input if msg.ResponsesToolMessage.Arguments != nil && *msg.ResponsesToolMessage.Arguments != "" { argumentsJSON := *msg.ResponsesToolMessage.Arguments // Sanitize WebSearch tool arguments to remove both allowed_domains and blocked_domains // Anthropic only allows one or the other, not both // Only do this for Claude CLI if ctx != nil { if IsClaudeCodeRequest(ctx) { if msg.ResponsesToolMessage.Name != nil && *msg.ResponsesToolMessage.Name == "WebSearch" { argumentsJSON = sanitizeWebSearchArguments(argumentsJSON) } } } toolUseBlock.Input = parseJSONInput(argumentsJSON) } return &toolUseBlock } return nil } // convertBifrostFunctionCallOutputToAnthropicToolResultBlock converts a Bifrost function call output to a single tool result block // This is used to accumulate multiple tool results into a single user message func convertBifrostFunctionCallOutputToAnthropicToolResultBlock(msg *schemas.ResponsesMessage) *AnthropicContentBlock { if msg.ResponsesToolMessage != nil { toolResultBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeToolResult, ToolUseID: msg.ResponsesToolMessage.CallID, CacheControl: msg.CacheControl, } if msg.ResponsesToolMessage.Output != nil { toolResultBlock.Content = convertToolOutputToAnthropicContent(msg.ResponsesToolMessage.Output) } // Set is_error if there's an error message or the status indicates an error if msg.ResponsesToolMessage.Error != nil && *msg.ResponsesToolMessage.Error != "" { toolResultBlock.IsError = schemas.Ptr(true) if toolResultBlock.Content == nil { toolResultBlock.Content = &AnthropicContent{ ContentStr: msg.ResponsesToolMessage.Error, } } } else if msg.Status != nil && *msg.Status == "incomplete" { toolResultBlock.IsError = schemas.Ptr(true) } return &toolResultBlock } return nil } // convertBifrostComputerCallOutputToAnthropicToolResultBlock converts a Bifrost computer call output to a single tool result block // This is used to accumulate multiple tool results into a single user message func convertBifrostComputerCallOutputToAnthropicToolResultBlock(msg *schemas.ResponsesMessage) *AnthropicContentBlock { if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.CallID != nil { toolResultBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeToolResult, ToolUseID: msg.ResponsesToolMessage.CallID, } // Handle output if msg.ResponsesToolMessage.Output != nil { toolResultBlock.Content = convertToolOutputToAnthropicContent(msg.ResponsesToolMessage.Output) } // Set is_error if there's an error message or the status indicates an error if msg.ResponsesToolMessage.Error != nil && *msg.ResponsesToolMessage.Error != "" { toolResultBlock.IsError = schemas.Ptr(true) if toolResultBlock.Content == nil { toolResultBlock.Content = &AnthropicContent{ ContentStr: msg.ResponsesToolMessage.Error, } } } else if msg.Status != nil && *msg.Status == "incomplete" { toolResultBlock.IsError = schemas.Ptr(true) } return &toolResultBlock } return nil } // convertBifrostMCPCallOutputToAnthropicToolResultBlock converts a Bifrost MCP call output to a single tool result block // This is used to accumulate multiple tool results into a single user message func convertBifrostMCPCallOutputToAnthropicToolResultBlock(msg *schemas.ResponsesMessage) *AnthropicContentBlock { if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.CallID != nil { toolResultBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeMCPToolResult, ToolUseID: msg.ResponsesToolMessage.CallID, } // Handle output if msg.ResponsesToolMessage.Output != nil { toolResultBlock.Content = convertToolOutputToAnthropicContent(msg.ResponsesToolMessage.Output) } // Set is_error if there's an error message or the status indicates an error if msg.ResponsesToolMessage.Error != nil && *msg.ResponsesToolMessage.Error != "" { toolResultBlock.IsError = schemas.Ptr(true) if toolResultBlock.Content == nil { toolResultBlock.Content = &AnthropicContent{ ContentStr: msg.ResponsesToolMessage.Error, } } } else if msg.Status != nil && *msg.Status == "incomplete" { toolResultBlock.IsError = schemas.Ptr(true) } return &toolResultBlock } return nil } // convertBifrostItemReferenceToAnthropicMessage converts a Bifrost item reference to Anthropic message func convertBifrostItemReferenceToAnthropicMessage(msg *schemas.ResponsesMessage) *AnthropicMessage { if msg.Content != nil && msg.Content.ContentStr != nil { referenceMsg := AnthropicMessage{ Role: AnthropicMessageRoleUser, // Default to user for references } if msg.Role != nil && *msg.Role == schemas.ResponsesInputMessageRoleAssistant { referenceMsg.Role = AnthropicMessageRoleAssistant } referenceMsg.Content = AnthropicContent{ ContentBlocks: []AnthropicContentBlock{{ Type: AnthropicContentBlockTypeText, Text: msg.Content.ContentStr, }}, } return &referenceMsg } return nil } // convertBifrostComputerCallToAnthropicToolUse converts a Bifrost computer call to Anthropic tool use func convertBifrostComputerCallToAnthropicToolUse(msg *schemas.ResponsesMessage) *AnthropicContentBlock { if msg.ResponsesToolMessage != nil { toolUseBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeToolUse, Name: schemas.Ptr(string(AnthropicToolNameComputer)), } if msg.ResponsesToolMessage.CallID != nil { toolUseBlock.ID = msg.ResponsesToolMessage.CallID } if msg.ResponsesToolMessage.Name != nil { toolUseBlock.Name = msg.ResponsesToolMessage.Name } if msg.ResponsesToolMessage.Action != nil && msg.ResponsesToolMessage.Action.ResponsesComputerToolCallAction != nil { inputMap := convertResponsesToAnthropicComputerAction(msg.ResponsesToolMessage.Action.ResponsesComputerToolCallAction) if inputBytes, err := providerUtils.MarshalSorted(inputMap); err == nil { toolUseBlock.Input = json.RawMessage(inputBytes) } } return &toolUseBlock } return nil } // convertBifrostMCPCallToAnthropicToolUse converts a Bifrost MCP call to Anthropic tool use func convertBifrostMCPCallToAnthropicToolUse(msg *schemas.ResponsesMessage) *AnthropicContentBlock { if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.Name != nil { toolUseBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeMCPToolUse, } if msg.ID != nil { toolUseBlock.ID = msg.ID } toolUseBlock.Name = msg.ResponsesToolMessage.Name // Set server name if present if msg.ResponsesToolMessage.ResponsesMCPToolCall != nil && msg.ResponsesToolMessage.ResponsesMCPToolCall.ServerLabel != "" { toolUseBlock.ServerName = &msg.ResponsesToolMessage.ResponsesMCPToolCall.ServerLabel } // Parse arguments as JSON input if msg.ResponsesToolMessage.Arguments != nil && *msg.ResponsesToolMessage.Arguments != "" { toolUseBlock.Input = parseJSONInput(*msg.ResponsesToolMessage.Arguments) } return &toolUseBlock } return nil } // convertBifrostMCPCallOutputToAnthropicMessage converts a Bifrost MCP call output to Anthropic message func convertBifrostMCPCallOutputToAnthropicMessage(msg *schemas.ResponsesMessage) *AnthropicMessage { toolResultBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeMCPToolResult, ID: msg.ResponsesToolMessage.CallID, } if msg.ResponsesToolMessage.Output != nil { toolResultBlock.Content = convertToolOutputToAnthropicContent(msg.ResponsesToolMessage.Output) } return &AnthropicMessage{ Role: AnthropicMessageRoleUser, Content: AnthropicContent{ ContentBlocks: []AnthropicContentBlock{toolResultBlock}, }, } } // convertBifrostMCPApprovalToAnthropicToolUse converts a Bifrost MCP approval request to Anthropic tool use func convertBifrostMCPApprovalToAnthropicToolUse(msg *schemas.ResponsesMessage) *AnthropicContentBlock { if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.Name != nil { toolUseBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeMCPToolUse, } if msg.ID != nil { toolUseBlock.ID = msg.ID } toolUseBlock.Name = msg.ResponsesToolMessage.Name // Set server name if present if msg.ResponsesToolMessage.ResponsesMCPToolCall != nil && msg.ResponsesToolMessage.ResponsesMCPToolCall.ServerLabel != "" { toolUseBlock.ServerName = &msg.ResponsesToolMessage.ResponsesMCPToolCall.ServerLabel } // Parse arguments as JSON input if msg.ResponsesToolMessage.Arguments != nil && *msg.ResponsesToolMessage.Arguments != "" { toolUseBlock.Input = parseJSONInput(*msg.ResponsesToolMessage.Arguments) } return &toolUseBlock } return nil } // convertBifrostWebSearchCallToAnthropicBlocks converts a Bifrost web_search_call to Anthropic server_tool_use and web_search_tool_result blocks func convertBifrostWebSearchCallToAnthropicBlocks(msg *schemas.ResponsesMessage) []AnthropicContentBlock { if msg.ResponsesToolMessage == nil || msg.ResponsesToolMessage.Action == nil || msg.ResponsesToolMessage.Action.ResponsesWebSearchToolCallAction == nil { return nil } var blocks []AnthropicContentBlock action := msg.ResponsesToolMessage.Action.ResponsesWebSearchToolCallAction // 1. Create server_tool_use block for the web search serverToolUseBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeServerToolUse, Name: schemas.Ptr("web_search"), } if msg.ID != nil { serverToolUseBlock.ID = msg.ID } // Extract the query from the action if action.Query != nil { inputBytes, err := providerUtils.MarshalSorted(map[string]interface{}{ "query": *action.Query, }) if err == nil { serverToolUseBlock.Input = json.RawMessage(inputBytes) } } blocks = append(blocks, serverToolUseBlock) // 2. Always create web_search_tool_result block — Anthropic requires it alongside every server_tool_use. // Without this block, the API returns: "web_search tool use was found without a corresponding web_search_tool_result block" var resultBlocks []AnthropicContentBlock for _, source := range action.Sources { if source.URL != "" { resultBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeWebSearchResult, URL: schemas.Ptr(source.URL), EncryptedContent: source.EncryptedContent, PageAge: source.PageAge, } if source.Title != nil { resultBlock.Title = source.Title } else if source.URL != "" { resultBlock.Title = schemas.Ptr(source.URL) } resultBlocks = append(resultBlocks, resultBlock) } } // Determine the tool use ID - prefer CallID (authoritative), fall back to msg.ID var toolUseID *string if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.CallID != nil { toolUseID = msg.ResponsesToolMessage.CallID } else { toolUseID = msg.ID } webSearchResultBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeWebSearchToolResult, ToolUseID: toolUseID, Content: &AnthropicContent{ ContentBlocks: resultBlocks, }, } blocks = append(blocks, webSearchResultBlock) return blocks } // convertBifrostUnsupportedToolCallToAnthropicMessage converts unsupported tool calls to text messages func convertBifrostUnsupportedToolCallToAnthropicMessage(msg *schemas.ResponsesMessage, msgType schemas.ResponsesMessageType) *AnthropicMessage { if msg.ResponsesToolMessage != nil { var description string if msg.ResponsesToolMessage.Name != nil { description = fmt.Sprintf("Tool call: %s", *msg.ResponsesToolMessage.Name) if msg.ResponsesToolMessage.Arguments != nil { description += fmt.Sprintf(" with arguments: %s", *msg.ResponsesToolMessage.Arguments) } } else { description = fmt.Sprintf("Tool call of type: %s", msgType) } return &AnthropicMessage{ Role: AnthropicMessageRoleAssistant, Content: AnthropicContent{ ContentBlocks: []AnthropicContentBlock{{ Type: AnthropicContentBlockTypeText, Text: &description, }}, }, } } return nil } // convertBifrostComputerCallOutputToAnthropicMessage converts a Bifrost computer call output to Anthropic message func convertBifrostComputerCallOutputToAnthropicMessage(msg *schemas.ResponsesMessage) *AnthropicMessage { if msg.ResponsesToolMessage != nil { toolResultBlock := AnthropicContentBlock{ Type: AnthropicContentBlockTypeToolResult, ToolUseID: msg.ResponsesToolMessage.CallID, } if msg.ResponsesToolMessage.Output != nil { toolResultBlock.Content = convertToolOutputToAnthropicContent(msg.ResponsesToolMessage.Output) } return &AnthropicMessage{ Role: AnthropicMessageRoleUser, Content: AnthropicContent{ ContentBlocks: []AnthropicContentBlock{toolResultBlock}, }, } } return nil } // convertBifrostToolOutputToAnthropicMessage converts tool outputs to user messages func convertBifrostToolOutputToAnthropicMessage(msg *schemas.ResponsesMessage) *AnthropicMessage { if msg.ResponsesToolMessage != nil { var outputText string // Try to extract output text based on tool type if msg.ResponsesToolMessage.Output != nil && msg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr != nil { outputText = *msg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr } if outputText != "" { return &AnthropicMessage{ Role: AnthropicMessageRoleUser, Content: AnthropicContent{ ContentBlocks: []AnthropicContentBlock{{ Type: AnthropicContentBlockTypeText, Text: &outputText, }}, }, } } } return nil } // convertAnthropicToolToBifrost converts AnthropicTool to schemas.Tool func convertAnthropicToolToBifrost(tool *AnthropicTool) *schemas.ResponsesTool { if tool == nil { return nil } // Skip mcp_toolset entries — these are merged with mcp_servers in ToBifrostResponsesRequest if tool.MCPToolset != nil { return nil } // Handle special tool types first if tool.Type != nil { switch *tool.Type { case AnthropicToolTypeComputer20250124, AnthropicToolTypeComputer20251124: bifrostTool := &schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeComputerUsePreview, } if tool.AnthropicToolComputerUse != nil { bifrostTool.ResponsesToolComputerUsePreview = &schemas.ResponsesToolComputerUsePreview{ Environment: "browser", // Default environment } if tool.AnthropicToolComputerUse.DisplayWidthPx != nil { bifrostTool.ResponsesToolComputerUsePreview.DisplayWidth = *tool.AnthropicToolComputerUse.DisplayWidthPx } if tool.AnthropicToolComputerUse.DisplayHeightPx != nil { bifrostTool.ResponsesToolComputerUsePreview.DisplayHeight = *tool.AnthropicToolComputerUse.DisplayHeightPx } if tool.AnthropicToolComputerUse.EnableZoom != nil { bifrostTool.ResponsesToolComputerUsePreview.EnableZoom = tool.AnthropicToolComputerUse.EnableZoom } } return bifrostTool case AnthropicToolTypeWebSearch20250305, AnthropicToolTypeWebSearch20260209: bifrostTool := &schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeWebSearch, } if tool.AnthropicToolWebSearch != nil { bifrostTool.ResponsesToolWebSearch = &schemas.ResponsesToolWebSearch{ Filters: &schemas.ResponsesToolWebSearchFilters{ AllowedDomains: tool.AnthropicToolWebSearch.AllowedDomains, BlockedDomains: tool.AnthropicToolWebSearch.BlockedDomains, }, } if tool.AnthropicToolWebSearch.MaxUses != nil { bifrostTool.ResponsesToolWebSearch.MaxUses = tool.AnthropicToolWebSearch.MaxUses } if tool.AnthropicToolWebSearch.UserLocation != nil { bifrostTool.ResponsesToolWebSearch.UserLocation = &schemas.ResponsesToolWebSearchUserLocation{ Type: tool.AnthropicToolWebSearch.UserLocation.Type, City: tool.AnthropicToolWebSearch.UserLocation.City, Country: tool.AnthropicToolWebSearch.UserLocation.Country, Timezone: tool.AnthropicToolWebSearch.UserLocation.Timezone, } } } return bifrostTool case AnthropicToolTypeWebFetch20250910, AnthropicToolTypeWebFetch20260209, AnthropicToolTypeWebFetch20260309: bifrostTool := &schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeWebFetch, } if tool.AnthropicToolWebFetch != nil { bifrostTool.ResponsesToolWebFetch = &schemas.ResponsesToolWebFetch{ MaxUses: tool.AnthropicToolWebFetch.MaxUses, MaxContentTokens: tool.AnthropicToolWebFetch.MaxContentTokens, } if len(tool.AnthropicToolWebFetch.AllowedDomains) > 0 || len(tool.AnthropicToolWebFetch.BlockedDomains) > 0 { bifrostTool.ResponsesToolWebFetch.Filters = &schemas.ResponsesToolWebSearchFilters{ AllowedDomains: tool.AnthropicToolWebFetch.AllowedDomains, BlockedDomains: tool.AnthropicToolWebFetch.BlockedDomains, } } } return bifrostTool case AnthropicToolTypeCodeExecution20250522, AnthropicToolTypeCodeExecution, AnthropicToolTypeCodeExecution20260120: return &schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeCodeInterpreter, } case AnthropicToolTypeMemory20250818: return &schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeMemory, Name: &tool.Name, } case AnthropicToolTypeToolSearchBM25, AnthropicToolTypeToolSearchBM2520251119, AnthropicToolTypeToolSearchRegex, AnthropicToolTypeToolSearchRegex20251119: return &schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeToolSearch, Name: &tool.Name, } case AnthropicToolTypeBash20250124: return &schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeLocalShell, } case AnthropicToolTypeTextEditor20250124: return &schemas.ResponsesTool{ Type: schemas.ResponsesToolType(AnthropicToolTypeTextEditor20250124), Name: &tool.Name, } case AnthropicToolTypeTextEditor20250429: return &schemas.ResponsesTool{ Type: schemas.ResponsesToolType(AnthropicToolTypeTextEditor20250429), Name: &tool.Name, } case AnthropicToolTypeTextEditor20250728: return &schemas.ResponsesTool{ Type: schemas.ResponsesToolType(AnthropicToolTypeTextEditor20250728), Name: &tool.Name, } } } // Handle custom/default tool type (function) bifrostTool := &schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeFunction, Name: &tool.Name, Description: tool.Description, } if tool.InputSchema != nil || tool.Strict != nil { bifrostTool.ResponsesToolFunction = &schemas.ResponsesToolFunction{ Parameters: tool.InputSchema, Strict: tool.Strict, } } if tool.CacheControl != nil { bifrostTool.CacheControl = tool.CacheControl } return bifrostTool } // convertAnthropicToolChoiceToBifrost converts AnthropicToolChoice to schemas.ToolChoice func convertAnthropicToolChoiceToBifrost(toolChoice *AnthropicToolChoice) *schemas.ResponsesToolChoice { if toolChoice == nil { return nil } bifrostToolChoice := &schemas.ResponsesToolChoice{} // Handle string format if toolChoice.Type != "" { switch toolChoice.Type { case "auto": bifrostToolChoice.ResponsesToolChoiceStr = schemas.Ptr(string(schemas.ResponsesToolChoiceTypeAuto)) case "any": bifrostToolChoice.ResponsesToolChoiceStr = schemas.Ptr(string(schemas.ResponsesToolChoiceTypeAny)) case "none": bifrostToolChoice.ResponsesToolChoiceStr = schemas.Ptr(string(schemas.ResponsesToolChoiceTypeNone)) case "tool": // Handle forced tool choice with specific function name bifrostToolChoice.ResponsesToolChoiceStruct = &schemas.ResponsesToolChoiceStruct{ Type: schemas.ResponsesToolChoiceTypeFunction, Name: &toolChoice.Name, } return bifrostToolChoice default: bifrostToolChoice.ResponsesToolChoiceStr = schemas.Ptr(string(schemas.ResponsesToolChoiceTypeAuto)) } } return bifrostToolChoice } // flushPendingContentBlocks is a helper that flushes accumulated content blocks into an assistant message func flushPendingContentBlocks( pendingContentBlocks []AnthropicContentBlock, currentAssistantMessage *AnthropicMessage, anthropicMessages []AnthropicMessage, ) ([]AnthropicContentBlock, *AnthropicMessage, []AnthropicMessage) { if len(pendingContentBlocks) > 0 && currentAssistantMessage != nil { // Copy the slice to avoid aliasing issues copied := make([]AnthropicContentBlock, len(pendingContentBlocks)) copy(copied, pendingContentBlocks) currentAssistantMessage.Content = AnthropicContent{ ContentBlocks: copied, } anthropicMessages = append(anthropicMessages, *currentAssistantMessage) // Return nil values to indicate flushed state return nil, nil, anthropicMessages } // Return unchanged values if no flush was needed return pendingContentBlocks, currentAssistantMessage, anthropicMessages } // convertToolOutputToAnthropicContent converts tool output to Anthropic content format func convertToolOutputToAnthropicContent(output *schemas.ResponsesToolMessageOutputStruct) *AnthropicContent { if output == nil { return nil } if output.ResponsesToolCallOutputStr != nil { return &AnthropicContent{ ContentStr: output.ResponsesToolCallOutputStr, } } if output.ResponsesFunctionToolCallOutputBlocks != nil { var resultBlocks []AnthropicContentBlock for _, block := range output.ResponsesFunctionToolCallOutputBlocks { if converted := convertContentBlockToAnthropic(block); converted != nil { resultBlocks = append(resultBlocks, *converted) } } if len(resultBlocks) > 0 { return &AnthropicContent{ ContentBlocks: resultBlocks, } } } if output.ResponsesComputerToolCallOutput != nil && output.ResponsesComputerToolCallOutput.ImageURL != nil { imgBlock := ConvertToAnthropicImageBlock(schemas.ChatContentBlock{ Type: schemas.ChatContentBlockTypeImage, ImageURLStruct: &schemas.ChatInputImage{ URL: *output.ResponsesComputerToolCallOutput.ImageURL, }, }) return &AnthropicContent{ ContentBlocks: []AnthropicContentBlock{imgBlock}, } } return nil } // convertBifrostToolsToAnthropic converts all Bifrost tools to Anthropic tools and MCP servers. // It handles context-dependent conversions like code_interpreter, which must be skipped when // web_search or web_fetch is present (Anthropic auto-injects code_execution in that case). func convertBifrostToolsToAnthropic(model string, tools []schemas.ResponsesTool, provider schemas.ModelProvider) ([]AnthropicTool, []AnthropicMCPServerV2) { // Check if web search or web fetch is present — when they are, Anthropic // auto-injects code_execution so we must skip it to avoid conflicts. hasWebSearchOrFetch := false for _, tool := range tools { if tool.Type == schemas.ResponsesToolTypeWebSearch || tool.Type == schemas.ResponsesToolTypeWebFetch { hasWebSearchOrFetch = true break } } anthropicTools := []AnthropicTool{} mcpServers := []AnthropicMCPServerV2{} for _, tool := range tools { if tool.Type == schemas.ResponsesToolTypeMCP && tool.ResponsesToolMCP != nil { server, toolset := convertBifrostMCPToolToAnthropicNew(&tool) if server != nil { mcpServers = append(mcpServers, *server) } if toolset != nil { mcpTool := AnthropicTool{MCPToolset: toolset} applyResponsesToolAnthropicFlags(&mcpTool, &tool) anthropicTools = append(anthropicTools, mcpTool) } continue } anthropicTool := convertBifrostToolToAnthropic(model, &tool, provider, hasWebSearchOrFetch) if anthropicTool != nil { applyResponsesToolAnthropicFlags(anthropicTool, &tool) anthropicTools = append(anthropicTools, *anthropicTool) } } return anthropicTools, mcpServers } // applyAnthropicToolFlagsToResponsesTool propagates the Anthropic-native tool // flags (DeferLoading, AllowedCallers, InputExamples, EagerInputStreaming) in // the inbound direction: from the incoming AnthropicTool onto the neutral // ResponsesTool when the native Anthropic /v1/messages endpoint is the entry // point. Called once per converted tool so every return path inside // convertAnthropicToolToBifrost benefits. func applyAnthropicToolFlagsToResponsesTool(at *AnthropicTool, rt *schemas.ResponsesTool) { if at == nil || rt == nil { return } if at.DeferLoading != nil { rt.DeferLoading = at.DeferLoading } if len(at.AllowedCallers) > 0 { rt.AllowedCallers = at.AllowedCallers } if len(at.InputExamples) > 0 { rt.InputExamples = make([]schemas.ChatToolInputExample, len(at.InputExamples)) for i, ex := range at.InputExamples { rt.InputExamples[i] = schemas.ChatToolInputExample{ Input: ex.Input, Description: ex.Description, } } } if at.EagerInputStreaming != nil { rt.EagerInputStreaming = at.EagerInputStreaming } } // applyResponsesToolAnthropicFlags propagates the Anthropic-native tool flags // (DeferLoading, AllowedCallers, InputExamples, EagerInputStreaming) from the // neutral ResponsesTool onto the provider-native AnthropicTool. Called once // per converted tool so every branch in convertBifrostToolToAnthropic // benefits without duplicating the logic on each return path. func applyResponsesToolAnthropicFlags(at *AnthropicTool, rt *schemas.ResponsesTool) { if at == nil || rt == nil { return } if rt.DeferLoading != nil { at.DeferLoading = rt.DeferLoading } if len(rt.AllowedCallers) > 0 { at.AllowedCallers = rt.AllowedCallers } if len(rt.InputExamples) > 0 { at.InputExamples = make([]AnthropicToolInputExample, len(rt.InputExamples)) for i, ex := range rt.InputExamples { at.InputExamples[i] = AnthropicToolInputExample{ Input: ex.Input, Description: ex.Description, } } } if rt.EagerInputStreaming != nil { at.EagerInputStreaming = rt.EagerInputStreaming } } // Helper function to convert Tool back to AnthropicTool func convertBifrostToolToAnthropic(model string, tool *schemas.ResponsesTool, provider schemas.ModelProvider, hasWebSearchOrFetch bool) *AnthropicTool { if tool == nil { return nil } switch tool.Type { case schemas.ResponsesToolTypeCodeInterpreter: if hasWebSearchOrFetch { // Skip code execution tools when web search/fetch is present — // the Anthropic API auto-injects code_execution in that case. // Including it explicitly causes "Auto-injecting tools would conflict" errors. return nil } // When no web search/fetch, explicitly include code_execution return &AnthropicTool{ Type: schemas.Ptr(AnthropicToolTypeCodeExecution), Name: string(AnthropicToolNameCodeExecution), } case schemas.ResponsesToolTypeComputerUsePreview: if tool.ResponsesToolComputerUsePreview != nil { computerToolType := AnthropicToolTypeComputer20250124 if strings.Contains(model, "4.6") || strings.Contains(model, "4-6") || (strings.Contains(model, "opus") && (strings.Contains(model, "4.5") || strings.Contains(model, "4-5"))) { computerToolType = AnthropicToolTypeComputer20251124 } return &AnthropicTool{ Type: schemas.Ptr(computerToolType), Name: string(AnthropicToolNameComputer), AnthropicToolComputerUse: &AnthropicToolComputerUse{ DisplayWidthPx: schemas.Ptr(tool.ResponsesToolComputerUsePreview.DisplayWidth), DisplayHeightPx: schemas.Ptr(tool.ResponsesToolComputerUsePreview.DisplayHeight), DisplayNumber: schemas.Ptr(1), EnableZoom: tool.ResponsesToolComputerUsePreview.EnableZoom, }, } } case schemas.ResponsesToolTypeWebSearch: webSearchType := AnthropicToolTypeWebSearch20250305 // Dynamic filtering (web_search_20260209) only available on Anthropic + Azure features, ok := ProviderFeatures[provider] if ok && features.WebSearchDynamic && (strings.Contains(model, "4.6") || strings.Contains(model, "4-6")) { webSearchType = AnthropicToolTypeWebSearch20260209 } anthropicTool := &AnthropicTool{ Type: schemas.Ptr(webSearchType), Name: string(AnthropicToolNameWebSearch), AnthropicToolWebSearch: &AnthropicToolWebSearch{}, } if tool.ResponsesToolWebSearch != nil { if tool.ResponsesToolWebSearch.MaxUses != nil { anthropicTool.AnthropicToolWebSearch.MaxUses = tool.ResponsesToolWebSearch.MaxUses } if tool.ResponsesToolWebSearch.Filters != nil { anthropicTool.AnthropicToolWebSearch.AllowedDomains = tool.ResponsesToolWebSearch.Filters.AllowedDomains anthropicTool.AnthropicToolWebSearch.BlockedDomains = tool.ResponsesToolWebSearch.Filters.BlockedDomains } if tool.ResponsesToolWebSearch.UserLocation != nil { anthropicTool.AnthropicToolWebSearch.UserLocation = &AnthropicToolWebSearchUserLocation{ Type: tool.ResponsesToolWebSearch.UserLocation.Type, City: tool.ResponsesToolWebSearch.UserLocation.City, Country: tool.ResponsesToolWebSearch.UserLocation.Country, Timezone: tool.ResponsesToolWebSearch.UserLocation.Timezone, } } } return anthropicTool case schemas.ResponsesToolTypeWebFetch: webFetchType := AnthropicToolTypeWebFetch20250910 // Dynamic filtering versions only available on Anthropic + Azure features, ok := ProviderFeatures[provider] if ok && features.WebSearchDynamic && (strings.Contains(model, "4.6") || strings.Contains(model, "4-6")) { webFetchType = AnthropicToolTypeWebFetch20260309 } anthropicTool := &AnthropicTool{ Type: schemas.Ptr(webFetchType), Name: string(AnthropicToolNameWebFetch), AnthropicToolWebFetch: &AnthropicToolWebFetch{}, } if tool.ResponsesToolWebFetch != nil { anthropicTool.AnthropicToolWebFetch.MaxUses = tool.ResponsesToolWebFetch.MaxUses anthropicTool.AnthropicToolWebFetch.MaxContentTokens = tool.ResponsesToolWebFetch.MaxContentTokens if tool.ResponsesToolWebFetch.Filters != nil { anthropicTool.AnthropicToolWebFetch.AllowedDomains = tool.ResponsesToolWebFetch.Filters.AllowedDomains anthropicTool.AnthropicToolWebFetch.BlockedDomains = tool.ResponsesToolWebFetch.Filters.BlockedDomains } } return anthropicTool case schemas.ResponsesToolTypeMemory: anthropicTool := &AnthropicTool{ Type: schemas.Ptr(AnthropicToolTypeMemory20250818), Name: string(AnthropicToolNameMemory), } return anthropicTool case schemas.ResponsesToolTypeToolSearch: toolSearchType := AnthropicToolTypeToolSearchBM2520251119 toolSearchName := AnthropicToolNameToolSearchBM25 if tool.Name != nil && strings.Contains(*tool.Name, "regex") { toolSearchType = AnthropicToolTypeToolSearchRegex20251119 toolSearchName = AnthropicToolNameToolSearchRegex } return &AnthropicTool{ Type: schemas.Ptr(toolSearchType), Name: string(toolSearchName), } case schemas.ResponsesToolTypeLocalShell: return &AnthropicTool{ Type: schemas.Ptr(AnthropicToolTypeBash20250124), Name: string(AnthropicToolNameBash), } case schemas.ResponsesToolType(AnthropicToolTypeTextEditor20250124): return &AnthropicTool{ Type: schemas.Ptr(AnthropicToolTypeTextEditor20250124), Name: string(AnthropicToolNameTextEditor), } case schemas.ResponsesToolType(AnthropicToolTypeTextEditor20250429): return &AnthropicTool{ Type: schemas.Ptr(AnthropicToolTypeTextEditor20250429), Name: string(AnthropicToolNameTextEditor), } case schemas.ResponsesToolType(AnthropicToolTypeTextEditor20250728): return &AnthropicTool{ Type: schemas.Ptr(AnthropicToolTypeTextEditor20250728), Name: string(AnthropicToolNameTextEditor), } } anthropicTool := &AnthropicTool{ Type: schemas.Ptr(AnthropicToolTypeCustom), // Custom tools require type: "custom" } if tool.Name != nil { anthropicTool.Name = *tool.Name } if tool.Description != nil { anthropicTool.Description = tool.Description } // Convert parameters and strict from ToolFunction if tool.ResponsesToolFunction != nil { anthropicTool.Strict = tool.ResponsesToolFunction.Strict } if tool.ResponsesToolFunction != nil && tool.ResponsesToolFunction.Parameters != nil { anthropicTool.InputSchema = tool.ResponsesToolFunction.Parameters } else { // Anthropic requires input_schema for custom tools, provide empty object schema if missing anthropicTool.InputSchema = &schemas.ToolFunctionParameters{ Type: "object", Properties: &schemas.OrderedMap{}, } } // Normalize tool schema key ordering to ensure deterministic serialization. // Clients (e.g. Claude Agent SDK) may send non-deterministic property orderings // across turns, which breaks Anthropic's prefix-based prompt caching since tool // definitions are part of the serialized request prefix. // Normalized() returns a shallow copy with sorted key slices, so the // caller-owned tool.ResponsesToolFunction.Parameters is never mutated. if anthropicTool.InputSchema != nil { anthropicTool.InputSchema = anthropicTool.InputSchema.Normalized() } if tool.CacheControl != nil { anthropicTool.CacheControl = tool.CacheControl } return anthropicTool } // Helper function to convert ResponsesToolChoice back to AnthropicToolChoice func convertResponsesToolChoiceToAnthropic(toolChoice *schemas.ResponsesToolChoice) *AnthropicToolChoice { if toolChoice == nil { return nil } // String-form choices (auto/any/none/required) have no struct payload. if toolChoice.ResponsesToolChoiceStruct == nil && toolChoice.ResponsesToolChoiceStr != nil { switch schemas.ResponsesToolChoiceType(*toolChoice.ResponsesToolChoiceStr) { case schemas.ResponsesToolChoiceTypeAuto: return &AnthropicToolChoice{Type: "auto"} case schemas.ResponsesToolChoiceTypeAny, schemas.ResponsesToolChoiceTypeRequired: return &AnthropicToolChoice{Type: "any"} case schemas.ResponsesToolChoiceTypeNone: return &AnthropicToolChoice{Type: "none"} default: return nil } } if toolChoice.ResponsesToolChoiceStruct == nil { return nil } anthropicChoice := &AnthropicToolChoice{} var toolChoiceType *string if toolChoice.ResponsesToolChoiceStruct != nil { toolChoiceType = schemas.Ptr(string(toolChoice.ResponsesToolChoiceStruct.Type)) } else { toolChoiceType = toolChoice.ResponsesToolChoiceStr } switch *toolChoiceType { case "auto": anthropicChoice.Type = "auto" case "required": anthropicChoice.Type = "any" case "function": // Handle function type - set as "tool" with specific function name if toolChoice.ResponsesToolChoiceStruct != nil && toolChoice.ResponsesToolChoiceStruct.Name != nil { anthropicChoice.Type = "tool" anthropicChoice.Name = *toolChoice.ResponsesToolChoiceStruct.Name } return anthropicChoice } // Legacy fallback: also check for Name field (for backward compatibility) if toolChoice.ResponsesToolChoiceStruct != nil && toolChoice.ResponsesToolChoiceStruct.Name != nil { anthropicChoice.Type = "tool" anthropicChoice.Name = *toolChoice.ResponsesToolChoiceStruct.Name } return anthropicChoice } // Helper function to convert ContentBlock to AnthropicContentBlock func convertContentBlockToAnthropic(block schemas.ResponsesMessageContentBlock) *AnthropicContentBlock { switch block.Type { case schemas.ResponsesInputMessageContentBlockTypeText, schemas.ResponsesOutputMessageContentTypeText: anthropicBlock := AnthropicContentBlock{} if block.Text != nil { anthropicBlock = AnthropicContentBlock{ Type: AnthropicContentBlockTypeText, Text: block.Text, CacheControl: block.CacheControl, } if block.ResponsesOutputMessageContentText != nil && len(block.ResponsesOutputMessageContentText.Annotations) > 0 { anthropicBlock.Citations = &AnthropicCitations{ TextCitations: make([]AnthropicTextCitation, len(block.ResponsesOutputMessageContentText.Annotations)), } for i, annotation := range block.ResponsesOutputMessageContentText.Annotations { anthropicBlock.Citations.TextCitations[i] = convertAnnotationToAnthropicCitation(annotation) } } return &anthropicBlock } case schemas.ResponsesInputMessageContentBlockTypeImage: if block.ResponsesInputMessageContentBlockImage != nil && block.ResponsesInputMessageContentBlockImage.ImageURL != nil { // Convert using the same logic as ConvertToAnthropicImageBlock chatBlock := schemas.ChatContentBlock{ Type: schemas.ChatContentBlockTypeImage, ImageURLStruct: &schemas.ChatInputImage{ URL: *block.ResponsesInputMessageContentBlockImage.ImageURL, }, CacheControl: block.CacheControl, } anthropicBlock := ConvertToAnthropicImageBlock(chatBlock) return &anthropicBlock } case schemas.ResponsesOutputMessageContentTypeCompaction: if block.ResponsesOutputMessageContentCompaction != nil { return &AnthropicContentBlock{ Type: AnthropicContentBlockTypeCompaction, Content: &AnthropicContent{ ContentStr: &block.ResponsesOutputMessageContentCompaction.Summary, }, CacheControl: block.CacheControl, } } case schemas.ResponsesInputMessageContentBlockTypeFile: if block.ResponsesInputMessageContentBlockFile != nil { // Direct conversion without intermediate ChatContentBlock anthropicBlock := ConvertResponsesFileBlockToAnthropic( block.ResponsesInputMessageContentBlockFile, block.CacheControl, block.Citations, ) return &anthropicBlock } case schemas.ResponsesOutputMessageContentTypeReasoning: if block.Text != nil { return &AnthropicContentBlock{ Type: AnthropicContentBlockTypeThinking, Thinking: block.Text, Signature: block.Signature, } } } return nil } // Helper to convert Bifrost content blocks slice to Anthropic content blocks func convertBifrostContentBlocksToAnthropic(blocks []schemas.ResponsesMessageContentBlock) []AnthropicContentBlock { if len(blocks) == 0 { return nil } var result []AnthropicContentBlock for _, block := range blocks { if converted := convertContentBlockToAnthropic(block); converted != nil { result = append(result, *converted) } } if len(result) > 0 { return result } return nil } func (block AnthropicContentBlock) toBifrostResponsesImageBlock() schemas.ResponsesMessageContentBlock { return schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesInputMessageContentBlockTypeImage, ResponsesInputMessageContentBlockImage: &schemas.ResponsesInputMessageContentBlockImage{ ImageURL: schemas.Ptr(getImageURLFromBlock(block)), }, CacheControl: block.CacheControl, } } func (block AnthropicContentBlock) toBifrostResponsesDocumentBlock() schemas.ResponsesMessageContentBlock { resultBlock := schemas.ResponsesMessageContentBlock{ Type: schemas.ResponsesInputMessageContentBlockTypeFile, CacheControl: block.CacheControl, ResponsesInputMessageContentBlockFile: &schemas.ResponsesInputMessageContentBlockFile{}, } if block.Citations != nil && block.Citations.Config != nil { resultBlock.Citations = block.Citations.Config } // Set filename from title if available if block.Title != nil { resultBlock.ResponsesInputMessageContentBlockFile.Filename = block.Title } if block.Source == nil || block.Source.SourceObj == nil { // File-block rendering only applies to object-form sources // (image / document). String-form sources (search_result) are // handled elsewhere. return resultBlock } src := block.Source.SourceObj // Handle different source types switch src.Type { case "url": // URL source if src.URL != nil { resultBlock.ResponsesInputMessageContentBlockFile.FileURL = src.URL } case "base64": // Base64 encoded data if src.Data != nil { // Construct data URL with media type mediaType := "application/pdf" if src.MediaType != nil { mediaType = *src.MediaType } dataURL := *src.Data if !strings.HasPrefix(dataURL, "data:") { dataURL = "data:" + mediaType + ";base64," + *src.Data } resultBlock.ResponsesInputMessageContentBlockFile.FileData = &dataURL } case "text": // Plain text source if src.Data != nil { resultBlock.ResponsesInputMessageContentBlockFile.FileType = schemas.Ptr("text/plain") resultBlock.ResponsesInputMessageContentBlockFile.FileData = src.Data } } return resultBlock } // Helper functions for MCP tool/server conversion // convertAnthropicMCPServerV2ToBifrostTool converts a new-format MCP server to a Bifrost ResponsesTool. func convertAnthropicMCPServerV2ToBifrostTool(mcpServer *AnthropicMCPServerV2) *schemas.ResponsesTool { if mcpServer == nil { return nil } bifrostTool := &schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeMCP, ResponsesToolMCP: &schemas.ResponsesToolMCP{ ServerLabel: mcpServer.Name, }, } if mcpServer.URL != "" { bifrostTool.ResponsesToolMCP.ServerURL = schemas.Ptr(mcpServer.URL) } if mcpServer.AuthorizationToken != nil { bifrostTool.ResponsesToolMCP.Authorization = mcpServer.AuthorizationToken } return bifrostTool } // applyMCPToolsetConfigToBifrostTool merges mcp_toolset tool configs (from tools[]) into a Bifrost MCP tool. // Extracts the allowlist pattern: tools explicitly enabled in configs while default_config has enabled=false. func applyMCPToolsetConfigToBifrostTool(bifrostTool *schemas.ResponsesTool, toolset *AnthropicMCPToolsetTool) { if bifrostTool == nil || bifrostTool.ResponsesToolMCP == nil || toolset == nil { return } // Extract allowed tools from the allowlist pattern: // default_config.enabled=false + individual tools enabled in configs if toolset.Configs != nil { defaultEnabled := true if toolset.DefaultConfig != nil && toolset.DefaultConfig.Enabled != nil { defaultEnabled = *toolset.DefaultConfig.Enabled } if !defaultEnabled { // Allowlist pattern: collect explicitly enabled tools. // Keep an empty allowlist to preserve the "deny all" case. allowedTools := make([]string, 0, len(toolset.Configs)) for toolName, config := range toolset.Configs { if config != nil && config.Enabled != nil && *config.Enabled { allowedTools = append(allowedTools, toolName) } } bifrostTool.ResponsesToolMCP.AllowedTools = &schemas.ResponsesToolMCPAllowedTools{ ToolNames: allowedTools, } } } // Apply cache control if present if toolset.CacheControl != nil { bifrostTool.CacheControl = toolset.CacheControl } } // convertAnthropicMCPServerToBifrostTool converts a deprecated-format Anthropic MCP server to a Bifrost ResponsesTool. func convertAnthropicMCPServerToBifrostTool(mcpServer *AnthropicMCPServer) *schemas.ResponsesTool { if mcpServer == nil { return nil } bifrostTool := &schemas.ResponsesTool{ Type: schemas.ResponsesToolTypeMCP, ResponsesToolMCP: &schemas.ResponsesToolMCP{ ServerLabel: mcpServer.Name, }, } // Set server URL if present if mcpServer.URL != "" { bifrostTool.ResponsesToolMCP.ServerURL = schemas.Ptr(mcpServer.URL) } // Set authorization token if present if mcpServer.AuthorizationToken != nil { bifrostTool.ResponsesToolMCP.Authorization = mcpServer.AuthorizationToken } // Set allowed tools from tool configuration if mcpServer.ToolConfiguration != nil && len(mcpServer.ToolConfiguration.AllowedTools) > 0 { bifrostTool.ResponsesToolMCP.AllowedTools = &schemas.ResponsesToolMCPAllowedTools{ ToolNames: mcpServer.ToolConfiguration.AllowedTools, } } return bifrostTool } // convertBifrostMCPToolToAnthropicNew converts a Bifrost MCP tool to the new mcp-client-2025-11-20 format. // Returns both a simplified server entry (for mcp_servers[]) and a toolset entry (for tools[]). func convertBifrostMCPToolToAnthropicNew(tool *schemas.ResponsesTool) (*AnthropicMCPServerV2, *AnthropicMCPToolsetTool) { if tool == nil || tool.Type != schemas.ResponsesToolTypeMCP || tool.ResponsesToolMCP == nil { return nil, nil } // Build simplified server (no tool_configuration) server := &AnthropicMCPServerV2{ Type: "url", Name: tool.ResponsesToolMCP.ServerLabel, } if tool.ResponsesToolMCP.ServerURL != nil { server.URL = *tool.ResponsesToolMCP.ServerURL } if tool.ResponsesToolMCP.Authorization != nil { server.AuthorizationToken = tool.ResponsesToolMCP.Authorization } // Build toolset tool (references server by name) toolset := &AnthropicMCPToolsetTool{ Type: "mcp_toolset", MCPServerName: tool.ResponsesToolMCP.ServerLabel, CacheControl: tool.CacheControl, } // Convert allowed tools to per-tool configs if tool.ResponsesToolMCP.AllowedTools != nil { // Allowlist pattern: default disabled, specific tools enabled toolset.DefaultConfig = &AnthropicMCPToolsetConfig{Enabled: new(false)} if len(tool.ResponsesToolMCP.AllowedTools.ToolNames) > 0 { toolset.Configs = make(map[string]*AnthropicMCPToolsetConfig, len(tool.ResponsesToolMCP.AllowedTools.ToolNames)) for _, toolName := range tool.ResponsesToolMCP.AllowedTools.ToolNames { toolset.Configs[toolName] = &AnthropicMCPToolsetConfig{Enabled: schemas.Ptr(true)} } } } return server, toolset } // convertBifrostMCPToolToAnthropicServer converts a Bifrost MCP tool to the deprecated mcp-client-2025-04-04 format. // Kept for backward compatibility. func convertBifrostMCPToolToAnthropicServer(tool *schemas.ResponsesTool) *AnthropicMCPServer { if tool == nil || tool.Type != schemas.ResponsesToolTypeMCP || tool.ResponsesToolMCP == nil { return nil } mcpServer := &AnthropicMCPServer{ Type: "url", Name: tool.ResponsesToolMCP.ServerLabel, ToolConfiguration: &AnthropicMCPToolConfig{ Enabled: true, }, } // Set server URL if present if tool.ResponsesToolMCP.ServerURL != nil { mcpServer.URL = *tool.ResponsesToolMCP.ServerURL } // Set allowed tools if present if tool.ResponsesToolMCP.AllowedTools != nil && len(tool.ResponsesToolMCP.AllowedTools.ToolNames) > 0 { mcpServer.ToolConfiguration.AllowedTools = tool.ResponsesToolMCP.AllowedTools.ToolNames } // Set authorization token if present if tool.ResponsesToolMCP.Authorization != nil { mcpServer.AuthorizationToken = tool.ResponsesToolMCP.Authorization } return mcpServer } // convertAnthropicCitationToAnnotation converts an Anthropic citation to an OpenAI annotation // fullText is the complete text content of the message block, used to compute citation indices for web search results func convertAnthropicCitationToAnnotation(citation AnthropicTextCitation, fullText string) schemas.ResponsesOutputMessageContentTextAnnotation { annotation := schemas.ResponsesOutputMessageContentTextAnnotation{ Type: string(citation.Type), Index: citation.DocumentIndex, Text: schemas.Ptr(citation.CitedText), } // Map type-specific fields based on citation type switch citation.Type { case AnthropicCitationTypeCharLocation: // Character location fields annotation.StartCharIndex = citation.StartCharIndex annotation.EndCharIndex = citation.EndCharIndex annotation.Filename = citation.DocumentTitle annotation.FileID = citation.FileID case AnthropicCitationTypePageLocation: // Page location fields annotation.StartPageNumber = citation.StartPageNumber annotation.EndPageNumber = citation.EndPageNumber annotation.Filename = citation.DocumentTitle annotation.FileID = citation.FileID case AnthropicCitationTypeContentBlockLocation: // Content block location fields annotation.StartBlockIndex = citation.StartBlockIndex annotation.EndBlockIndex = citation.EndBlockIndex annotation.Filename = citation.DocumentTitle annotation.FileID = citation.FileID case AnthropicCitationTypeWebSearchResultLocation: // Web search result fields - map to OpenAI url_citation format annotation.Type = "url_citation" annotation.Title = citation.Title annotation.URL = citation.URL annotation.EncryptedIndex = citation.EncryptedIndex // Compute start_index and end_index by findin if fullText != "" && citation.URL != nil && *citation.URL != "" { startIdx := strings.Index(fullText, *citation.URL) if startIdx != -1 { endIdx := startIdx + len(*citation.URL) annotation.StartIndex = schemas.Ptr(startIdx) annotation.EndIndex = schemas.Ptr(endIdx) } else { // assign start_index and end_index to the entire text annotation.StartIndex = schemas.Ptr(0) annotation.EndIndex = schemas.Ptr(len(fullText)) } } case AnthropicCitationTypeSearchResultLocation: // Search result location fields annotation.StartBlockIndex = citation.StartBlockIndex annotation.EndBlockIndex = citation.EndBlockIndex annotation.Title = citation.Title annotation.Source = citation.Source } return annotation } // convertAnnotationToAnthropicCitation converts an OpenAI annotation to an Anthropic citation func convertAnnotationToAnthropicCitation(annotation schemas.ResponsesOutputMessageContentTextAnnotation) AnthropicTextCitation { citation := AnthropicTextCitation{ Type: AnthropicCitationType(annotation.Type), CitedText: "", } // Map common fields if annotation.Text != nil { citation.CitedText = *annotation.Text } // Map type-specific fields based on annotation type switch annotation.Type { case string(AnthropicCitationTypeCharLocation): // Character location citation.StartCharIndex = annotation.StartCharIndex citation.EndCharIndex = annotation.EndCharIndex citation.DocumentTitle = annotation.Filename citation.DocumentIndex = annotation.Index citation.FileID = annotation.FileID case string(AnthropicCitationTypePageLocation): // Page location citation.StartPageNumber = annotation.StartPageNumber citation.EndPageNumber = annotation.EndPageNumber citation.DocumentTitle = annotation.Filename citation.DocumentIndex = annotation.Index citation.FileID = annotation.FileID case string(AnthropicCitationTypeContentBlockLocation): // Content block location citation.StartBlockIndex = annotation.StartBlockIndex citation.EndBlockIndex = annotation.EndBlockIndex citation.DocumentTitle = annotation.Filename citation.DocumentIndex = annotation.Index citation.FileID = annotation.FileID case string(AnthropicCitationTypeWebSearchResultLocation): // Web search result citation.Title = annotation.Title citation.URL = annotation.URL citation.EncryptedIndex = annotation.EncryptedIndex case string(AnthropicCitationTypeSearchResultLocation): // Search result location citation.StartBlockIndex = annotation.StartBlockIndex citation.EndBlockIndex = annotation.EndBlockIndex citation.Title = annotation.Title citation.Source = annotation.Source case "url_citation": citation.Type = AnthropicCitationTypeWebSearchResultLocation citation.URL = annotation.URL citation.Title = annotation.Title citation.EncryptedIndex = annotation.EncryptedIndex case "file_citation", "container_file_citation", "file_path", "text_annotation": // OpenAI native types - map to char_location citation.Type = "char_location" citation.StartCharIndex = annotation.StartIndex citation.EndCharIndex = annotation.EndIndex citation.DocumentTitle = annotation.Filename citation.Title = annotation.Title citation.FileID = annotation.FileID } return citation } // convertResponsesToAnthropicComputerAction converts ResponsesComputerToolCallAction to Anthropic input map func convertResponsesToAnthropicComputerAction(action *schemas.ResponsesComputerToolCallAction) map[string]any { input := map[string]any{} var actionStr string // Map action type from OpenAI to Anthropic format switch action.Type { case "screenshot": actionStr = "screenshot" case "click": // Map click with button variants if action.Button != nil { switch *action.Button { case "right": actionStr = "right_click" case "wheel": actionStr = "middle_click" default: // "left", "back", "forward" or others actionStr = "left_click" } } else { actionStr = "left_click" } // Add coordinates if action.X != nil && action.Y != nil { input["coordinate"] = []int{*action.X, *action.Y} } case "double_click": actionStr = "double_click" if action.X != nil && action.Y != nil { input["coordinate"] = []int{*action.X, *action.Y} } case "move": actionStr = "mouse_move" if action.X != nil && action.Y != nil { input["coordinate"] = []int{*action.X, *action.Y} } case "type": actionStr = "type" if action.Text != nil { input["text"] = *action.Text } case "keypress": actionStr = "key" if len(action.Keys) > 0 { // Convert array of keys to "key1+key2+..." format text := "" for i, key := range action.Keys { if i > 0 { text += "+" } text += key } input["text"] = text } case "scroll": actionStr = "scroll" if action.X != nil && action.Y != nil { input["coordinate"] = []int{*action.X, *action.Y} } // Handle scroll direction - Anthropic supports one direction at a time // If both ScrollX and ScrollY are present, use the one with larger absolute value scrollX := 0 scrollY := 0 if action.ScrollX != nil { scrollX = *action.ScrollX } if action.ScrollY != nil { scrollY = *action.ScrollY } if math.Abs(float64(scrollY)) >= math.Abs(float64(scrollX)) && scrollY != 0 { // Vertical scroll is dominant or only one present if scrollY > 0 { input["scroll_direction"] = "down" input["scroll_amount"] = scrollY / 100 } else { input["scroll_direction"] = "up" input["scroll_amount"] = (-scrollY) / 100 } } else if scrollX != 0 { // Horizontal scroll is dominant or only one present if scrollX > 0 { input["scroll_direction"] = "right" input["scroll_amount"] = scrollX / 100 } else { input["scroll_direction"] = "left" input["scroll_amount"] = (-scrollX) / 100 } } case "drag": actionStr = "left_click_drag" if len(action.Path) >= 2 { // Map first and last points as start and end coordinates input["start_coordinate"] = []int{action.Path[0].X, action.Path[0].Y} input["end_coordinate"] = []int{action.Path[len(action.Path)-1].X, action.Path[len(action.Path)-1].Y} } case "wait": actionStr = "wait" input["duration"] = 2 case "zoom": actionStr = "zoom" // Anthropic zoom action expects region as [x1, y1, x2, y2] if len(action.Region) == 4 { input["region"] = action.Region } default: // Pass through any unknown action types actionStr = action.Type } input["action"] = actionStr return input } // convertAnthropicToResponsesComputerAction converts Anthropic input map to ResponsesComputerToolCallAction func convertAnthropicToResponsesComputerAction(inputMap map[string]interface{}) *schemas.ResponsesComputerToolCallAction { action := &schemas.ResponsesComputerToolCallAction{} // Extract action type actionStr, ok := inputMap["action"].(string) if !ok { return action } // Map action type from Anthropic to OpenAI format switch actionStr { case "screenshot": action.Type = "screenshot" case "left_click": action.Type = "click" action.Button = schemas.Ptr("left") case "right_click": action.Type = "click" action.Button = schemas.Ptr("right") case "middle_click": action.Type = "click" action.Button = schemas.Ptr("wheel") case "double_click": action.Type = "double_click" case "mouse_move": action.Type = "move" case "type": action.Type = "type" if text, ok := inputMap["text"].(string); ok { action.Text = schemas.Ptr(text) } case "key": action.Type = "keypress" if text, ok := inputMap["text"].(string); ok { // Convert "key1+key2+..." format to array of keys keys := strings.Split(text, "+") action.Keys = keys } case "scroll": action.Type = "scroll" // Convert scroll_direction and scroll_amount to pixel values if direction, ok := inputMap["scroll_direction"].(string); ok { amount := 100 // Default scroll amount in pixels if scrollAmount, ok := inputMap["scroll_amount"].(float64); ok { amount = int(scrollAmount) * 100 // Convert scroll units to pixels } switch direction { case "down": action.ScrollY = schemas.Ptr(amount) action.ScrollX = schemas.Ptr(0) case "up": action.ScrollY = schemas.Ptr(-amount) action.ScrollX = schemas.Ptr(0) case "right": action.ScrollX = schemas.Ptr(amount) action.ScrollY = schemas.Ptr(0) case "left": action.ScrollX = schemas.Ptr(-amount) action.ScrollY = schemas.Ptr(0) } } case "left_click_drag": action.Type = "drag" // Extract start and end coordinates if startCoord, ok := inputMap["start_coordinate"].([]interface{}); ok && len(startCoord) == 2 { if endCoord, ok := inputMap["end_coordinate"].([]interface{}); ok && len(endCoord) == 2 { // JSON unmarshaling produces float64 for numbers, so convert them startX, startXOk := startCoord[0].(float64) startY, startYOk := startCoord[1].(float64) endX, endXOk := endCoord[0].(float64) endY, endYOk := endCoord[1].(float64) if startXOk && startYOk && endXOk && endYOk { action.Path = []schemas.ResponsesComputerToolCallActionPath{ {X: int(startX), Y: int(startY)}, {X: int(endX), Y: int(endY)}, } } } } case "wait": action.Type = "wait" case "zoom": action.Type = "zoom" // Extract region [x1, y1, x2, y2] for zoom action if region, ok := inputMap["region"].([]interface{}); ok && len(region) == 4 { // JSON unmarshaling produces float64 for numbers, so convert them x1, x1Ok := region[0].(float64) y1, y1Ok := region[1].(float64) x2, x2Ok := region[2].(float64) y2, y2Ok := region[3].(float64) if x1Ok && y1Ok && x2Ok && y2Ok { action.Region = []int{int(x1), int(y1), int(x2), int(y2)} } } default: // Pass through any unknown action types action.Type = actionStr } // Extract coordinates for all actions that use them (click, double_click, move, scroll, etc.) if coordinate, ok := inputMap["coordinate"].([]interface{}); ok && len(coordinate) == 2 { // JSON unmarshaling produces float64 for numbers, so convert them if x, xOk := coordinate[0].(float64); xOk { if y, yOk := coordinate[1].(float64); yOk { action.X = schemas.Ptr(int(x)) action.Y = schemas.Ptr(int(y)) } } } return action } // generateSyntheticInputJSONDeltas creates synthetic input_json_delta events from complete JSON arguments // This simulates the streaming behavior that Anthropic provides natively func generateSyntheticInputJSONDeltas(argumentsJSON string, contentIndex *int) []*AnthropicStreamEvent { var events []*AnthropicStreamEvent // Chunk size for synthetic streaming (similar to how Anthropic chunks arguments) chunkSize := 8 // Small chunks to simulate realistic streaming // Start with empty delta to match Anthropic's behavior events = append(events, &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeContentBlockDelta, Index: contentIndex, Delta: &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeInputJSON, PartialJSON: schemas.Ptr(""), }, }) // Break the JSON into chunks for i := 0; i < len(argumentsJSON); i += chunkSize { end := min(i+chunkSize, len(argumentsJSON)) chunk := argumentsJSON[i:end] events = append(events, &AnthropicStreamEvent{ Type: AnthropicStreamEventTypeContentBlockDelta, Index: contentIndex, Delta: &AnthropicStreamDelta{ Type: AnthropicStreamDeltaTypeInputJSON, PartialJSON: &chunk, }, }) } return events }