Skip to content

Viewer silently strips <think> tags from assistant text, hiding model-emitted reasoning #4510

Description

@alexandrasouly-aisi

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 familyreasoning_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions