diff --git a/docs/prompt-compatibility.md b/docs/prompt-compatibility.md
index fb03021..6fcc1ab 100644
--- a/docs/prompt-compatibility.md
+++ b/docs/prompt-compatibility.md
@@ -168,7 +168,7 @@ OpenAI Chat / Responses 在标准化后、current input file 之前,会默认
4. 把这整段内容并入 system prompt。
工具调用正例现在优先示范全角分隔符 DSML 风格:`<|DSML|tool_calls>` → `<|DSML|invoke name="...">` → `<|DSML|parameter name="...">`。
-兼容层仍接受旧式纯 `` wrapper,并会容错若干 DSML 标签变体,包括短横线形式 `` / `` / ``、下划线形式 `` / `` / ``,以及其他前缀分隔形态如 `` / `` / ``;标签壳扫描还会把全角 ASCII 漂移归一化,例如 `<dSML|tool_calls>` 与全角 `>` 结束符,也会容错 CJK 尖括号、全角感叹号或顿号分隔符、弯引号属性值、PascalCase 本地名和属性尾部分隔符漂移,例如 `...〈/DSM|parameter〉`、`<!DSML!invoke name=“Bash”>`、`<、DSML、tool_calls>`、``、``。更一般地,Go / Node tag 扫描以固定本地标签名 `tool_calls` / `invoke` / `parameter` 为准,标签名前或标签名后的非结构性协议分隔符都会在解析入口剥离,例如 ``、`` 这类控制符或非 ASCII 分隔符漂移也会归一化回现有 XML 标签后继续走同一套 parser;结构性字符如 `<` / `>` / `/` / `=` / 引号、空白和 ASCII 字母数字不会被当作这类分隔符。CDATA 开头也使用同一类扫描式容错,`` wrapper,并会容错若干 DSML 标签变体,包括短横线形式 `` / `` / ``、下划线形式 `` / `` / ``,以及其他前缀分隔形态如 `` / `` / ``;标签壳扫描还会把全角 ASCII 漂移归一化,例如 `<dSML|tool_calls>` 与全角 `>` 结束符,也会容错 CJK 尖括号、全角感叹号或顿号分隔符、弯引号属性值、PascalCase 本地名和属性尾部分隔符漂移,例如 `...〈/DSM|parameter〉`、`<!DSML!invoke name=“Bash”>`、`<、DSML、tool_calls>`、``、``。更一般地,Go / Node tag 扫描以固定本地标签名 `tool_calls` / `invoke` / `parameter` 为准,标签名前或标签名后的非结构性协议分隔符都会在解析入口剥离,例如 ``、`` 这类控制符或非 ASCII 分隔符漂移也会归一化回现有 XML 标签后继续走同一套 parser;结构性字符如 `<` / `>` / `/` / `=` / 引号、空白和 ASCII 字母数字不会被当作这类分隔符。进入现有 DSML rewrite / XML parse 之前,Go / Node 还会先对“已经识别成工具标签壳的 candidate span”做一次窄 canonicalization:只折叠 wrapper / `invoke` / `parameter` / `name` / `CDATA` / `DSML` 及其壳层分隔符里的 confusable 字符,清理零宽 / BOM / 控制类干扰,并把引号、空白、dash / underscore 变体等统一回可解析的工具语法。这个阶段不会广义改写普通正文、参数内容、CDATA 里的示例文本或其他非工具 XML。CDATA 开头也使用同一类扫描式容错,`...` 子节点表示;当某个参数体只包含 item 子节点时,Go / Node 解析器会把它还原成数组,避免 `questions` / `options` 这类 schema 中要求 array 的参数被误解析成 `{ "item": ... }` 对象。除此之外,解析器还会回收一些更松散的列表写法,例如 JSON array 字面量或逗号分隔的 JSON 项序列,只要它们足够明确;但 `- ` 仍然是首选形态。若模型把完整结构化 XML fragment 误包进 CDATA,兼容层会在保护 `content` / `command` 等原文字段的前提下,尝试把非原文字段中的 CDATA XML fragment 还原成 object / array。不过,如果 CDATA 只是单个平面的 XML/HTML 标签,例如 `urgent` 这种行内标记,兼容层会保留原始字符串,不会强行升成 object / array;只有明显表示结构的 CDATA 片段,例如多兄弟节点、嵌套子节点或 `item` 列表,才会触发结构化恢复。对 `command` / `content` 等长文本参数,CDATA 内部的 Markdown fenced DSML / XML 示例会作为原文保护;示例里的 `]]>` 或 `
` 不会截断外层工具调用,解析器会继续等待围栏外真正的参数 / wrapper 结束标签。
Go 侧读取 DeepSeek SSE 时不再依赖 `bufio.Scanner` 的固定 2MiB 单行上限;当写文件类工具把很长的 `content` 放在单个 `data:` 行里返回时,非流式收集、流式解析和 auto-continue 透传都会保留完整行,再进入同一套工具解析与序列化流程。
在 assistant 最终回包阶段,如果某个 tool 参数在声明 schema 中明确是 `string`,兼容层会在把解析后的 `tool_calls` / `function_call` 重新序列化成 OpenAI / Responses / Claude 可见参数前,递归把该路径上的 number / bool / object / array 统一转成字符串;其中 object / array 会压成紧凑 JSON 字符串。这个保护只对 schema 明确声明为 string 的路径生效,不会改写本来就是 `number` / `boolean` / `object` / `array` 的参数。这样可以兼容 DeepSeek 输出了结构化片段、但上游客户端工具 schema 又严格要求字符串参数的场景(例如 `content`、`prompt`、`path`、`taskId` 等)。
@@ -225,7 +225,7 @@ assistant 历史 `tool_calls` 不会保留成 OpenAI 原生 JSON,而会转成
如果客户端历史里没有结构化 `tool_calls` 字段、却把一个可独立解析的 assistant 工具块放进了普通 `content`,兼容层会在写入后续 prompt 前先按工具调用解析它,再重渲染为规范 DSML 历史外壳。这样可以避免一次 malformed 工具块未被结构化保存后,作为普通 assistant 文本回灌,继续污染后续模型的 few-shot 工具格式。
解析层同时兼容旧式纯 XML 形态:`` / `` / ``。两者都会先归一到现有 XML 解析语义;其他旧格式都会作为普通文本保留,不会作为可执行调用语法。
-例外是 parser 会对一个非常窄的模型失误做修复:如果 assistant 输出了 `` ... ``(或 DSML 对应标签),但漏掉最前面的 opening wrapper,解析阶段会补回 wrapper 后再尝试识别。
+例外是 parser 会对一个非常窄的模型失误做修复:如果 assistant 输出了 `` ... ``(或 DSML 对应标签),但漏掉最前面的 opening wrapper,解析阶段会在 wrapper-confidence 足够高时补回 wrapper 后再尝试识别。这里的 wrapper-confidence 指 scanner 已经识别出白名单工具壳结构,剩余失败只像壳层结构漂移,而不是语义上接近但不在白名单内的 near-miss 标签名。修复成功时,wrapper 后面的 suffix prose 会继续保留在可见文本里;修复失败时,该块仍按普通文本处理。
这件事很重要,因为它决定了:
diff --git a/docs/toolcall-semantics.md b/docs/toolcall-semantics.md
index 4deb80d..598eb47 100644
--- a/docs/toolcall-semantics.md
+++ b/docs/toolcall-semantics.md
@@ -39,8 +39,11 @@
兼容修复:
- 如果模型漏掉 opening wrapper,但后面仍输出了一个或多个 invoke 并以 closing wrapper 收尾,Go 解析链路会在解析前补回缺失的 opening wrapper。
+- 在进入现有 DSML rewrite / XML parse 之前,Go / Node 都会先做一次非常窄的 candidate-span canonicalization:只处理已经被 scanner 识别为工具标签壳的 wrapper / `invoke` / `parameter` / `name` / `CDATA` / `DSML` 及其结构分隔符;这里会移除零宽 / BOM / 控制类干扰字符,并把 `<`、`>`、`/`、`|`、`=`、引号、Unicode 空白、常见 dash / underscore 变体这类工具语法外壳符号折回 ASCII 语义。
- Go / Node 解析层不再枚举每一种 DSML typo。它以固定本地标签名 `tool_calls` / `invoke` / `parameter` 为准,把标签名前的任意协议前缀壳视为可容忍噪声,并继续兼容管道符 `|` / `|`、全角感叹号 `!`、顿号 `、`、空白、重复 leading `<`、可视控制符 `␂`、原始 STX `\x02`、非 ASCII 分隔符、CJK 尖括号 `〈` / `〉`、弯引号属性值、PascalCase 本地名等漂移。例如 ``、`<<|DSML|tool_calls>`、`<|DSML tool_calls>`、``、``、`<`、``、``、`...〈/DSM|tool_calls〉`、`<!DSML!tool_calls>...<!/DSML!tool_calls>`、`<、DSML、tool_calls>...<、/DSML、tool_calls>` 都会归一化;相似但非固定标签名(如 `tool_calls_extra` / `ToolCallsExtra`)仍按普通文本处理。
+- 这个 candidate-span canonicalization 不会对普通 prose、参数正文、CDATA 内容或嵌套的非工具 XML 做广义 Unicode 归一化。也就是说,参数里的示例 ``、普通聊天文本里的 confusable 单词、或其他非工具壳 XML 片段都保持原样;只有真正落在工具标签壳上的 whitelist 关键字和结构符号会被折叠。
- 如果模型在固定工具标签名后多输出一个非结构性分隔符,例如 `<|DSML|tool_calls|` / `<|DSML|invoke|` / `<|DSML|parameter|` / ``,或在带属性标签的结束符前多输出一个尾部分隔符(如 ``),兼容层会把这个尾部分隔符当作异常标签终止符并补齐或归一化;如果后面已经有 `>` / `〉`,也会消费这个多余分隔符后再归一化。结构性字符如 `<` / `>` / `/` / `=` / 引号、空白和 ASCII 字母数字不会被当作这类分隔符。
+- “缺失 opening wrapper”的修复只会在 wrapper-confidence 足够高时触发:scanner 必须已经识别出白名单工具壳结构(wrapper / invoke / parameter / `name=` 等),且剩余失败看起来只是壳层结构问题。相似但不在白名单内的 near-miss 标签名,或缺少足够 wrapper 证据的 malformed 片段,仍会按普通文本透传。
- 这是一个针对常见模型失误的窄修复,不改变推荐输出格式;prompt 仍要求模型直接输出完整 DSML 外壳。
- 裸 `` / `` 不会被当成“已支持的工具语法”;只有 `tool_calls` wrapper 或可修复的缺失 opening wrapper 才会进入工具调用路径。
@@ -58,6 +61,7 @@
- 如果流里直接从 invoke 开始,但后面补上了 closing wrapper,Go 流式筛分也会按缺失 opening wrapper 的修复路径尝试恢复
- 已识别成功的工具调用不会再次回流到普通文本
- 不符合新格式的块不会执行,并继续按原样文本透传
+- 如果一个 confusable / 漂移过的工具壳在 candidate-span canonicalization + repair 后仍能形成有效工具调用,wrapper 后面的 suffix prose 会继续按普通文本输出;如果 canonicalization 后仍不满足 wrapper-confidence 或 XML 语义,整块就作为普通文本释放,不会半吞半漏。
- fenced code block(反引号 `` ``` `` 和波浪线 `~~~`)中的 XML 示例始终按普通文本处理
- 支持嵌套围栏(如 4 反引号嵌套 3 反引号)和 CDATA 内围栏保护
- 对 `command` / `content` 等长文本参数,CDATA 内部如果包含 Markdown fenced DSML / XML 示例,即使示例里出现 `]]>` / `` 这类看起来像外层结束标签的片段,也会继续按参数原文保留,直到真正位于围栏外的外层结束标签
diff --git a/internal/httpapi/openai/leaked_output_sanitize_test.go b/internal/httpapi/openai/leaked_output_sanitize_test.go
index acaf720..3b2884b 100644
--- a/internal/httpapi/openai/leaked_output_sanitize_test.go
+++ b/internal/httpapi/openai/leaked_output_sanitize_test.go
@@ -34,6 +34,14 @@ func TestSanitizeLeakedOutputRemovesThinkAndBosMarkers(t *testing.T) {
}
}
+func TestSanitizeLeakedOutputRemovesThoughtMarkers(t *testing.T) {
+ raw := "A<|▁of▁thought|>B<| of_thought |>C<| begin_of_thought |>D<| end_of_thought |>E"
+ got := sanitizeLeakedOutput(raw)
+ if got != "ABCDE" {
+ t.Fatalf("unexpected sanitize result for leaked thought markers: %q", got)
+ }
+}
+
func TestSanitizeLeakedOutputRemovesDanglingThinkBlock(t *testing.T) {
raw := "Answer prefixinternal reasoning that never closes"
got := sanitizeLeakedOutput(raw)
diff --git a/internal/httpapi/openai/shared/leaked_output_sanitize.go b/internal/httpapi/openai/shared/leaked_output_sanitize.go
index 5e54637..b45a3ac 100644
--- a/internal/httpapi/openai/shared/leaked_output_sanitize.go
+++ b/internal/httpapi/openai/shared/leaked_output_sanitize.go
@@ -18,10 +18,16 @@ var leakedThinkTagPattern = regexp.MustCompile(`(?is)?\s*think\s*>`)
// - U+2581 variant: <|begin▁of▁sentence|>
var leakedBOSMarkerPattern = regexp.MustCompile(`(?i)<[|\|]\s*begin[_▁]of[_▁]sentence\s*[|\|]>`)
+// leakedThoughtMarkerPattern matches leaked thought control markers in both
+// explicit and compact forms:
+// - ASCII underscore: <| of_thought |>, <| begin_of_thought |>
+// - U+2581 variant: <|▁of▁thought|>, <|begin▁of▁thought|>
+var leakedThoughtMarkerPattern = regexp.MustCompile(`(?i)<[|\|]\s*(?:begin[_▁])?[_▁]*of[_▁]thought\s*[|\|]>`)
+
// leakedMetaMarkerPattern matches the remaining DeepSeek special tokens in BOTH forms:
// - ASCII underscore: <|end_of_sentence|>, <|end_of_toolresults|>, <|end_of_instructions|>
// - U+2581 variant: <|end▁of▁sentence|>, <|end▁of▁toolresults|>, <|end▁of▁instructions|>
-var leakedMetaMarkerPattern = regexp.MustCompile(`(?i)<[|\|]\s*(?:assistant|tool|end[_▁]of[_▁]sentence|end[_▁]of[_▁]thinking|end[_▁]of[_▁]toolresults|end[_▁]of[_▁]instructions)\s*[|\|]>`)
+var leakedMetaMarkerPattern = regexp.MustCompile(`(?i)<[|\|]\s*(?:assistant|tool|end[_▁]of[_▁]sentence|end[_▁]of[_▁]thinking|end[_▁]of[_▁]thought|end[_▁]of[_▁]toolresults|end[_▁]of[_▁]instructions)\s*[|\|]>`)
// leakedAgentXMLBlockPatterns catch agent-style XML blocks that leak through
// when the sieve fails to capture them. These are applied only to complete
@@ -48,6 +54,7 @@ func sanitizeLeakedOutput(text string) string {
out = stripDanglingThinkSuffix(out)
out = leakedThinkTagPattern.ReplaceAllString(out, "")
out = leakedBOSMarkerPattern.ReplaceAllString(out, "")
+ out = leakedThoughtMarkerPattern.ReplaceAllString(out, "")
out = leakedMetaMarkerPattern.ReplaceAllString(out, "")
out = stripLeakedToolCallWrapperBlocks(out)
out = sanitizeLeakedAgentXMLBlocks(out)
diff --git a/internal/js/chat-stream/sse_parse_impl.js b/internal/js/chat-stream/sse_parse_impl.js
index 6f5922e..4d9a121 100644
--- a/internal/js/chat-stream/sse_parse_impl.js
+++ b/internal/js/chat-stream/sse_parse_impl.js
@@ -7,6 +7,10 @@ const {
SKIP_EXACT_PATHS,
} = require('../shared/deepseek-constants');
+const LEAKED_BOS_MARKER_PATTERN = /<[||]\s*begin[_▁]of[_▁]sentence\s*[||]>/gi;
+const LEAKED_THOUGHT_MARKER_PATTERN = /<[||]\s*(?:begin[_▁])?[_▁]*of[_▁]thought\s*[||]>/gi;
+const LEAKED_META_MARKER_PATTERN = /<[||]\s*(?:assistant|tool|end[_▁]of[_▁]sentence|end[_▁]of[_▁]thinking|end[_▁]of[_▁]thought|end[_▁]of[_▁]toolresults|end[_▁]of[_▁]instructions)\s*[||]>/gi;
+
function stripThinkTags(text) {
@@ -621,7 +625,11 @@ function stripReferenceMarkersText(text) {
if (!text) {
return text;
}
- return text.replace(/\[(?:citation|reference):\s*\d+\]/gi, '');
+ return text
+ .replace(/\[(?:citation|reference):\s*\d+\]/gi, '')
+ .replace(LEAKED_BOS_MARKER_PATTERN, '')
+ .replace(LEAKED_THOUGHT_MARKER_PATTERN, '')
+ .replace(LEAKED_META_MARKER_PATTERN, '');
}
function asString(v) {
diff --git a/internal/js/helpers/stream-tool-sieve/parse.js b/internal/js/helpers/stream-tool-sieve/parse.js
index f2ba3dc..7a70769 100644
--- a/internal/js/helpers/stream-tool-sieve/parse.js
+++ b/internal/js/helpers/stream-tool-sieve/parse.js
@@ -7,6 +7,9 @@ const {
parseMarkupToolCalls,
stripFencedCodeBlocks,
containsToolCallWrapperSyntaxOutsideIgnored,
+ normalizeDSMLToolCallMarkup,
+ hasRepairableXMLToolCallsWrapper,
+ indexToolCDATAOpen,
sanitizeLooseCDATA,
} = require('./parse_payload');
@@ -37,19 +40,23 @@ function parseToolCalls(text, toolNames) {
function parseToolCallsDetailed(text, toolNames) {
const result = emptyParseResult();
- const normalized = toStringSafe(text);
- if (!normalized) {
+ const raw = toStringSafe(text);
+ if (!raw) {
return result;
}
- result.sawToolCallSyntax = looksLikeToolCallSyntax(normalized);
- if (shouldSkipToolCallParsingForCodeFenceExample(normalized)) {
+ if (shouldSkipToolCallParsingForCodeFenceExample(raw)) {
return result;
}
+ const normalized = normalizeDSMLToolCallMarkup(stripFencedCodeBlocks(raw).trim());
+ if (!normalized.ok || !normalized.text) {
+ return result;
+ }
+ result.sawToolCallSyntax = looksLikeToolCallSyntax(normalized.text) || hasRepairableXMLToolCallsWrapper(normalized.text);
// XML markup parsing only.
- let parsed = parseMarkupToolCalls(normalized);
- if (parsed.length === 0 && normalized.toLowerCase().includes('= 0) {
+ const recovered = sanitizeLooseCDATA(normalized.text);
+ if (recovered !== normalized.text) {
parsed = parseMarkupToolCalls(recovered);
}
}
@@ -70,19 +77,23 @@ function parseStandaloneToolCalls(text, toolNames) {
function parseStandaloneToolCallsDetailed(text, toolNames) {
const result = emptyParseResult();
- const trimmed = toStringSafe(text);
- if (!trimmed) {
+ const raw = toStringSafe(text);
+ if (!raw) {
return result;
}
- result.sawToolCallSyntax = looksLikeToolCallSyntax(trimmed);
- if (shouldSkipToolCallParsingForCodeFenceExample(trimmed)) {
+ if (shouldSkipToolCallParsingForCodeFenceExample(raw)) {
return result;
}
+ const normalized = normalizeDSMLToolCallMarkup(stripFencedCodeBlocks(raw).trim());
+ if (!normalized.ok || !normalized.text) {
+ return result;
+ }
+ result.sawToolCallSyntax = looksLikeToolCallSyntax(normalized.text) || hasRepairableXMLToolCallsWrapper(normalized.text);
// XML markup parsing only.
- let parsed = parseMarkupToolCalls(trimmed);
- if (parsed.length === 0 && trimmed.toLowerCase().includes('= 0) {
+ const recovered = sanitizeLooseCDATA(normalized.text);
+ if (recovered !== normalized.text) {
parsed = parseMarkupToolCalls(recovered);
}
}
diff --git a/internal/js/helpers/stream-tool-sieve/parse_payload.js b/internal/js/helpers/stream-tool-sieve/parse_payload.js
index a24bd62..37c4df6 100644
--- a/internal/js/helpers/stream-tool-sieve/parse_payload.js
+++ b/internal/js/helpers/stream-tool-sieve/parse_payload.js
@@ -2,6 +2,8 @@
const CDATA_PATTERN = /^(?:<|〈)(?:!|!)\[CDATA\[([\s\S]*?)]](?:>|>|〉)$/i;
const XML_ATTR_PATTERN = /\b([a-z0-9_:-]+)\s*=\s*("([^"]*)"|'([^']*)')/gi;
+const XML_TOOL_CALLS_CLOSE_PATTERN = /[<<][\//]tool_calls\s*[>>]/gi;
+const XML_INVOKE_START_PATTERN = /[<<]invoke\b[^>>]*\bname\s*[==]\s*(?:"([^"]*)"|'([^']*)'|“([^”]*)”|‘([^’]*)’|"([^"]*)"|'([^']*)')/i;
const TOOL_MARKUP_NAMES = [
{ raw: 'tool_calls', canonical: 'tool_calls' },
{ raw: 'tool-calls', canonical: 'tool_calls', dsmlOnly: true },
@@ -88,8 +90,7 @@ function isFenceCloseLine(trimmed, fenceChar, fenceLen) {
}
function cdataStartsBeforeFence(line) {
- const cdataOpen = findNextCDATAOpen(line, 0);
- const cdataIdx = cdataOpen.ok ? cdataOpen.start : -1;
+ const cdataIdx = indexToolCDATAOpen(line, 0);
if (cdataIdx < 0) return false;
const fenceIdx = Math.min(
line.indexOf('```') >= 0 ? line.indexOf('```') : Infinity,
@@ -99,21 +100,28 @@ function cdataStartsBeforeFence(line) {
}
function updateCDATAStateLine(inCDATA, line) {
- const lower = line.toLowerCase();
let pos = 0;
let state = inCDATA;
- while (pos < lower.length) {
+ while (pos < line.length) {
if (state) {
- const cdataEnd = findCDATAEnd(lower, pos);
- const end = cdataEnd.index;
+ let end = -1;
+ let closeLen = 0;
+ for (let i = pos; i < line.length; i += 1) {
+ const foundLen = toolCDATACloseLenAt(line, i);
+ if (foundLen > 0) {
+ end = i;
+ closeLen = foundLen;
+ break;
+ }
+ }
if (end < 0) return true;
- pos = end + cdataEnd.len;
+ pos = end + closeLen;
state = false;
continue;
}
- const start = findNextCDATAOpen(line, pos);
- if (!start.ok) return false;
- pos = start.bodyStart;
+ const start = indexToolCDATAOpen(line, pos);
+ if (start < 0) return false;
+ pos = start + toolCDATAOpenLenAt(line, start);
state = true;
}
return state;
@@ -124,12 +132,20 @@ function parseMarkupToolCalls(text) {
if (!normalized.ok) {
return [];
}
- const raw = normalized.text.trim();
+ let raw = normalized.text.trim();
if (!raw) {
return [];
}
+ let wrappers = findXmlElementBlocks(raw, 'tool_calls');
+ if (wrappers.length === 0 && hasRepairableXMLToolCallsWrapper(raw)) {
+ const repaired = repairMissingXMLToolCallsOpeningWrapper(raw);
+ if (repaired !== raw) {
+ raw = repaired;
+ wrappers = findXmlElementBlocks(raw, 'tool_calls');
+ }
+ }
const out = [];
- for (const wrapper of findXmlElementBlocks(raw, 'tool_calls')) {
+ for (const wrapper of wrappers) {
const body = toStringSafe(wrapper.body);
for (const block of findXmlElementBlocks(body, 'invoke')) {
const parsed = parseMarkupSingleToolCall(block);
@@ -146,12 +162,13 @@ function normalizeDSMLToolCallMarkup(text) {
if (!raw) {
return { text: '', ok: true };
}
- const styles = containsToolMarkupSyntaxOutsideIgnored(raw);
- if (!styles.dsml) {
- return { text: raw, ok: true };
+ const canonicalized = canonicalizeToolCallCandidateSpans(raw);
+ const styles = containsToolMarkupSyntaxOutsideIgnored(canonicalized);
+ if (!styles.dsml && !styles.canonical) {
+ return { text: canonicalized, ok: true };
}
return {
- text: replaceDSMLToolMarkupOutsideIgnored(raw),
+ text: replaceDSMLToolMarkupOutsideIgnored(canonicalized),
ok: true,
};
}
@@ -170,9 +187,8 @@ function containsToolCallWrapperSyntaxOutsideIgnored(text) {
if (!raw) {
return styles;
}
- const lower = raw.toLowerCase();
for (let i = 0; i < raw.length;) {
- const skipped = skipXmlIgnoredSection(lower, i);
+ const skipped = skipXmlIgnoredSection(raw, i);
if (skipped.blocked) {
return styles;
}
@@ -208,7 +224,7 @@ function containsToolMarkupSyntaxOutsideIgnored(text) {
return styles;
}
for (let i = 0; i < raw.length;) {
- const skipped = skipXmlIgnoredSection(raw.toLowerCase(), i);
+ const skipped = skipXmlIgnoredSection(raw, i);
if (skipped.blocked) {
return styles;
}
@@ -239,10 +255,9 @@ function replaceDSMLToolMarkupOutsideIgnored(text) {
if (!raw) {
return '';
}
- const lower = raw.toLowerCase();
let out = '';
for (let i = 0; i < raw.length;) {
- const skipped = skipXmlIgnoredSection(lower, i);
+ const skipped = skipXmlIgnoredSection(raw, i);
if (skipped.blocked) {
out += raw.slice(i);
break;
@@ -254,15 +269,7 @@ function replaceDSMLToolMarkupOutsideIgnored(text) {
}
const tag = scanToolMarkupTagAt(raw, i);
if (tag) {
- if (tag.dsmlLike) {
- const tail = normalizeToolMarkupTagTailForXML(raw.slice(tag.nameEnd, tag.end + 1));
- out += `<${tag.closing ? '/' : ''}${tag.name}${tail}`;
- if (!tail.endsWith('>')) {
- out += '>';
- }
- } else {
- out += raw.slice(tag.start, tag.end + 1);
- }
+ out += `<${tag.closing ? '/' : ''}${tag.name}${raw.slice(tag.nameEnd, tag.end)}>`;
i = tag.end + 1;
continue;
}
@@ -345,7 +352,7 @@ function findXmlStartTagOutsideCDATA(text, tag, from) {
const lower = text.toLowerCase();
const target = `<${tag}`;
for (let i = Math.max(0, from || 0); i < text.length;) {
- const skipped = skipXmlIgnoredSection(lower, i);
+ const skipped = skipXmlIgnoredSection(text, i);
if (skipped.blocked) {
return null;
}
@@ -375,7 +382,7 @@ function findMatchingXmlEndTagOutsideCDATA(text, tag, from) {
const closeTarget = `${tag}`;
let depth = 1;
for (let i = Math.max(0, from || 0); i < text.length;) {
- const skipped = skipXmlIgnoredSection(lower, i);
+ const skipped = skipXmlIgnoredSection(text, i);
if (skipped.blocked) {
return null;
}
@@ -411,18 +418,18 @@ function findMatchingXmlEndTagOutsideCDATA(text, tag, from) {
return null;
}
-function skipXmlIgnoredSection(lower, i) {
- const cdataOpen = matchCDATAOpenAt(lower, i);
- if (cdataOpen.ok) {
- const cdataEnd = findCDATAEnd(lower, cdataOpen.bodyStart);
- const end = cdataEnd.index;
+function skipXmlIgnoredSection(text, i) {
+ const raw = toStringSafe(text);
+ const openLen = toolCDATAOpenLenAt(raw, i);
+ if (openLen > 0) {
+ const end = findToolCDATAEnd(raw, i + openLen);
if (end < 0) {
return { advanced: false, blocked: true, next: i };
}
- return { advanced: true, blocked: false, next: end + cdataEnd.len };
+ return { advanced: true, blocked: false, next: end + toolCDATACloseLenAt(raw, end) };
}
- if (lower.startsWith('', i + '', i + '