Skip to content

Inline reasoning-tag extraction only ever matches <think>, so other reasoning tags leak into the visible answer #6184

Description

@M3gA-Mind

Summary

Inline reasoning-tag extraction ships its built-in default and is never configured, so it strips <think>…</think> and nothing else. A model that emits <thinking>, <reasoning> or any other tag name has its chain-of-thought pass through as visible answer text.

Found while tracing how reasoning reaches the chat UI (context: #5934).

Verified

No production caller. with_reasoning_tag_extraction is invoked exactly twice in the whole tree, both in the vendored crate's own tests:

vendor/tinyagents/vendor/tinyinference/crates/tinyinference/src/providers/openai/test.rs:589
vendor/tinyagents/vendor/tinyinference/crates/tinyinference/src/providers/openai/test.rs:595

Zero hits under src/. So reasoning_tags_overridden is always false in a real run and the default governs.

What the default doestransport.rs:504-509:

pub(super) fn effective_reasoning_tags(&self) -> Option<&ReasoningTagExtraction> {
    if !self.reasoning_tags_overridden && self.base_url == DEFAULT_BASE_URL {
        return None;
    }
    self.reasoning_tags.as_ref()
}
  • base_url == api.openai.com → extraction off.
  • every other base URL → extraction on.

And it matches one tag namereasoning_tags.rs:53:

tag_name: "think".to_string(),

So the effective behaviour everywhere except OpenAI is: extract <think>, pass every other reasoning convention through untouched into content.

Why this matters

Reasoning is separated from the answer by exactly one bit — MessageDelta.reasoning vs MessageDelta.text — decided in the provider adapter. There is no heuristic anywhere downstream: not in the core, not in the renderer. Nothing inspects text to decide whether it is thinking.

That is the right design, and it means this is the only place the distinction can be made for a model that inlines its reasoning. If the tag name does not match here, the chain-of-thought becomes answer text and no UI change can recover it — including #6169, which moved reasoning off the main transcript but only acts on content already marked as reasoning.

What I have NOT established

Whether any model we actually route to emits a non-think tag. I did not run a live capture. The affected population is "OpenAI-compatible endpoints that inline reasoning under a different tag name" — plausibly some Ollama / LM Studio / llama.cpp models and some hosted OpenAI-compatible providers, but I am not claiming a specific one is broken today.

So this is filed as a latent defect with a verified mechanism, not an observed incident. One live capture per configured provider would settle it; that is the natural first step.

Why it is worth attention now

The single-tag default is fine while managed inference goes to a small, known set of upstreams. It becomes less safe as that set widens — a managed route that resolves through an aggregator can reach many more model families than the default was written against, and each one brings its own convention. A leak here is not cosmetic: internal reasoning appearing as the assistant's answer is a content-correctness and potentially a disclosure problem, not a rendering nit.

Fix shapes (in increasing cost)

  1. Widen the default tag set — accept think, thinking, reasoning rather than one name. Smallest change, covers the common conventions, no per-provider configuration. Needs care that the streaming-safe partial-tag buffering (potential_start_index) still holds with multiple candidate tags.
  2. Configure it per provider where we know the convention, using the seam that already exists and is currently unused.
  3. Leave the default and document it — acceptable only if a capture shows nothing we route to inlines anything but <think>.

I would do (1) after doing the capture, and not before — widening a matcher against a hazard nobody has observed is how a stripping bug gets introduced in the other direction.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions