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
23 changes: 23 additions & 0 deletions core/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
- feat: add direct API key header support (#3817)
- feat: introduce MCPCredentialStore abstraction and per-user MCP credential reconciliation (#3656, #3702, #3705)
- feat: add MCP per-user headers auth type with credential storage (#3703)
- feat: add TLS configuration support for MCP HTTP/SSE client connections (#3779)
- feat: propagate request context through MCP client connection (#3768)
- feat: rotate keys on 401/402/403 and return 502 upstream_credentials_exhausted when all keys are permanently dead (#3491)
- feat: add triggered_rotation to KeyAttemptRecord and tighten bifrost_key_rotation_events_total semantics (#3430)
- feat: add OTel spec compatible metrics, backward compatible (#3865)
- feat: add provider cache and semantic cache attributes in metrics export (#3816)
- feat: add additional_attributes to model pricing rows (#3829)
- feat: system messages handling for Opus 4.8 (#3878)
- fix: Opus 4.8 compatibility (#3868)
- fix: truncate Bedrock function/tool names to the provider length limit
- fix: set guardrail config in Bedrock request from responses (#3862)
- fix: default Anthropic tool_use input to {} when arguments are absent (#3880)
- fix: responses stream events (#3838)
- fix: missing parameter parsing on compat flow (#3881)
- fix: set default api version in passthrough requests as a fallback (#3853)
- fix: add prompt cache retention parameter on responses request (#3810)
- fix: avoid overriding optional fields in virtual key update (#3855)
- refactor: expose RunWithPluginPipeline on ClientManager and route Starlark nested tool calls through the canonical plugin gate (#3794)
- refactor: rename staticHeadersResolver/serverOAuthResolver to sharedHeadersResolver/sharedOAuthResolver (#3840)
- chore: upgrade to Go 1.26.3 (#3782)
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)
}

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
// ---------------------------------------------------------------------------
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