Skip to content

fix: strip all reasoning blocks in removeReasoningContent, not just the first - #243

Open
Ayush7614 wants to merge 1 commit into
AtomicBot-ai:mainfrom
Ayush7614:fix/reasoning-strip-all-think-blocks
Open

fix: strip all reasoning blocks in removeReasoningContent, not just the first#243
Ayush7614 wants to merge 1 commit into
AtomicBot-ai:mainfrom
Ayush7614:fix/reasoning-strip-all-think-blocks

Conversation

@Ayush7614

Copy link
Copy Markdown
Contributor

What

removeReasoningContent (used by useTokensCount to compute what gets sent to the model) only removed the first complete thinking… response block and completely ignored unterminated blocks:

  • A model can emit several reasoning spans across one turn — only the first was stripped, the rest leaked.
  • A stream cut off mid-reasoning leaves an unterminated thinking… block with no closing tag — it was never removed at all.

Both cases meant reasoning content leaked into the token count and, potentially, into the prompt sent to the model.

Fix

  • Strip every complete thinking… response block (global replace) instead of the first match.
  • Strip a trailing unterminated thinking… block.
  • Apply the same two rules to the DeepSeek <|channel|>analysis<|message|>…<|start|>assistant<|channel|>final<|message|> format.

Verification

  • New test suite (web-app/src/utils/__tests__/reasoning.test.ts, 12 tests) covering single / multiple / unterminated blocks for both formats plus extractReasoningFromMessage edge cases.
  • tsc -b clean, eslint 0 errors, full utils suite 67/67 pass.

…he first

removeReasoningContent only removed the first complete  thinking… response
block and ignored unterminated blocks. A model can emit several reasoning
spans across one turn, and a stream cut off mid-reasoning leaves an
unterminated block — both cases leaked reasoning into the token count and,
potentially, the prompt sent to the model.

Now:
- strips every complete  thinking… response block (global replace)
- strips a trailing unterminated  thinking block
- applies the same treatment to the DeepSeek <|channel|>analysis…final format

Adds a test suite covering single/multiple/unterminated blocks for both
formats, plus extractReasoningFromMessage edge cases.
@Ayush7614
Ayush7614 requested a review from Vect0rM as a code owner August 19, 2026 10:22

Vect0rM commented Aug 20, 2026

Copy link
Copy Markdown
Member

Thanks @Ayush7614 — the core observation is correct, and the regex-with-/g rewrite is cleaner than the match + slice dance it replaces. Handling the unterminated block is a good addition too.

Verified on your branch merged onto current main:

  • vitest --run src/utils/__tests__/reasoning.test.ts — 12 passed.
  • tsc -b — exit 0. eslint src/utils/reasoning.ts — clean. Merges cleanly.

Three things, none of them large.

1. The framing needs correcting — and it's good news

removeReasoningContent has exactly one caller in the app:

web-app/src/hooks/useTokensCount.ts:125

It feeds the token-count estimate behind the context-usage indicator. It is not on the path that builds the outbound request. So the PR description ("doesn't leak into the prompt") and the function's own comment ("Reasoning content should not be sent to the model") both overstate what this code does — the comment has been stale for a while, and your PR is a good moment to fix it.

That's worth getting right in the description because it also lowers the stakes on point 3 below: the worst case is a slightly-off token estimate, not lost or leaked content.

2. An undocumented improvement worth claiming

The old code did content.slice(match.index + match[0].length) — it dropped everything before the first <think> block, not just the block. So a message like Question\n<think>…</think>\nAnswer lost Question from the count entirely.

Your .replace() keeps the prefix. That's strictly more accurate, and it's a bigger deal than the multi-block fix, but the title and description don't mention it. Worth a line.

3. The catch-all is greedy

result = result.replace(/<think>[\s\S]*$/, '')

If a user pastes a snippet that contains a literal <think> — asking about prompt formats, say — everything after it disappears from the estimate. Given this is only an estimate I'm happy to take that trade, but a short comment saying so would stop the next reader from "fixing" it the wrong way.

Nits

  • Neither reasoning.ts nor the new test file ends with a newline; npx prettier --write on both. Prettier isn't part of make verify-fast, so nothing else will catch it.
  • result.trim() now runs unconditionally, where before it only ran when a block was found. Harmless here — just a behaviour change for content with intentional leading/trailing whitespace.

Fix the description and the stale comment, run Prettier, and this is ready. Thanks for the tests — the unterminated-block cases are the ones I'd have forgotten 🧠


Generated by Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants