diff --git a/models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModel.java b/models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModel.java index 49df9ac777..225eedeed0 100644 --- a/models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModel.java +++ b/models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModel.java @@ -288,7 +288,12 @@ ConverseRequest createRequest(Prompt prompt) { List contents = new ArrayList<>(); if (message instanceof UserMessage) { var userMessage = (UserMessage) message; - contents.add(ContentBlock.fromText(userMessage.getText())); + // The Converse API rejects empty text content blocks, so only send + // the text when there is any. A user message may legitimately carry + // media only (gh-6695). + if (StringUtils.hasText(userMessage.getText())) { + contents.add(ContentBlock.fromText(userMessage.getText())); + } if (!CollectionUtils.isEmpty(userMessage.getMedia())) { List mediaContent = userMessage.getMedia() diff --git a/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModelTest.java b/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModelTest.java index 377bc76fea..353ba28121 100644 --- a/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModelTest.java +++ b/models/spring-ai-bedrock-converse/src/test/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModelTest.java @@ -415,6 +415,58 @@ void shouldApplyCacheTtlOnToolsCachePoint() { assertThat(tools.get(1).cachePoint().ttlAsString()).isEqualTo("1h"); } + // ------------------------------------------------------------------------- + // Empty user message text (gh-6695) + // ------------------------------------------------------------------------- + + @Test + void mediaOnlyUserMessageOmitsEmptyTextContentBlock() { + BedrockProxyChatModel model = newModel(); + + Prompt prompt = new Prompt(List.of(UserMessage.builder().text("").media(pngMedia()).build()), + BedrockChatOptions.builder().build()); + + List contents = model.createRequest(prompt).messages().get(0).content(); + + assertThat(contents).hasSize(1); + assertThat(contents.get(0).image()).isNotNull(); + assertThat(contents).noneMatch(content -> content.text() != null); + } + + @Test + void blankUserMessageTextOmitsEmptyTextContentBlock() { + BedrockProxyChatModel model = newModel(); + + Prompt prompt = new Prompt(List.of(UserMessage.builder().text(" \n\t").media(pngMedia()).build()), + BedrockChatOptions.builder().build()); + + List contents = model.createRequest(prompt).messages().get(0).content(); + + assertThat(contents).hasSize(1); + assertThat(contents.get(0).image()).isNotNull(); + } + + @Test + void userMessageWithTextAndMediaKeepsBothContentBlocks() { + BedrockProxyChatModel model = newModel(); + + Prompt prompt = new Prompt(List.of(UserMessage.builder().text("Describe the image").media(pngMedia()).build()), + BedrockChatOptions.builder().build()); + + List contents = model.createRequest(prompt).messages().get(0).content(); + + assertThat(contents).hasSize(2); + assertThat(contents.get(0).text()).isEqualTo("Describe the image"); + assertThat(contents.get(1).image()).isNotNull(); + } + + private static Media pngMedia() { + return Media.builder() + .mimeType(MimeType.valueOf("image/png")) + .data(new byte[] { (byte) 0x89, 'P', 'N', 'G' }) + .build(); + } + public record WeatherRequest(String location, String unit) { }