Skip to content

Commit b2787a1

Browse files
maweibinclaude
authored andcommitted
fix(text): strip antml:namespaced tool call XML from visible content
When a proxied Anthropic-Messages endpoint degrades a native tool_use block into plain text, the assistant emits antml:namespaced XML tags (<antml:invoke>, <antml:parameter>). These were not recognized by the visible-text sanitizer, leaking tool-call XML into user-visible replies. Added antml: (and mm:) namespace handling to: - TOOL_CALL_QUICK_RE regex for fast pre-check - TOOL_CALL_TAG_NAMES set for tag name recognition - Tag name scanner to accept ':' in tag names - TOOL_CALL_XML_PAYLOAD_START_RE for XML payload detection - shouldDetectXmlPayload to include antml:invoke tags Fixes #97750. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f284ce3 commit b2787a1

2 files changed

Lines changed: 23 additions & 3 deletions

File tree

src/shared/text/assistant-visible-text.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,23 @@ describe("stripToolCallXmlTags", () => {
763763
"Checking. Done.",
764764
);
765765
});
766+
767+
it("strips antml:invoke/parameter tool call XML from visible content", () => {
768+
const input =
769+
'before <antml:invoke name="exec"><antml:parameter name="command">ls</antml:parameter></antml:invoke> after';
770+
expect(stripToolCallXmlTags(input)).toBe("before after");
771+
});
772+
773+
it("strips antml:invoke with function_call payload", () => {
774+
const input =
775+
'prefix <antml:invoke name="exec"><function_call>test</function_call></antml:invoke> suffix';
776+
expect(stripToolCallXmlTags(input)).toBe("prefix suffix");
777+
});
778+
779+
it("does not strip non-namespaced invoke tags (unrelated XML)", () => {
780+
const input = 'keep <invoke name="something">content</invoke> keep';
781+
expect(stripToolCallXmlTags(input)).toBe(input);
782+
});
766783
});
767784

768785
describe("stripMinimaxToolCallXml", () => {

src/shared/text/assistant-visible-text.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const INTERNAL_CHANNEL_TRACE_LINE_RE =
3030
* closing tag, or to end-of-string if the stream was truncated mid-tag.
3131
*/
3232
const TOOL_CALL_QUICK_RE =
33-
/<\s*\/?\s*(?:tool_call|tool_result|function_calls?|function_response|function|tool_calls)\b/i;
33+
/<\s*\/?\s*(?:antml:)?(?:tool_call|tool_result|function_calls?|function_response|function|tool_calls|invoke|parameter)\b/i;
3434
const TOOL_CALL_TAG_NAMES = new Set([
3535
"tool_call",
3636
"tool_result",
@@ -39,11 +39,13 @@ const TOOL_CALL_TAG_NAMES = new Set([
3939
"function_response",
4040
"function",
4141
"tool_calls",
42+
"antml:invoke",
43+
"antml:parameter",
4244
]);
4345
const TOOL_CALL_JSON_PAYLOAD_START_RE =
4446
/^(?:\s+[A-Za-z_:][-A-Za-z0-9_:.]*\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))*\s*(?:\r?\n\s*)?[[{]/;
4547
const TOOL_CALL_XML_PAYLOAD_START_RE =
46-
/^\s*(?:\r?\n\s*)?<(?:function_call|tool_call|function|invoke|parameters?|arguments?)\b/i;
48+
/^\s*(?:\r?\n\s*)?<(?:antml:)?(?:function_call|tool_call|function|invoke|parameters?|arguments?)\b/i;
4749
const NESTED_JSON_TOOL_CALL_PAYLOAD_START_RE = /^\s*(?:\r?\n\s*)?<(?:function_call|tool_call)\b/i;
4850

4951
type ToolCallPayloadKind = "json" | "xml" | null;
@@ -296,7 +298,7 @@ function parseToolCallTagAt(text: string, start: number): ParsedToolCallTag | nu
296298
}
297299

298300
const nameStart = cursor;
299-
while (cursor < text.length && /[A-Za-z_]/.test(text[cursor])) {
301+
while (cursor < text.length && /[A-Za-z_:]/.test(text[cursor])) {
300302
cursor += 1;
301303
}
302304

@@ -404,6 +406,7 @@ export function stripToolCallXmlTags(
404406
const shouldDetectXmlPayload =
405407
tag.tagName === "tool_call" ||
406408
tag.tagName === "function" ||
409+
tag.tagName === "antml:invoke" ||
407410
((options.stripFunctionCallsXmlPayloads === true ||
408411
shouldStripPluralWrapperBeforeResponse) &&
409412
isPluralToolCallWrapper);

0 commit comments

Comments
 (0)