Skip to content

feat: add NADIRCLAW_CLASSIFIER_STRIP_PATTERNS env var for classifier input cleaning - #77

Merged
doramirdor merged 2 commits into
NadirRouter:mainfrom
BunpGhost:feat/classifier-input-cleaner
Jun 30, 2026
Merged

feat: add NADIRCLAW_CLASSIFIER_STRIP_PATTERNS env var for classifier input cleaning#77
doramirdor merged 2 commits into
NadirRouter:mainfrom
BunpGhost:feat/classifier-input-cleaner

Conversation

@BunpGhost

Copy link
Copy Markdown
Contributor

Summary

Adds a new env var NADIRCLAW_CLASSIFIER_STRIP_PATTERNS that strips regex-matched blocks from user prompts before complexity classification. The LLM still sees the full, unmodified prompt.

Motivation

Agent frameworks (OpenClaw, Claude Code, NanoBot, Hermes, etc.) often wrap the human's actual prompt in a structured envelope - metadata blocks, memory context, system notes, tool call dumps. Without stripping, the classifier sees predominantly JSON schemas and technical text, inflating the complexity score even for trivial requests.

Changes

  • nadirclaw/settings.py - new CLASSIFIER_STRIP_PATTERNS property (default empty string = off)
  • nadirclaw/server.py - new _strip_classifier_input() function compiled lazily from the env var, wired into 3 call sites:
    • _smart_route_full() (the main routing path)
    • /v1/classify
    • /v1/classify/batch

Design

  • Zero overhead when unset: regex stays None, function is a no-op
  • Invalid regex logs a warning and is silently ignored (never crashes the server)
  • DOTALL flag is applied internally so patterns can match across lines
  • The LLM prompt is never modified - only the text sent to the classifier

Example env var

NADIRCLAW_CLASSIFIER_STRIP_PATTERNS=<envelope>.*?(?:</envelope>|\Z)|\[system note:.*?\]

…input cleaning

Agent frameworks (OpenClaw, Claude Code, NanoBot, Hermes, etc.) often wrap
the human's actual prompt in a structured envelope - metadata blocks, memory
context, system notes - that does not reflect the complexity of the request.
The classifier sees JSON schemas, memory dumps, and tool definitions,
inflating the score even for trivial requests like "hello".

Changes:
- settings.py: new CLASSIFIER_STRIP_PATTERNS property (env var, default empty = off)
- server.py: _strip_classifier_input() function + call sites in _smart_route_full,
  /v1/classify, and /v1/classify/batch
- Invalid regex logs a warning and is silently ignored (no crash)
- Zero overhead when unset: regex is None, function is a no-op
@doramirdor

Copy link
Copy Markdown
Collaborator

Thanks for this — the shape is right: opt-in, default-off, classifier-only (LLM still sees the full prompt), and fail-safe on invalid regex. The motivation (agent envelopes inflating the complexity score) is real and matches how OpenClaw/Claude Code wrap prompts. A few notes from an automated triage review before a maintainer takes it:

Substantive

  1. Empty-strip fallback. If a pattern is greedy enough to consume the whole prompt, _strip_classifier_input returns '', and the classifier then scores an empty string — which routes everything to the cheapest tier silently. Worth guarding:

    stripped = _strip_regex.sub('', text).strip()
    return stripped or text   # don't let an over-broad pattern empty the prompt

    This keeps a misconfigured pattern from quietly degrading routing quality.

  2. Tests. This touches the main routing path (_smart_route_full) plus both /v1/classify endpoints, but there are no tests. A small tests/test_classifier_strip.py monkeypatching NADIRCLAW_CLASSIFIER_STRIP_PATTERNS would lock in the contract: (a) unset → identity, (b) a pattern strips the envelope, (c) invalid regex → no-op + warning (not a crash), (d) the empty-strip fallback above. The repo's convention is tests-with-features (CI runs pytest tests/ --ignore=tests/test_server.py), and a fork PR's CI won't run until a maintainer approves the workflow.

Minor

  1. "Zero overhead when unset" isn't quite exact — because _compile_strip_regex() returns None when the env var is empty, _strip_classifier_input re-reads the env and re-checks on every call (the _strip_regex is None cache only sticks once a pattern compiles). Cheap, but if you want true one-shot init, cache with a sentinel (e.g. a module-level _compiled = False flag) rather than None.

  2. Tiny: the doc example pattern uses \Z inside the doc but the design note says DOTALL — worth a one-line note that you intentionally do not set re.MULTILINE, so \Z/$ behave as end-of-string.

None of these are blockers; (1) is the one I'd want before merge. Nice, self-contained change otherwise.

🤖 Automated triage review (claude code). A maintainer will make the merge call.

Addresses the one pre-merge blocker from review: an over-broad
NADIRCLAW_CLASSIFIER_STRIP_PATTERNS could consume the entire prompt,
leaving the classifier an empty string and silently routing everything
to the cheapest tier. _strip_classifier_input now returns the original
text when stripping empties it.

Adds tests/test_classifier_strip.py covering the contract: unset =>
identity, pattern strips envelope (incl. DOTALL across newlines), invalid
regex => no-op + warning, and the over-broad => fall-back-to-original
guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@doramirdor

Copy link
Copy Markdown
Collaborator

Pushed the pre-merge blocker fix and tests directly to this branch (maintainerCanModify was on) so it's ready to go — commit f9b4fe5:

  1. Empty-strip guard (the one blocker). _strip_classifier_input now returns the original prompt when an over-broad pattern strips it down to nothing, so a greedy NADIRCLAW_CLASSIFIER_STRIP_PATTERNS can no longer silently route every request to the cheapest tier:
    stripped = _strip_regex.sub('', text).strip()
    return stripped or text
  2. Tests — added tests/test_classifier_strip.py (7 cases, all green locally) covering the contract from the earlier review: unset → identity, pattern strips the envelope (incl. DOTALL across newlines), invalid regex → no-op + warning (no crash), and the over-broad → fall-back-to-original guard. The file follows the tests/test_fallback_chain.py import convention, so it runs under the repo's pytest tests/ --ignore=tests/test_server.py.

I approved the workflow run so CI validates this on the fork. The two minor notes from the prior review (one-shot init via a sentinel instead of re-reading the env when unset; the \Z / no-re.MULTILINE doc note) are left as optional polish — not blockers.

Once CI is green this is good to merge from my side. Thanks for the clean, self-contained change @BunpGhost.

🤖 Automated triage (claude code). A maintainer makes the final merge call.

@doramirdor
doramirdor merged commit 7912321 into NadirRouter:main Jun 30, 2026
3 checks passed
@BunpGhost

Copy link
Copy Markdown
Contributor Author

Thanks for the review and for applying the fixes directly, @doramirdor. The empty-strip guard and tests were solid catches. I'll wait for the next release to pull this in. Appreciate the help!

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.

2 participants