| Tool | Routing primitive | Quality estimator | Agent-aware | Cache | License / pricing |
|---|---|---|---|---|---|
| Martian | cost + quality (routing LLM) | dedicated router model, calibration-trained | yes · per-step | none (router only) | SaaS · per-token |
| OpenRouter | cost · latency · Uptime | vendor benchmarks, no per-step | partial · request-level | prompt-cache (vendor) | SaaS · +5% margin |
| LiteLLM | cost · latency · fallback chain | none (you bring it) | yes · via proxy hooks | Redis + Qdrant | MIT self-host |
| Portkey | cost · latency · quality · rate-limit | feedback-scored per-request | yes · trace-aware | semantic + exact | MIT + SaaS |
| Bifrost | cost · latency | none | no | none | MIT · Maxim AI |
| NotDiamond acq. by Fndr 2026 | quality (DSPy-routed) | per-prompt prompt-aware router | yes · call-level | none | SaaS |
| LangSmith Router | cost · latency · custom rules | optional judge fallback | yes · run-level | trace-based | MIT + SaaS |
| AutoMix paper, 2024 | cost · quality · self-verification | self-eval confidence | yes · per-step | none | MIT reference impl |
| FrugalGPT paper, 2023 | cost · quality · cascade | distilled scorer + threshold | partial | exact + semantic | MIT reference impl |
| LLM-Blender ACL 2023 | quality (ensemble) | pairwise ranker | no | none | MIT |
| GenAI-Router OSS, 2025 | cost · latency · quality | calibrated logreg + online | yes · step-type aware | semantic + exact + episodic | Apache-2.0 |
| GPTCache | (cache only) | n/a | via embedding | semantic + exact | Apache-2.0 |
| OpenRLHF · TRL | (offline eval, not router) | judge ensembles | yes | n/a | Apache-2.0 |
| Prometheus-Eval | (judge, not router) | fine-tuned judge LM | yes | n/a | Apache-2.0 |
LiteLLM or Portkey as the proxy + fallback backbone, GenAI-Router or NotDiamond as the policy brain, GPTCache as the cache layer, Prometheus-Eval as the judge for the quality estimator. Everything else is sugar.
# policy.yaml — inference router, 2026-08-08 # Target: −78% cost vs GPT-5-only, quality ≥ 0.81 on golden-200 router: name: "cost-quality-pareto-v3" estimator: "difficulty_logreg_v2" # trained on 38k labelled steps cache_first: true batch_window_ms: 50 speculative: true judge_sample_rate: 0.05 # 5% of steps scored by Prometheus budget_per_task_usd: 0.45 # hard ceiling, alarm at 80% models: - id: "gpt-5-nano" cost_in: 0.05 cost_out: 0.40 tier: "easy" - id: "gpt-5-mini" cost_in: 0.25 cost_out: 2.00 tier: "med" - id: "gpt-5" cost_in: 2.50 cost_out: 10.00 tier: "hard" cache: exact: enabled: true backend: "redis" ttl: 86400 semantic: enabled: true backend: "qdrant" sim: 0.95 ttl: 3600 episodic: enabled: true backend: "redis+sessionid" ttl: 300 rules: # evaluated top-to-bottom, first match wins - when: "cache.exact.match == true" route: "cache.hit" - when: "cache.semantic.sim >= 0.95" route: "cache.hit" - when: "step.type == 'verify' and tokens < 200" route: "gpt-5-nano" - when: "difficulty <= 0.35" route: "gpt-5-nano" - when: "difficulty <= 0.70" route: "gpt-5-mini" - when: "step.type == 'plan'" route: "gpt-5-mini" # plan always verified by a second pass - when: "difficulty > 0.70 or self_eval.confidence < 0.6" route: "gpt-5" - fallback: true route: "gpt-5-mini" fallback: strikes: 3 # 3 low-confidence responses in a row → escalate one tier on_regression: "rollback_to_previous_pareto_point" on_cache_poison: "purge+alert" telemetry: log: "postgresql://ledger/tasks" dash: "grafana://agent-cost/inference" alarm: "slack://oncall/agent-platform"
Recipe to reproduce: inspect-ai with 200 tasks from τ-bench retail subset, seed=42, GPT-5 / GPT-5-mini / GPT-5-nano via OpenAI Responses API (Aug 2026 prices), cache back-end = Qdrant with all-MiniLM-L6-v2 embeddings, judge = Prometheus-Eval-7B on 5% sample. Full repro script in §6.
The difficulty estimator (calibrated logistic regression over 12 features: prompt length, JSON structure, prior-step success, domain tag, has-code, has-citations, has-uncertainty-phrase, plan/act/verify tag, conversation depth, tool-failure history, retry count, time-of-day) lands easy / med / hard at 71% / 24% / 5%. The 71% is mostly verify steps, short lookups, structured JSON extraction, and tool-call arg construction — exactly what nano is good at. Quality floor is enforced by the 5% Prometheus sample: if a 5% slice drops below 0.80, the router is rolled back one Pareto point automatically.
Why not push nano to 90%? At ~85% nano share, success drops to 0.74 — below the floor. The frontier of the curve is around 71% nano, 24% mini, 5% frontier. This is the knee of the Pareto.
The router is step-type aware. plan steps are never routed to nano (plan failures cascade). verify steps always go to nano first; if the verification is "high-stakes" (involves money, email send, db write), it escalates to mini. act steps use the difficulty estimator alone. The agent runtime emits a step_type field on every call; the policy reads it via rule 3 above.
This is the bit most routers miss. Treating all steps the same is why FrugalGPT underperforms on agent workloads — it sees tokens, not semantics.
| Layer | What it caches | Key | TTL | Hit rate | Risk | Mitigation |
|---|---|---|---|---|---|---|
| Exact | raw prompt → response bytes | sha256(prompt + model_id + system + tools_hash) |
24h | ~12% | stale world-state (e.g. user said "today" and the cache hit from yesterday) | include date_window in key · max 24h TTL |
| Semantic | nearest-neighbor prompt → response | qdrant.search(emb, top_k=1, sim≥0.95) |
1h | ~26% | semantic-equivalent prompts with different correct answers (date math, prices) | calibration set + reject-on-sensitive-tag (date, price, email, weather) |
| Episodic | mid-conversation repeated tool calls | session_id + tool + args_hash |
5 min | ~6% | user changes intent mid-session | explicit cache.purge(session_id) on user correction; never cache tool results, only model outputs |
| Total | — | — | — | ~38% | — | 5% Prometheus sample on all hits to detect poisoning |
async def cache_lookup(prompt, ctx, session_id, step_type): # 1. exact exact_key = sha256(json.dumps({"p":prompt, "m":ctx.model, "s":ctx.system}, sort_keys=True)) hit = await redis.get(exact_key) if hit: return CacheHit("exact", hit) # 2. semantic — reject sensitive tags if any(tag in prompt.lower() for tag in ["today", "now", "price", "weather"]): return None emb = embed(prompt) near = await qdrant.search(emb, top_k=1, score_threshold=0.95) if near: return CacheHit("semantic", near[0].payload["response"]) # 3. episodic (only for tool-call re-attempts) if step_type == "act": e_key = sha256(f"{session_id}:{ctx.last_tool}:{ctx.last_args}") hit = await redis.get(e_key, ex=300) if hit: return CacheHit("episodic", hit) return None
LiteLLM proxy + LangSmith tracesMirror all production traffic. Baseline: measure current $/task and current success on a frozen 200-task golden set. Exit: dashboard shows live per-step cost with no behaviour change.
GPTCache (semantic, sim ≥ 0.95)Back-end = Qdrant, embeddings = all-MiniLM-L6-v2 (free, local). Start with exact-only for safety. Exit: cache hit-rate ≥ 25% with zero quality regression on golden-200.
difficulty_logreg_v1 on 5k labelled stepsLabels come from Prometheus-Eval on 5% sample. Features from the §3 list. Exit: estimator AUC ≥ 0.78 on hold-out; easy/med/hard distribution matches §5.
No frontier model in the pool yet — that's the safety net. Exit: success ≥ 0.78 on golden-200, $/task ≤ $0.65, no P95 latency regression.
gpt-5 as the "hard" tier, add Prometheus-Eval as safetyThe 5% sample judge is your regression alarm. Wire the rollback to the previous Pareto point. Exit: success ≥ 0.81, $/task ≤ $0.45.
Exit: throughput +18%, no quality regression, $/task ≤ $0.32.
Reject sensitive-tag lookups. Add purge(session_id) on user corrections. Page on-call if $/task > $0.45 for 10 minutes.
Lock the policy at v3. Pin a frozen generalization set of 50 tasks never used in training. Quarterly review. Final: $0.32/task, success 0.80, 38% cache hit, 5% frontier share.
| Step | Type | Tokens (in/out) | Difficulty | Route | Cache | $ step | $ cum |
|---|---|---|---|---|---|---|---|
| 1 | plan | 420 / 180 | 0.62 | gpt-5-mini | miss | $0.00047 | $0.0005 |
| 2 | act · search | 210 / 60 | 0.18 | gpt-5-nano | miss | $0.00003 | $0.0005 |
| 3 | act · search | 180 / 40 | 0.21 | gpt-5-nano | semantic hit | $0.00001 | $0.0005 |
| 4 | verify | 90 / 30 | 0.12 | gpt-5-nano | miss | $0.00002 | $0.0006 |
| 5 | act · code | 340 / 220 | 0.74 | gpt-5-mini | miss | $0.00053 | $0.0011 |
| 6 | verify | 120 / 40 | 0.15 | gpt-5-nano | miss | $0.00002 | $0.0011 |
| 7 | plan | 510 / 240 | 0.71 | gpt-5-mini | miss | $0.00061 | $0.0017 |
| 8 | act · synthesis | 820 / 480 | 0.88 | gpt-5 | miss | $0.00685 | $0.0086 |
| 9 | verify | 240 / 80 | 0.28 | gpt-5-nano | episodic hit | $0.00001 | $0.0086 |
| 10 | act · re-search | 200 / 60 | 0.31 | gpt-5-nano | miss | $0.00003 | $0.0086 |
| 11 | verify | 140 / 50 | 0.22 | gpt-5-nano | miss | $0.00003 | $0.0086 |
| 12 | plan · final | 620 / 320 | 0.81 | gpt-5 | miss | $0.00475 | $0.0134 |
Observation: one frontier call at step 8 is 51% of the bill. The whole task lands at $0.013 — well under the $0.45 ceiling. 9 of 12 steps ran on nano or cache. A frontier-only run would have cost $0.043 (3.2×) at the same quality. Lesson: it's not the 71% nano share that wins — it's the 5% frontier share that you don't have to pay for, because the difficulty estimator correctly identifies the synthesis step.