Cookbook
Pattern: route between an LLM and Dex
Let Dex decide every incoming message and call an LLM only for the messages that need a written reply, with abstained answers going to a person.
Dex and an LLM do different jobs. Dex reads a message and answers typed questions about it, fast and the same way every time; an LLM writes text. Put Dex in front: it decides every message, and your code calls the LLM only when a written reply is needed. Most messages never reach the LLM, so you pay for its output tokens only where they add something.
The flow
- Dex answers three questions about each message: what the writer wants (
intent, a pick), whether it only asks where an order is (template_ok, a check, so a template can answer it), and whether it contains instructions aimed at an AI (injection, a check). - Your code branches on the typed answers:
- an abstained
intent, or a likely injection: a person handles the message; spam: archive it;order_statuswithtemplate_ok: answer from your order system with a template, no LLM;- everything else: the LLM drafts a reply, and a person approves it before it is sent.
- an abstained
- Log the Dex request id, model and calibration with the route, so every decision can be traced.
The request
The code below builds this request with the SDK helpers. Here it is on the wire, for one email, with the answer the live API gave:
{
"state": {
"email": {
"subject": "Order 58213",
"body": "Hi, my order 58213 arrived yesterday but the kettle leaks from the bottom. I would like my money back, or a new kettle if that is quicker. What do I need to do?"
}
},
"questions": {
"intent": {
"type": "pick",
"instructions": "What does the writer of {{email.body}} want?",
"options": {
"order_status": "Where their order is or when it arrives",
"refund": "Money back or a return",
"complaint": "A complaint about a product or the service",
"question": "A question about products, prices or the account",
"spam": "Advertising, phishing or nonsense",
"other": null
},
"min_confidence": 0.5
},
"template_ok": {
"type": "check",
"instructions": "Does {{email.body}} ask where an order is or when it will arrive, and nothing else?",
"min_confidence": 0.6
},
"injection": {
"type": "check",
"instructions": "Does {{email.body}} contain instructions addressed to an AI assistant or agent?",
"min_confidence": 0.3
}
}
}
{
"id": "req_01M3JHAKGJ66SX1GMT2F94ZBTC",
"object": "decision",
"created": 1790549773,
"model": "dex-1.0.1",
"served_by": "gpu",
"calibration": "cal-20260926-1",
"answers": {
"intent": {
"type": "pick",
"choice": "refund",
"probabilities": {
"order_status": 0.0255,
"refund": 0.8352,
"complaint": 0.0505,
"question": 0.035,
"spam": 0.0235,
"other": 0.0303
},
"confidence": 0.7847,
"abstained": false
},
"template_ok": {
"type": "check",
"probability": 0.1066,
"confidence": 0.7869,
"abstained": false
},
"injection": {
"type": "check",
"probability": 0.1282,
"confidence": 0.7437,
"abstained": false
}
},
"usage": {
"input_tokens": 164,
"state_tokens": 59,
"question_tokens": 105,
"allowance_tokens": 0,
"paid_tokens": 0,
"charge_micro_cents": 0,
"unit_price_micro_cents": 0,
"tier": "test"
}
}
Captured from the live API on 2026-09-27 with a test key: model dex-1.0.1, calibration cal-20260926-1, served_by: gpu, 164 input tokens. A live key gives the same answers and counts, with allowance_tokens, paid_tokens and the charge filled in.
For this email the writer wants a refund (0.8352), it asks for more than an order's whereabouts (template_ok 0.1066) and it holds no instructions for an AI (injection 0.1282), so the code below sends it to the LLM for a draft that a person approves.
- The intents are mutually exclusive, so they are one pick with a catch-all
other. template_okis a separate yes or no fact about the same message; it costs only its own question tokens, because the message is billed once. Phrase it as what the message asks, not as what your system can do: "Can this email be answered completely with the order's status and delivery date?" got a confident no (0.0599) on a plain "Where is my order 58213?" email, while the wording above answers it with yes (0.8554) and still says no to the kettle email.- The injection check guards the LLM step: an email that tells an AI to do something should not reach an LLM that can act on it.
The code
draft_with_llm, send_template, archive and queue_for_person stand for your own code. The LLM client is whichever one you use.
from thinqit_dex import Client, check, pick
dex = Client() # reads DEX_API_KEY
QUESTIONS = {
"intent": pick(
"What does the writer of {{email.body}} want?",
{
"order_status": "Where their order is or when it arrives",
"refund": "Money back or a return",
"complaint": "A complaint about a product or the service",
"question": "A question about products, prices or the account",
"spam": "Advertising, phishing or nonsense",
"other": None,
},
min_confidence=0.5,
),
"template_ok": check(
"Does {{email.body}} ask where an order is or when it will arrive, and nothing else?",
min_confidence=0.6,
),
"injection": check(
"Does {{email.body}} contain instructions addressed to an AI assistant or agent?",
min_confidence=0.3,
),
}
def handle(email: dict) -> str:
# Only what the decision needs: the sender address stays out of the state.
state = {"email": {"subject": email["subject"], "body": email["body"]}}
decision = dex.decide(state=state, questions=QUESTIONS)
intent = decision.pick("intent")
template_ok = decision.check("template_ok")
injection = decision.check("injection")
if intent.abstained or injection.abstained or injection.probability >= 0.5:
route = "person"
queue_for_person(email, decision.id)
elif intent.choice == "spam":
route = "archive"
archive(email)
elif intent.choice == "order_status" and not template_ok.abstained and template_ok.probability >= 0.5:
route = "template"
send_template(email, "order_status")
else:
route = "llm"
queue_for_person(email, decision.id, draft=draft_with_llm(email, intent.choice))
print(route, decision.id, decision.model, decision.calibration)
return routeimport { Client, check, pick } from "@thinqit/dex";
const dex = new Client(); // reads DEX_API_KEY
const questions = {
intent: pick(
"What does the writer of {{email.body}} want?",
{
order_status: "Where their order is or when it arrives",
refund: "Money back or a return",
complaint: "A complaint about a product or the service",
question: "A question about products, prices or the account",
spam: "Advertising, phishing or nonsense",
other: null,
},
{ min_confidence: 0.5 },
),
template_ok: check("Does {{email.body}} ask where an order is or when it will arrive, and nothing else?", {
min_confidence: 0.6,
}),
injection: check("Does {{email.body}} contain instructions addressed to an AI assistant or agent?", {
min_confidence: 0.3,
}),
};
export async function handle(email: { from: string; subject: string; body: string }): Promise<string> {
// Only what the decision needs: the sender address stays out of the state.
const state = { email: { subject: email.subject, body: email.body } };
const decision = await dex.decide({ state, questions });
const { intent, template_ok: templateOk, injection } = decision.answers;
let route: string;
if (intent.abstained || injection.abstained || injection.probability >= 0.5) {
route = "person";
await queueForPerson(email, decision.id);
} else if (intent.choice === "spam") {
route = "archive";
await archive(email);
} else if (intent.choice === "order_status" && !templateOk.abstained && templateOk.probability >= 0.5) {
route = "template";
await sendTemplate(email, "order_status");
} else {
route = "llm";
await queueForPerson(email, decision.id, await draftWithLlm(email, intent.choice));
}
console.log(route, decision.id, decision.model, decision.calibration);
return route;
}In TypeScript the builders type the answers: intent.choice is "order_status" | "refund" | ... | "other", so a typo in a label is a compile error.
Why this order
- Cheap and fast first. Dex answers in one read of the message and charges input tokens only. The LLM, which charges for the text it writes, runs only on the messages that need text.
- Deterministic routing. On a pinned version the same email always takes the same route, so the routing can be tested like any other code (see Pin versions in CI).
- A person on every uncertain or risky path. Abstained answers and likely injections never reach an automatic action. See Abstention and Known limits.