From 790c2dd0970576a8d80350bcd43c8b143db800ec Mon Sep 17 00:00:00 2001 From: chabinhwang <7chabin@gmail.com> Date: Thu, 30 Jul 2026 12:20:03 +0900 Subject: [PATCH] Skip empty text blocks for Bedrock user messages The Converse API rejects requests that contain an empty text content block, so a user message carrying media but no text failed with an HTTP 400. Only add the text block when the user message actually has text, matching the guard the assistant message branch already uses. Fixes #6695 Signed-off-by: chabinhwang <7chabin@gmail.com> --- .../converse/BedrockProxyChatModel.java | 7 ++- .../converse/BedrockProxyChatModelTest.java | 52 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) 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) { }