Skip to content

fix(ai): keep response content when a stray </think> follows a real think block - #836

Merged
altic-dev merged 1 commit into
altic-dev:mainfrom
YuriNachos:fix/strip-thinking-tags-stray-close
Aug 13, 2026
Merged

fix(ai): keep response content when a stray </think> follows a real think block#836
altic-dev merged 1 commit into
altic-dev:mainfrom
YuriNachos:fix/strip-thinking-tags-stray-close

Conversation

@YuriNachos

Copy link
Copy Markdown
Contributor

Description

LLMClient.stripThinkingTags (the non-streaming thinking-token extractor used by every non-streaming LLM response) ran its orphan </think> pass unconditionally. That pass is meant for Nemotron-style output that has no opening tag and uses </think> as the separator (thoughts</think>response). But when a model emits a proper <think>…</think> block and then leaks a second, stray </think> later in its answer, the orphan pass reclassified every character between the real close and the stray close as thinking — silently dropping it from the visible response. Example:

  • Input: <think>reasoning here</think>The literal </think> tag is stray.
  • Before: thinking = reasoning hereThe literal , content = tag is stray. (the answer text The literal is lost)
  • After: thinking = reasoning here, content = The literal tag is stray.

The fix only applies the orphan separator when the original text had no opening <think>/<thinking> tag (the Nemotron case it is meant for). When an opening tag was present, any remaining </think> is stray markup and is left to the existing stray-tag stripping, preserving the answer.

Type of Change

  • 🐞 Bug fix

Related Issue or Discussion

Same thinking-token extraction area as the reasoning-content regression in #445 and the Anthropic thinking/temperature handling in #285. No standalone issue exists for the stray-close case, so linking the adjacent regressions.

Testing

  • Tested on Intel Mac
  • Tested on Apple Silicon Mac
  • Tested on macOS version: macOS 26 (Tahoe), arm64
  • Ran linter locally: swiftlint --strict --config .swiftlint.yml Sources on the edited files → 0 violations
  • Ran formatter locally: swiftformat --config .swiftformat Sources (not a CI check; skipped to avoid unrelated formatting churn)
  • Ran tests locally: xcodebuild test -project Fluid.xcodeproj -scheme Fluid -destination 'platform=macOS,arch=arm64' -only-testing:FluidDictationIntegrationTests/StripThinkingTagsTests → 5 tests, 0 failures. testStrayCloseAfterRealThinkBlockKeepsContent fails on the previous unconditional orphan pass and passes with the opening-tag guard; the Nemotron (testOrphanCloseWithoutOpeningTagStillSplits, …SupportsLongFormTag), well-formed-block, and plain-content cases are unchanged.

Screenshots / Video

  • No UI/visual changes; screenshots/video are not applicable.

Notes

Non-streaming path only; streaming uses the per-model ThinkingParser finalize (which already strips stray </think> without reclassifying content), so this brings the non-streaming path to the same behavior. The genuinely ambiguous case (a stray </think> in content from a model that never emits an opening tag) is intentionally left as-is — it is indistinguishable from Nemotron's separator.

…hink block

stripThinkingTags ran its orphan </think> pass unconditionally. For models that
emit a proper <think>…</think> block and then leak a stray </think> later in the
answer, everything between the real close and the stray close was reclassified as
thinking and dropped from the visible response. Only apply the orphan separator
when the original text had no opening tag (the Nemotron case it is meant for);
otherwise leave any remaining </think> to the existing stray-tag stripping.
Adds regression tests covering the stray-close case plus the preserved Nemotron,
long-form, and plain-content paths.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2ab61eae3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +904 to +905
let hasOpeningThinkTag = text.range(of: "<think>") != nil
|| text.range(of: "<thinking>") != nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep orphan parsing when only the answer mentions tags

When Nemotron-style non-streaming output uses reasoning</think>answer, this flag now disables the orphan-separator pass if the visible answer happens to contain the literal text <think> or <thinking> anywhere. For example, reasoning</think>Explain the <think> tag is returned as visible content with the reasoning leaked at the front instead of extracting reasoning, because there was no real opening tag before the separator—only tag text in the answer. Consider basing this guard on whether the proper-pair regex actually removed a real block before skipping orphan parsing.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR corrects non-streaming thinking-token extraction so a stray closing tag after a valid thinking block no longer causes answer content to be reclassified as reasoning and discarded.

  • Records whether the original response contained a supported opening thinking tag.
  • Restricts orphan-separator parsing to responses without an opening tag.
  • Adds focused regression and compatibility tests to the Xcode test target.

Confidence Score: 5/5

The PR appears safe to merge, with the intended parsing behavior covered while the existing closing-only separator cases remain supported.

The opening-tag guard aligns with the same tag variants recognized by the paired-tag parser, and the orphan parser continues to run when no opening tag exists; no concrete blocking or non-blocking defect remains.

Reviews (1): Last reviewed commit: "fix(ai): keep response content when a st..." | Re-trigger Greptile

@altic-dev
altic-dev merged commit ab7faa1 into altic-dev:main Aug 13, 2026
6 checks passed
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