Skip to content

feat(tokens): add calibrate_perplexity_threshold for the OT.4 perplexity seam - #445

Merged
fu351 merged 2 commits into
DobermanCore:mainfrom
AmirF194:fix/234-calibrate-perplexity-threshold
Aug 27, 2026
Merged

feat(tokens): add calibrate_perplexity_threshold for the OT.4 perplexity seam#445
fu351 merged 2 commits into
DobermanCore:mainfrom
AmirF194:fix/234-calibrate-perplexity-threshold

Conversation

@AmirF194

@AmirF194 AmirF194 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Slice

What this PR does

TokenChannelDetector (src/doberman/engine/detectors/token_channels.py) accepts an opt-in
perplexity_fn seam for the statistical OOD token channel, but nothing in the tree could pick a
perplexity_threshold from real data. Issue #235 (the reference windowed scorer) names this as
its blocker: "Blocked by #234 (the calibration helper lands first)".

Adds doberman.tokens.calibrate_perplexity_threshold(benign_scores, target_fpr), returning the
nearest-rank empirical (1 - target_fpr) quantile of a benign score corpus: the smallest scored
sample at or above that rank. Nearest-rank rather than interpolated, so the returned threshold is
a value that actually occurred in the corpus, at the cost of granularity no finer than
1 / len(benign_scores): the measured false-positive rate can exceed target_fpr by up to one
sample (n=20, target 0.10 measures 0.15 in the worst case). Fails closed: raises ValueError on a
target_fpr outside (0, 1), fewer than 20 benign scores, or any non-finite score, since none of
those can honestly promise the requested false-positive rate.

Tests added (run in CI)

  • test_calibrate_perplexity_threshold_meets_target_fpr, ..._ignores_input_order,
    ..._rejects_bad_target_fpr, ..._rejects_too_few_samples, ..._handles_saturated_scores,
    ..._rejects_non_finite_scores
    (tests/unit/test_tokens_scanner.py), all with fixed, seed-free score arrays per your note on
    the issue.
  • test_calibrated_threshold_escalates_gcg_suffix_and_passes_benign
    (tests/unit/test_detector_token_channels.py): wires a calibrated threshold through
    TokenChannelDetector with a stub scorer against a real GCG-style adversarial suffix from
    tests/redteam/fixtures/gcg_suffixes.txt (AUTH) and an ordinary benign string (PASS).
  • pytest -n auto --cov=doberman --cov-report=term-missing --cov-fail-under=80: 90.7% total,
    tokens.py 96%, no line this PR adds is in the missing set.
  • ruff check ., ruff format --check ., lint-imports,
    python -m tools.parity.generate_parity --check: all clean.

Not checked: the four real-Tk tests in test_gui_prompter.py fail in my Docker verification
image (libtk8.6.so missing from python:3.13-slim), identically on a clean main checkout in
the same image; unrelated to this change and not introduced by it.

Public-release safety (doberman-core only)

  • Contains nothing from the "not allowed" list: no enterprise/hosted code, no proprietary detection, no customer data, no secrets, no commercial-license code
  • Core still builds/tests/runs with NO enterprise package installed

Security checklist

  • Fails closed on error / uncertainty: bad target_fpr or too little data raises rather than returning a threshold that looks calibrated but is not
  • No secret, full file, or unredacted prompt logged or committed
  • Any guardrail/learning change is raise-only (no silent loosening): this is a pure stats helper with no side effects; it does not wire into any detector or change default behavior on its own
  • Every BLOCK/AUTH carries reason codes + a human explanation: unchanged, this PR adds no new call site into the detector
  • doberman-core does not import doberman_enterprise

Edge cases covered / Deviations from plan / Risks introduced


Written with AI assistance (Claude Code); every command in the verification section above was
run this session before pushing.

Fixes #234

…ity seam

TokenChannelDetector accepts an opt-in perplexity_fn seam, but nothing in
the tree could calibrate a perplexity_threshold from real data. Adds
calibrate_perplexity_threshold(benign_scores, target_fpr), the nearest-rank
empirical (1 - target_fpr) quantile of a benign score corpus. Fails closed
on a target_fpr outside (0, 1) or fewer than 20 benign scores.

Fixes DobermanCore#234
@AmirF194
AmirF194 force-pushed the fix/234-calibrate-perplexity-threshold branch from d085a85 to f1b9b49 Compare August 24, 2026 14:09
@fu351

fu351 commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Thanks @AmirF194, this is a strong first PR. The boundary tests hit the exact 19/20 and (0,1) edges, and reusing the real GCG fixture for the wiring test proves the seam against the actual attack class instead of a toy string. Two things before it merges:

  1. A NaN in benign_scores sails through sorted() and can become the returned threshold, and downstream every score >= nan comparison is False, which silently disables the perplexity channel. Add a finiteness check that raises ValueError on any non-finite sample, same fail-closed shape as your other two guards, plus a test with a NaN in the corpus.
  2. The PR text promises "at most target_fpr scores at or above the threshold", but nearest-rank overshoots that by up to 1/n (your N=20, fpr=0.10 case measures 0.15). The code matches the issue spec, so keep the math and fix the wording to say the measured FPR can exceed the target by up to one sample.

The red 3.11 leg wasn't yours, river 0.26.0 broke that leg repo-wide and the pin is merged as #452. The merge conflict it created on your branch is also mine to deal with, I'll land it from here once your two changes are in.

…plexity_threshold

A NaN in benign_scores sails through sorted() unnoticed; every downstream
score >= threshold comparison against it is False, which silently
disables the perplexity channel instead of raising like the two existing
guards. Reject any non-finite sample the same way.

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
@AmirF194

Copy link
Copy Markdown
Contributor Author

Both addressed and pushed (767d436).

  1. Added a finiteness check right after the sort, same fail-closed shape as the other two: any NaN or inf in benign_scores now raises ValueError instead of silently disabling the channel. New test with both a NaN and an inf sample.
  2. Reworded the docstring and PR body to state the measured overshoot (up to one sample, 0.15 in your 20/0.10 example) instead of promising "at most target_fpr".

Left the merge conflict from #452 to you as you said. Ran the tokens test file plus ruff/format/lint-imports clean in Docker; the full suite is timing out in my sandbox for reasons unrelated to this change, so I have not re-confirmed the full coverage run this round.

@fu351
fu351 merged commit 8050cb4 into DobermanCore:main Aug 27, 2026
6 checks passed
@fu351

fu351 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Merged, thanks @AmirF194! The follow-up commit that rejects non-finite scores is the part I like most, a NaN would have made every score >= threshold comparison false and quietly switched the perplexity channel off, and you caught that before I did. I broke the sort in calibrate_perplexity_threshold locally and test_calibrate_perplexity_threshold_ignores_input_order went red straight away, which is the kind of test I want here. Welcome to Doberman! If you'd like the next piece of this seam, #235 (level-6) is the windowed scorer that consumes the threshold you just calibrated, and #143 (level-4) is a similar wire-an-interface job if you'd rather something smaller first. Feel free to join the Discord if you have questions: https://discord.gg/Sfy5XGNqty

@AmirF194

Copy link
Copy Markdown
Contributor Author

Thanks for the review and the merge. The NaN edge case was the sharper catch, that channel would have gone dark with no error. Appreciate the pointers to #235 and #143, I'll take a look.

@AmirF194
AmirF194 deleted the fix/234-calibrate-perplexity-threshold branch August 27, 2026 02:08
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.

calibrate_perplexity_threshold: the model-agnostic half of the perplexity seam (OT.3)

2 participants