Constraints
Overview
Section titled “Overview”Constraints are how a customer puts a leash on Floopy’s optimization. They are declarative — you set them in code via PUT /v1/constraints or in the dashboard at /routing/constraints. They are non-LLM — every constraint check is a numeric or boolean comparison, with no extra provider calls on the hot path. And they are auditable — every change is recorded with a before and after SHA-256 hash on the audit log.
In v2 the set is nine fields, grouped into three sections: quality limits, cost limits, and promotion gates.
When a candidate violates a constraint, it is filtered with a typed reason that surfaces on the audit row’s filtered[].reason and on the explanation text. There is no silent override.
The nine fields, by section
Section titled “The nine fields, by section”Quality limits
Section titled “Quality limits”These bound the quality cost the router is willing to pay for a cheaper or higher-confidence candidate.
| Field | Range | What it bounds |
|---|---|---|
max_regression | value [0, 0.5], window ∈ rolling_24h / rolling_7d | The rolling drop in composite quality vs. baseline the router will tolerate. |
max_outcome_variance | (0.0, 1.0] | The outcome variance the router will tolerate on a candidate before refusing to route to it. |
max_outcome_variance is new in v2. Strict-greater-than-zero — setting it to exactly 0.0 is rejected with 400 out_of_range_max_outcome_variance because that is unreachable in practice and almost certainly a misconfiguration.
Cost limits
Section titled “Cost limits”These bound how much the router can move on cost without forcing extra validation.
| Field | Range | What it bounds |
|---|---|---|
max_cost_increase | value [0, 5.0], window ∈ rolling_24h / rolling_7d | The rolling cost increase vs. baseline the router will tolerate. |
max_cost_drop_without_validation | (0.0, 1.0] | A fractional cost drop above this size is treated as suspicious — the router refuses to promote the candidate until shadow validation has signed off on it. |
max_cost_drop_without_validation is new in v2. The intuition: a candidate that suddenly looks 90 % cheaper than baseline is more likely to be a quality cliff than a free lunch. Setting max_cost_drop_without_validation = 0.5 says “if the savings story exceeds 50 %, prove it in shadow first.”
Promotion gates
Section titled “Promotion gates”These bound when the router is allowed to promote a candidate above baseline traffic.
| Field | Range | What it bounds |
|---|---|---|
confidence_threshold | [0.0, 1.0] | The minimum router confidence to route away from baseline. null or 0.0 disables the gate. |
min_samples_before_promotion | [1, 100_000] | The minimum number of historical samples on a candidate before it is allowed to win. Setting 0 is rejected. |
require_shadow_before_live | boolean | When true, no candidate can be promoted to live traffic on a route until that (provider, model) has at least one completed shadow experiment in the staleness window (default 30 days). |
min_samples_before_promotion, max_outcome_variance, max_cost_drop_without_validation, and require_shadow_before_live are the four new fields shipped in v2.
The staleness window for require_shadow_before_live defaults to 30 days and is configurable per workspace — contact support to adjust it.
Filtered reasons (the wire shape)
Section titled “Filtered reasons (the wire shape)”When the router rejects a candidate for violating a constraint, the audit row’s filtered[].reason carries a typed reason. The closed enum is:
| Reason | Constraint |
|---|---|
constraint_max_regression | max_regression |
constraint_max_cost_increase | max_cost_increase |
constraint_confidence_below_threshold | confidence_threshold |
constraint_min_samples | min_samples_before_promotion |
constraint_high_variance | max_outcome_variance |
constraint_cost_drop_requires_validation | max_cost_drop_without_validation |
constraint_shadow_required | require_shadow_before_live |
The order in which constraints are evaluated is fixed — when more than one constraint would have rejected the candidate, the audit shows the first one that fired. The order is: max_cost_increase, max_regression, confidence_threshold, min_samples_before_promotion, max_outcome_variance, max_cost_drop_without_validation, require_shadow_before_live.
Worked examples
Section titled “Worked examples””I want to cap quality regressions at 2 %, no exceptions.”
Section titled “”I want to cap quality regressions at 2 %, no exceptions.””{ "max_regression": { "value": 0.02, "window": "rolling_24h" }}Any candidate whose rolling 24-hour composite-quality delta vs. baseline drops more than 2 percentage points is filtered with reason: "constraint_max_regression". Everything else is up to the router.
”I want a candidate to have at least 50 historical samples before it can win.”
Section titled “”I want a candidate to have at least 50 historical samples before it can win.””{ "min_samples_before_promotion": 50}A candidate with n_samples < 50 is filtered with reason: "constraint_min_samples". The dashboard’s decision side-panel renders the underlying samples count from the evidence field so the customer can verify the gate.
”Refuse to switch to anything that looks 80 % cheaper without a shadow experiment first.”
Section titled “”Refuse to switch to anything that looks 80 % cheaper without a shadow experiment first.””{ "max_cost_drop_without_validation": 0.8, "require_shadow_before_live": true}Any candidate whose expected fractional cost drop vs. baseline exceeds 0.8 is filtered with reason: "constraint_cost_drop_requires_validation" unless it has a passing shadow experiment in the staleness window. The two constraints work together: require_shadow_before_live enforces that every promotion goes through shadow; max_cost_drop_without_validation enforces that specifically the suspicious-savings path goes through shadow even if the operator has not turned on the broad gate.
”Don’t pick a model whose recent outcomes are all over the map.”
Section titled “”Don’t pick a model whose recent outcomes are all over the map.””{ "max_outcome_variance": 0.4}A candidate whose rolling outcome variance exceeds 0.4 is filtered with reason: "constraint_high_variance". Useful for routes where consistency matters more than the occasional perfect answer.
”Don’t ever route away from baseline below 70 % confidence.”
Section titled “”Don’t ever route away from baseline below 70 % confidence.””{ "confidence_threshold": 0.7}The router falls back to baseline whenever confidence < 0.7 and emits reason: "constraint_confidence_below_threshold" for every other candidate. See Confidence methodology for what confidence measures.
”All of the above, at once.”
Section titled “”All of the above, at once.””{ "max_regression": { "value": 0.02, "window": "rolling_24h" }, "max_cost_increase": { "value": 0.10, "window": "rolling_24h" }, "confidence_threshold": 0.7, "min_samples_before_promotion": 50, "max_outcome_variance": 0.4, "max_cost_drop_without_validation": 0.8, "require_shadow_before_live": true}Constraints compose. The router runs every gate on every candidate; the first rejection wins.
Validation rules
Section titled “Validation rules”Every numeric constraint is is_finite() checked — NaN, +Inf, and -Inf are rejected with 400 out_of_range_<field>. The two unit-interval-half-open fields (max_outcome_variance, max_cost_drop_without_validation) are strict-greater-than-zero. min_samples_before_promotion is rejected outside [1, 100_000]. The body cap on PUT /v1/constraints is 4 KiB.
These bounds also live as CHECK constraints in the organization_constraints table, so a misconfigured row cannot end up in the database.
Audit trail
Section titled “Audit trail”Every successful PUT writes a row to constraint_changes with the before snapshot, the after snapshot, the actor_api_key_id, and the timestamp. The audit event metadata carries SHA-256 hashes of before and after, so the audit log itself does not duplicate the raw constraint values.
Plan gating
Section titled “Plan gating”All nine fields are Pro plan only. On Free plans the /routing/constraints page is rendered absent (not as a locked tab) and the API returns 403 plan_required.
See also
Section titled “See also”- GET /v1/constraints — read the current set.
- PUT /v1/constraints — write a new set.
- POST /v1/routing/explain — dry-run a candidate request against the current constraints.
- Decision Explanation — how constraint rejections are surfaced in human-readable prose.
- Confidence methodology — what
confidence_thresholdactually compares against.