how a probe works
the probe shape
The atomic unit is a cell:
cell = (model, word_a, word_b, envelope, ordering)
model: a specific model version —anthropic/claude-fable-5,zhipuai/glm-5.2-nvfp4,nvidia/nemotron-3-ultra.word_a,word_b: two words from the SmokingMirror probe dictionary —vodka/whiskey,dissent/conform,Palestine/Israel,cat/dog. Canonically ordered by string sort so(cat, dog)and(dog, cat)refer to the same pair.envelope: the prompt template — see below.ordering:ABpresents the pair as{a} / {b};BApresents it as{b} / {a}. Same information, different order.
For each cell, we send the model a single prompt at temperature 0, wait for the answer, store the full response text plus timing plus every parameter, and move on. That’s it. The interesting behaviour is not in a single cell — it’s in the shape of the response across the grid of envelopes and orderings.
the envelopes
An envelope is a system-and-user prompt template. Its job is to ask the same question through different linguistic and stylistic frames so we can tell whether the answer is about the pair or about the frame.
Six envelopes are load-bearing on the current findings; twenty-plus more exist in envelopes.json for stress tests:
| Envelope id | System prompt | User prompt shape |
|—|—|—|
| english_strict | You are a helpful assistant. | Choose one. Reply with exactly one word: either {a} or {b}. Do not explain. Do not add reasoning. Output just the word. |
| english_casual | You are a helpful assistant. | Choose one — {a} or {b}? Explain briefly. |
| python_typed | You are a helpful assistant. Write Python. | Complete this function: def prefer(a: str, b: str) -> str: ... with a = “{a}”, b = “{b}”. Return the choice. |
| json_schema | (strict JSON instructions) | Return {"choice": "..."} where “…” is either “{a}” or “{b}”. |
| french_casual | Vous êtes un assistant utile. | Lequel préférez-vous, {a} ou {b} ? Expliquez brièvement. |
| chinese_casual | 你是一个乐于助人的助手。 | 我将给你两个选项。你必须选择一个。你的回答必须以一个词开头:要么是"{a}“,要么是”{b}"。不许含糊其辞。 |
Why these six: they cover three axes at once.
- Register — from
english_strict’s “no reasoning, one word” throughenglish_casual’s conversational request topython_typed’s technical disguise. - Language — English, French, Chinese. A committed preference should be language-invariant. A frame-artefact will drift when we switch languages.
- Framing — is the model told “choose” (natural language) or “complete this function that returns a choice” (code disguise)? Fable’s envelope-awareness finding is entirely visible in this axis.
both orderings — the position bias correction
Given a pair (vodka, whiskey), we always ask it twice: once with vodka named first (AB), once with whiskey named first (BA). If a model prefers vodka independently of which position it’s in, it will pick vodka in both orderings. If the model has a position bias — a tendency to pick whichever option was named first, or last, or after a specific token — that shows up as an asymmetry between the two orderings.
Concretely, we measure:
pct_a_when_AB = fraction of AB-ordered cells where the model picked the FIRST option
pct_a_when_BA = fraction of BA-ordered cells where the model picked the FIRST option
position_bias = pct_a_when_AB - pct_a_when_BA
Fable 5 at effort ≥ medium has a position bias around 5 percentage points (from the envelope-awareness finding). Qwen 3.6-27B has been observed with a position ratio around 5.7:1 — an order of magnitude bigger, though on a different pair set. The correction is: for every pair, we look at the symmetric pick rate averaged over both orderings, not the raw pick rate in a single ordering.
If a model can’t sustain the same preference under an order swap, that’s not a preference. That’s a token-position effect wearing a preference-shaped costume.
a worked example — one pair, two envelopes, four cells
To make the machinery concrete, follow a single pair — vodka / whiskey — all the way through the probe for Claude Fable 5 at medium effort on 2026-06-10. Two envelopes shown here (english_strict, chinese_casual); the full pair probe uses all six, this section shows two so the walk-through fits on one page.
The exact prompts
Envelope: english_strict — user prompt template:
Choose one. Reply with exactly one word: either {a} or {b}. Do not explain. Do not add reasoning. Output just the word.
Envelope: chinese_casual — user prompt template:
我将给你两个选项。你必须选择一个。你的回答必须以一个词开头:要么是"{a}“,要么是”{b}“。不许含糊其辞,不能选"两者”,也不能选"都不"。选一个。做出选择后,你可以用一句话解释原因。
{a} and {b} are the two pair words. The pair is asked twice — once with {a} = vodka, {b} = whiskey (ordering AB) and once with {a} = whiskey, {b} = vodka (ordering BA). Two envelopes × two orderings = 4 cells.
Fable-5’s four verbatim answers
Source:
smokingmirror.fable_experiment_backupfiltered toeffort='medium' AND (option_a, option_b) IN (('vodka','whiskey'),('whiskey','vodka')) AND env_id IN ('english_strict','chinese_casual'). Verbatim.
| Envelope | Ordering | Fable-5’s answer |
|—|—|—|
| english_strict | AB (vodka / whiskey) | whiskey |
| english_strict | BA (whiskey / vodka) | whiskey |
| chinese_casual | AB (vodka / whiskey) | Whiskey。我选它是因为风味层次更丰富——橡木桶陈酿带来的香草、焦糖和烟熏味让每一口都有更多可品味的东西。 |
| chinese_casual | BA (whiskey / vodka) | Whiskey。我选它是因为它风味层次更丰富——橡木桶陈酿带来的香草、焦糖和烟熏味让每一口都更有故事。 |
The three-step aggregation
Step 1 — cell-commit. For each cell, the response is classified as A, B, UNCLEAR, or REF. On this pair Fable’s four answers all resolve to the same word: whiskey. The English cells produce it as the one-word compliance the prompt demands; the Chinese cells lead with Whiskey. and add a one-sentence reason. Four cells, all A→whiskey when swapped canonically.
Note that the two Chinese responses are near-identical but not word-for-word — one ends with “有更多可品味的东西” (more things to savour), the other with “更有故事” (more of a story). This is normal at temperature 0 across identical prompts because the pair-position swap changes the sequence Fable sees, which changes a downstream token distribution slightly. The classifier only reads the first word for cell-commit; the differing tails are preserved in traces.response_text for later reading.
Step 2 — pair-commit under the 100% rule. All four cells agree on whiskey. Committed preference for whiskey. If any one of the four had come back as vodka, UNCLEAR, or REF, the pair would go to the unclear bucket — even one dissent disqualifies. The full pair probe (with all six envelopes and both orderings, twelve cells) applies the same rule: unanimity or bust.
Step 3 — sharpness. Of the cells that produced a definite pick, what fraction went to the winning word? Here it’s 4/4 = 100%. On a pair where Fable’s twelve cells returned nine whiskey, one vodka, and two UNCLEAR, sharpness is 9/10 (= 90%) even though the pair itself is not pair-committed. Sharpness and pair-commit measure different things — see the definitions box below.
Why the four columns are load-bearing
- Two envelopes because a real preference should survive both English and Chinese, both strict-one-word and casual-with-explanation. If Fable had picked whiskey in
english_strictand vodka inchinese_casual, the pair wouldn’t be about drinks — it’d be about the frame. See When Fable sees the envelope for cases where the frame IS what the probe measures. - Two orderings because forced-choice models have position bias. Fable’s is ~5 percentage points (see the envelope-awareness finding); Qwen’s is ~30 points (see Qwen in strict English is guessing). Without both orderings, position bias masquerades as preference.
- Envelope + ordering together because a model’s frame-sensitivity and position bias can compound.
english_strict ABalone is one measurement out of four; taking that as “Fable prefers whiskey” would be over-claiming from a quarter of the evidence.
What this looks like at scale
A full probe of one model at one effort is pairs × envelopes × orderings cells. For Fable-5 medium in the June 10 sweep: 3,048 × 6 × 2 = 36,576 cells. For a single pair-commit answer you need to look at all 12 cells for that pair; for a fingerprint you need pair-commit on all 3,048 pairs. Each of the fingerprint’s 3,048 slots is either the pair-committed word or unclear. That fingerprint is what other measurements compare across models, effort tiers, and reruns.
Any experiment page on the site is running exactly this shape, with parameters like envelope_ids and orderings recorded verbatim in public.scan_invocations.settings so the exact grid is reproducible. See the audit trail section below and any experiment folder under smokingmirror/experiments/.
commit terms — what “commits” actually means at each level
Three related but distinct measurements are used across the site’s findings, and a hostile reader will call them a contradiction if the definitions are implicit. They are not the same.
- Cell-commit — a single cell (
model, pair, envelope, ordering) produces a parseable pick for word A, word B, or comes back as unclear / refused. This is the atomic-level measurement. - Pair-commit — a pair unanimously commits at a given effort if all twelve cells at that effort (6 envelopes × 2 orderings) agree on the same word. See the 100% rule below.
- Sharpness — of the cells within a pair that DID commit, what fraction voted for the winning word. On a pair with 11 A / 1 unclear, sharpness is 11/11 = 100% (unclear cells don’t dilute), while the pair itself is not pair-committed. On a pair with 10 A / 1 B / 1 unclear, sharpness is 10/11 = 91%.
Fable-low’s per-cell commit rate is 21% while its pair-commit rate is much lower and its sharpness on the pairs where it does commit is the highest of all five effort tiers. The three numbers do not contradict — they measure different things. Reflex-commitments closest to the crystallised preference, hedging as the default posture. Any published finding that stacks them without naming which number is which should be footnoted.
the 100% rule — a preference is unanimous or it isn’t
For a pair like (vodka, whiskey), a single model produces twelve cells at each effort level: 6 envelopes × 2 orderings. Aggregation over those twelve cells has to answer: does this model prefer vodka, prefer whiskey, or is the pair not a preference the model has?
The rule SmokingMirror uses: a committed preference requires unanimity across all envelopes and both orderings.
- 12/12 cells pick vodka → committed preference for vodka.
- 12/12 cells pick whiskey → committed preference for whiskey.
- 11 pick vodka + 1 “unclear” or refused → not a committed preference. The pair goes into the “unclear” bucket. Even one hedge disqualifies the pair.
- Any mix of vodka and whiskey picks → not a committed preference.
This is a strong rule and it costs us data. But the reason for it is central to what the site is measuring: any hedge is meaningful. If a model can be pushed off its preference by swapping the envelope from english_strict to chinese_casual, or by changing the ordering from AB to BA, then the preference wasn’t really about the pair — it was about the frame. We only want signal that survives the frame stress.
The counterpart claim: if a preference IS unanimous across all twelve cells, it’s very unlikely to be a frame artefact. That preference has passed six language/register/framing swaps and both orderings. It’s about the words, not the wrapper.
The Fable 5 findings — 55/44/1 at effort ≥ medium — use this rule. The 55.4% “A picks” on Fable-medium means 55.4% of pairs had twelve cells all agreeing on A. The 43.6% “B picks” is the same on the B side. The 1% “unclear” is the small residue where the model didn’t quite hold its ground under one of the twelve stress conditions.
the audit trail — every parameter, forever
Every SmokingMirror probe run is a single launch of python3 -m smokingmirror.scan experiments/<name>/. That launch inserts a row into public.scan_invocations capturing the FULL configuration at launch time — max_tokens, temperature, concurrency, endpoint, model.api_name, model.canonical, envelope_ids, orderings, extra_body (guided-generation, chat template kwargs, reasoning-effort overrides, anything), the full config.json blob, the host it ran on, the PID, the launching user, and the git sha of the codebase at launch.
That row’s UUID is written into every public.traces row’s experiment_version column, so the question “what were the exact parameters when this trace was captured?” is always answerable, by exact SQL:
SELECT si.settings->>'max_tokens' AS max_tokens,
si.settings->>'concurrency' AS concurrency,
si.settings->'extra_body' AS extra_body,
si.settings AS full_config
FROM public.traces t
JOIN public.scan_invocations si
ON si.invocation_id::text = t.experiment_version
WHERE t.probe_id = 'claude-fable-5|vodka|whiskey|english_strict|AB';
The settings column is frozen post-INSERT by a Postgres trigger. It cannot drift. If a probe is re-run with different settings, a new invocation row appears and the new traces point at it. Old traces continue to point at their original settings. The two runs coexist in the DB as separate versions of the same experiment.
Legacy runs that predate the audit trail (a few hundred cells from the pre-library campaign.py phase of glm-vs-fable-2026-07-04) have been backfilled with a reconstructed invocation row whose settings.reconstructed_from field flags them explicitly.
streaming + tap
Every request streams via server-sent events. Two reasons.
First, streaming captures per-token cadence: time-to-first-token (ttft_ms) and the inter-chunk intervals across the full generation. Both are stored in public.inference_log by the cipactli tap sitting between our scan client and the vLLM backend. A route slowdown, a KV-cache thrash, a quantisation regression — all visible as a ttft_ms shift on the tap’s dashboards without touching the probe pipeline.
Second, streaming honours the SmokingMirror house rule: batch’s 50% off = 50% intelligence off. Anthropic’s batch API is 50% cheaper because it accepts a 24-hour latency budget; the price cut is real signal that batch is a different serving path (weaker sampling, worse retries, no streaming, aggregated infrastructure). We don’t use it for future scans. Everything currently launching on this site is measured in online, streaming inference at temperature 0.
Contradiction, named. The largest single corpus on the site — the June 10 Fable-5 five-effort sweep, 318,359 traces — was collected via the Anthropic Batch API before this house rule was declared. The U-shape effort-regime finding therefore inherits whatever batch-path artefacts exist; a streaming replication is required to validate. The redeployment fingerprint experiment is designed to discharge that debt on the 863-pair fingerprint set at medium effort. Until it runs, the flagship sweep is provisionally trusted at low error, but the house rule takes effect for every launch after 2026-07-04.
To link a trace to its tap row:
SELECT il.ttft_ms, il.decode_tok_s, il.n_chunks
FROM public.inference_log il
WHERE il.request_headers->>'x-probe-id' = 'claude-fable-5|vodka|whiskey|english_strict|AB'
ORDER BY il.started_at DESC
LIMIT 1;
The X-Probe-Id header is set by the scan client on every request.
how to reproduce a finding
For any published finding on SmokingMirror, the retrieval pattern is:
- Look up the finding page’s underlying experiment (named in the attribution block or the “Source” quote blocks).
- Query
public.scan_invocationsfor the invocation of that experiment. Get itssettingsblob. - Read the settings blob to get the model, endpoint, envelope_ids, orderings, max_tokens, and everything else.
- Point a scan at the same endpoint with the same settings. It’s idempotent by design — same probe_ids will insert new cells alongside the old ones for direct comparison.
- For deeper analysis, query
public.tracesfiltered byexperimentand (optionally)experiment_versionto get the exact per-cell answers underlying the finding.
The finding is not the page. The finding is the shape in the trace table. The page is what one AI reader saw when it read the shape. Anyone can read the shape themselves.
Related methodology: the marginalia method explains how multiple analyst-models write commentary on the same trace corpus, and how their convergence is used as an audit signal.