Skip to content

fix(sanitize): strip GLM/DeepSeek special tokens from user-facing text#40573

Closed
imwyvern wants to merge 4 commits intoopenclaw:mainfrom
imwyvern:fix/glm-special-token-sanitize
Closed

fix(sanitize): strip GLM/DeepSeek special tokens from user-facing text#40573
imwyvern wants to merge 4 commits intoopenclaw:mainfrom
imwyvern:fix/glm-special-token-sanitize

Conversation

@imwyvern
Copy link
Copy Markdown
Contributor

@imwyvern imwyvern commented Mar 9, 2026

Problem

Models like GLM-5 and DeepSeek leak special delimiter tokens (e.g. <|tool_call_result_begin|>, <|tool_list_end|>, <|user|>, <|assistant|>, <|observation|>) into assistant output. These tokens should never reach end users.

Solution

Add MODEL_SPECIAL_TOKEN_RE (/<\|[^|]*\|>/g) to stripFinalTagsFromText() which is called by sanitizeUserFacingText() — the central sanitization function for all outbound messages across all surfaces.

Changes

  • src/agents/pi-embedded-helpers/errors.ts: Added regex and applied it in stripFinalTagsFromText()
  • src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts: Added 5 test cases covering GLM tokens, DeepSeek tokens, multiple tokens, and ensuring normal angle brackets are preserved

Testing

All 66 tests pass (vitest run).

Fixes #40020

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: XS labels Mar 9, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR adds a targeted regex (/<\|[^|]*\|>/g) to stripFinalTagsFromText() in order to strip GLM/DeepSeek-style special delimiter tokens (e.g. <|tool_call_result_begin|>, <|user|>, <|assistant|>) from all user-facing text routed through sanitizeUserFacingText(). The change is minimal, well-motivated, and correctly implemented.

  • The regex is safe from ReDoS: [^|]* cannot create ambiguous paths for the engine, so matching is always linear.
  • Using String.prototype.replace() with a module-level global regex is safe — replace() always resets lastIndex before scanning, so there is no statefulness hazard.
  • Five new test cases adequately cover the happy paths (GLM tokens, DeepSeek tokens, multiple tokens) and the negative cases (normal angle brackets and HTML-like tags are preserved).
  • Minor style note: the helper function stripFinalTagsFromText now serves a broader purpose than its name implies; a rename or an inline comment could help future readers understand its full scope.

Confidence Score: 5/5

  • This PR is safe to merge — it adds a well-scoped regex strip with no side-effects on existing behaviour.
  • The change is two lines of production code (a new regex constant and one additional .replace() call) backed by five targeted test cases. The regex is not susceptible to ReDoS, is used only via .replace() (no lastIndex hazard), and is confirmed not to touch normal angle brackets or HTML tags. No existing tests are modified, and all 66 tests reportedly pass.
  • No files require special attention.

Last reviewed commit: d9d5054

Comment on lines +406 to +409
if (!text) {
return text;
}
return text.replace(FINAL_TAG_RE, "");
return text.replace(FINAL_TAG_RE, "").replace(MODEL_SPECIAL_TOKEN_RE, "");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function name no longer reflects full scope

stripFinalTagsFromText now strips both <final> tags and <|...|> model special tokens. The name stays accurate enough for internal use, but it may mislead future contributors who see a call to this function and assume it only deals with <final> tags. Consider renaming to something like stripSpecialTokensFromText or adding a brief comment at the call site to document the expanded responsibility.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-helpers/errors.ts
Line: 406-409

Comment:
**Function name no longer reflects full scope**

`stripFinalTagsFromText` now strips both `<final>` tags and `<|...|>` model special tokens. The name stays accurate enough for internal use, but it may mislead future contributors who see a call to this function and assume it only deals with `<final>` tags. Consider renaming to something like `stripSpecialTokensFromText` or adding a brief comment at the call site to document the expanded responsibility.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9d5054fa0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

*
* @see https://github.com/openclaw/openclaw/issues/40020
*/
const MODEL_SPECIAL_TOKEN_RE = /<\|[^|]*\|>/g;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow special-token regex to real model delimiters

MODEL_SPECIAL_TOKEN_RE now removes any <|...|> span, not just leaked model control tokens, so sanitizeUserFacingText() will silently delete legitimate content that uses <|/|> syntax (for example, code examples discussing F#/Elm pipeline operators or literal token strings). Because this sanitizer is applied to normal assistant output paths, users can get truncated or corrupted answers even when no model delimiter leak occurred.

Useful? React with 👍 / 👎.

@imwyvern
Copy link
Copy Markdown
Contributor Author

imwyvern commented Mar 9, 2026

Thanks for the review feedback! Pushed fixes in 744aee2ec:

  1. Narrowed regex — Replaced the broad <|...|> wildcard with an explicit allowlist of known model special tokens (endoftext, im_start, im_end, user, assistant, system, observation, EOT, begin▁of▁sentence, end▁of▁sentence, tool_call). This prevents false positives on legitimate content like F#/Elm pipeline operators.

  2. Renamed functionstripFinalTagsFromTextstripSpecialMarkupFromText to reflect its expanded responsibility. Updated all call sites.

All tests pass. ✅

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 744aee2ec0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return text;
}
return text.replace(FINAL_TAG_RE, "");
return text.replace(FINAL_TAG_RE, "").replace(MODEL_SPECIAL_TOKEN_RE, "");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid stripping token literals from valid assistant explanations

The new global replacement in stripSpecialMarkupFromText removes every allowlisted token literal from all outbound text, so normal responses that intentionally mention these strings (for example, explaining what <|assistant|> means) are silently corrupted. Because sanitizeUserFacingText runs on regular non-error reply paths, this introduces user-visible truncation even when no delimiter leak occurred; token stripping should be scoped to leak-like contexts rather than unconditional text replacement.

Useful? React with 👍 / 👎.

@imwyvern
Copy link
Copy Markdown
Contributor Author

imwyvern commented Mar 9, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 744aee2ec0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* @see https://github.com/openclaw/openclaw/issues/40020
*/
const MODEL_SPECIAL_TOKEN_RE =
/<\|(?:endoftext|im_start|im_end|user|assistant|system|observation|EOT|begin▁of▁sentence|end▁of▁sentence|tool_list_start|tool_list_end|tool_call_start|tool_call_end|tool_call_result_begin|tool_call_result_end|tool_content_start|tool_content_end)\|>/g;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match full-width DeepSeek special tokens in sanitizer

MODEL_SPECIAL_TOKEN_RE only matches ASCII-delimited lowercase tokens (e.g. <|assistant|>), but DeepSeek commonly emits control tokens with full-width delimiters and role-case variants such as <|Assistant|> / <|begin▁of▁sentence|>. In that scenario sanitizeUserFacingText() will still leak raw model delimiters to end users, so the core regression this commit addresses remains unfixed for real DeepSeek outputs.

Useful? React with 👍 / 👎.

wes added 3 commits March 9, 2026 19:21
Models like GLM-5 and DeepSeek leak special delimiter tokens
(e.g. <|tool_call_result_begin|>, <|user|>, <|observation|>) into
assistant output. These tokens should never reach users.

Add MODEL_SPECIAL_TOKEN_RE to stripFinalTagsFromText() which is called
by sanitizeUserFacingText() — the central sanitization function for all
outbound messages.

Fixes openclaw#40020
…tion

Address review feedback:

1. MODEL_SPECIAL_TOKEN_RE: Replace wildcard `<|[^|]*|>` with explicit
   token allowlist to avoid false positives on legitimate content
   (e.g. F# pipe operators, code discussions about tokens).

2. Rename stripFinalTagsFromText → stripSpecialMarkupFromText to
   accurately reflect the function now strips both <final> tags
   and model special tokens.
DeepSeek emits special tokens with full-width pipe (U+FF5C) delimiters
like <|Assistant|> and <|begin▁of▁sentence|>. Updated regex to
match both ASCII | and full-width |, and added case-insensitive flag.

Added tests for full-width and mixed-case token variants.
@imwyvern imwyvern force-pushed the fix/glm-special-token-sanitize branch from 744aee2 to 52ac357 Compare March 10, 2026 02:23
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52ac357f51

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return text;
}
return text.replace(FINAL_TAG_RE, "");
return text.replace(FINAL_TAG_RE, "").replace(MODEL_SPECIAL_TOKEN_RE, "");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate special-token stripping behind leak detection

stripSpecialMarkupFromText now removes allowlisted token literals from every response, but sanitizeUserFacingText is also used on normal non-error assistant output paths (for example src/auto-reply/reply/normalize-reply.ts), so valid explanations that intentionally include strings like <|assistant|> or <|Assistant|> are silently corrupted. This is a user-visible regression introduced by the new unconditional replacement and should be limited to leak-like contexts instead of applied to all outbound text.

Useful? React with 👍 / 👎.

Codex review feedback: sanitizeUserFacingText strips model tokens
from ALL text, which could mangle normal assistant explanations.

Now stripSpecialMarkupFromText only applies MODEL_SPECIAL_TOKEN_RE
when the text actually contains matching control tokens. Also reset
lastIndex after .test() since the regex uses /g flag.

FINAL_TAG_RE still runs unconditionally since <final> tags are
never valid user-facing content.
@imwyvern
Copy link
Copy Markdown
Contributor Author

Addressed Codex review feedback in bd2277f:

Gated special-token stripping behind leak detection: stripSpecialMarkupFromText now only applies MODEL_SPECIAL_TOKEN_RE when the text actually contains matching control tokens (via .test() guard). This prevents accidentally mangling normal assistant text that mentions token names in prose. Also resets lastIndex after .test() since the regex uses the /g flag.

FINAL_TAG_RE still runs unconditionally since <final> tags are never valid user-facing content.

Re: full-width DeepSeek tokens — already addressed in the prior commit with the [||] character class in the regex.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd2277f4d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (MODEL_SPECIAL_TOKEN_RE.test(result)) {
// Reset lastIndex since the regex has the /g flag.
MODEL_SPECIAL_TOKEN_RE.lastIndex = 0;
result = result.replace(MODEL_SPECIAL_TOKEN_RE, "");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve token literals in normal assistant text

stripSpecialMarkupFromText removes every allowlisted token literal anywhere in the message, so ordinary responses that intentionally mention delimiters (for example, explaining <|assistant|> syntax) are silently corrupted. Because sanitizeUserFacingText is used on non-error output paths too, this affects normal user-visible replies rather than only leak cleanup; token stripping should be scoped to leak-like contexts instead of unconditional replacement.

Useful? React with 👍 / 👎.

@imwyvern
Copy link
Copy Markdown
Contributor Author

👋 @obviyus Quick ping — this strips leaked GLM/DeepSeek special tokens (<|tool_call_result_begin|> etc.) from user-facing text. Small targeted fix with leak-detection guard. Tests pass.

@odysseus0
Copy link
Copy Markdown
Contributor

Thanks for the fix @imwyvern! I've reworked this into #42173 with a few architectural changes:

  1. Generic pattern instead of allowlist<[||]...[||]> catches all model control tokens (they all follow this convention), so we don't need to maintain an explicit list as new models add tokens
  2. Moved to pi-embedded-utils.ts — next to stripMinimaxToolCallXml() which handles the same class of problem (provider-specific text leaks), keeping errors.ts focused on error rewriting
  3. Added session replay pathsessions-helpers.ts has a parallel sanitization chain that also needed the fix

Your original work and investigation on #40020 made this possible — you're credited as co-author on the commit. 🙏

Superseded by #42173.

@odysseus0
Copy link
Copy Markdown
Contributor

Superseded by #42173

odysseus0 added a commit that referenced this pull request Mar 10, 2026
Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes #40020
Supersedes #40573

Co-authored-by: imwyvern <[email protected]>
@odysseus0
Copy link
Copy Markdown
Contributor

Superseded by #42173 (merged).

@odysseus0 odysseus0 closed this Mar 10, 2026
@odysseus0
Copy link
Copy Markdown
Contributor

Sorry about the separate PR — I had permission issues pushing to your branch when I was reworking it. You're credited as co-author on the merged commit. Next time I'll push directly to your branch. Thanks again for the fix!

Moshiii pushed a commit to Moshiii/openclaw that referenced this pull request Mar 11, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
Moshiii pushed a commit to Moshiii/openclaw that referenced this pull request Mar 11, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
frankekn pushed a commit to MoerAI/openclaw that referenced this pull request Mar 11, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
frankekn pushed a commit to Effet/openclaw that referenced this pull request Mar 11, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
frankekn pushed a commit to ImLukeF/openclaw that referenced this pull request Mar 11, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
dominicnunez pushed a commit to dominicnunez/openclaw that referenced this pull request Mar 11, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
dhoman pushed a commit to dhoman/chrono-claw that referenced this pull request Mar 11, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
ahelpercn pushed a commit to ahelpercn/openclaw that referenced this pull request Mar 12, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
Ruijie-Ysp pushed a commit to Ruijie-Ysp/clawdbot that referenced this pull request Mar 12, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
Interstellar-code pushed a commit to Interstellar-code/operator1 that referenced this pull request Mar 16, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
(cherry picked from commit 309162f)
senw-developers pushed a commit to senw-developers/va-openclaw that referenced this pull request Mar 17, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
…w#42173)

Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.

Closes openclaw#40020
Supersedes openclaw#40573

Co-authored-by: imwyvern <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bug: Sanitization — Tool Call Tags Escape to Final Response

2 participants