Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10
github.com/aws/smithy-go v1.25.1
github.com/bytedance/sonic v1.15.0
github.com/cespare/xxhash/v2 v2.3.0
github.com/fasthttp/websocket v1.5.12
github.com/google/uuid v1.6.0
github.com/hajimehoshi/go-mp3 v0.3.4
Expand Down
2 changes: 2 additions & 0 deletions core/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uS
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
Expand Down
3 changes: 2 additions & 1 deletion core/providers/bedrock/bedrock.go
Original file line number Diff line number Diff line change
Expand Up @@ -1222,7 +1222,7 @@ func (provider *BedrockProvider) ChatCompletionStream(ctx *schemas.BifrostContex
var structuredOutputBuilder strings.Builder
var isAccumulatingStructuredOutput bool

streamState := NewBedrockStreamState()
streamState := NewBedrockStreamStateWithContext(ctx)

for {
// If context was cancelled/timed out, let defer handle it
Expand Down Expand Up @@ -1590,6 +1590,7 @@ func (provider *BedrockProvider) ResponsesStream(ctx *schemas.BifrostContext, po
// Create stream state for stateful conversions
streamState := acquireBedrockResponsesStreamState()
streamState.Model = &request.Model
streamState.Ctx = ctx
defer releaseBedrockResponsesStreamState(streamState)

// Check for structured output mode - if set, we need to intercept tool calls
Expand Down
230 changes: 230 additions & 0 deletions core/providers/bedrock/bedrock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5340,6 +5340,236 @@ func TestToBedrockResponsesRequest_NonLlamaConvertResponsesToolChoiceForcesToolC
assert.Equal(t, toolName, bedrockReq.ToolConfig.ToolChoice.Tool.Name)
}

func TestToBedrockChatCompletionRequest_AliasesLongMCPToolNames(t *testing.T) {
toolName := "mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_network_requests"
req := &schemas.BifrostChatRequest{
Model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
Input: []schemas.ChatMessage{{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("use devtools")},
}},
Params: &schemas.ChatParameters{
Tools: []schemas.ChatTool{{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: toolName,
Description: schemas.Ptr("List network requests"),
},
}},
ToolChoice: &schemas.ChatToolChoice{
ChatToolChoiceStruct: &schemas.ChatToolChoiceStruct{
Type: schemas.ChatToolChoiceTypeFunction,
Function: &schemas.ChatToolChoiceFunction{Name: toolName},
},
},
},
}

ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
result, err := bedrock.ToBedrockChatCompletionRequest(ctx, req)
require.NoError(t, err)
require.NotNil(t, result.ToolConfig)
require.Len(t, result.ToolConfig.Tools, 1)

alias := result.ToolConfig.Tools[0].ToolSpec.Name
require.LessOrEqual(t, len(alias), 64)
assert.NotEqual(t, toolName, alias)
assert.Contains(t, alias, "_list_network_requests")
assert.Regexp(t, `^[A-Za-z0-9_-]{1,64}$`, alias)
assert.Regexp(t, `^[0-9a-f]{8}_`, alias)
require.NotNil(t, result.ToolConfig.ToolChoice)
require.NotNil(t, result.ToolConfig.ToolChoice.Tool)
assert.Equal(t, alias, result.ToolConfig.ToolChoice.Tool.Name)
}

func TestToBedrockChatCompletionRequest_AliasesToolNamesWithInvalidChars(t *testing.T) {
// Short name (<=64 chars) but with characters disallowed by Bedrock's
// `[a-zA-Z0-9_-]{1,64}` tool-name pattern. It must still be aliased into a
// Bedrock-valid name rather than passed through unchanged.
toolName := "search files/in dir:now.fast"
require.LessOrEqual(t, len(toolName), 64)
req := &schemas.BifrostChatRequest{
Model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
Input: []schemas.ChatMessage{{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("search")},
}},
Params: &schemas.ChatParameters{
Tools: []schemas.ChatTool{{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: toolName,
Description: schemas.Ptr("Search files"),
},
}},
ToolChoice: &schemas.ChatToolChoice{
ChatToolChoiceStruct: &schemas.ChatToolChoiceStruct{
Type: schemas.ChatToolChoiceTypeFunction,
Function: &schemas.ChatToolChoiceFunction{Name: toolName},
},
},
},
}

ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
result, err := bedrock.ToBedrockChatCompletionRequest(ctx, req)
require.NoError(t, err)
require.NotNil(t, result.ToolConfig)
require.Len(t, result.ToolConfig.Tools, 1)

alias := result.ToolConfig.Tools[0].ToolSpec.Name
assert.NotEqual(t, toolName, alias, "name with disallowed chars must be aliased")
assert.Regexp(t, `^[A-Za-z0-9_-]{1,64}$`, alias)
assert.Regexp(t, `^[0-9a-f]{8}_`, alias)
require.NotNil(t, result.ToolConfig.ToolChoice)
require.NotNil(t, result.ToolConfig.ToolChoice.Tool)
assert.Equal(t, alias, result.ToolConfig.ToolChoice.Tool.Name)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestBedrockToBifrostChatResponse_RestoresAliasedToolName(t *testing.T) {
toolName := "mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_network_requests"
req := &schemas.BifrostChatRequest{
Model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
Input: []schemas.ChatMessage{{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("use devtools")},
}},
Params: &schemas.ChatParameters{
Tools: []schemas.ChatTool{{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{Name: toolName},
}},
},
}

ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
result, err := bedrock.ToBedrockChatCompletionRequest(ctx, req)
require.NoError(t, err)
alias := result.ToolConfig.Tools[0].ToolSpec.Name

response := &bedrock.BedrockConverseResponse{
StopReason: "tool_use",
Output: &bedrock.BedrockConverseOutput{
Message: &bedrock.BedrockMessage{
Role: bedrock.BedrockMessageRoleAssistant,
Content: []bedrock.BedrockContentBlock{{
ToolUse: &bedrock.BedrockToolUse{
ToolUseID: "tooluse_123",
Name: alias,
Input: json.RawMessage(`{"limit":10}`),
},
}},
},
},
}

converted, err := response.ToBifrostChatResponse(ctx, req.Model)
require.NoError(t, err)
require.Len(t, converted.Choices, 1)
toolCalls := converted.Choices[0].ChatNonStreamResponseChoice.Message.ChatAssistantMessage.ToolCalls
require.Len(t, toolCalls, 1)
require.NotNil(t, toolCalls[0].Function.Name)
assert.Equal(t, toolName, *toolCalls[0].Function.Name)
}

func TestToBedrockResponsesRequest_AliasesLongMCPToolNames(t *testing.T) {
toolName := "mcp__bifrost-this-is-imp-nasdkjadk-kanbsdjkabdkjbaskjdbasdaskjdbajksdkas__notion-notion-search"
req := &schemas.BifrostResponsesRequest{
Model: "us.anthropic.claude-opus-4-7",
Input: []schemas.ResponsesMessage{{
Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage),
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleUser),
Content: &schemas.ResponsesMessageContent{
ContentStr: schemas.Ptr("search docs for openai"),
},
}},
Params: &schemas.ResponsesParameters{
Tools: []schemas.ResponsesTool{{
Type: schemas.ResponsesToolTypeFunction,
Name: &toolName,
Description: schemas.Ptr("Search Notion"),
ResponsesToolFunction: &schemas.ResponsesToolFunction{
Parameters: &schemas.ToolFunctionParameters{
Type: "object",
Properties: schemas.NewOrderedMap(),
},
},
}},
ToolChoice: &schemas.ResponsesToolChoice{
ResponsesToolChoiceStruct: &schemas.ResponsesToolChoiceStruct{
Type: schemas.ResponsesToolChoiceTypeFunction,
Name: &toolName,
},
},
},
}

ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
result, err := bedrock.ToBedrockResponsesRequest(ctx, req)
require.NoError(t, err)
require.NotNil(t, result.ToolConfig)
require.Len(t, result.ToolConfig.Tools, 1)

alias := result.ToolConfig.Tools[0].ToolSpec.Name
require.LessOrEqual(t, len(alias), 64)
assert.NotEqual(t, toolName, alias)
assert.Contains(t, alias, "_notion-notion-search")
assert.Regexp(t, `^[A-Za-z0-9_-]{1,64}$`, alias)
assert.Regexp(t, `^[0-9a-f]{8}_`, alias)
require.NotNil(t, result.ToolConfig.ToolChoice)
require.NotNil(t, result.ToolConfig.ToolChoice.Tool)
assert.Equal(t, alias, result.ToolConfig.ToolChoice.Tool.Name)
}

func TestBedrockToBifrostResponsesResponse_RestoresAliasedToolName(t *testing.T) {
toolName := "mcp__bifrost-this-is-imp-nasdkjadk-kanbsdjkabdkjbaskjdbasdaskjdbajksdkas__notion-notion-search"
req := &schemas.BifrostResponsesRequest{
Model: "us.anthropic.claude-opus-4-7",
Input: []schemas.ResponsesMessage{{
Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage),
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleUser),
Content: &schemas.ResponsesMessageContent{
ContentStr: schemas.Ptr("search docs for openai"),
},
}},
Params: &schemas.ResponsesParameters{
Tools: []schemas.ResponsesTool{{
Type: schemas.ResponsesToolTypeFunction,
Name: &toolName,
ResponsesToolFunction: &schemas.ResponsesToolFunction{},
}},
},
}

ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
result, err := bedrock.ToBedrockResponsesRequest(ctx, req)
require.NoError(t, err)
alias := result.ToolConfig.Tools[0].ToolSpec.Name

response := &bedrock.BedrockConverseResponse{
StopReason: "tool_use",
Output: &bedrock.BedrockConverseOutput{
Message: &bedrock.BedrockMessage{
Role: bedrock.BedrockMessageRoleAssistant,
Content: []bedrock.BedrockContentBlock{{
ToolUse: &bedrock.BedrockToolUse{
ToolUseID: "tooluse_456",
Name: alias,
Input: json.RawMessage(`{"query":"openai"}`),
},
}},
},
},
}

converted, err := response.ToBifrostResponsesResponse(ctx)
require.NoError(t, err)
require.Len(t, converted.Output, 1)
require.NotNil(t, converted.Output[0].ResponsesToolMessage)
require.NotNil(t, converted.Output[0].ResponsesToolMessage.Name)
assert.Equal(t, toolName, *converted.Output[0].ResponsesToolMessage.Name)
}

// ---------------------------------------------------------------------------
// Structured output (response_format: json_schema) round-trip tests – Bedrock
// ---------------------------------------------------------------------------
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Expand Down
14 changes: 11 additions & 3 deletions core/providers/bedrock/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func ToBedrockChatCompletionRequest(ctx *schemas.BifrostContext, bifrostReq *sch
}

// Ensure tool config is present when needed
ensureChatToolConfigForConversation(bifrostReq, bedrockReq)
ensureChatToolConfigForConversation(ctx, bifrostReq, bedrockReq)

if !schemas.BedrockModelSupportsCachePoints(bifrostReq.Model) {
stripCachePointsFromBedrockRequest(bedrockReq)
Expand Down Expand Up @@ -117,7 +117,7 @@ func (response *BedrockConverseResponse) ToBifrostChatResponse(ctx context.Conte
}

toolUseID := contentBlock.ToolUse.ToolUseID
toolUseName := contentBlock.ToolUse.Name
toolUseName := bedrockRestoreToolName(ctx, contentBlock.ToolUse.Name)

toolCalls = append(toolCalls, schemas.ChatAssistantMessageToolCall{
Index: uint16(len(toolCalls)),
Expand Down Expand Up @@ -310,6 +310,7 @@ func (response *BedrockConverseResponse) ToBifrostChatResponse(ctx context.Conte
type BedrockStreamState struct {
nextToolCallIndex int
contentBlockToToolCallIdx map[int]int
ctx context.Context
}

// NewBedrockStreamState returns initialised stream state for one streaming response.
Expand All @@ -319,6 +320,13 @@ func NewBedrockStreamState() *BedrockStreamState {
}
}

// NewBedrockStreamStateWithContext returns stream state that can restore aliased tool names.
func NewBedrockStreamStateWithContext(ctx context.Context) *BedrockStreamState {
state := NewBedrockStreamState()
state.ctx = ctx
return state
}

func (chunk *BedrockStreamEvent) ToBifrostChatCompletionStream(state *BedrockStreamState) (*schemas.BifrostChatResponse, *schemas.BifrostError, bool) {
if state == nil {
state = NewBedrockStreamState()
Expand Down Expand Up @@ -361,7 +369,7 @@ func (chunk *BedrockStreamEvent) ToBifrostChatCompletionStream(state *BedrockStr
toolCall.Index = uint16(toolCallIdx)
toolCall.ID = schemas.Ptr(toolUseStart.ToolUseID)
toolCall.Type = schemas.Ptr("function")
toolCall.Function.Name = schemas.Ptr(toolUseStart.Name)
toolCall.Function.Name = schemas.Ptr(bedrockRestoreToolName(state.ctx, toolUseStart.Name))
toolCall.Function.Arguments = "" // Start with empty arguments

streamResponse := &schemas.BifrostChatResponse{
Expand Down
Loading
Loading