From 7c87dc5bcaddefb9bad7448cb8580da303115cdd Mon Sep 17 00:00:00 2001 From: Xun Date: Sun, 19 Apr 2026 19:27:34 +0800 Subject: [PATCH] fix(reasoning): prevent LLM-hallucinated HTML tags from rendering as DOM elements (#2321) * fix * add test * fix --- frontend/src/components/ai-elements/reasoning.tsx | 3 ++- frontend/src/core/streamdown/plugins.ts | 9 +++++++++ frontend/tests/unit/core/streamdown/plugins.test.ts | 13 +++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 frontend/tests/unit/core/streamdown/plugins.test.ts diff --git a/frontend/src/components/ai-elements/reasoning.tsx b/frontend/src/components/ai-elements/reasoning.tsx index 8c9d5b206..5f1d2321e 100644 --- a/frontend/src/components/ai-elements/reasoning.tsx +++ b/frontend/src/components/ai-elements/reasoning.tsx @@ -11,6 +11,7 @@ import { BrainIcon, ChevronDownIcon } from "lucide-react"; import type { ComponentProps, ReactNode } from "react"; import { createContext, memo, useContext, useEffect, useState } from "react"; import { Streamdown } from "streamdown"; +import { reasoningPlugins } from "@/core/streamdown/plugins"; import { Shimmer } from "./shimmer"; type ReasoningContextValue = { @@ -177,7 +178,7 @@ export const ReasoningContent = memo( )} {...props} > - {children} + {children} ), ); diff --git a/frontend/src/core/streamdown/plugins.ts b/frontend/src/core/streamdown/plugins.ts index e9214031b..d576252c5 100644 --- a/frontend/src/core/streamdown/plugins.ts +++ b/frontend/src/core/streamdown/plugins.ts @@ -28,6 +28,15 @@ export const streamdownPluginsWithWordAnimation = { ] as StreamdownProps["rehypePlugins"], }; +// Plugins for reasoning/thinking content — derived from streamdownPlugins but without rehypeRaw, +// to prevent LLM-hallucinated HTML tags (e.g. ) from being rendered as DOM elements. +export const reasoningPlugins = { + remarkPlugins: streamdownPlugins.remarkPlugins, + rehypePlugins: streamdownPlugins.rehypePlugins?.filter( + (p) => p !== rehypeRaw, + ) as StreamdownProps["rehypePlugins"], +}; + // Plugins for human messages - no autolink to prevent URL bleeding into adjacent text export const humanMessagePlugins = { remarkPlugins: [ diff --git a/frontend/tests/unit/core/streamdown/plugins.test.ts b/frontend/tests/unit/core/streamdown/plugins.test.ts new file mode 100644 index 000000000..efe0ab719 --- /dev/null +++ b/frontend/tests/unit/core/streamdown/plugins.test.ts @@ -0,0 +1,13 @@ +import rehypeRaw from "rehype-raw"; +import { expect, test } from "vitest"; + +import { reasoningPlugins, streamdownPlugins } from "@/core/streamdown/plugins"; + +test("streamdownPlugins includes rehypeRaw", () => { + expect(streamdownPlugins.rehypePlugins).toContain(rehypeRaw); +}); + +test("reasoningPlugins does not include rehypeRaw", () => { + const flat = reasoningPlugins.rehypePlugins?.flat(); + expect(flat).not.toContain(rehypeRaw); +});