Skip to content

[Bug]: Orphaned </think> closing tags leak into user-facing responses #4285

@arasovic

Description

@arasovic

Bug Description

Some models (e.g. Kimi K2.5 on Alibaba's OpenAI-compatible endpoint) emit reasoning as plain text followed by a closing </think> tag without a matching opening <think>. _strip_think_blocks() uses paired-tag regexes (<think>.*?</think>) that require both tags — orphaned </think> passes through unmatched and appears in user-facing responses.

Steps to Reproduce

  1. Configure Kimi K2.5 via Alibaba's OpenAI-compatible endpoint (/v1)
  2. Run gateway via systemd (hermes gateway)
  3. Send a message via Telegram that triggers model reasoning (e.g. ask about available models)
  4. Model outputs reasoning as plain text, then </think>, then the actual response

Expected Behavior

No raw XML tags in user-facing responses. _strip_think_blocks() should remove orphaned tags.

Actual Behavior

</think> appears in the Telegram message:

The user wants to see available models. I can list them via the gateway command. However I don't have permission to run this...You're currently using kimi-k2.5.

Verified from session data — reasoning and reasoning_content fields are both empty. The model does not use structured reasoning; the </think> appears as literal text in assistant message content.

Affected Component

Agent Core (conversation loop, context compression, memory), Gateway (Telegram/Discord/Slack/WhatsApp)

Messaging Platform (if gateway-related)

Telegram

Operating System

Ubuntu 24.04

Python Version

Python 3.12

Hermes Version

Hermes v0.6.0

Relevant Logs / Traceback

No traceback. The orphaned tag appears as literal text in the assistant message content. Session log excerpt:

...I can list them via the gateway command. However I don't have permission...</think>You're currently using **kimi-k2.5**.

Message metadata: reasoning=None, reasoning_content=None

Root Cause Analysis (optional)

_strip_think_blocks() has four paired-tag regexes:

  • re.sub(r'<think>.*?</think>', ...)
  • re.sub(r'<thinking>.*?</thinking>', ...)
  • re.sub(r'<reasoning>.*?</reasoning>', ...)
  • re.sub(r'<REASONING_SCRATCHPAD>.*?</REASONING_SCRATCHPAD>', ...)

All require matching open+close pairs. When a model writes </think> without <think>, none of these match.

Proposed Fix (optional)

Add a catch-all regex after the existing paired-block removal:

content = re.sub(r'</?(?:think|thinking|reasoning|REASONING_SCRATCHPAD)>\s*', '', content, flags=re.IGNORECASE)

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions