"""Why the primary classifier's answer was not used, as data. A classifier that declines to answer is the router's most consequential quiet failure: the request is then routed on a borrowed or guessed label. Before this module the reason existed only as a log line, and the number that decided it (how far below ``confidence_min`` the answer was) existed nowhere durable, so a threshold could not be tuned from the database. ``ClassifierRejected`` carries both out of the raise site, and the ``REASON_*`` strings are the stable codes stored in ``route_decisions.classifier_reject``. Stdlib only, and it imports nothing from this package, so ``local_decision`` can raise it without importing ``dispatcher`` (which imports ``local_decision``). """ from __future__ import annotations from typing import Final # The model answered, and the dispatcher declined the answer. REASON_BELOW_CONFIDENCE: Final = "below_confidence_min" REASON_BELOW_COVERAGE: Final = "below_coverage_min" REASON_NO_LOGPROBS: Final = "no_logprobs" # The model did not answer usefully. REASON_TIMEOUT: Final = "timeout" REASON_TRANSPORT: Final = "transport_error" REASON_PARSE: Final = "parse_error" REASON_PRIMARY_FAILED: Final = "primary_failed" # The model was never asked. Followed by the skip reason, e.g. # ``skipped_gaming_mode`` or ``skipped_backoff``. REASON_SKIPPED_PREFIX: Final = "skipped_" class ClassifierRejected(RuntimeError): """The classifier answered, but the answer was refused. A ``RuntimeError`` subclass on purpose: ``classify()`` already treats a ``RuntimeError`` from the primary as "the primary failed" and walks the cascade, so existing handlers keep working and only the data is new. The message text is unchanged from the plain ``RuntimeError`` it replaces, because it is what the ``fallback`` log line prints. ``confidence`` and ``coverage`` are whatever the raise site knew, and None where it did not: a coverage rejection happens before a confidence exists. """ def __init__( self, reason: str, message: str, *, confidence: float | None = None, coverage: float | None = None, ) -> None: super().__init__(message) self.reason = reason self.confidence = confidence self.coverage = coverage