Files
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

1389 lines
49 KiB
Go

package anthropic
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/bytedance/sonic"
providerUtils "github.com/maximhq/bifrost/core/providers/utils"
"github.com/maximhq/bifrost/core/schemas"
)
// convertFunctionToolToAnthropic turns an OpenAI-style function tool
// (schemas.ChatTool with non-nil Function) into an AnthropicTool.
// Factored out from ToAnthropicChatRequest's tool loop so the loop can branch
// cleanly between function and server-tool shapes.
func convertFunctionToolToAnthropic(tool schemas.ChatTool) AnthropicTool {
anthropicTool := AnthropicTool{
Name: tool.Function.Name,
}
if tool.Function.Description != nil {
anthropicTool.Description = tool.Function.Description
}
// Convert function parameters to input_schema
if tool.Function.Parameters != nil && (tool.Function.Parameters.Type != "" || tool.Function.Parameters.Properties != nil) {
anthropicTool.InputSchema = &schemas.ToolFunctionParameters{
Type: tool.Function.Parameters.Type,
Description: tool.Function.Parameters.Description,
Properties: tool.Function.Parameters.Properties,
Required: tool.Function.Parameters.Required,
Enum: tool.Function.Parameters.Enum,
AdditionalProperties: tool.Function.Parameters.AdditionalProperties,
Defs: tool.Function.Parameters.Defs,
Definitions: tool.Function.Parameters.Definitions,
Ref: tool.Function.Parameters.Ref,
Items: tool.Function.Parameters.Items,
MinItems: tool.Function.Parameters.MinItems,
MaxItems: tool.Function.Parameters.MaxItems,
AnyOf: tool.Function.Parameters.AnyOf,
OneOf: tool.Function.Parameters.OneOf,
AllOf: tool.Function.Parameters.AllOf,
Format: tool.Function.Parameters.Format,
Pattern: tool.Function.Parameters.Pattern,
MinLength: tool.Function.Parameters.MinLength,
MaxLength: tool.Function.Parameters.MaxLength,
Minimum: tool.Function.Parameters.Minimum,
Maximum: tool.Function.Parameters.Maximum,
Title: tool.Function.Parameters.Title,
Default: tool.Function.Parameters.Default,
Nullable: tool.Function.Parameters.Nullable,
}
}
if anthropicTool.InputSchema != nil {
anthropicTool.InputSchema = anthropicTool.InputSchema.Normalized()
}
if tool.CacheControl != nil {
anthropicTool.CacheControl = tool.CacheControl
}
if tool.DeferLoading != nil {
anthropicTool.DeferLoading = tool.DeferLoading
}
if len(tool.AllowedCallers) > 0 {
anthropicTool.AllowedCallers = tool.AllowedCallers
}
if len(tool.InputExamples) > 0 {
anthropicTool.InputExamples = make([]AnthropicToolInputExample, len(tool.InputExamples))
for i, ex := range tool.InputExamples {
anthropicTool.InputExamples[i] = AnthropicToolInputExample{
Input: ex.Input,
Description: ex.Description,
}
}
}
if tool.EagerInputStreaming != nil {
anthropicTool.EagerInputStreaming = tool.EagerInputStreaming
}
// ChatToolFunction.Strict is the canonical neutral slot for Anthropic's strict.
if tool.Function.Strict != nil {
anthropicTool.Strict = tool.Function.Strict
}
return anthropicTool
}
// convertServerToolToAnthropic reconstructs an AnthropicTool from the
// server-tool shape of a schemas.ChatTool (Function=nil, Name+Type+variant
// fields populated). Returns (tool, true) when Type looks like a known
// server-tool; (zero, false) when it doesn't, so the caller can drop it
// cleanly rather than forward a malformed tool.
//
// Supported type prefixes:
// - web_search_* → AnthropicToolWebSearch
// - web_fetch_* → AnthropicToolWebFetch
// - computer_* → AnthropicToolComputerUse
// - text_editor_* → AnthropicToolTextEditor
// - mcp_toolset → AnthropicMCPToolsetTool (via MCPToolset pointer)
//
// bash_*, memory_*, code_execution_*, and tool_search_* carry no variant
// config — their Type + Name alone are enough, handled in the default branch.
func convertServerToolToAnthropic(tool schemas.ChatTool) (AnthropicTool, bool) {
typeStr := string(tool.Type)
if typeStr == "" {
return AnthropicTool{}, false
}
// mcp_toolset is serialized via a dedicated embedded type (AnthropicMCPToolsetTool)
// and carries its identity in MCPServerName, not Name — handle before the
// generic Name guard below.
if typeStr == "mcp_toolset" {
if tool.MCPServerName == "" {
return AnthropicTool{}, false
}
toolset := &AnthropicMCPToolsetTool{
Type: "mcp_toolset",
MCPServerName: tool.MCPServerName,
DefaultConfig: convertMCPToolsetConfig(tool.DefaultConfig),
Configs: convertMCPToolsetConfigMap(tool.Configs),
CacheControl: tool.CacheControl,
}
return AnthropicTool{MCPToolset: toolset}, true
}
// Remaining server tools (web_search, web_fetch, computer, text_editor, etc.)
// identify themselves via Name.
if tool.Name == "" {
return AnthropicTool{}, false
}
atype := AnthropicToolType(typeStr)
anthropicTool := AnthropicTool{
Name: tool.Name,
Type: &atype,
CacheControl: tool.CacheControl,
DeferLoading: tool.DeferLoading,
AllowedCallers: tool.AllowedCallers,
EagerInputStreaming: tool.EagerInputStreaming,
}
if len(tool.InputExamples) > 0 {
anthropicTool.InputExamples = make([]AnthropicToolInputExample, len(tool.InputExamples))
for i, ex := range tool.InputExamples {
anthropicTool.InputExamples[i] = AnthropicToolInputExample{
Input: ex.Input,
Description: ex.Description,
}
}
}
switch {
case strings.HasPrefix(typeStr, "web_search_"):
anthropicTool.AnthropicToolWebSearch = &AnthropicToolWebSearch{
MaxUses: tool.MaxUses,
AllowedDomains: tool.AllowedDomains,
BlockedDomains: tool.BlockedDomains,
UserLocation: convertUserLocation(tool.UserLocation),
}
case strings.HasPrefix(typeStr, "web_fetch_"):
anthropicTool.AnthropicToolWebFetch = &AnthropicToolWebFetch{
MaxUses: tool.MaxUses,
AllowedDomains: tool.AllowedDomains,
BlockedDomains: tool.BlockedDomains,
MaxContentTokens: tool.MaxContentTokens,
Citations: convertCitationsConfig(tool.Citations),
UseCache: tool.UseCache,
}
case strings.HasPrefix(typeStr, "computer_"):
anthropicTool.AnthropicToolComputerUse = &AnthropicToolComputerUse{
DisplayWidthPx: tool.DisplayWidthPx,
DisplayHeightPx: tool.DisplayHeightPx,
DisplayNumber: tool.DisplayNumber,
EnableZoom: tool.EnableZoom,
}
case strings.HasPrefix(typeStr, "text_editor_"):
anthropicTool.AnthropicToolTextEditor = &AnthropicToolTextEditor{
MaxCharacters: tool.MaxCharacters,
}
case strings.HasPrefix(typeStr, "bash_"),
strings.HasPrefix(typeStr, "memory_"),
strings.HasPrefix(typeStr, "code_execution_"),
strings.HasPrefix(typeStr, "tool_search_tool_"):
// No variant-specific config — Type + Name alone.
default:
// Unknown type — pass through Type + Name and let Anthropic reject
// if it's truly invalid. This keeps forward-compat for new tool
// versions that aren't yet known to Bifrost.
}
return anthropicTool, true
}
// convertUserLocation mirrors schemas.ChatToolUserLocation onto
// AnthropicToolWebSearchUserLocation.
func convertUserLocation(loc *schemas.ChatToolUserLocation) *AnthropicToolWebSearchUserLocation {
if loc == nil {
return nil
}
return &AnthropicToolWebSearchUserLocation{
Type: loc.Type,
City: loc.City,
Region: loc.Region,
Country: loc.Country,
Timezone: loc.Timezone,
}
}
// convertCitationsConfig mirrors the request-side citations config
// ({"enabled": true/false}) onto AnthropicCitations' request form.
func convertCitationsConfig(c *schemas.ChatToolCitationsConfig) *AnthropicCitations {
if c == nil {
return nil
}
return &AnthropicCitations{Config: &schemas.Citations{Enabled: c.Enabled}}
}
// convertMCPToolsetConfig mirrors a single mcp_toolset config.
func convertMCPToolsetConfig(c *schemas.ChatMCPToolsetConfig) *AnthropicMCPToolsetConfig {
if c == nil {
return nil
}
return &AnthropicMCPToolsetConfig{
Enabled: c.Enabled,
DeferLoading: c.DeferLoading,
}
}
// convertMCPToolsetConfigMap mirrors the per-tool mcp_toolset configs map.
func convertMCPToolsetConfigMap(m map[string]*schemas.ChatMCPToolsetConfig) map[string]*AnthropicMCPToolsetConfig {
if len(m) == 0 {
return nil
}
out := make(map[string]*AnthropicMCPToolsetConfig, len(m))
for k, v := range m {
out[k] = convertMCPToolsetConfig(v)
}
return out
}
// ToAnthropicChatRequest converts a Bifrost request to Anthropic format
// This is the reverse of ConvertChatRequestToBifrost for provider-side usage
func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.BifrostChatRequest) (*AnthropicMessageRequest, error) {
if bifrostReq == nil || bifrostReq.Input == nil {
return nil, fmt.Errorf("bifrost request is nil or input is nil")
}
messages := bifrostReq.Input
anthropicReq := &AnthropicMessageRequest{
Model: bifrostReq.Model,
MaxTokens: providerUtils.GetMaxOutputTokensOrDefault(bifrostReq.Model, AnthropicDefaultMaxTokens),
}
// Convert parameters
if bifrostReq.Params != nil {
anthropicReq.ExtraParams = bifrostReq.Params.ExtraParams
if bifrostReq.Params.MaxCompletionTokens != nil {
anthropicReq.MaxTokens = *bifrostReq.Params.MaxCompletionTokens
}
// 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
}
}
anthropicReq.StopSequences = bifrostReq.Params.Stop
// TopK — prefer the promoted neutral field; fall back to ExtraParams.
// Opus 4.7+ rejects top_k with a 400 error.
if bifrostReq.Params.TopK != nil {
if !IsOpus47(bifrostReq.Model) {
anthropicReq.TopK = bifrostReq.Params.TopK
}
} else if topK, ok := schemas.SafeExtractIntPointer(bifrostReq.Params.ExtraParams["top_k"]); ok {
delete(anthropicReq.ExtraParams, "top_k")
if !IsOpus47(bifrostReq.Model) {
anthropicReq.TopK = topK
}
}
// Speed — prefer neutral field, then ExtraParams.
if bifrostReq.Params.Speed != nil {
anthropicReq.Speed = bifrostReq.Params.Speed
} else if speed, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["speed"]); ok {
delete(anthropicReq.ExtraParams, "speed")
anthropicReq.Speed = speed
}
// InferenceGeo — prefer neutral field, then ExtraParams.
if bifrostReq.Params.InferenceGeo != nil {
anthropicReq.InferenceGeo = bifrostReq.Params.InferenceGeo
} else if inferenceGeo, ok := schemas.SafeExtractStringPointer(bifrostReq.Params.ExtraParams["inference_geo"]); ok {
delete(anthropicReq.ExtraParams, "inference_geo")
anthropicReq.InferenceGeo = inferenceGeo
}
// ContextManagement — the neutral type is json.RawMessage; decode to
// the Anthropic-shape ContextManagement. Fall back to ExtraParams
// (legacy map-valued or typed-pointer paths) if the raw is empty.
// Surface decode errors on the typed path so callers get immediate
// feedback on malformed config instead of a silent drop.
if len(bifrostReq.Params.ContextManagement) > 0 {
var cm ContextManagement
if err := sonic.Unmarshal(bifrostReq.Params.ContextManagement, &cm); err != nil {
return nil, fmt.Errorf("context_management: failed to parse: %w", err)
}
anthropicReq.ContextManagement = &cm
} else 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
}
}
}
// Container — map the neutral ChatContainer union onto the Anthropic
// AnthropicContainer union. Both follow the string-or-object pattern.
if bifrostReq.Params.Container != nil {
c := &AnthropicContainer{}
if bifrostReq.Params.Container.ContainerStr != nil {
c.ContainerStr = bifrostReq.Params.Container.ContainerStr
} else if bifrostReq.Params.Container.ContainerObject != nil {
obj := &AnthropicContainerObject{
ID: bifrostReq.Params.Container.ContainerObject.ID,
}
if len(bifrostReq.Params.Container.ContainerObject.Skills) > 0 {
obj.Skills = make([]AnthropicContainerSkill, len(bifrostReq.Params.Container.ContainerObject.Skills))
for i, sk := range bifrostReq.Params.Container.ContainerObject.Skills {
obj.Skills[i] = AnthropicContainerSkill{
SkillID: sk.SkillID,
Type: sk.Type,
Version: sk.Version,
}
}
}
c.ContainerObject = obj
}
anthropicReq.Container = c
}
// Top-level CacheControl on the request.
if bifrostReq.Params.CacheControl != nil {
anthropicReq.CacheControl = bifrostReq.Params.CacheControl
}
// TaskBudget — maps onto output_config.task_budget. If an OutputConfig
// already exists (e.g. from structured outputs), attach the budget to
// it; otherwise create one.
if bifrostReq.Params.TaskBudget != nil {
tb := &AnthropicTaskBudget{
Type: bifrostReq.Params.TaskBudget.Type,
Total: bifrostReq.Params.TaskBudget.Total,
Remaining: bifrostReq.Params.TaskBudget.Remaining,
}
if anthropicReq.OutputConfig == nil {
anthropicReq.OutputConfig = &AnthropicOutputConfig{}
}
anthropicReq.OutputConfig.TaskBudget = tb
}
// MCPServers — mirror the neutral ChatMCPServer[] to AnthropicMCPServerV2[].
if len(bifrostReq.Params.MCPServers) > 0 {
servers := make([]AnthropicMCPServerV2, len(bifrostReq.Params.MCPServers))
for i, s := range bifrostReq.Params.MCPServers {
servers[i] = AnthropicMCPServerV2{
Type: s.Type,
URL: s.URL,
Name: s.Name,
AuthorizationToken: s.AuthorizationToken,
}
}
anthropicReq.MCPServers = servers
}
if bifrostReq.Params.ResponseFormat != nil {
// Vertex doesn't support native structured outputs, so convert to tool
if bifrostReq.Provider == schemas.Vertex {
responseFormatTool := convertChatResponseFormatToTool(ctx, bifrostReq.Params)
if responseFormatTool != nil {
anthropicReq.Tools = append(anthropicReq.Tools, *responseFormatTool)
// Force the model to use this specific tool
anthropicReq.ToolChoice = &AnthropicToolChoice{
Type: "tool",
Name: responseFormatTool.Name,
}
}
} else {
// Use GA structured outputs (output_config.format) instead of beta (output_format)
outputFormat := convertChatResponseFormatToAnthropicOutputFormat(bifrostReq.Params.ResponseFormat)
if outputFormat != nil {
anthropicReq.OutputConfig = &AnthropicOutputConfig{
Format: outputFormat,
}
}
}
}
// Convert tools. Three neutral ChatTool shapes are supported:
// (1) Function tool (tool.Function != nil) — existing path.
// (2) Anthropic server tool (tool.Function == nil, Type is a
// server-tool version string, Name populated at top level) —
// new path handled by convertServerToolToAnthropic.
// (3) Custom tool (tool.Custom != nil) — not currently forwarded
// to Anthropic; skipped.
if bifrostReq.Params.Tools != nil {
// Strip server tools the target provider doesn't support per
// ProviderFeatures (e.g. web_search on Vertex's non-supporting
// model variants, or MCP on Bedrock when this converter is used
// by non-Bedrock providers). Function/custom tools are always
// kept. The dropped set is discarded — "silent strip + continue"
// policy per user direction. See Bedrock's convertToolConfig for
// the direct-Bedrock-path equivalent.
filtered, _ := ValidateChatToolsForProvider(bifrostReq.Params.Tools, bifrostReq.Provider)
tools := make([]AnthropicTool, 0, len(filtered))
for _, tool := range filtered {
if tool.Function != nil {
tools = append(tools, convertFunctionToolToAnthropic(tool))
continue
}
// Non-function tool: attempt server-tool reconstruction.
if converted, ok := convertServerToolToAnthropic(tool); ok {
tools = append(tools, converted)
}
}
if anthropicReq.Tools == nil {
anthropicReq.Tools = tools
} else {
anthropicReq.Tools = append(anthropicReq.Tools, tools...)
}
}
// Convert tool choice
if bifrostReq.Params.ToolChoice != nil {
toolChoice := &AnthropicToolChoice{}
if bifrostReq.Params.ToolChoice.ChatToolChoiceStr != nil {
switch schemas.ChatToolChoiceType(*bifrostReq.Params.ToolChoice.ChatToolChoiceStr) {
case schemas.ChatToolChoiceTypeAny:
toolChoice.Type = "any"
case schemas.ChatToolChoiceTypeRequired:
toolChoice.Type = "any"
case schemas.ChatToolChoiceTypeNone:
toolChoice.Type = "none"
default:
toolChoice.Type = "auto"
}
} else if bifrostReq.Params.ToolChoice.ChatToolChoiceStruct != nil {
switch bifrostReq.Params.ToolChoice.ChatToolChoiceStruct.Type {
case schemas.ChatToolChoiceTypeFunction:
toolChoice.Type = "tool"
if bifrostReq.Params.ToolChoice.ChatToolChoiceStruct.Function != nil {
toolChoice.Name = bifrostReq.Params.ToolChoice.ChatToolChoiceStruct.Function.Name
}
case schemas.ChatToolChoiceTypeAllowedTools:
toolChoice.Type = "any"
case schemas.ChatToolChoiceTypeCustom:
toolChoice.Type = "auto"
default:
toolChoice.Type = "auto"
}
}
anthropicReq.ToolChoice = toolChoice
}
// Convert reasoning
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 && *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(*bifrostReq.Params.Reasoning.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",
}
}
// thinking.display — map the neutral ChatReasoning.Display onto
// AnthropicThinking.Display. Valid for "enabled" and "adaptive"
// modes only; Anthropic rejects display on "disabled" ("there is
// nothing to display", per the extended-thinking doc). We attach
// on non-disabled modes and let the upstream provider enforce
// model-level support.
if bifrostReq.Params.Reasoning.Display != nil &&
anthropicReq.Thinking != nil &&
anthropicReq.Thinking.Type != "disabled" {
anthropicReq.Thinking.Display = bifrostReq.Params.Reasoning.Display
}
}
// Convert service tier
anthropicReq.ServiceTier = bifrostReq.Params.ServiceTier
}
// Convert messages - group consecutive tool messages into single user messages
var anthropicMessages []AnthropicMessage
var systemContent *AnthropicContent
i := 0
for i < len(messages) {
msg := messages[i]
switch msg.Role {
case schemas.ChatMessageRoleSystem:
// Handle system message separately
if msg.Content != nil {
if msg.Content.ContentStr != nil && *msg.Content.ContentStr != "" {
systemContent = &AnthropicContent{ContentStr: msg.Content.ContentStr}
} else if msg.Content.ContentBlocks != nil {
blocks := make([]AnthropicContentBlock, 0, len(msg.Content.ContentBlocks))
for _, block := range msg.Content.ContentBlocks {
if block.Text != nil && *block.Text != "" {
blocks = append(blocks, AnthropicContentBlock{
Type: AnthropicContentBlockTypeText,
Text: block.Text,
CacheControl: block.CacheControl,
})
}
}
if len(blocks) > 0 {
systemContent = &AnthropicContent{ContentBlocks: blocks}
}
}
}
i++
case schemas.ChatMessageRoleTool:
// Group consecutive tool messages into a single user message
var toolResults []AnthropicContentBlock
// Collect all consecutive tool messages
for i < len(messages) && messages[i].Role == schemas.ChatMessageRoleTool {
toolMsg := messages[i]
if toolMsg.ChatToolMessage != nil && toolMsg.ChatToolMessage.ToolCallID != nil {
toolResult := AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolResult,
ToolUseID: toolMsg.ChatToolMessage.ToolCallID,
}
// Convert tool result content
if toolMsg.Content != nil {
if toolMsg.Content.ContentStr != nil && *toolMsg.Content.ContentStr != "" {
toolResult.Content = &AnthropicContent{ContentStr: toolMsg.Content.ContentStr}
} else if toolMsg.Content.ContentBlocks != nil {
blocks := make([]AnthropicContentBlock, 0, len(toolMsg.Content.ContentBlocks))
for _, block := range toolMsg.Content.ContentBlocks {
if block.Text != nil && *block.Text != "" {
blocks = append(blocks, AnthropicContentBlock{
Type: AnthropicContentBlockTypeText,
Text: block.Text,
CacheControl: block.CacheControl,
})
} else if block.ImageURLStruct != nil {
blocks = append(blocks, ConvertToAnthropicImageBlock(block))
}
}
if len(blocks) > 0 {
toolResult.Content = &AnthropicContent{ContentBlocks: blocks}
}
}
}
toolResults = append(toolResults, toolResult)
}
i++
}
// Create a single user message with all tool results
if len(toolResults) > 0 {
anthropicMessages = append(anthropicMessages, AnthropicMessage{
Role: "user", // Tool results are sent as user messages in Anthropic
Content: AnthropicContent{ContentBlocks: toolResults},
})
}
default:
// Handle user and assistant messages
anthropicMsg := AnthropicMessage{
Role: AnthropicMessageRole(msg.Role),
}
var content []AnthropicContentBlock
// First add reasoning details
if msg.ChatAssistantMessage != nil && msg.ChatAssistantMessage.ReasoningDetails != nil {
for _, reasoningDetail := range msg.ChatAssistantMessage.ReasoningDetails {
content = append(content, AnthropicContentBlock{
Type: AnthropicContentBlockTypeThinking,
Signature: reasoningDetail.Signature,
Thinking: reasoningDetail.Text,
})
}
}
if msg.Content != nil {
// Convert text content
if msg.Content.ContentStr != nil && *msg.Content.ContentStr != "" {
content = append(content, AnthropicContentBlock{
Type: AnthropicContentBlockTypeText,
Text: msg.Content.ContentStr,
})
} else if msg.Content.ContentBlocks != nil {
for _, block := range msg.Content.ContentBlocks {
if block.Text != nil && *block.Text != "" {
content = append(content, AnthropicContentBlock{
Type: AnthropicContentBlockTypeText,
Text: block.Text,
CacheControl: block.CacheControl,
})
} else if block.ImageURLStruct != nil {
content = append(content, ConvertToAnthropicImageBlock(block))
} else if block.File != nil {
content = append(content, ConvertToAnthropicDocumentBlock(block))
}
}
}
}
// Convert tool calls
if msg.ChatAssistantMessage != nil && msg.ChatAssistantMessage.ToolCalls != nil {
for _, toolCall := range msg.ChatAssistantMessage.ToolCalls {
toolUse := AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolUse,
ID: toolCall.ID,
Name: toolCall.Function.Name,
}
// Preserve original key ordering of tool arguments for prompt caching.
// Using json.RawMessage avoids the map[string]interface{} round-trip
// that would destroy key order.
if toolCall.Function.Arguments == "" {
toolUse.Input = json.RawMessage("{}")
} else if compacted := compactJSONBytes([]byte(toolCall.Function.Arguments)); compacted != nil {
toolUse.Input = json.RawMessage(compacted)
} else {
// Preserve original payload instead of silently dropping args.
toolUse.Input = json.RawMessage([]byte(toolCall.Function.Arguments))
}
content = append(content, toolUse)
}
}
// Set content
if len(content) == 1 && content[0].Type == AnthropicContentBlockTypeText {
// Always use ContentBlocks for consistent array serialization
anthropicMsg.Content = AnthropicContent{ContentBlocks: content}
} else if len(content) > 0 {
// Multiple content blocks
anthropicMsg.Content = AnthropicContent{ContentBlocks: content}
}
anthropicMessages = append(anthropicMessages, anthropicMsg)
i++
}
}
anthropicReq.Messages = anthropicMessages
anthropicReq.System = systemContent
// Strip request- and tool-level fields the target Anthropic-family
// provider does not support. Fail-closed tool validation stays in
// ValidateToolsForProvider; this is strip-silently for additive fields.
stripUnsupportedAnthropicFields(anthropicReq, bifrostReq.Provider, bifrostReq.Model)
return anthropicReq, nil
}
// ToBifrostChatResponse converts an Anthropic message response to Bifrost format
func (response *AnthropicMessageResponse) ToBifrostChatResponse(ctx *schemas.BifrostContext) *schemas.BifrostChatResponse {
if response == nil {
return nil
}
// Initialize Bifrost response
bifrostResponse := &schemas.BifrostChatResponse{
ID: response.ID,
Model: response.Model,
Created: int(time.Now().Unix()),
}
// Check if we have a structured output tool
var structuredOutputToolName string
if ctx != nil {
if toolName, ok := ctx.Value(schemas.BifrostContextKeyStructuredOutputToolName).(string); ok {
structuredOutputToolName = toolName
}
}
// Collect all content and tool calls into a single message
var toolCalls []schemas.ChatAssistantMessageToolCall
var contentBlocks []schemas.ChatContentBlock
var reasoningDetails []schemas.ChatReasoningDetails
var reasoningText string
var contentStr *string
// Process content and tool calls
if response.Content != nil {
for _, c := range response.Content {
switch c.Type {
case AnthropicContentBlockTypeText:
if c.Text != nil {
contentBlocks = append(contentBlocks, schemas.ChatContentBlock{
Type: schemas.ChatContentBlockTypeText,
Text: c.Text,
})
}
case AnthropicContentBlockTypeToolUse:
if c.ID != nil && c.Name != nil {
// Check if this is the structured output tool - if so, convert to text content
if structuredOutputToolName != "" && *c.Name == structuredOutputToolName {
// This is a structured output tool - convert to text content
var jsonStr string
if c.Input != nil {
if argBytes, err := providerUtils.MarshalSorted(c.Input); err == nil {
jsonStr = string(argBytes)
} else {
jsonStr = fmt.Sprintf("%v", c.Input)
}
} else {
jsonStr = "{}"
}
contentStr = &jsonStr
continue // Skip adding to toolCalls
}
function := schemas.ChatAssistantMessageToolCallFunction{
Name: c.Name,
}
// Marshal the input to JSON string
if c.Input != nil {
args, err := providerUtils.MarshalSorted(c.Input)
if err != nil {
function.Arguments = fmt.Sprintf("%v", c.Input)
} else {
function.Arguments = string(args)
}
} else {
function.Arguments = "{}"
}
toolCalls = append(toolCalls, schemas.ChatAssistantMessageToolCall{
Index: uint16(len(toolCalls)),
Type: schemas.Ptr(string(schemas.ChatToolTypeFunction)),
ID: c.ID,
Function: function,
})
}
case AnthropicContentBlockTypeThinking:
reasoningDetails = append(reasoningDetails, schemas.ChatReasoningDetails{
Index: len(reasoningDetails),
Type: schemas.BifrostReasoningDetailsTypeText,
Text: c.Thinking,
Signature: c.Signature,
})
if c.Thinking != nil {
reasoningText += *c.Thinking + "\n"
}
}
}
}
if len(contentBlocks) == 1 && contentBlocks[0].Type == schemas.ChatContentBlockTypeText {
contentStr = contentBlocks[0].Text
contentBlocks = nil
}
// Create a single choice with the collected content
// Create message content
messageContent := schemas.ChatMessageContent{
ContentStr: contentStr,
ContentBlocks: contentBlocks,
}
// Create the assistant message
var assistantMessage *schemas.ChatAssistantMessage
// Create AssistantMessage if we have tool calls or thinking
if len(toolCalls) > 0 {
assistantMessage = &schemas.ChatAssistantMessage{
ToolCalls: toolCalls,
}
}
if len(reasoningDetails) > 0 {
if assistantMessage == nil {
assistantMessage = &schemas.ChatAssistantMessage{}
}
assistantMessage.ReasoningDetails = reasoningDetails
if reasoningText != "" {
assistantMessage.Reasoning = &reasoningText
}
}
// Create message
message := schemas.ChatMessage{
Role: schemas.ChatMessageRoleAssistant,
Content: &messageContent,
ChatAssistantMessage: assistantMessage,
}
// Create choice
choice := schemas.BifrostResponseChoice{
Index: 0,
ChatNonStreamResponseChoice: &schemas.ChatNonStreamResponseChoice{
Message: &message,
StopString: response.StopSequence,
},
FinishReason: func() *string {
if response.StopReason != "" {
mapped := ConvertAnthropicFinishReasonToBifrost(response.StopReason)
return &mapped
}
return nil
}(),
}
bifrostResponse.Choices = []schemas.BifrostResponseChoice{choice}
// Convert usage information
if response.Usage != nil {
bifrostResponse.Usage = &schemas.BifrostLLMUsage{
PromptTokens: response.Usage.InputTokens + response.Usage.CacheReadInputTokens + response.Usage.CacheCreationInputTokens,
PromptTokensDetails: &schemas.ChatPromptTokensDetails{
CachedReadTokens: response.Usage.CacheReadInputTokens,
CachedWriteTokens: response.Usage.CacheCreationInputTokens,
},
CompletionTokens: response.Usage.OutputTokens,
}
bifrostResponse.Usage.TotalTokens = bifrostResponse.Usage.PromptTokens + bifrostResponse.Usage.CompletionTokens
// Forward service tier from usage to response
if response.Usage.ServiceTier != nil {
bifrostResponse.ServiceTier = response.Usage.ServiceTier
}
}
return bifrostResponse
}
// ToAnthropicChatResponse converts a Bifrost response to Anthropic format
func ToAnthropicChatResponse(bifrostResp *schemas.BifrostChatResponse) *AnthropicMessageResponse {
if bifrostResp == nil {
return nil
}
anthropicResp := &AnthropicMessageResponse{
ID: bifrostResp.ID,
Type: "message",
Role: string(schemas.ChatMessageRoleAssistant),
Model: bifrostResp.Model,
}
// Convert usage information
if bifrostResp.Usage != nil {
anthropicResp.Usage = &AnthropicUsage{
InputTokens: bifrostResp.Usage.PromptTokens,
OutputTokens: bifrostResp.Usage.CompletionTokens,
}
// Cache read/write are now segregated via PromptTokensDetails. We map CachedReadTokens ->
// CacheReadInputTokens and CachedWriteTokens -> CacheCreationInputTokens, subtracting each
// from InputTokens so the non-cached input count is correct.
if bifrostResp.Usage.PromptTokensDetails != nil && bifrostResp.Usage.PromptTokensDetails.CachedReadTokens > 0 {
anthropicResp.Usage.CacheReadInputTokens = bifrostResp.Usage.PromptTokensDetails.CachedReadTokens
anthropicResp.Usage.InputTokens = anthropicResp.Usage.InputTokens - bifrostResp.Usage.PromptTokensDetails.CachedReadTokens
}
if bifrostResp.Usage.PromptTokensDetails != nil && bifrostResp.Usage.PromptTokensDetails.CachedWriteTokens > 0 {
anthropicResp.Usage.CacheCreationInputTokens = bifrostResp.Usage.PromptTokensDetails.CachedWriteTokens
anthropicResp.Usage.InputTokens = anthropicResp.Usage.InputTokens - bifrostResp.Usage.PromptTokensDetails.CachedWriteTokens
}
// Forward service tier
if bifrostResp.ServiceTier != nil {
anthropicResp.Usage.ServiceTier = bifrostResp.ServiceTier
}
}
// Convert choices to content
var content []AnthropicContentBlock
if len(bifrostResp.Choices) > 0 {
choice := bifrostResp.Choices[0] // Anthropic typically returns one choice
if choice.FinishReason != nil {
anthropicResp.StopReason = ConvertBifrostFinishReasonToAnthropic(*choice.FinishReason)
}
if choice.ChatNonStreamResponseChoice != nil && choice.StopString != nil {
anthropicResp.StopSequence = choice.StopString
}
// Add reasoning content
if choice.ChatNonStreamResponseChoice != nil && choice.Message != nil && choice.Message.ChatAssistantMessage != nil && choice.Message.ChatAssistantMessage.ReasoningDetails != nil {
for _, reasoningDetail := range choice.Message.ChatAssistantMessage.ReasoningDetails {
if reasoningDetail.Type == schemas.BifrostReasoningDetailsTypeText && reasoningDetail.Text != nil &&
((reasoningDetail.Text != nil && *reasoningDetail.Text != "") ||
(reasoningDetail.Signature != nil && *reasoningDetail.Signature != "")) {
content = append(content, AnthropicContentBlock{
Type: AnthropicContentBlockTypeThinking,
Thinking: reasoningDetail.Text,
Signature: reasoningDetail.Signature,
})
}
}
}
// Add text content
if choice.ChatNonStreamResponseChoice != nil && choice.Message != nil && choice.Message.Content != nil && choice.Message.Content.ContentStr != nil && *choice.Message.Content.ContentStr != "" {
content = append(content, AnthropicContentBlock{
Type: AnthropicContentBlockTypeText,
Text: choice.Message.Content.ContentStr,
})
} else if choice.ChatNonStreamResponseChoice != nil && choice.Message != nil && choice.Message.Content != nil && choice.Message.Content.ContentBlocks != nil {
for _, block := range choice.Message.Content.ContentBlocks {
if block.Text != nil {
content = append(content, AnthropicContentBlock{
Type: AnthropicContentBlockTypeText,
Text: block.Text,
})
}
}
}
// Add tool calls as tool_use content
if choice.ChatNonStreamResponseChoice != nil && choice.Message != nil && choice.Message.ChatAssistantMessage != nil && choice.Message.ChatAssistantMessage.ToolCalls != nil {
for _, toolCall := range choice.Message.ChatAssistantMessage.ToolCalls {
// Parse arguments JSON string to raw message
var inputRaw json.RawMessage
if toolCall.Function.Arguments != "" {
// Validate it's valid JSON, otherwise use empty object
if json.Valid([]byte(toolCall.Function.Arguments)) {
inputRaw = json.RawMessage(toolCall.Function.Arguments)
} else {
inputRaw = json.RawMessage("{}")
}
} else {
inputRaw = json.RawMessage("{}")
}
content = append(content, AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolUse,
ID: toolCall.ID,
Name: toolCall.Function.Name,
Input: inputRaw,
})
}
}
}
if content == nil {
content = []AnthropicContentBlock{}
}
anthropicResp.Content = content
return anthropicResp
}
// AnthropicStreamState tracks per-stream tool call index state.
type AnthropicStreamState struct {
nextToolCallIndex int
contentBlockToToolCallIdx map[int]int
}
// NewAnthropicStreamState returns an initialised stream state for one streaming response.
func NewAnthropicStreamState() *AnthropicStreamState {
return &AnthropicStreamState{
contentBlockToToolCallIdx: make(map[int]int),
}
}
// ToBifrostChatCompletionStream converts an Anthropic stream event to a Bifrost Chat Completion Stream response
func (chunk *AnthropicStreamEvent) ToBifrostChatCompletionStream(ctx *schemas.BifrostContext, structuredOutputToolName string, state *AnthropicStreamState) (*schemas.BifrostChatResponse, *schemas.BifrostError, bool) {
if state == nil {
state = NewAnthropicStreamState()
} else if state.contentBlockToToolCallIdx == nil {
state.contentBlockToToolCallIdx = make(map[int]int)
}
switch chunk.Type {
case AnthropicStreamEventTypeMessageStart:
return nil, nil, false
case AnthropicStreamEventTypeMessageStop:
return nil, nil, true
case AnthropicStreamEventTypeContentBlockStart:
// Emit tool-call metadata when starting a tool_use content block
if chunk.Index != nil && chunk.ContentBlock != nil && chunk.ContentBlock.Type == AnthropicContentBlockTypeToolUse {
// Check if this is the structured output tool - if so, skip emitting tool call metadata
if structuredOutputToolName != "" && chunk.ContentBlock.Name != nil && *chunk.ContentBlock.Name == structuredOutputToolName {
// Skip emitting tool call for structured output - it will be emitted as content later
return nil, nil, false
}
// Assign the next sequential tool-call index
toolCallIdx := state.nextToolCallIndex
state.contentBlockToToolCallIdx[*chunk.Index] = toolCallIdx
state.nextToolCallIndex++
// Create streaming response with tool call metadata
streamResponse := &schemas.BifrostChatResponse{
Object: "chat.completion.chunk",
Choices: []schemas.BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &schemas.ChatStreamResponseChoice{
Delta: &schemas.ChatStreamResponseChoiceDelta{
ToolCalls: []schemas.ChatAssistantMessageToolCall{
{
Index: uint16(toolCallIdx),
Type: schemas.Ptr(string(schemas.ChatToolTypeFunction)),
ID: chunk.ContentBlock.ID,
Function: schemas.ChatAssistantMessageToolCallFunction{
Name: chunk.ContentBlock.Name,
Arguments: "", // Empty arguments initially, will be filled by subsequent deltas
},
},
},
},
},
},
},
}
return streamResponse, nil, false
}
return nil, nil, false
case AnthropicStreamEventTypeContentBlockDelta:
if chunk.Index != nil && chunk.Delta != nil {
// Handle different delta types
switch chunk.Delta.Type {
case AnthropicStreamDeltaTypeText:
if chunk.Delta.Text != nil && *chunk.Delta.Text != "" {
// Create streaming response for this delta
streamResponse := &schemas.BifrostChatResponse{
Object: "chat.completion.chunk",
Choices: []schemas.BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &schemas.ChatStreamResponseChoice{
Delta: &schemas.ChatStreamResponseChoiceDelta{
Content: chunk.Delta.Text,
},
},
},
},
}
return streamResponse, nil, false
}
case AnthropicStreamDeltaTypeInputJSON:
// Handle tool use streaming - accumulate partial JSON
if chunk.Delta.PartialJSON != nil {
if structuredOutputToolName != "" {
// Structured output: stream JSON as content
streamResponse := &schemas.BifrostChatResponse{
Object: "chat.completion.chunk",
Choices: []schemas.BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &schemas.ChatStreamResponseChoice{
Delta: &schemas.ChatStreamResponseChoiceDelta{
Content: chunk.Delta.PartialJSON,
},
},
},
},
}
return streamResponse, nil, false
}
// Resolve which tool-call this delta belongs to via the content-block index.
toolCallIdx := state.contentBlockToToolCallIdx[*chunk.Index]
// Create streaming response for tool input delta
streamResponse := &schemas.BifrostChatResponse{
Object: "chat.completion.chunk",
Choices: []schemas.BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &schemas.ChatStreamResponseChoice{
Delta: &schemas.ChatStreamResponseChoiceDelta{
ToolCalls: []schemas.ChatAssistantMessageToolCall{
{
Index: uint16(toolCallIdx),
Type: schemas.Ptr(string(schemas.ChatToolTypeFunction)),
Function: schemas.ChatAssistantMessageToolCallFunction{
Arguments: *chunk.Delta.PartialJSON,
},
},
},
},
},
},
},
}
return streamResponse, nil, false
}
case AnthropicStreamDeltaTypeThinking:
// Handle thinking content streaming
if chunk.Delta.Thinking != nil && *chunk.Delta.Thinking != "" {
thinkingText := *chunk.Delta.Thinking
// Create streaming response for thinking delta
streamResponse := &schemas.BifrostChatResponse{
Object: "chat.completion.chunk",
Choices: []schemas.BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &schemas.ChatStreamResponseChoice{
Delta: &schemas.ChatStreamResponseChoiceDelta{
Reasoning: schemas.Ptr(thinkingText),
ReasoningDetails: []schemas.ChatReasoningDetails{
{
Index: 0,
Type: schemas.BifrostReasoningDetailsTypeText,
Text: schemas.Ptr(thinkingText),
},
},
},
},
},
},
}
return streamResponse, nil, false
}
case AnthropicStreamDeltaTypeSignature:
if chunk.Delta.Signature != nil && *chunk.Delta.Signature != "" {
// Create streaming response for signature delta
streamResponse := &schemas.BifrostChatResponse{
Object: "chat.completion.chunk",
Choices: []schemas.BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &schemas.ChatStreamResponseChoice{
Delta: &schemas.ChatStreamResponseChoiceDelta{
ReasoningDetails: []schemas.ChatReasoningDetails{
{
Index: 0,
Type: schemas.BifrostReasoningDetailsTypeText,
Signature: chunk.Delta.Signature,
},
},
},
},
},
},
}
return streamResponse, nil, false
}
}
}
case AnthropicStreamEventTypeContentBlockStop:
// Content block is complete, no specific action needed for streaming
return nil, nil, false
case AnthropicStreamEventTypeMessageDelta:
return nil, nil, false
case AnthropicStreamEventTypePing:
// Ping events are just keepalive, no action needed
return nil, nil, false
case AnthropicStreamEventTypeError:
if chunk.Error != nil {
// Send error through channel before closing
bifrostErr := &schemas.BifrostError{
IsBifrostError: false,
Error: &schemas.ErrorField{
Type: &chunk.Error.Type,
Message: chunk.Error.Message,
},
}
return nil, bifrostErr, true
}
}
return nil, nil, false
}
// ToAnthropicChatStreamResponse converts a Bifrost streaming response to Anthropic SSE string format
func ToAnthropicChatStreamResponse(bifrostResp *schemas.BifrostChatResponse) string {
if bifrostResp == nil {
return ""
}
streamResp := &AnthropicStreamEvent{}
// Handle different streaming event types based on the response content
if len(bifrostResp.Choices) > 0 {
choice := bifrostResp.Choices[0] // Anthropic typically returns one choice
// Handle streaming responses
if choice.ChatStreamResponseChoice != nil && choice.ChatStreamResponseChoice.Delta != nil {
delta := choice.ChatStreamResponseChoice.Delta
// Handle text content deltas
if delta.Content != nil {
streamResp.Type = "content_block_delta"
streamResp.Index = &choice.Index
streamResp.Delta = &AnthropicStreamDelta{
Type: AnthropicStreamDeltaTypeText,
Text: delta.Content,
}
} else if delta.Reasoning != nil {
// Handle thinking content deltas
streamResp.Type = "content_block_delta"
streamResp.Index = &choice.Index
streamResp.Delta = &AnthropicStreamDelta{
Type: AnthropicStreamDeltaTypeThinking,
Thinking: delta.Reasoning,
}
} else if len(delta.ReasoningDetails) > 0 && delta.ReasoningDetails[0].Signature != nil && *delta.ReasoningDetails[0].Signature != "" {
// Handle signature deltas
streamResp.Type = "content_block_delta"
streamResp.Index = &choice.Index
streamResp.Delta = &AnthropicStreamDelta{
Type: AnthropicStreamDeltaTypeSignature,
Signature: delta.ReasoningDetails[0].Signature,
}
} else if len(delta.ToolCalls) > 0 {
// Handle tool call deltas
toolCall := delta.ToolCalls[0] // Take first tool call
if toolCall.Function.Name != nil && *toolCall.Function.Name != "" {
// Tool use start event
streamResp.Type = "content_block_start"
streamResp.Index = &choice.Index
streamResp.ContentBlock = &AnthropicContentBlock{
Type: AnthropicContentBlockTypeToolUse,
ID: toolCall.ID,
Name: toolCall.Function.Name,
}
} else if toolCall.Function.Arguments != "" {
// Tool input delta
streamResp.Type = "content_block_delta"
streamResp.Index = &choice.Index
streamResp.Delta = &AnthropicStreamDelta{
Type: AnthropicStreamDeltaTypeInputJSON,
PartialJSON: &toolCall.Function.Arguments,
}
}
} else if choice.FinishReason != nil && *choice.FinishReason != "" {
// Handle finish reason - map back to Anthropic format
stopReason := ConvertBifrostFinishReasonToAnthropic(*choice.FinishReason)
streamResp.Type = "message_delta"
streamResp.Delta = &AnthropicStreamDelta{
Type: "message_delta",
StopReason: &stopReason,
}
}
} else if choice.ChatNonStreamResponseChoice != nil {
// Handle non-streaming response converted to streaming format
streamResp.Type = "message_start"
// Create message start event
streamMessage := &AnthropicMessageResponse{
ID: bifrostResp.ID,
Type: "message",
Role: string(choice.ChatNonStreamResponseChoice.Message.Role),
Model: bifrostResp.Model,
}
// Convert content
var content []AnthropicContentBlock
if choice.ChatNonStreamResponseChoice.Message.Content.ContentStr != nil {
content = append(content, AnthropicContentBlock{
Type: AnthropicContentBlockTypeText,
Text: choice.ChatNonStreamResponseChoice.Message.Content.ContentStr,
})
}
streamMessage.Content = content
streamResp.Message = streamMessage
}
}
// Handle usage information
if bifrostResp.Usage != nil {
if streamResp.Type == "" {
streamResp.Type = "message_delta"
}
streamResp.Usage = &AnthropicUsage{
InputTokens: bifrostResp.Usage.PromptTokens,
OutputTokens: bifrostResp.Usage.CompletionTokens,
}
}
// Set common fields
if bifrostResp.ID != "" {
streamResp.ID = &bifrostResp.ID
}
if bifrostResp.Model != "" {
if streamResp.Message == nil {
streamResp.Message = &AnthropicMessageResponse{}
}
streamResp.Message.Model = bifrostResp.Model
}
// Default to empty content_block_delta if no specific type was set
if streamResp.Type == "" {
streamResp.Type = "content_block_delta"
streamResp.Index = schemas.Ptr(0)
streamResp.Delta = &AnthropicStreamDelta{
Type: AnthropicStreamDeltaTypeText,
Text: schemas.Ptr(""),
}
}
// Marshal to JSON and format as SSE
jsonData, err := providerUtils.MarshalSorted(streamResp)
if err != nil {
return ""
}
// Format as Anthropic SSE
return fmt.Sprintf("event: %s\ndata: %s\n\n", streamResp.Type, jsonData)
}
// ToAnthropicChatStreamError converts a BifrostError to Anthropic streaming error in SSE format
func ToAnthropicChatStreamError(bifrostErr *schemas.BifrostError) string {
errorResp := ToAnthropicChatCompletionError(bifrostErr)
if errorResp == nil {
return ""
}
// Marshal to JSON
jsonData, err := providerUtils.MarshalSorted(errorResp)
if err != nil {
return ""
}
// Format as Anthropic SSE error event
return fmt.Sprintf("event: error\ndata: %s\n\n", jsonData)
}