Skip to content

fix(json_parser): strip <think> reasoning tags before parsing - #675

Merged
pancacake merged 1 commit into
HKUDS:devfrom
Cyriellewu:fix/json-parser-think-tags
Jul 24, 2026
Merged

fix(json_parser): strip <think> reasoning tags before parsing#675
pancacake merged 1 commit into
HKUDS:devfrom
Cyriellewu:fix/json-parser-think-tags

Conversation

@Cyriellewu

Copy link
Copy Markdown
Contributor

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 the raw_decode scan and returned instead of the real object, silently yielding wrong or empty data (e.g. empty BookEngine proposal description / scope / rationale).

Fixes #673

Root cause

For input like:

<think>
1. Analyze user intent, maybe output {"draft": true}
</think>
{"title":"T","description":"D",...}

direct json.loads fails (the <think> prefix), so the raw_decode scan 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:

  • No regression for valid JSON: direct json.loads runs 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.
  • Handles closed <think>...</think> blocks (one or many), case-insensitively.
  • Drops an unclosed leading <think> prelude up to the first JSON opener.
  • Returns the caller's fallback when the response is reasoning-only with no JSON payload.

Testing

New TestParseJsonResponseThinkTags class (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 passed
  • ruff check / ruff format --check → clean
  • mypy deeptutor/utils/json_parser.py → clean
  • Verified the brace-leak regression test fails on dev without the fix.

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>
@pancacake
pancacake merged commit b020686 into HKUDS:dev Jul 24, 2026
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
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