# Gate routing on a proven quality floor

Add a min_quality_lcb floor to a routing selector so a cheaper tier is eligible only while its measured lower bound clears it.

Source: https://tokentriage.com/docs/calibration/gate-routing-on-quality

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';

Spend an active calibration as a routing constraint: a cheaper tier becomes eligible
**only while its calibrated quality lower bound clears a floor**. The floor is a
selector parameter, `min_quality_lcb`, and it gates *candidacy, not rank*. A tier below
the floor is not a cheap option, it is not an option.

## Prerequisites

- An **active** quality calibration for the metric and served-model you want to gate.
  See [Calibrate quality from your logs](/docs/calibration/calibrate-from-logs).
- A [routing policy](/docs/build/policy-language) using a `select` node (the floor is a
  parameter of a cost-aware selector).
- **If the metric is scored by an LLM judge, a standing promotion for that judge on that
  segment.** A judge's verdicts carry `reported` until they have been measured against human
  labels, and a `reported` observation licenses no bound for the floor to read. That precondition
  is a walk of its own: [Gate a rollout on a validated judge](/docs/judges/gate-a-rollout) runs it
  end to end, and [Why your number is reported, not measured](/docs/judges/basis-ceilings)
  explains what a promotion moves and what it leaves alone.

## 1. Add the floor to a selector

`min_quality_lcb` lives in a `select` node's `by.params`, beside the `quality_metric` it
reads under. This admits the cheapest tier whose calibrated lower bound clears `0.98` on
`output_nonempty`, and sends everything else to the frontier when none qualifies:

```yaml
routing:
  default_tier: frontier
  policy:
    version: 1
    root:
      rules:
        - id: extraction-floor
          to:
            select:
              from: { tiers: all }
              by:
                selector: cheapest_meeting
                params:
                  min_quality_lcb: 0.98        # a bound on a pass rate, in [0,1]
                  quality_metric: output_nonempty
              # A candidate that cannot meet the floor is not an option.
              # When none can, traffic goes here.
              on_empty: { tier: frontier }
      default: { tier: frontier }
```

<Callout type="key">
The floor is compared against the calibrated **lower confidence bound**
(`candidate.quality.lcb`), never the point estimate, because routing on a point estimate is the
named overconfidence anti-pattern, and naming `candidate.quality.mean` in a policy is a
compile *and* `tokentriage validate` error. `min_quality_lcb` gates **candidacy, not
rank**: it decides whether a tier is admitted at all, then the selector's normal cost
comparison ranks the survivors.
</Callout>

<Callout type="note">
`min_quality_lcb` / `quality_metric` are parameters of the cost/reward selectors —
`cheapest_meeting`, `epsilon_greedy`, and `weighted_objective`. Setting `quality_metric`
without `min_quality_lcb` is refused (no bound would be read), and a floor outside
`[0,1]` is refused. For the full selector grammar and where a `select` node sits in the
tree, see [the policy language](/docs/build/policy-language).
</Callout>

## 2. Validate and reload

Validate strictly, then reload the config. The router reads calibration off an immutable
snapshot published off the request path, so the floor takes effect on the next reload,
not mid-request:

```console
$ tokentriage validate --config tokentriage.yaml
```

## 3. Verify on a real decision

Send traffic and read a decision back. When the chosen candidate's quality was actually
consulted, the row records a `QualityConsult` and a `decision.predicted.*` block (the
metric, the tier, the calibration version, and the fact):

```console
$ tokentriage policy explain --req-id <req-id> --log ~/.tokentriage/decisions.jsonl
```

The selector's score table carries **every** candidate's quality verdict, not only the
admitted ones, because once the floor gates candidacy, the admitted-arm count is what an
off-policy propensity is computed over.

## 4. Read the two exclusion terms

Absence and below-floor are **different facts** that call for different operator actions,
and the score table keeps them apart:

- **`quality_below_min_lcb`**. The tier was *measured*, admitted to the comparison, and
  its calibrated bound fell below the floor. It carries the numbers.
- **`quality_absent:<reason>`**. The tier's quality *could not be read at all*, with the
  reason riding the term name (e.g. `quality_absent:segment_uncalibrated`,
  `quality_absent:below_min_n`). An absent tier is not scored as if it had failed.

<Callout type="warning">
**Fail-open everywhere.** Absence is disclosure and not a penalty: a tier the router
could not measure is excluded and disclosed, not marked as failing. With the feature
unused the dependency is inert: the join is skipped, every quality fact is absent, and
the decision context is byte-identical to a pre-feature build.
</Callout>

<NextSteps items={[
  { title: 'Quality-aware routing', href: '/docs/calibration/quality-aware-routing', description: 'The off-path fold, the served-model subject key, the five licensing conjuncts, and the LCB-not-mean rule in full.' },
  { title: 'The policy language', href: '/docs/build/policy-language', description: 'The five node kinds, the select grammar, and the selector params a floor is set on.' },
  { title: 'Calibration & evals vocabularies', href: '/docs/reference/quality-vocabularies', description: 'The closed enums: score-table terms, licensing refusal tokens, and the verdict vocabulary.' },
  { title: 'Gate a rollout on a validated judge', href: '/docs/judges/gate-a-rollout', description: 'The same floor when the metric is judge-scored, from confirming the promotion to reading the term that explains an excluded tier.' },
]} />
