Compare commits

...
Author SHA1 Message Date
Gerald LonlasandClaude Opus 4.8 dd60ac08d3 fix(client): strip orphan </think> tags left by nested think blocks
The non-greedy `<think>.*?</think>` regex removes only up to the first
closing tag. When a model nests `<think>` inside its own reasoning (e.g.
`<think><think>inner</think></think>`), the outer `</think>` was never
consumed, causing it to appear verbatim in the visible output returned to
the caller.

Added a third pass (`_THINK_CLOSE_RE`) that removes any `</think>` tags
still present after the block and dangling-open passes; such tags can only
be orphaned closing tags at that point.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 01:29:21 +08:00
2 changed files with 23 additions and 1 deletions
+8 -1
View File
@@ -245,18 +245,25 @@ def extract_responses_text(response: Any) -> str:
_THINK_BLOCK_RE = re.compile(r"<think>.*?</think>", flags=re.IGNORECASE | re.DOTALL)
_THINK_START_RE = re.compile(r"<think>.*$", flags=re.IGNORECASE | re.DOTALL)
_THINK_CLOSE_RE = re.compile(r"</think>", flags=re.IGNORECASE)
def strip_think_content(text: str | None) -> str:
"""
Remove reasoning blocks enclosed in <think>...</think>.
Also removes dangling <think> blocks without a closing tag.
Also removes dangling <think> blocks without a closing tag and any
orphan </think> closing tags left by nested or malformed think blocks.
"""
if not text:
return ""
cleaned = _THINK_BLOCK_RE.sub("", text)
cleaned = _THINK_START_RE.sub("", cleaned)
# After the above two passes, any remaining </think> tags are orphaned
# closing tags produced by nested blocks (e.g. <think><think>…</think></think>).
# The non-greedy block regex only removes up to the *first* </think>, leaving
# the outer one behind; strip it here so it never appears in visible output.
cleaned = _THINK_CLOSE_RE.sub("", cleaned)
return cleaned.strip()
+15
View File
@@ -64,3 +64,18 @@ def test_strip_think_content_removes_dangling_start_tag() -> None:
# strip() removes the trailing real newline left after the dangling block is excised.
text = "Visible output\n<think>unfinished"
assert client.strip_think_content(text) == "Visible output"
def test_strip_think_content_nested_think_tags_no_orphan_close() -> None:
# When a think block contains nested <think> tags (e.g. a model quoting the tag
# in its own reasoning), the non-greedy regex matches up to the *first* </think>,
# which would leave the outer closing tag as an orphan in the visible output.
# The fix strips any remaining </think> tags after the block/dangling passes.
assert client.strip_think_content("<think><think>nested</think></think>") == ""
# Regression: visible text surrounding nested blocks must be preserved intact.
result = client.strip_think_content(
"Before <think>reasoning <think>deep</think></think> after"
)
assert "</think>" not in result
assert "Before" in result
assert "after" in result