Skip to content

fix(coach): strip <think> CoT blocks from LiteRT-LM output before emitting#99

Merged
ber4444 merged 1 commit into
mainfrom
fix/litert-strip-think-cot
Jul 17, 2026
Merged

fix(coach): strip <think> CoT blocks from LiteRT-LM output before emitting#99
ber4444 merged 1 commit into
mainfrom
fix/litert-strip-think-cot

Conversation

@ber4444

@ber4444 ber4444 commented Jul 17, 2026

Copy link
Copy Markdown
Owner

What this fixes

Follow-up to #98, which merged the LiteRT-LM driver + candidates.json fix + the SIGTRAP context-size fix, but not the production-side <think> chain-of-thought stripping. This PR lands that missing piece.

The problem

Qwen3-0.6B-int4 (the default .litertlm model) emits <think>…</think> chain-of-thought blocks before its answer. After #98 merged, LitertLmTextGenerator.generate() emits the full accumulated output unmodified — so the Move Coach panel shows the model's internal deliberation to the user.

Two consequences:

  1. UX bug — chain-of-thought reasoning ("let me consider… does this give check? no…") is shown as if it were the coaching explanation.
  2. Faithfulness bugMoveCoachResponseValidator then judges the CoT, not the delivered answer. A phrase like "does this give check?" inside CoT looks like an invented check claim.

ferryman #11's scorer already strips <think> before scoring, so the published LiteRT scorecard numbers (honesty 100%, faithfulness 50%, piece-type 80%) are honest about the delivered answer. But the production app still leaks CoT. This closes that gap.

The fix

LitertLmTextGenerator.stripThinkBlocks(text) (companion-level, unit-tested):

  • Strips closed <think>…</think> blocks (case-insensitive, DOTALL)
  • Strips an unterminated trailing <think>… (generation cut off mid-reasoning)
  • Applied in generate() before the Token is emitted

Defense in depth with ferryman's Python-side stripping — either alone is sufficient, both makes a model swap or generation hiccup safe.

Verification

Check Command Result
Compile ./gradlew :onDeviceAi:compileKotlinDesktop BUILD SUCCESSFUL
Strip test ./gradlew :onDeviceAi:desktopTest --tests *StripThinkTest 6 tests pass (closed block, newlines, unterminated, no-block, case-insensitive, only-think)

Scope

One file changed (LitertLmTextGenerator.kt, +30/-1), one test added (6 cases). Cherry-picked from the eval/litert-faithfulness-bypass branch where it was originally written alongside #98's foundation work.

…tting

Qwen3 (the default .litertlm model) emits <think>...</think> chain-of-thought
before its answer. The Move Coach contract wants only the delivered answer —
leaking internal deliberation to the user is a UX bug, and downstream
validation (MoveCoachResponseValidator) should judge only the answer, not the
reasoning.

Adds LitertLmTextGenerator.stripThinkBlocks (companion-level, unit-tested):
strips closed <think>...</think> blocks and an unterminated trailing <think>.
Applied in generate() before the Token is emitted. Mirrors the Python-side
stripping in ferryman's score_litert_outputs.py (defense in depth — either
side stripping is sufficient, but both makes a model swap or generation
hiccup safe).
@ber4444
ber4444 merged commit 90887fd into main Jul 17, 2026
12 checks passed
@ber4444
ber4444 deleted the fix/litert-strip-think-cot branch July 18, 2026 23:27
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.

1 participant