fix(json_parser): strip <think> reasoning tags before parsing - #675
Merged
Conversation
Reasoning models (Qwen, DeepSeek, etc.) emit chain-of-thought inside <think>...</think> blocks before the JSON payload. parse_json_response() did not account for this: a brace inside the reasoning was picked up by the raw_decode scan and returned instead of the real object, silently yielding wrong or empty data (e.g. empty BookEngine proposal fields). Strip <think> blocks only on the direct-parse failure path, so a valid JSON payload whose string values legitimately contain the substring '<think>' is preserved exactly. Also drop an unclosed leading <think> prelude up to the first JSON opener, and return the fallback when the response is reasoning-only with no payload. Fixes HKUDS#673 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 tasks
pancacake
added a commit
that referenced
this pull request
Jul 24, 2026
Same-day maintenance follow-up to v1.5.3, focused on how chat feels. - Chat responsiveness: the post-answer "generating" stall is gone (DONE carries the persisted message ids so the frontend reconciles in place instead of refetching the session), turn events flush in one transaction (#678), the streaming autoscroll stops forcing per-frame layouts, and Enter during streaming no longer fires an interleaved message (#674). - Partners: markdown-table row splitting unified across channels — empty cells survive Slack (#679) and Feishu (#683) tables, and empty rows are no longer misread as header separators (#682). - LLM-output parsing: <think> reasoning tags stripped before parsing (#675), adjacent JSON values no longer break Deep Research extraction (#680), and the parser returns the longest decodable value instead of the first prefix (#692). - Assorted: streaming quiz cards stay scoped to their own turn (#677), the create-KB form survives the background indexing poll (#691), and Math Animator reads ms as milliseconds, not minutes (#681). - Typing: SQLite session store add_message accepts str parents to match SessionStoreProtocol (PocketBase record ids). Release notes: assets/releases/ver1-5-4.md
vaskoyudha
added a commit
to vaskoyudha/deeptutor-for-programmer-fork
that referenced
this pull request
Jul 25, 2026
) Reasoning models (Qwen, DeepSeek, etc.) emit chain-of-thought inside <think>...</think> blocks before the JSON payload. parse_json_response() did not account for this: a brace inside the reasoning was picked up by the raw_decode scan and returned instead of the real object, silently yielding wrong or empty data (e.g. empty BookEngine proposal fields). Strip <think> blocks only on the direct-parse failure path, so a valid JSON payload whose string values legitimately contain the substring '<think>' is preserved exactly. Also drop an unclosed leading <think> prelude up to the first JSON opener, and return the fallback when the response is reasoning-only with no payload. Fixes HKUDS#673 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
vaskoyudha
added a commit
to vaskoyudha/deeptutor-for-programmer-fork
that referenced
this pull request
Jul 25, 2026
Same-day maintenance follow-up to v1.5.3, focused on how chat feels. - Chat responsiveness: the post-answer "generating" stall is gone (DONE carries the persisted message ids so the frontend reconciles in place instead of refetching the session), turn events flush in one transaction (HKUDS#678), the streaming autoscroll stops forcing per-frame layouts, and Enter during streaming no longer fires an interleaved message (HKUDS#674). - Partners: markdown-table row splitting unified across channels — empty cells survive Slack (HKUDS#679) and Feishu (HKUDS#683) tables, and empty rows are no longer misread as header separators (HKUDS#682). - LLM-output parsing: <think> reasoning tags stripped before parsing (HKUDS#675), adjacent JSON values no longer break Deep Research extraction (HKUDS#680), and the parser returns the longest decodable value instead of the first prefix (HKUDS#692). - Assorted: streaming quiz cards stay scoped to their own turn (HKUDS#677), the create-KB form survives the background indexing poll (HKUDS#691), and Math Animator reads ms as milliseconds, not minutes (HKUDS#681). - Typing: SQLite session store add_message accepts str parents to match SessionStoreProtocol (PocketBase record ids). Release notes: assets/releases/ver1-5-4.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
parse_json_response()did not account for chain-of-thought<think>...</think>blocks that reasoning models (Qwen, DeepSeek, etc.) emit before the JSON payload. A brace inside the reasoning was picked up by theraw_decodescan and returned instead of the real object, silently yielding wrong or empty data (e.g. empty BookEngine proposaldescription/scope/rationale).Fixes #673
Root cause
For input like:
direct
json.loadsfails (the<think>prefix), so theraw_decodescan runs and stops at the first{— which is the{"draft": true}inside the reasoning — returning that instead of the real object.Fix
Strip
<think>blocks only on the direct-parse failure path, then retry. Key properties:json.loadsruns before any stripping, so a valid payload whose string values legitimately contain the substring<think>(e.g.{"x":"a <think>b</think> c"}) is preserved exactly.<think>...</think>blocks (one or many), case-insensitively.<think>prelude up to the first JSON opener.fallbackwhen the response is reasoning-only with no JSON payload.Testing
New
TestParseJsonResponseThinkTagsclass (8 cases) covering: think-before-json, brace-in-reasoning-does-not-leak, markdown-fenced payload, case-insensitivity, reasoning-only fallback, multiple blocks, literal<think>inside valid JSON preserved, and unclosed prelude.pytest tests/utils/→ 90 passedruff check/ruff format --check→ cleanmypy deeptutor/utils/json_parser.py→ cleandevwithout the fix.