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/changelog.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- fix: Gemini provider - handle content block tool outputs in Responses API path
- fix: case-insensitive `anthropic-beta` merge in `MergeBetaHeaders`
- fix: Bedrock provider - emit message_stop event for Anthropic invoke stream [@tefimov](https://github.com/tefimov)
64 changes: 64 additions & 0 deletions core/providers/gemini/gemini_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,70 @@ func TestResponsesAPIParallelFunctionCalling(t *testing.T) {
}
},
},
{
name: "ResponsesAPI_FunctionCallOutput_ContentBlocks",
input: &schemas.BifrostResponsesRequest{
Provider: schemas.Gemini,
Model: "gemini-2.0-flash",
Input: []schemas.ResponsesMessage{
{
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleUser),
Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage),
Content: &schemas.ResponsesMessageContent{
ContentStr: schemas.Ptr("List browser tabs"),
},
},
{
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall),
ResponsesToolMessage: &schemas.ResponsesToolMessage{
CallID: schemas.Ptr("call_tabs"),
Name: schemas.Ptr("browser_tabs"),
Arguments: schemas.Ptr(`{"action":"list"}`),
},
},
{
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCallOutput),
ResponsesToolMessage: &schemas.ResponsesToolMessage{
CallID: schemas.Ptr("call_tabs"),
Output: &schemas.ResponsesToolMessageOutputStruct{
// Output as content blocks (Anthropic Responses API format)
ResponsesFunctionToolCallOutputBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesInputMessageContentBlockTypeText,
Text: schemas.Ptr("### Open tabs\n- 0: (current) [Google] (https://google.com)\n- 1: [GitHub] (https://github.com)\n"),
},
},
},
},
},
},
},
validate: func(t *testing.T, result *gemini.GeminiGenerationRequest) {
// Find the Content with function response
var toolResponseContent *gemini.Content
for i := range result.Contents {
content := &result.Contents[i]
if len(content.Parts) > 0 && content.Parts[0].FunctionResponse != nil {
toolResponseContent = content
break
}
}

require.NotNil(t, toolResponseContent, "Should have a content with functionResponse")
require.Len(t, toolResponseContent.Parts, 1)

part := toolResponseContent.Parts[0]
require.NotNil(t, part.FunctionResponse, "Part must have functionResponse")
assert.Equal(t, "call_tabs", part.FunctionResponse.ID)
assert.Equal(t, "browser_tabs", part.FunctionResponse.Name)

// Verify the response data contains the tool output (not empty)
require.NotNil(t, part.FunctionResponse.Response, "FunctionResponse.Response must not be nil")
responseStr := string(part.FunctionResponse.Response)
assert.Contains(t, responseStr, "Open tabs", "Response should contain the tool output text")
assert.Contains(t, responseStr, "Google", "Response should contain tab content")
},
},
}

for _, tt := range tests {
Expand Down
24 changes: 24 additions & 0 deletions core/providers/gemini/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -3016,6 +3016,30 @@ func convertResponsesMessagesToGeminiContents(messages []schemas.ResponsesMessag
} else {
responseMap["output"] = output
}
} else if msg.ResponsesToolMessage.Output != nil && msg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks != nil {
// Handle structured output blocks (e.g. from Anthropic Responses API format
// where output is an array of content blocks like [{"type":"input_text","text":"..."}])
var textParts []string
for _, block := range msg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks {
if block.Text != nil && *block.Text != "" {
textParts = append(textParts, *block.Text)
}
}
if len(textParts) > 0 {
combined := strings.Join(textParts, "\n")
if json.Valid([]byte(combined)) {
responseMap["output"] = json.RawMessage(combined)
} else {
responseMap["output"] = combined
}
} else {
// Fallback for non-text blocks (e.g. images, files): marshal the raw blocks
// so responseMap["output"] is never left empty when blocks are present
rawBlocks, err := providerUtils.MarshalSorted(msg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks)
if err == nil && len(rawBlocks) > 0 {
responseMap["output"] = json.RawMessage(rawBlocks)
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else if msg.Content != nil && msg.Content.ContentStr != nil {
// Fallback to Content.ContentStr for backward compatibility
output := *msg.Content.ContentStr
Expand Down