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
6 changes: 5 additions & 1 deletion internal/translator/openai_awsbedrock.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,11 @@ func (o *openAIToAWSBedrockTranslatorV1ChatCompletion) openAIMessageToBedrockMes
if err != nil {
return err
}
bedrockReq.Messages = append(bedrockReq.Messages, bedrockMessage)
// Some clients, like OpenCode, can send assistant messages with nil or empty string content and no tool calls,
// which would translate to an empty content array that Bedrock Converse rejects.
if len(bedrockMessage.Content) > 0 {
Comment thread
Killusions marked this conversation as resolved.
bedrockReq.Messages = append(bedrockReq.Messages, bedrockMessage)
}
case msg.OfSystem != nil:
if bedrockReq.System == nil {
bedrockReq.System = make([]*awsbedrock.SystemContentBlock, 0)
Expand Down
111 changes: 107 additions & 4 deletions internal/translator/openai_awsbedrock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,6 @@ func TestOpenAIToAWSBedrockTranslatorV1ChatCompletion_RequestBody(t *testing.T)
},
},
},
{
Role: openai.ChatMessageRoleAssistant,
Content: []*awsbedrock.ContentBlock{},
},
},
},
},
Expand Down Expand Up @@ -2582,3 +2578,110 @@ func TestCacheControlHelpers(t *testing.T) {
require.Nil(t, cachePoint3)
})
}

func TestOpenAIToAWSBedrockTranslator_EmptyContentMessages(t *testing.T) {
t.Run("empty assistant message without tool calls is dropped", func(t *testing.T) {
for _, name := range []string{"empty string", "nil content"} {
t.Run(name, func(t *testing.T) {
assistantMsg := &openai.ChatCompletionAssistantMessageParam{
Role: openai.ChatMessageRoleAssistant,
}
if name == "empty string" {
assistantMsg.Content = openai.StringOrAssistantRoleContentUnion{Value: ""}
}

o := &openAIToAWSBedrockTranslatorV1ChatCompletion{}
req := openai.ChatCompletionRequest{
Model: "anthropic.claude-3-5-sonnet-20241022-v2:0",
Messages: []openai.ChatCompletionMessageParamUnion{
{OfUser: &openai.ChatCompletionUserMessageParam{Role: openai.ChatMessageRoleUser, Content: openai.StringOrUserRoleContentUnion{Value: "Hello"}}},
{OfAssistant: assistantMsg},
{OfUser: &openai.ChatCompletionUserMessageParam{Role: openai.ChatMessageRoleUser, Content: openai.StringOrUserRoleContentUnion{Value: "Can you help?"}}},
},
}

_, newBody, err := o.RequestBody(nil, &req, false)
require.NoError(t, err)

var awsReq awsbedrock.ConverseInput
require.NoError(t, json.Unmarshal(newBody, &awsReq))
requireNoEmptyAssistantContent(t, awsReq.Messages)
})
}
})

t.Run("assistant message with only tool calls and no content should work", func(t *testing.T) {
o := &openAIToAWSBedrockTranslatorV1ChatCompletion{}
req := openai.ChatCompletionRequest{
Model: "anthropic.claude-3-5-sonnet-20241022-v2:0",
Messages: []openai.ChatCompletionMessageParamUnion{
{OfUser: &openai.ChatCompletionUserMessageParam{Role: openai.ChatMessageRoleUser, Content: openai.StringOrUserRoleContentUnion{Value: "What is the weather?"}}},
{OfAssistant: &openai.ChatCompletionAssistantMessageParam{
Role: openai.ChatMessageRoleAssistant,
ToolCalls: []openai.ChatCompletionMessageToolCallParam{
{ID: ptr.To("call_abc123"), Type: openai.ChatCompletionMessageToolCallTypeFunction, Function: openai.ChatCompletionMessageToolCallFunctionParam{Name: "get_weather", Arguments: `{"location":"NYC"}`}},
},
}},
{OfTool: &openai.ChatCompletionToolMessageParam{Role: openai.ChatMessageRoleTool, ToolCallID: "call_abc123", Content: openai.ContentUnion{Value: `{"temp":72}`}}},
},
}

_, newBody, err := o.RequestBody(nil, &req, false)
require.NoError(t, err)

var awsReq awsbedrock.ConverseInput
require.NoError(t, json.Unmarshal(newBody, &awsReq))

var foundAssistant bool
for _, msg := range awsReq.Messages {
if msg.Role == openai.ChatMessageRoleAssistant {
foundAssistant = true
require.NotEmpty(t, msg.Content, "assistant message with tool calls should have non-empty content")
require.NotNil(t, msg.Content[0].ToolUse, "expected a ToolUse block")
require.Equal(t, "get_weather", msg.Content[0].ToolUse.Name)
}
}
require.True(t, foundAssistant, "should have found an assistant message")
})

t.Run("mixed messages with some empty assistant messages should handle correctly", func(t *testing.T) {
o := &openAIToAWSBedrockTranslatorV1ChatCompletion{}
req := openai.ChatCompletionRequest{
Model: "anthropic.claude-3-5-sonnet-20241022-v2:0",
Messages: []openai.ChatCompletionMessageParamUnion{
{OfUser: &openai.ChatCompletionUserMessageParam{Role: openai.ChatMessageRoleUser, Content: openai.StringOrUserRoleContentUnion{Value: "Hello"}}},
{OfAssistant: &openai.ChatCompletionAssistantMessageParam{Role: openai.ChatMessageRoleAssistant, Content: openai.StringOrAssistantRoleContentUnion{Value: ""}}},
{OfUser: &openai.ChatCompletionUserMessageParam{Role: openai.ChatMessageRoleUser, Content: openai.StringOrUserRoleContentUnion{Value: "Tell me more"}}},
{OfAssistant: &openai.ChatCompletionAssistantMessageParam{Role: openai.ChatMessageRoleAssistant, Content: openai.StringOrAssistantRoleContentUnion{Value: "Here is the info"}}},
},
}

_, newBody, err := o.RequestBody(nil, &req, false)
require.NoError(t, err)

var awsReq awsbedrock.ConverseInput
require.NoError(t, json.Unmarshal(newBody, &awsReq))
requireNoEmptyAssistantContent(t, awsReq.Messages)

var assistantWithContent bool
for _, msg := range awsReq.Messages {
if msg.Role == openai.ChatMessageRoleAssistant {
for _, block := range msg.Content {
if block.Text != nil && *block.Text == "Here is the info" {
assistantWithContent = true
}
}
}
}
require.True(t, assistantWithContent, "should preserve assistant message with actual content")
})
}

func requireNoEmptyAssistantContent(t *testing.T, messages []*awsbedrock.Message) {
t.Helper()
for i, msg := range messages {
if msg.Role == openai.ChatMessageRoleAssistant && len(msg.Content) == 0 {
t.Errorf("message at index %d is an assistant message with empty content array, which Bedrock will reject", i)
}
}
}