1389 lines
49 KiB
Go
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)
|
|
}
|