On July 9, 2026, OpenAI made GPT-5.6 generally available in three tiers: Luna ($1 in / $6 out per 1M tokens), Terra ($2.50 / $15), and Sol ($5 / $30). That 5x spread is the whole reason to care about routing. Send every request to Sol "to be safe" and you pay flagship prices for work Luna could have finished. The fix is an escalation ladder: run the cheapest capable model first, check whether its answer is trustworthy, and climb to a pricier tier only when it isn't. It's the practical version of the LLM-cascade idea from the FrugalGPT paper, which matched top-model accuracy at up to 98% lower cost.
Here's how to build one for production — and how to tell whether it's actually saving money.
Step 1: Order your rungs cheapest-first#
Put the models in a list from cheapest to most expensive. The ladder walks it top to bottom and stops as soon as an answer passes your check.
# Cheapest capable model first, most expensive last.
# Use your provider's exact model-id strings here.
LADDER = ["gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"]
Step 2: Pick an honest confidence signal#
You need a signal that says "this answer is good enough, stop climbing." Three options, from least to most trustworthy:
- Self-reported confidence ("rate your certainty 0–1"). Cheap, but models are miscalibrated and happily report 0.95 on a wrong answer. Don't rely on it.
- Logprobs. If your provider exposes them (OpenAI documents
logprobson Chat Completions), you can read per-token probabilities and treat a low minimum as "uncertain." The limit: logprobs measure fluency, not correctness. A confidently wrong model looks certain, so this catches hedging, not hallucination. - A validator. Check the output against a schema plus the business rules the schema can't express. This is the most honest default because it tests the thing you actually care about — is this answer safe to ship? — instead of the model's feelings about it.
Use the validator as your primary signal. One subtlety: OpenAI's Structured Outputs already guarantees schema adherence via constrained decoding, so a schema-only check won't fail. The value is in the rules on top — cross-field constraints, plausible enum values, downstream sanity checks.
Step 3: Wire the cheap-first call + validator#
import logging
from collections import Counter
from pydantic import BaseModel, field_validator, ValidationError
from openai import OpenAI
client = OpenAI()
log = logging.getLogger("ladder")
class Ticket(BaseModel):
category: str
priority: int
needs_human: bool
@field_validator("priority")
@classmethod
def _prio_in_range(cls, v: int) -> int:
if not 1 <= v <= 5: # a rule the schema alone can't enforce
raise ValueError("priority must be 1-5")
return v
def classify(model: str, text: str) -> Ticket | None:
"""Call one model; return a valid Ticket, or None if the output fails validation."""
resp = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content":
"Classify the support ticket. Reply with JSON: "
"{category, priority (1-5), needs_human}."},
{"role": "user", "content": text},
],
response_format={"type": "json_object"},
)
raw = resp.choices[0].message.content or "{}"
try:
return Ticket.model_validate_json(raw) # our confidence signal
except ValidationError:
return None # low confidence -> escalate
Step 4: Climb only when the validator fails, and log it#
metrics = Counter()
def run_ladder(text: str) -> Ticket:
for rung, model in enumerate(LADDER):
metrics[f"tried:{model}"] += 1
result = classify(model, text)
if result is not None:
metrics["served"] += 1
if rung > 0:
metrics["escalated"] += 1 # served by a pricier tier
return result
log.info("%s was uncertain — escalating", model)
metrics["exhausted"] += 1 # even the top rung failed
raise RuntimeError("no model produced a valid answer")
def escalation_rate() -> float:
return metrics["escalated"] / max(metrics["served"], 1)
Print escalation_rate() on a dashboard. It is the one number that tells you whether the ladder is earning its keep — and the knob you tune by making the validator stricter or looser.
Step 5: Do the break-even math before you trust it#
The ladder is not free. Every escalated request first pays for the cheap model's failed attempt, then pays again for the next rung — so you spend more than a flat mid-tier whenever escalations get common. Using GPT-5.6 output prices, the break-even is easy to see. Serving on Luna costs $6/1M out; escalating a fraction p of requests to Sol adds p × $30. Set that equal to always-Terra ($15):
$6 + $30·p = $15 -> p ≈ 0.30
So if more than ~30% of requests escalate to the top rung, just use Terra. Factor in the wasted cheap-model call and the real threshold sits closer to 25%.
Rule of thumb: the ladder pays off when you have high volume and a cheap-wins-most-of-the-time distribution — the cheap model cleanly resolves the large majority, and only a thin tail needs the flagship. If your traffic is uniformly hard, skip the ladder and pick one tier.
The tradeoffs, plainly#
- Latency on the hard path. An escalated request eats the round-trip of every rung it climbed. Average cost drops; p99 latency rises. Cap the ladder at two rungs for latency-sensitive flows.
- A bad signal is worse than no ladder. If your validator waves through wrong answers (false confidence) or rejects good ones (needless escalation), you pay more and ship worse output. Correlate your signal against a labeled set before trusting it — the same discipline as shadow-testing a cheaper model before you switch.
- Complexity. Three code paths and a metric to watch versus one model call. Only worth it at volume.
Bonus: the same ladder in TypeScript
import OpenAI from "openai";
import { z } from "zod";
const client = new OpenAI();
const LADDER = ["gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"];
const Ticket = z.object({
category: z.string(),
priority: z.number().int().min(1).max(5),
needs_human: z.boolean(),
});
export async function runLadder(text: string) {
for (const model of LADDER) {
const r = await client.chat.completions.create({
model,
messages: [{ role: "user", content: text }],
response_format: { type: "json_object" },
});
const parsed = Ticket.safeParse(JSON.parse(r.choices[0].message.content ?? "{}"));
if (parsed.success) return parsed.data; // confident enough — stop climbing
}
throw new Error("no model produced a valid answer");
}
Takeaway: An escalation ladder is a bet that most of your work is easy — build the cheapest-first path, gate it on a validator you've actually tested, log the escalation rate, and kill the ladder the moment that rate crosses ~25–30%.



