fix(sanitize): strip GLM/DeepSeek special tokens from user-facing text#40573
fix(sanitize): strip GLM/DeepSeek special tokens from user-facing text#40573imwyvern wants to merge 4 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR adds a targeted regex (
Confidence Score: 5/5
Last reviewed commit: d9d5054 |
| if (!text) { | ||
| return text; | ||
| } | ||
| return text.replace(FINAL_TAG_RE, ""); | ||
| return text.replace(FINAL_TAG_RE, "").replace(MODEL_SPECIAL_TOKEN_RE, ""); |
There was a problem hiding this 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.
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!
There was a problem hiding this comment.
💡 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; |
There was a problem hiding this comment.
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 👍 / 👎.
|
Thanks for the review feedback! Pushed fixes in
All tests pass. ✅ |
There was a problem hiding this comment.
💡 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, ""); |
There was a problem hiding this comment.
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 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 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; |
There was a problem hiding this comment.
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 👍 / 👎.
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.
744aee2 to
52ac357
Compare
There was a problem hiding this comment.
💡 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, ""); |
There was a problem hiding this comment.
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.
|
Addressed Codex review feedback in bd2277f: Gated special-token stripping behind leak detection:
Re: full-width DeepSeek tokens — already addressed in the prior commit with the |
There was a problem hiding this comment.
💡 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, ""); |
There was a problem hiding this comment.
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 👍 / 👎.
|
👋 @obviyus Quick ping — this strips leaked GLM/DeepSeek special tokens ( |
|
Thanks for the fix @imwyvern! I've reworked this into #42173 with a few architectural changes:
Your original work and investigation on #40020 made this possible — you're credited as co-author on the commit. 🙏 Superseded by #42173. |
|
Superseded by #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 #40020 Supersedes #40573 Co-authored-by: imwyvern <[email protected]>
|
Superseded by #42173 (merged). |
|
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! |
…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]>
…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]>
…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]>
…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]>
…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]>
…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]>
…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]>
…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]>
…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]>
…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)
…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]>
…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]>
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) tostripFinalTagsFromText()which is called bysanitizeUserFacingText()— the central sanitization function for all outbound messages across all surfaces.Changes
src/agents/pi-embedded-helpers/errors.ts: Added regex and applied it instripFinalTagsFromText()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 preservedTesting
All 66 tests pass (
vitest run).Fixes #40020