Skip to content
Closed
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
43 changes: 42 additions & 1 deletion core/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -5114,6 +5114,17 @@ func shouldConvertTextToChat(ctx *schemas.BifrostContext, requestType schemas.Re
return ok && shouldConvert
}

func shouldConvertChatToResponses(ctx *schemas.BifrostContext, requestType schemas.RequestType, request *schemas.BifrostChatRequest) bool {
if ctx == nil || request == nil {
return false
}
if requestType != schemas.ChatCompletionRequest && requestType != schemas.ChatCompletionStreamRequest {
return false
}
shouldConvert, ok := ctx.Value(schemas.BifrostContextKeyShouldConvertChatToResponses).(bool)
return ok && shouldConvert
}

func wrapTextToChatStreamPostHookRunner(postHookRunner schemas.PostHookRunner) schemas.PostHookRunner {
return func(ctx *schemas.BifrostContext, result *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError) {
if result != nil && result.ChatResponse != nil {
Expand All @@ -5127,6 +5138,19 @@ func wrapTextToChatStreamPostHookRunner(postHookRunner schemas.PostHookRunner) s
}
}

func wrapChatToResponsesStreamPostHookRunner(postHookRunner schemas.PostHookRunner) schemas.PostHookRunner {
return func(ctx *schemas.BifrostContext, result *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError) {
if result != nil && result.ResponsesStreamResponse != nil {
if convertedResponse := result.ResponsesStreamResponse.ToBifrostChatResponse(); convertedResponse != nil {
result = &schemas.BifrostResponse{
ChatResponse: convertedResponse,
}
}
}
return postHookRunner(ctx, result, bifrostErr)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// handleProviderRequest handles the request to the provider based on the request type
// key is used for single-key operations, keys is used for batch/file operations that need multiple keys
func (bifrost *Bifrost) handleProviderRequest(provider schemas.Provider, req *ChannelMessage, key schemas.Key, keys []schemas.Key) (*schemas.BifrostResponse, *schemas.BifrostError) {
Expand Down Expand Up @@ -5156,6 +5180,17 @@ func (bifrost *Bifrost) handleProviderRequest(provider schemas.Provider, req *Ch
}
response.TextCompletionResponse = textCompletionResponse
case schemas.ChatCompletionRequest:
if shouldConvertChatToResponses(req.Context, req.RequestType, req.BifrostRequest.ChatRequest) {
responsesRequest := req.BifrostRequest.ChatRequest.ToResponsesRequest()
if responsesRequest != nil {
responsesResponse, bifrostError := provider.Responses(req.Context, key, responsesRequest)
if bifrostError != nil {
return nil, bifrostError
}
response.ChatResponse = responsesResponse.ToBifrostChatResponse()
break
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
chatCompletionResponse, bifrostError := provider.ChatCompletion(req.Context, key, req.BifrostRequest.ChatRequest)
if bifrostError != nil {
return nil, bifrostError
Expand Down Expand Up @@ -5411,6 +5446,12 @@ func (bifrost *Bifrost) handleProviderStreamRequest(provider schemas.Provider, r
}
return provider.TextCompletionStream(req.Context, postHookRunner, key, req.BifrostRequest.TextCompletionRequest)
case schemas.ChatCompletionStreamRequest:
if shouldConvertChatToResponses(req.Context, req.RequestType, req.BifrostRequest.ChatRequest) {
responsesRequest := req.BifrostRequest.ChatRequest.ToResponsesRequest()
if responsesRequest != nil {
return provider.ResponsesStream(req.Context, wrapChatToResponsesStreamPostHookRunner(postHookRunner), key, responsesRequest)
}
}
return provider.ChatCompletionStream(req.Context, postHookRunner, key, req.BifrostRequest.ChatRequest)
case schemas.ResponsesStreamRequest:
return provider.ResponsesStream(req.Context, postHookRunner, key, req.BifrostRequest.ResponsesRequest)
Expand Down Expand Up @@ -6514,4 +6555,4 @@ func (bifrost *Bifrost) Shutdown() {
}
}
bifrost.logger.Info("all request channels closed")
}
}
3 changes: 2 additions & 1 deletion core/schemas/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const (
BifrostContextKeyURLPath BifrostContextKey = "bifrost-extra-url-path" // string
BifrostContextKeyUseRawRequestBody BifrostContextKey = "bifrost-use-raw-request-body"
BifrostContextKeyShouldConvertTextToChat BifrostContextKey = "bifrost-should-convert-text-to-chat" // bool (set by plugins to trigger text->chat provider conversion in core)
BifrostContextKeyShouldConvertChatToResponses BifrostContextKey = "bifrost-should-convert-chat-to-responses" // bool (set by plugins to trigger chat->responses provider conversion in core)
BifrostContextKeySendBackRawRequest BifrostContextKey = "bifrost-send-back-raw-request" // bool
BifrostContextKeySendBackRawResponse BifrostContextKey = "bifrost-send-back-raw-response" // bool
BifrostContextKeyIntegrationType BifrostContextKey = "bifrost-integration-type" // integration used in gateway (e.g. openai, anthropic, bedrock, etc.)
Expand Down Expand Up @@ -963,4 +964,4 @@ type BifrostErrorExtraFields struct {
RawResponse interface{} `json:"raw_response,omitempty"`
LiteLLMCompat bool `json:"litellm_compat,omitempty"`
KeyStatuses []KeyStatus `json:"key_statuses,omitempty"`
}
}
260 changes: 259 additions & 1 deletion core/schemas/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,10 @@ func (responsesResp *BifrostResponsesResponse) ToBifrostChatResponse() *BifrostC
Videos: responsesResp.Videos,
}

if responsesResp.ID != nil {
chatResp.ID = *responsesResp.ID
}

// Create Choices from ResponsesResponse
if len(responsesResp.Output) > 0 {
// Convert ResponsesMessages back to ChatMessages
Expand Down Expand Up @@ -1991,6 +1995,34 @@ func (cr *BifrostChatResponse) ToBifrostResponsesStreamResponse(state *ChatToRes
response.Output = allOutput
}

// Append finalized function call items so the terminal response carries them in Output.
for toolCallID, args := range state.ToolArgumentBuffers {
if args == "" {
continue
}
statusFinal := terminalStatus
messageType := ResponsesMessageTypeFunctionCall
callName := state.ToolCallNames[toolCallID]
var callNamePtr *string
if callName != "" {
callNamePtr = &callName
}
argsValue := args
fcMsg := ResponsesMessage{
Type: &messageType,
Status: &statusFinal,
ResponsesToolMessage: &ResponsesToolMessage{
CallID: &toolCallID,
Name: callNamePtr,
Arguments: &argsValue,
},
}
if itemID := state.ItemIDs[toolCallID]; itemID != "" {
fcMsg.ID = &itemID
}
response.Output = append(response.Output, fcMsg)
}
Comment on lines +1998 to +2024
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't append terminal tool calls twice.

Lines 1955-1992 already add one ResponsesMessageTypeFunctionCall per tool call, in outputIndex order. This second map walk appends the same items again, and in random order. Because ToChatMessages() turns every function_call item into a chat tool call, the stacked chat→responses→chat fallback can ask the client to execute the same tool twice.


responses = append(responses, &BifrostResponsesStreamResponse{
Type: terminalEventType,
SequenceNumber: state.SequenceNumber,
Expand All @@ -2014,6 +2046,232 @@ func (cr *BifrostChatResponse) ToBifrostResponsesStreamResponse(state *ChatToRes
return responses
}

// ToBifrostChatResponse converts a BifrostResponsesStreamResponse chunk to a BifrostChatResponse (chat.completion.chunk).
// Returns nil for events that have no meaningful chat completion equivalent (lifecycle events, etc.).
func (rsr *BifrostResponsesStreamResponse) ToBifrostChatResponse() *BifrostChatResponse {
if rsr == nil {
return nil
}

extraFields := rsr.ExtraFields
extraFields.RequestType = ChatCompletionStreamRequest

resp := &BifrostChatResponse{
Object: "chat.completion.chunk",
ExtraFields: extraFields,
SearchResults: rsr.SearchResults,
Videos: rsr.Videos,
Citations: rsr.Citations,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if rsr.Response != nil {
if rsr.Response.ID != nil {
resp.ID = *rsr.Response.ID
}
resp.Created = rsr.Response.CreatedAt
resp.Model = rsr.Response.Model
}
Comment thread
sammaji marked this conversation as resolved.

switch rsr.Type {
case ResponsesStreamResponseTypeOutputTextDelta:
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{
Content: rsr.Delta,
},
},
},
}
return resp

case ResponsesStreamResponseTypeReasoningSummaryTextDelta:
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{
Reasoning: rsr.Delta,
},
},
},
}
return resp

case ResponsesStreamResponseTypeRefusalDelta:
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{
Refusal: rsr.Refusal,
},
},
},
}
return resp

case ResponsesStreamResponseTypeOutputItemAdded:
if rsr.Item == nil || rsr.Item.Type == nil {
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{},
},
},
}
return resp
}

switch *rsr.Item.Type {
case ResponsesMessageTypeFunctionCall:
if rsr.Item.ResponsesToolMessage == nil {
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{},
},
},
}
return resp
}
funcType := "function"
var idx uint16
if rsr.OutputIndex != nil && *rsr.OutputIndex > 0 {
idx = uint16(*rsr.OutputIndex - 1)
}
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{
ToolCalls: []ChatAssistantMessageToolCall{
{
Index: idx,
Type: &funcType,
ID: rsr.Item.ResponsesToolMessage.CallID,
Function: ChatAssistantMessageToolCallFunction{
Name: rsr.Item.ResponsesToolMessage.Name,
},
},
},
},
},
},
}
return resp

case ResponsesMessageTypeMessage:
role := "assistant"
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{
Role: &role,
},
},
},
}
return resp

default:
// reasoning, file_search_call, web_search_call, etc. — no chat equivalent,
// actual content arrives via separate delta events.
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{},
},
},
}
return resp
}

case ResponsesStreamResponseTypeFunctionCallArgumentsDelta:
if rsr.Delta == nil {
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{},
},
},
}
return resp
}
var idx uint16
if rsr.OutputIndex != nil && *rsr.OutputIndex > 0 {
idx = uint16(*rsr.OutputIndex - 1)
}

resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{
ToolCalls: []ChatAssistantMessageToolCall{
{
Index: idx,
Function: ChatAssistantMessageToolCallFunction{
Arguments: *rsr.Delta,
},
},
},
},
},
},
}
return resp

case ResponsesStreamResponseTypeCompleted, ResponsesStreamResponseTypeIncomplete:
finishReason := string(BifrostFinishReasonStop)
if rsr.Type == ResponsesStreamResponseTypeIncomplete {
finishReason = string(BifrostFinishReasonLength)
}
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
FinishReason: &finishReason,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{},
},
},
}
if rsr.Response != nil {
if rsr.Response.Usage != nil {
resp.Usage = rsr.Response.Usage.ToBifrostLLMUsage()
}
// Check for tool_calls finish reason
for _, output := range rsr.Response.Output {
if output.Type != nil && *output.Type == ResponsesMessageTypeFunctionCall {
finishReason = string(BifrostFinishReasonToolCalls)
resp.Choices[0].FinishReason = &finishReason
break
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return resp

default:
// Lifecycle events (created, in_progress, content_part.added/done, output_text.done,
// output_item.done, function_call_arguments.done, etc.) → empty chat chunk with no content.
resp.Choices = []BifrostResponseChoice{
{
Index: 0,
ChatStreamResponseChoice: &ChatStreamResponseChoice{
Delta: &ChatStreamResponseChoiceDelta{},
},
},
}
return resp
}
}

// =============================================================================
// RESPONSE CONVERSION METHODS
// =============================================================================
Expand Down Expand Up @@ -2130,4 +2388,4 @@ func (cr *BifrostChatResponse) ToBifrostTextCompletionResponse() *BifrostTextCom
CacheDebug: cr.ExtraFields.CacheDebug,
},
}
}
}
4 changes: 3 additions & 1 deletion core/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ func clearCtxForFallback(ctx *schemas.BifrostContext) {
ctx.ClearValue(schemas.BifrostContextKeyAPIKeyID)
ctx.ClearValue(schemas.BifrostContextKeyAPIKeyName)
ctx.ClearValue(schemas.BifrostContextKeyGovernanceIncludeOnlyKeys)
ctx.ClearValue(schemas.BifrostContextKeyShouldConvertTextToChat)
ctx.ClearValue(schemas.BifrostContextKeyShouldConvertChatToResponses)
}

var supportedBaseProvidersSet = func() map[schemas.ModelProvider]struct{} {
Expand Down Expand Up @@ -507,4 +509,4 @@ func buildSessionKey(providerKey schemas.ModelProvider, sessionID string, model
discriminator = "__modelless__"
}
return "session:" + string(providerKey) + ":" + hashedSessionID + ":" + hashSHA256(discriminator)
}
}
Loading