The One-Sided Signal

Most cost-aware routing research trains as if every request is labeled good or bad, but real user feedback shows up for roughly one request in five. It also skews toward complaints, not praise. A June 2026 paper from TU Munich and the University of Exeter, SLARouter, builds a routing policy around that exact asymmetry, a satisfaction predictor standing in for missing labels, a virtual queue tracking SLA violations under a Lyapunov drift-plus-penalty bound, and exploration that decays as the predictor earns trust, reporting up to 2.2x lower operating cost than existing routing baselines while holding a formal quality guarantee across 11 benchmarks with no per-workload tuning. This post breaks down the mechanism and ships a minimal version to prototype.

Published 2026-09-08 by Dor Amir on the Nadir blog.

Filed under Routing & Cascades.

Every routing paper on this blog eventually runs into the same assumption: you know, for every request, whether the model got it right. Production doesn't work that way. Most users never rate a response. The ones who do are the ones who were unhappy. A router trained as if every request came back labeled "good" or "bad" is trained on data that doesn't exist. A June 2026 paper from TU Munich and the University of Exeter, SLARouter, builds a routing policy that assumes the opposite from the start: feedback arrives for maybe one request in five, it's one-sided (you mostly learn about failures, not successes), and the router still has to hold a formal quality guarantee, a Service Level Agreement, on every request in between. It reports up to 2.2x lower operating cost than existing routing baselines while doing it. This post is about the mechanism that makes that possible, and what a minimal version looks like if you wanted to prototype it yourself.

The feedback you don't have.

Most published cost-aware routing work trains offline, on a labeled dataset where every (query, model, outcome) triple is known. That's a reasonable way to build a benchmark. It's not how a production system sees the world. In production, a satisfaction signal, a thumbs-down, a regenerate click, a support ticket, shows up for a small slice of traffic, and it skews toward the requests that went badly. The requests that went fine mostly generate silence, not a labeled positive.

This blog has already covered why that same asymmetry breaks router calibration: a confidence score trained without knowing the true failure rate looks fine on paper and fails quietly in production. SLARouter's contribution is narrower and more useful than "route better": it's a way to keep a formal cost-quality guarantee standing on a feedback stream that's sparse and biased toward bad news, instead of assuming the labeled dataset a benchmark gives you for free.

How SLARouter closes the loop.

The mechanism has three moving parts, and none of them require retraining offline between deployments.

A satisfaction predictor. A lightweight multi-label classifier estimates, per request, how likely the response is to satisfy the user. It's trained continuously on whatever labels actually arrive, not on a fixed offline set.

A virtual queue. Instead of tracking SLA compliance as a single running average, SLARouter keeps a queue that accumulates when the guarantee is being violated and drains when it's being met. The queue, not a fixed threshold, is what the policy reacts to, which is what lets the guarantee hold even when the input signal is noisy.

Exploration that decays. Early on, the router samples across the model pool more freely to learn the predictor faster. As the predictor matures, exploration probability decays and the policy shifts toward the cost-optimal choice it now has evidence for.

Flow diagram of SLARouter's routing loop: a request is routed by an explore/exploit policy, the response is served, roughly 20% of requests return real user feedback while the remaining 80% fall back to the satisfaction predictor's score as a proxy, both feed a virtual queue tracking SLA violations under a Lyapunov drift-plus-penalty bound, and the updated queue state shifts the next request's exploration probability further toward exploit.
Flow diagram of SLARouter's routing loop: a request is routed by an explore/exploit policy, the response is served, roughly 20% of requests return real user feedback while the remaining 80% fall back to the satisfaction predictor's score as a proxy, both feed a virtual queue tracking SLA violations under a Lyapunov drift-plus-penalty bound, and the updated queue state shifts the next request's exploration probability further toward exploit.

The detail worth sitting with is the middle branch. When real feedback doesn't arrive, the queue doesn't just skip the update, it substitutes the predictor's own score as a proxy. That's the one-sided-feedback problem solved directly rather than worked around: the system doesn't wait for ground truth to keep the guarantee moving, it uses its best current estimate and lets the theory (a Lyapunov drift-plus-penalty bound) say how much error that introduces.

The target moves with the task.

The paper doesn't test one SLA target across the board. Across 11 benchmarks, from GSM8K and MMLU to ARC, BoolQ, GPQA, and ACPBench, the satisfaction target SLARouter is asked to hold ranges from 45% to 96%, calibrated to what's actually achievable on that task with the Qwen 3.5 model pool (2B, 9B, 35B, 122B) it routes across.

Bar chart showing the satisfaction target SLARouter is asked to hold, by benchmark: 80% on ARC Easy, 65% on ACPBench, 52% on ARC Challenge, and a 45%-to-96% spread across the remaining benchmarks in the 11-benchmark suite.
Bar chart showing the satisfaction target SLARouter is asked to hold, by benchmark: 80% on ARC Easy, 65% on ACPBench, 52% on ARC Challenge, and a 45%-to-96% spread across the remaining benchmarks in the 11-benchmark suite.

That's a design choice worth borrowing on its own, independent of the routing algorithm underneath it: a single global "95% satisfaction" SLA is meaningless if even the best available model can't clear it on the hardest slice of your traffic. Setting the guarantee per task, per difficulty band, is what makes the guarantee enforceable instead of aspirational.

The numbers.

MeasurementResult
Cost reduction vs. existing routing baselinesUp to 2.2x, while holding SLA compliance
Feedback rate tested0.2 (one in five requests returns real feedback)
Model poolQwen 3.5 family: 2B, 9B, 35B, 122B
Benchmark suite11 datasets: MMLU, GSM8K, ARC Challenge, ARC Easy, BoolQ, GPQA, SciQ, LAMBADA, WinoGrande, SocialIQa, ACPBench
Tuning required per benchmarkNone; theoretical guarantee holds without per-workload calibration

Source: Woisetschläger, Mammadli, Zhang, Wang, "Cost-Optimal LLM Routing with Limited User Feedback under User Satisfaction Guarantees," arXiv:2606.19376, submitted June 12, 2026

The "no per-benchmark tuning" line is the part that separates this from a routing paper that just reports a good number on its own benchmark. RouterArena's oracle-gap finding and LLMRouterBench's result that most published routers lose to a static baseline off their home turf both make the same point from the failure side: a router that needs re-tuning per workload to hit its headline number isn't a router you can trust on traffic it hasn't seen. A formal guarantee that holds across 11 benchmarks without retuning is a different, stronger claim.

A minimal version to prototype.

The full system trains a classifier and solves a constrained optimization over the Lyapunov bound. The part worth prototyping first is smaller: a virtual queue, and exploration that decays as the queue tells you the predictor is trustworthy.

import random

class SLAQueue:
    """Tracks SLA violations as a virtual queue instead of a running
    average. Grows when the guarantee is missed, drains when it's met."""

    def __init__(self, target_satisfaction: float, drain_rate: float = 0.05):
        self.target = target_satisfaction   # e.g. 0.65 for a harder task
        self.queue = 0.0
        self.drain_rate = drain_rate

    def update(self, satisfied: float):
        """satisfied: 1.0/0.0 from real feedback, or the predictor's
        probability score when no feedback arrived this request."""
        violation = self.target - satisfied
        self.queue = max(0.0, self.queue + violation - self.drain_rate)
        return self.queue


class ExploreDecayRouter:
    """Routes across a cost-ordered model pool. Exploration probability
    decays as the queue confirms the predictor is holding the target."""

    def __init__(self, models: list[str], queue: SLAQueue, decay: float = 0.999):
        self.models = models  # cheapest first
        self.queue = queue
        self.explore_p = 0.3
        self.decay = decay

    def route(self, predict_satisfaction) -> str:
        self.explore_p *= self.decay
        if random.random() < self.explore_p or self.queue.queue > 1.0:
            # explore, or the queue says the cheap tier is under-delivering
            return random.choice(self.models)
        # exploit: cheapest model the predictor believes clears the target
        for model in self.models:
            if predict_satisfaction(model) >= self.queue.target:
                return model
        return self.models[-1]  # nothing cleared the bar, go to the top

    def observe(self, real_feedback: float | None, predicted_score: float):
        signal = real_feedback if real_feedback is not None else predicted_score
        self.queue.update(signal)

The mechanism that makes this different from a plain epsilon-greedy bandit is self.queue.queue > 1.0 forcing exploration back open. A bandit that's decayed its exploration to near zero and then starts silently missing its target has no way to notice; a queue that's allowed to grow when violations accumulate gives the policy a reason to look again, which is the practical form of the paper's theoretical guarantee.

What this doesn't solve.

The benchmark suite is public QA and reasoning tasks scored against a model pool the authors controlled end to end; production traffic is messier, and a satisfaction predictor trained on one model roster needs re-validation, not just re-weighting, when the roster changes. A 0.2 feedback rate is also a specific, tested point, not a floor; the paper notes the system degrades gracefully at lower rates by leaning more on the predictor, but a predictor with too little ground truth to correct against is a predictor that can drift confidently wrong, the exact failure mode this blog covered in router calibration work. And a virtual queue's whole guarantee is asymptotic: early in a cold start, before the predictor has enough signal, the theory doesn't promise the SLA holds turn by turn, only that the time-averaged violation converges toward the paper's stated error floor.

What to check before you build one.

Conclusion.

Most routing research quietly assumes a dataset that production never hands you: complete, timely, symmetric feedback on every request. SLARouter's real contribution isn't the 2.2x number, it's building the guarantee around the feedback stream that actually exists, sparse, one-sided, and biased toward complaints, using a virtual queue and a decaying exploration policy instead of a fixed threshold that silently breaks when the input data doesn't match the paper it came from. The practical takeaway travels even if you never touch the Lyapunov math: calibrate your quality bar per task instead of globally, and build your monitoring around the assumption that most of your traffic will never tell you how it went.


Sources: [Woisetschläger, Mammadli, Zhang, Wang, "Cost-Optimal LLM Routing with Limited User Feedback under User Satisfaction Guarantees," arXiv:2606.19376, submitted June 12, 2026](https://arxiv.org/abs/2606.19376). [Confidently Wrong: LLM Router Calibration](/blog/confidently-wrong-llm-router-calibration). [The Oracle Gap: RouterArena](/blog/routerarena-llm-router-benchmark-oracle-gap). [LLMRouterBench: Most Routers Fail Baseline](/blog/llmrouterbench-acl-2026-most-routers-fail-baseline). [FrugalGPT Cascade Routing: Implementation Guide](/blog/frugalgpt-cascade-routing-implementation-tutorial).

More on routing & cascades

What Nadir is

Nadir is an LLM router. Nadir sends each prompt to the cheapest model that can handle it. A small decision model reads each prompt against your model menu and picks the lowest-cost model it expects to handle it. When the decision model is uncertain or unavailable, the request goes to the strongest model on the menu. The routing step makes no call to your model provider.

Nadir runs two ways. The decision API returns a model, reasoning-effort, cache, context, and policy recommendation without calling a model provider, beside the gateway you already run. That is how a shadow-mode evaluation works, and its projected savings stay advisory. The OpenAI compatible managed proxy executes the route, and migration is a two-line change: point the base URL at api.getnadir.com and set model to auto. On that path an optional verifier can score a complete non-streaming answer and escalate to a stronger model when it misses the configured bar. Streaming bypasses post-generation verification. BYOK is supported on every tier.

For coding agents, Nadir Auto is a terminal launcher for Claude Code or Codex that asks Nadir for the main-session model before each turn, with your configured model as the fallback and cost ceiling. Delegation integrations for Claude Code, Codex, or Cursor instead recommend a model tier for subagent work, and the agent decides; the Claude plugin can also move a main-thread turn to Haiku when Nadir's cache check prices it cheaper, unless its switch_main_model option is off. Either way inference runs on your own provider account, so Nadir holds no provider keys on that path.

What the numbers are, and what they are not

Nadir publishes each evaluation with its scope. On checkable code, run-check-escalate solved 392 of 395 common HumanEval and MBPP problems (99.2%), graded by running the canonical tests; that applies only to tasks with runnable deterministic tests. Nadir-Tumbler posts an arena_score of 72.3 on RouterArena's public scorer, 5th of 23 routers, which measures the routing decision on RouterArena's own model pool. A reference-assisted RouterBench evaluation over 11,420 held-out triples produced a 60% lower projected cost than always-Opus with about 98% retained quality and a 1.7% catastrophic-route rate. That experiment gave the verifier the expensive-model reference answer, which production does not have, so it is a research ceiling and not the deployed path.

None of these is a production guarantee, a universal savings rate, or a forecast for any particular workload. Customer savings are reported from measured execution against a declared baseline, and customer quality only from outcome-labelled traffic. Projected savings and realized savings are separate artifacts and are never blended.

Design-partner program

Three rungs, picked by risk appetite. Rung 0 Shadow runs advisory decision calls alongside live traffic and returns a projected receipt, with nothing in the request path changed. Rung 1 Hosted is the two-line swap on a production slice and returns a realized receipt. Rung 2 On-prem is a supervised six-week proof of concept inside the partner's VPC, where no prompt, response, or usage reaches Nadir. The commitments are the same at every rung. Apply for a rung directly: Rung 0 Shadow, Rung 1 Hosted, or Rung 2 On-prem. Not sure which fits? Start at getnadir.com/contact/?reason=design-partner.

Licensing

NadirClaw is the self-hosted core, source-available under the PolyForm Noncommercial License. Source-available is the correct label; NadirClaw is not open source. Nadir Route's hosted plan has no base fee and charges a variable fee only on measured savings from requests Nadir executed.

Pages on this site

Machine-readable summaries of this site: llms.txt and llms-full.txt.