Files
OmniRoute/open-sse/utils/zeroWidth.ts
Paco Cartones 0ec7504024 fix(sse): preserve ZWNJ and ZWJ in sanitized responses (#12359)
The response de-obfuscation stripped the whole U+200B..U+200D range, so Persian/Kurdish half-spaces (U+200C), Arabic/Indic shaping and emoji ZWJ sequences (U+200D) were deleted from every assistant response — text, reasoning and tool-call arguments, streaming and non-streaming, every provider: ارائه‌دهنده came back as ارائهدهنده.

The request side only ever inserts a U+200D between two ASCII word characters, so the new stripObfuscationZeroWidth() removes a joiner only there, or at a string edge next to one so a word split across streaming deltas is still cleaned; U+200B and U+FEFF keep their unconditional removal. All seven copies of the old regex now go through the helper.

Validated in a combined worktree with all 25 PRs of this batch boarded together: typecheck:core clean, 443/443 node-runner tests plus 14/14 vitest across every test file the batch touches, and check-changelog-integrity, check:cycles (418 files), check:provider-consistency (272 REGISTRY entries, 355 canonical providers), check:docs-counts, check:docs-sync (42 locales) and check-file-size all green.

Thanks @pacocartones.
2026-09-02 03:15:19 -03:00

39 lines
1.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Zero-width character cleanup for model output.
*
* The request side obfuscates configurable agent words by inserting a
* U+200D ZERO WIDTH JOINER after their first letter (`o\u200Dpencode`, see
* `services/claudeCodeObfuscation.ts` and `services/systemTransforms.ts`), and
* the response side removes zero-width code points again so an echoed word is
* not corrupted. Removing every U+200B..U+200D also deletes U+200C ZERO WIDTH
* NON-JOINER and U+200D where they belong to the text itself: the Persian and
* Kurdish half-space (ارائه\u200Cدهنده, می\u200Cروم, کتاب\u200Cها), Arabic and Indic shaping,
* and emoji ZWJ sequences (👨\u200D👩\u200D👧). See #12186.
*
* The obfuscator only ever places a joiner between two ASCII word characters,
* so a joiner is removed only there. A joiner touching the edge of the string
* is removed as well when its other neighbour is an ASCII word character, so
* an obfuscated word split across streaming deltas (`o\u200D` + `pencode`)
* is still cleaned. A joiner next to non-ASCII text, and a delta that consists
* of nothing but a joiner (an emoji sequence split by the tokenizer), pass
* through untouched.
*
* U+200B ZERO WIDTH SPACE and U+FEFF have no shaping role and keep the
* unconditional removal they always had.
*/
const ANY_ZERO_WIDTH = /[\u200B-\u200D\uFEFF]/;
const ZERO_WIDTH_SPACE_OR_BOM = /[\u200B\uFEFF]/g;
const JOINER_BETWEEN_ASCII_WORD_CHARS =
/(?<=[A-Za-z0-9_])[\u200C\u200D]+(?=[A-Za-z0-9_]|$)|^[\u200C\u200D]+(?=[A-Za-z0-9_])/g;
/**
* Strip the zero-width markers used for agent-word obfuscation while keeping
* ZWNJ/ZWJ that are part of the text (Persian half-space, Arabic/Indic
* shaping, emoji sequences).
*/
export function stripObfuscationZeroWidth(text: string): string {
if (!text || !ANY_ZERO_WIDTH.test(text)) return text;
return text.replace(ZERO_WIDTH_SPACE_OR_BOM, "").replace(JOINER_BETWEEN_ASCII_WORD_CHARS, "");
}