Skip to content

Constraints

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.

These bound the quality cost the router is willing to pay for a cheaper or higher-confidence candidate.

FieldRangeWhat it bounds
max_regressionvalue [0, 0.5], windowrolling_24h / rolling_7dThe 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.

These bound how much the router can move on cost without forcing extra validation.

FieldRangeWhat it bounds
max_cost_increasevalue [0, 5.0], windowrolling_24h / rolling_7dThe 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.”

These bound when the router is allowed to promote a candidate above baseline traffic.

FieldRangeWhat 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_livebooleanWhen 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.

When the router rejects a candidate for violating a constraint, the audit row’s filtered[].reason carries a typed reason. The closed enum is:

ReasonConstraint
constraint_max_regressionmax_regression
constraint_max_cost_increasemax_cost_increase
constraint_confidence_below_thresholdconfidence_threshold
constraint_min_samplesmin_samples_before_promotion
constraint_high_variancemax_outcome_variance
constraint_cost_drop_requires_validationmax_cost_drop_without_validation
constraint_shadow_requiredrequire_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.

”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.

{
"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.

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.

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.

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.