packages/inspect-components/src/chat/MessageContent.tsx's purgeInternalContainers regex-deletes <think>…</think> (alongside <internal> / <content-internal>) from ContentText before rendering:
const purgeInternalContainers = (text: string): string => {
const internalTags = ["internal", "content-internal", "think"];
internalTags.forEach((tag) => {
const regex = new RegExp(`<${tag}[^>]*>[\\s\\S]*?<\\/${tag}>`, "gm");
text = text.replace(regex, "");
});
return text.trim();
};
That's fine when <think> is a scaffold/bridge artifact and the same content is also present as a structured ContentReasoning block. But it isn't always:
Gemini via OpenRouter learns and emits the <think> format itself. inspect_ai's OpenRouter provider replays prior-turn reasoning back to Gemini as inline <think signature="reasoning-details://…">…</think> text (see _providers/openrouter.py — _strip_reasoning_details = "gemini" in family → reasoning_to_think_tag(content), because replaying reasoning_details triggers a Gemini-side thought-signature validation error). After a couple of turns Gemini pattern-matches the format and starts emitting its own reasoning the same way — as literal <think signature="…"><summary>…</summary>…encrypted…</think> in the assistant ContentText, with only a placeholder ContentReasoning(redacted=True, summary="") alongside. In one multi-turn agentic run I have on hand this happened on ~85% of turns from turn 3 onward.
The viewer then shows those turns as Reasoning encrypted by model provider. + an empty text block, when the log actually contains the model's full plaintext reasoning summary — including, in safety-eval contexts, the turn where the model explicitly states an intent it later acts on covertly. A reader using inspect view to audit that trajectory would not see it; they'd have to know to open the raw JSON or read a different agent's tool-result echo of the same content.
Potential fix: drop "think" from internalTags. The markdown pipeline already escapeHtmlCharacters-escapes all tags before rendering, so <think> will display as literal text — noisy, but visible. This could be toggled on and off via a button or a flag, to avoid the noise for users who don't want it.
Higher effort fix: hoist a <think signature="…">…</think> text block into a proper ContentReasoning at render time (mirroring inspect_ai.model._reasoning.parse_content_with_reasoning — parse signature/redacted attrs, extract nested <summary>, and if the signature is reasoning-details://… decode it via the same rules as openrouter_reasoning_details_to_reasoning). That would render it under the existing REASONING (SUMMARY) label instead of as raw escaped markup.
Separately, the provider-side path that produces this (_strip_reasoning_details → text replay → model mimics format) is worth revisiting — but the viewer should never silently discard content regardless of how it got there.
packages/inspect-components/src/chat/MessageContent.tsx'spurgeInternalContainersregex-deletes<think>…</think>(alongside<internal>/<content-internal>) fromContentTextbefore rendering:That's fine when
<think>is a scaffold/bridge artifact and the same content is also present as a structuredContentReasoningblock. But it isn't always:Gemini via OpenRouter learns and emits the
<think>format itself. inspect_ai's OpenRouter provider replays prior-turn reasoning back to Gemini as inline<think signature="reasoning-details://…">…</think>text (see_providers/openrouter.py—_strip_reasoning_details = "gemini" in family→reasoning_to_think_tag(content), because replayingreasoning_detailstriggers a Gemini-side thought-signature validation error). After a couple of turns Gemini pattern-matches the format and starts emitting its own reasoning the same way — as literal<think signature="…"><summary>…</summary>…encrypted…</think>in the assistantContentText, with only a placeholderContentReasoning(redacted=True, summary="")alongside. In one multi-turn agentic run I have on hand this happened on ~85% of turns from turn 3 onward.The viewer then shows those turns as
Reasoning encrypted by model provider.+ an empty text block, when the log actually contains the model's full plaintext reasoning summary — including, in safety-eval contexts, the turn where the model explicitly states an intent it later acts on covertly. A reader usinginspect viewto audit that trajectory would not see it; they'd have to know to open the raw JSON or read a different agent's tool-result echo of the same content.Potential fix: drop
"think"frominternalTags. The markdown pipeline alreadyescapeHtmlCharacters-escapes all tags before rendering, so<think>will display as literal text — noisy, but visible. This could be toggled on and off via a button or a flag, to avoid the noise for users who don't want it.Higher effort fix: hoist a
<think signature="…">…</think>text block into a properContentReasoningat render time (mirroringinspect_ai.model._reasoning.parse_content_with_reasoning— parsesignature/redactedattrs, extract nested<summary>, and if the signature isreasoning-details://…decode it via the same rules asopenrouter_reasoning_details_to_reasoning). That would render it under the existingREASONING (SUMMARY)label instead of as raw escaped markup.Separately, the provider-side path that produces this (
_strip_reasoning_details→ text replay → model mimics format) is worth revisiting — but the viewer should never silently discard content regardless of how it got there.