From 68f6cfa46edb7a451e73dac3f1adf9f2592f216c Mon Sep 17 00:00:00 2001
From: guanxu <1510424541@qq.com>
Date: Sun, 19 Jul 2026 23:00:43 +0800
Subject: [PATCH] Support unterminated markdown code blocks in cleaner
MarkdownCodeBlockCleaner previously required both an opening and a
closing triple-backtick fence. Responses that omitted the closing
fence (common with truncated or streamed output) were returned
unchanged. The cleaner now strips the opening fence unconditionally
and only removes the closing fence when present, so unterminated
blocks such as ```json {"key": "value"} are cleaned correctly.
Signed-off-by: guanxu <1510424541@qq.com>
---
.../converter/MarkdownCodeBlockCleaner.java | 8 ++-
.../MarkdownCodeBlockCleanerTest.java | 52 +++++++++++++++++++
2 files changed, 58 insertions(+), 2 deletions(-)
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/converter/MarkdownCodeBlockCleaner.java b/spring-ai-model/src/main/java/org/springframework/ai/converter/MarkdownCodeBlockCleaner.java
index 142f46fb4e..8c5401b37d 100644
--- a/spring-ai-model/src/main/java/org/springframework/ai/converter/MarkdownCodeBlockCleaner.java
+++ b/spring-ai-model/src/main/java/org/springframework/ai/converter/MarkdownCodeBlockCleaner.java
@@ -24,6 +24,8 @@
*
* - {@code ```json ... ```}
* - {@code ``` ... ```}
+ * - {@code ```json ...}
+ * - {@code ``` ...}
*
*
* @author liugddx
@@ -41,7 +43,7 @@ public class MarkdownCodeBlockCleaner implements ResponseTextCleaner {
text = text.trim();
// Check for and remove triple backticks
- if (text.startsWith("```") && text.endsWith("```")) {
+ if (text.startsWith("```")) {
String[] lines = text.split("\n", 2);
String firstLine = lines[0].trim();
if (lines.length > 1) {
@@ -58,7 +60,9 @@ public class MarkdownCodeBlockCleaner implements ResponseTextCleaner {
}
// Remove trailing ```
- text = text.substring(0, text.length() - 3);
+ if (text.endsWith("```")) {
+ text = text.substring(0, text.length() - 3);
+ }
// Trim again to remove any potential whitespace
text = text.trim();
diff --git a/spring-ai-model/src/test/java/org/springframework/ai/converter/MarkdownCodeBlockCleanerTest.java b/spring-ai-model/src/test/java/org/springframework/ai/converter/MarkdownCodeBlockCleanerTest.java
index 4d3df26995..898f27981e 100644
--- a/spring-ai-model/src/test/java/org/springframework/ai/converter/MarkdownCodeBlockCleanerTest.java
+++ b/spring-ai-model/src/test/java/org/springframework/ai/converter/MarkdownCodeBlockCleanerTest.java
@@ -70,4 +70,56 @@ void shouldNotModifyTextWithoutCodeBlocks() {
assertThat(cleaner.clean(input)).isEqualTo(input);
}
+ @Test
+ void shouldRemoveMultilineCodeBlockWithLanguageIdentifierWithoutClosingFence() {
+ MarkdownCodeBlockCleaner cleaner = new MarkdownCodeBlockCleaner();
+ String input = "```json\n{\"key\": \"value\"}";
+ String result = cleaner.clean(input);
+ assertThat(result).isEqualTo("{\"key\": \"value\"}");
+ }
+
+ @Test
+ void shouldRemoveMultilineCodeBlockWithoutLanguageIdentifierWithoutClosingFence() {
+ MarkdownCodeBlockCleaner cleaner = new MarkdownCodeBlockCleaner();
+ String input = "```\n{\"key\": \"value\"}";
+ String result = cleaner.clean(input);
+ assertThat(result).isEqualTo("{\"key\": \"value\"}");
+ }
+
+ @Test
+ void shouldRemoveSingleLineCodeBlockWithoutClosingFence() {
+ MarkdownCodeBlockCleaner cleaner = new MarkdownCodeBlockCleaner();
+ String input = "```{\"key\": \"value\"}";
+ String result = cleaner.clean(input);
+ assertThat(result).isEqualTo("{\"key\": \"value\"}");
+ }
+
+ @Test
+ void shouldRemoveMultilineJsonObjectWithoutClosingFence() {
+ MarkdownCodeBlockCleaner cleaner = new MarkdownCodeBlockCleaner();
+ String input = "```json\n{\n \"name\": \"Alice\",\n \"age\": 30\n}";
+ String result = cleaner.clean(input);
+ assertThat(result).isEqualTo("{\n \"name\": \"Alice\",\n \"age\": 30\n}");
+ }
+
+ @Test
+ void shouldRemoveCodeBlockWithoutClosingFenceAndTrailingWhitespace() {
+ MarkdownCodeBlockCleaner cleaner = new MarkdownCodeBlockCleaner();
+ String input = "```json\n{\"key\": \"value\"} ";
+ String result = cleaner.clean(input);
+ assertThat(result).isEqualTo("{\"key\": \"value\"}");
+ }
+
+ @Test
+ void shouldHandleMalformedInput() {
+ MarkdownCodeBlockCleaner cleaner = new MarkdownCodeBlockCleaner();
+ assertThat(cleaner.clean("`")).isEqualTo("`");
+ assertThat(cleaner.clean("``")).isEqualTo("``");
+ assertThat(cleaner.clean("```")).isEmpty();
+ assertThat(cleaner.clean("````")).isEqualTo("`");
+ assertThat(cleaner.clean("`````")).isEqualTo("``");
+ assertThat(cleaner.clean("``````")).isEmpty();
+ assertThat(cleaner.clean("```````")).isEqualTo("`");
+ }
+
}