Skip to content

scripts: add thinking probes for measuring what controls reasoning#6

Merged
TheTom merged 1 commit into
TheTom:mainfrom
Defilan:thinking-probes
Jul 26, 2026
Merged

scripts: add thinking probes for measuring what controls reasoning#6
TheTom merged 1 commit into
TheTom:mainfrom
Defilan:thinking-probes

Conversation

@Defilan

@Defilan Defilan commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #5, as offered there.

Three small tools, standard library only, no install step. They live in
scripts/thinking-probes/ rather than alongside the spine probes, since they
measure a different thing: whether the model reasons, not how it behaves under
integrity pressure. Happy to move or merge them if you would rather one
directory.

file what it does
render_check.py compares what the template renders per kwarg value via /apply-template. Deterministic, no sampling, no statistics. Optionally diffs against a local jinja2 render of the same file.
thinking_ab.py interleaved sampling across the same arms, classifying reasoned-and-parsed vs reasoned-but-unparsed vs not-reasoned
persona_ab.py crosses a persona system prompt against the kwarg, with the two structurally-impossible cells as a control

render_check.py is the one I would reach for first. Against our endpoint it
reproduces the whole of #5 in a single command:

=== which arms render identically? ===
  absent        vs empty-kwargs  identical
  absent        vs false         DIFFERENT
  absent        vs true          identical

=== server versus local, per arm ===
  absent        SERVER OVERRIDES TEMPLATE
  false         template decides
  true          template decides

That SERVER OVERRIDES TEMPLATE line is the thing I wish had existed a week
ago. It is the difference between reading a template and measuring one.

The README states the two habits these encode: reading is not measuring, and a
finding needs a cell where it cannot happen. It also documents the artifact that
produced our wrong result, since the failure mode is invisible from the outside.

Verified against a live gfx1151 endpoint before opening: all three run, exit
clean, no em or en dashes in any added file so check_guides.py is unaffected.

I have not committed the raw JSON from the #5 runs, 280KB across 117 requests
with full response bodies and reasoning traces. Say the word and I will add it
here or attach it to the issue, whichever you prefer.

Assisted-by: Claude Code (Opus), reviewed and run by me against a live endpoint

Companion to spine-probes, different question: whether a model actually
reasons, and what controls it.

  render_check.py  compares what the chat template renders per kwarg value via
                   /apply-template, deterministic and needing no statistics,
                   and optionally diffs that against a local jinja2 render of
                   the same file to show when the server overrides the template
  thinking_ab.py   interleaved sampling across the same arms, classifying
                   reasoned-and-parsed vs reasoned-but-unparsed vs not-reasoned
  persona_ab.py    crosses a persona system prompt against the kwarg, with the
                   two structurally-impossible cells as a control

These come out of getting a result wrong (TheTom#5). We reported the enable_thinking
kwarg as inert on Laguna S 2.1, reasoning from the template source, which sets
default(false). The template reading was correct; the conclusion was not,
because the server supplies that kwarg itself when the client omits it, so the
default is unreachable.

Two habits are baked in as a result. Reading is not measuring, so render_check
inspects rendered output rather than source. And a finding needs a cell where it
cannot happen, so both sampling tools carry a control arm.

Both sampling tools interleave arms, vary prompt shape, disable the prompt cache
with a unique nonce per request, and hold max_tokens fixed with no retry. Our
first pass did none of that and produced 5/5 against 0/5 that was mostly
artifact. Manufactured consistency is indistinguishable from a strong result by
inspection.

Signed-off-by: Christopher Maher <chris@mahercode.io>
@Blackwellboy

Copy link
Copy Markdown
Contributor

Read all four files before commenting, since that is the house style here. This is a genuinely useful addition and I hope it merges. The SERVER OVERRIDES TEMPLATE line is the tool form of the #5 lesson, and the structurally-impossible false cells as controls in persona_ab.py are exactly what the earlier persona numbers were missing.

Two observations from our stack, one of which might earn a README line.

1. Server-vs-local disagreement has a second cause the README does not name: the server may not be running the template file you passed. We hit this in the wild. Our rev 0761412 NVFP4 checkpoint ships an on-disk template that defaults enable_thinking to true (Poolside's current HF upload does too), while the pinned template in this repo defaults false. An operator serving a newer checkpoint but diffing against the repo's jinja would get SERVER OVERRIDES TEMPLATE in the absent arm when nothing is being overridden: the served template itself moved. Both causes are worth catching, but they mean different fixes (send the kwarg explicitly vs re-pin the revision). A sentence like "make sure --template is the file the server actually loaded (llama.cpp's /props shows the active template on recent builds), otherwise template drift between revisions can masquerade as a server override" would disambiguate cheaply.

2. Scope note, fine to leave as-is: /apply-template is llama.cpp-specific, so render_check.py will not run against vLLM or most other OpenAI-compatible servers. Not a defect, the README already says llama.cpp server root, but a one-line "llama.cpp only; on other engines use thinking_ab.py plus reading the served template" would save someone the confused first run. For what it is worth, on our vLLM/poolside_v1 path we checked the class thinking_ab.py calls REASONED_UNPARSED and found zero instances across our bare-prompt verification, so the three-way classification is what makes firing rates comparable across stacks at all.

The raw JSON from the #5 runs would be worth having in-repo alongside these, since the scripts encode the method and the JSON is the evidence.

@TheTom
TheTom merged commit ae02d6e into TheTom:main Jul 26, 2026
1 check passed
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.

3 participants