Commit graph

2 commits

Author SHA1 Message Date
Helldez
55b8579396 feat(core): surface the reasoning span alongside the answer (#70)
Since #49 wired the chat parser correctly, a reasoning model's thinking is
stripped from the shown answer unconditionally — including when the user asked
for thinking. With the in-app Thinking toggle ON, the answer area stays blank
while the model reasons and only the final answer ever appears; on a slow
streamed decode that reads as a hang for the whole thinking span.

The reasoning was being parsed and thrown away: shown_text() kept only
common_chat_parse(...).content and dropped .reasoning_content, and no field
downstream carried it. Surface it instead of discarding it:

  - TokenMetrics gains `reasoning`, RunResult gains `reasoning_text`; session's
    shown_view returns {content, reasoning} from a single parse (the partial
    parse already fills reasoning_content incrementally, so it streams).
  - The line protocol carries a `reasoning` field on BMOE_PROGRESS and
    BMOE_DONE, kept apart from `text` so a UI can render it as a distinct
    thinking block rather than inline. Documented in docs/telemetry.md.

The answer in `text`/generated_text is unchanged (reasoning still stripped),
so this is display-only: the byte-identity gates are untouched. The chat-parse
host gate now also asserts a partial parse exposes the reasoning span — the
contract the live thinking block depends on.
2026-07-18 22:43:23 +02:00
Helldez
94246d8849 fix(core): strip reasoning from chat answers (#49)
The reasoning parser was wired two ways short of ever running, so <think>
markers leaked into the shown answer on Qwen and Gemma:

  1. common_chat_templates_apply was called with the default
     reasoning_format = NONE, which bakes a content-only grammar that never
     strips a reasoning span. AUTO must be set on the templates inputs,
     before apply — that is where stripping is decided, not at parse time.
  2. parse params were built with the common_chat_parser_params(cp)
     convenience constructor, which copies only format/generation_prompt and
     leaves the PEG grammar arena empty. common_chat_parse then threw on the
     first token and a bare catch returned the raw stream verbatim.

Fix both: set reasoning_format = AUTO on the inputs, and load the serialized
grammar into the arena (extracted into detail::build_parse_params so the seam
is unit-testable without a model). The two swallowing catches now warn once
instead of silently passing the raw stream through — a silent passthrough is
how this stayed invisible.

gpt-oss was unaffected only by accident: --no-think primes the harmony final
channel and returns the raw stream by design, never exercising the parser.

Adds tests/chat_parse_test.cpp: a model-free host gate that drives
build_parse_params against the vendored Qwen3 template, asserts the reasoning
is stripped, and pins the pre-fix wiring (empty arena) as a negative control.
Runs unconditionally in ctest (no gguf needed). Byte-identity gates unchanged.
2026-07-18 10:31:40 +02:00