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 does — transport.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 name — reasoning_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)
- 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.
- Configure it per provider where we know the convention, using the seam that already exists and is currently unused.
- 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
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_extractionis invoked exactly twice in the whole tree, both in the vendored crate's own tests:Zero hits under
src/. Soreasoning_tags_overriddenis alwaysfalsein a real run and the default governs.What the default does —
transport.rs:504-509:base_url == api.openai.com→ extraction off.And it matches one tag name —
reasoning_tags.rs:53:So the effective behaviour everywhere except OpenAI is: extract
<think>, pass every other reasoning convention through untouched intocontent.Why this matters
Reasoning is separated from the answer by exactly one bit —
MessageDelta.reasoningvsMessageDelta.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-
thinktag. 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)
think,thinking,reasoningrather 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.<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