Skip to content

Commit 56f9e93

Browse files
SkylarKeltyclaude
andcommitted
fix: strip text-based tool call blocks from LLM responses
Some models (e.g. MiniMax) emit tool calls as XML-like text content (<minimax:tool_call>…</minimax:tool_call>) instead of using the structured tool_calls response field. These would pass through as the essay text since content was non-empty. Broadens _strip_think_tags into _strip_llm_artifacts which also removes <*tool_call>…</*tool_call> blocks (with optional namespace prefixes and pipe delimiters), plus orphaned opening tags from token-limit truncation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0afc622 commit 56f9e93

File tree

1 file changed

+34
-4
lines changed

1 file changed

+34
-4
lines changed

artemis/llm.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,41 @@ async def _post_completion(
312312
raise UpstreamServiceError("The LLM backend returned invalid JSON.") from exc
313313

314314

315-
def _strip_think_tags(content: str) -> str:
316-
"""Strip <think>…</think> reasoning blocks and orphaned tags."""
315+
def _strip_llm_artifacts(content: str) -> str:
316+
"""Strip non-content artifacts that models sometimes emit as plain text.
317+
318+
Handles:
319+
- ``<think>…</think>`` reasoning blocks (and orphaned open/close tags)
320+
- Text-based tool-call blocks that some models emit instead of using the
321+
structured ``tool_calls`` response field, e.g.::
322+
323+
<minimax:tool_call>…</minimax:tool_call>
324+
<tool_call>…</tool_call>
325+
<|tool_call|>…<|/tool_call|>
326+
327+
Returns the cleaned string (may be empty).
328+
"""
329+
# Think / reasoning blocks (including orphaned tags)
317330
content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL)
318331
content = re.sub(r"^.*?</think>", "", content, flags=re.DOTALL)
319332
content = re.sub(r"<think>.*$", "", content, flags=re.DOTALL)
333+
334+
# Text-based tool calls: <prefix:tool_call>…</prefix:tool_call> and
335+
# <tool_call>…</tool_call> with optional pipe delimiters
336+
content = re.sub(
337+
r"<\|?[\w:]*tool_call\|?>.*?<\|?/[\w:]*tool_call\|?>",
338+
"",
339+
content,
340+
flags=re.DOTALL,
341+
)
342+
# Orphaned opening tag at the end (model hit token limit mid-call)
343+
content = re.sub(
344+
r"<\|?[\w:]*tool_call\|?>.*$",
345+
"",
346+
content,
347+
flags=re.DOTALL,
348+
)
349+
320350
return content.strip()
321351

322352

@@ -438,7 +468,7 @@ async def agentic_chat_completion(
438468
message, content = _extract_message_content(data)
439469

440470
if content is not None:
441-
content = _strip_think_tags(content)
471+
content = _strip_llm_artifacts(content)
442472
if content:
443473
return {
444474
"content": content,
@@ -661,7 +691,7 @@ async def chat_completion(
661691
"The LLM backend returned empty content even after fulfilling tool calls."
662692
)
663693

664-
content = _strip_think_tags(content)
694+
content = _strip_llm_artifacts(content)
665695
if not content:
666696
raise UpstreamServiceError("The LLM backend returned empty content.")
667697

0 commit comments

Comments
 (0)