Skip to content

fix(chat_logger): reroute leaked think drafts, rescue truncated tool calls (#8) - #10

Merged
Hrt-Htk merged 2 commits into
mainfrom
fix/issue8-think-leak-rescue
Jul 16, 2026
Merged

fix(chat_logger): reroute leaked think drafts, rescue truncated tool calls (#8)#10
Hrt-Htk merged 2 commits into
mainfrom
fix/issue8-think-leak-rescue

Conversation

@Hrt-Htk

@Hrt-Htk Hrt-Htk commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the two production failure modes of Qwen3.6 think-tag handling tracked in #8, verified against proxy traces and a scan of ~33k logged assistant turns:

  • </think> leaks into visible content (~0.5–0.6%/turn, all presets, not MTP-specific): the model either double-emits </think> (draft answer between two close tags) or its thinking mentions `</think>` in backticks and llama-server's parser consumes the mention as the real close tag. Either way, thinking text plus a literal </think> spills down the content channel.
  • Thinking-only stalls with a trapped tool call: the model ends its turn inside an unclosed think block with complete tool-call XML in reasoning; the truncated variant (stream ends mid-</tool_call) was unhandled.

Changes

SSEChunkLogger (chat_logger.py):

  • Content-side think-leak window: buffer the first 4KB (THINK_LEAK_WINDOW) of content deltas; on a standalone </think> line outside a code fence, reroute pre-tag text to reasoning_content, drop the tag, stream the rest as content. No trigger → flush and pass through. Inline/fenced tag mentions are untouched.
  • EOF rescue of truncated tool calls: a pending capture with a complete <function=…>…</function> block and a residue that is a prefix of </tool_call> is synthesized as a real tool call (finish_reason rewritten to tool_calls).
  • Stream-contract hardening: held state (buffered content, held finish events, pending rescue) resolves before [DONE] is forwarded; empty transform output no longer terminates readany() early (its b"" = EOF contract).

Tests: new replication harness tests/test_think_leak_replication.py built from real production seams (sessions 8d62feae, 093cd785), shared SSE fakes extracted to tests/_helpers.py, plus control tests pinning passthrough for legit tag mentions, pure stalls, and normal streams.

Behavioral note: responses under 4KB now arrive as a single content delta after thinking completes (window flush at the finish event); reasoning still streams live.

Test plan

  • ./.venv/Scripts/python.exe -m unittest discover -s tests — 20/20 green (run twice)
  • Proxy restarted with the fix and smoke-tested: reasoning streams live, content intact, finish_reason: stop preserved, MTP drafting healthy, pi round-trip OK

Closes #8

🤖 Generated with Claude Code

Hrt-Htk and others added 2 commits July 5, 2026 09:22
Replicates two production failure modes of Qwen3.6 think-tag handling,
verified against proxy traces and ~33k logged assistant turns:
- content-channel </think> leaks (double tag emission, and thinking that
  mentions the tag in backticks being consumed as the real close tag)
- thinking-only stalls, including tool calls trapped in an unclosed
  think block (complete and truncated </tool_call XML tails)

Shared SSE fakes extracted from test_toolcall_rescue.py into
tests/_helpers.py; control tests pin passthrough behaviour for legit
tag mentions (inline and fenced), pure stalls, and normal streams.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…calls (#8)

Two stream-repair mechanisms in SSEChunkLogger:

- Content-side think-leak window: buffer the first 4KB of content
  deltas; if a standalone </think> line appears outside a code fence,
  emit the pre-tag text as reasoning_content, drop the tag, and stream
  the remainder as content. No trigger: flush and pass through.
- EOF rescue of truncated tool calls: when the stream ends with a
  pending capture whose <function=...>...</function> block is complete
  and only the closing </tool_call> is cut off, synthesize the tool
  call and rewrite finish_reason to tool_calls.

Held state (buffered content, held finish events, pending rescue) is
resolved before [DONE] is forwarded, and empty transform output no
longer terminates the read loop early (readany's b"" contract).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Hrt-Htk
Hrt-Htk merged commit ab1361e into main Jul 16, 2026
1 check passed
@Hrt-Htk
Hrt-Htk deleted the fix/issue8-think-leak-rescue branch July 16, 2026 07:05
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.

SSEChunkLogger rescue state machine splits reasoning_content on literal </thinking> text

1 participant