"Do not auto-accept" needs somewhere for the row to go. Two destinations cover most cases.
Escalate to a stronger model. This works when a bigger model plausibly fixes the specific errors the cheap model makes. Confidence-threshold escalation is a cascade: the cheap model answers, a scorer evaluates the answer, and escalation fires only below your cut, so nobody has to predict difficulty before seeing an answer (the escalation survey). The catch, shown by that task-dependence data, is that escalation assumes low confidence marks errors the larger model can correct (routing study). Validate the pairing on your labeled sample before you depend on it. If the strong model is right on fewer than most of the routed-error rows, you are buying latency and cost without buying safety, and the answer should be a person.
Route to a person. Human in the loop is the design pattern where uncertain predictions go to a human reviewer in real time, and the distinction from traditional QA is that the escalation trigger is the system's own uncertainty gate rather than a periodic audit (Roboflow on HITL, Appen on HITL design). A real HITL loop also feeds corrections back into training data, evaluation sets or the threshold itself, so the share of rows that need a person should trend down. If it never does, the cut or the model is wrong. Appen's comparison of human in the loop, human on the loop and active learning is worth reading before you commit: if what you actually want is a human supervisor who can override aggregate behavior rather than decide every routed row, that is human on the loop, and it needs a different operations model.
A third option is to re-run the cheap model with more context or a stricter prompt, but only if it demonstrably converges on the routed rows in your sample. Otherwise it is just a slower path to the same fallback.
Chaining the paths. Many teams want both. A workable chain: below the cut, first try the stronger model; if the stronger model's own confidence also falls below its cut, or if the row carries a risk flag, route to a person. This keeps reviewer volume down while guaranteeing that no low-confidence row is ever auto-accepted.
def route(row, cheap_result, strong_model, queue):
conf = cheap_result.confidence
if conf >= CUT_ACCEPT and not cheap_result.risk_flags:
return accept(row, cheap_result)
reason = "below_cut" if conf < CUT_ACCEPT else "risk_flag"
strong = strong_model.extract(row)
if strong.confidence >= CUT_STRONG and strong.status == "valid":
return accept(row, strong, fallback_reason=reason, path="strong_model")
queue.push(row, strong=strong, reason=reason + "+strong_low")
return pend(row)
Note what that function refuses to do: there is no branch that returns a result for a row whose only evidence is a below-cut cheap model answer.
If your fallback involves several providers, one consideration is whether you will need a single interface for the cheap model, the strong model and the embedding model used for similarity checks. That is what Quanta Inference is for: text, image, video, audio and embedding models through one OpenAI-compatible API key, prepaid and priced in US dollars. It does not change the design above; it reduces the glue code around it.