Decisions API example: triage a support ticket by urgency, department and frustration
Triage a support ticket in one Decisions API call: urgency (noul), team (choice) and frustration (score), with curl, routing code and model prices.
A Decisions API takes one piece of text plus a set of named questions and returns typed answers instead of prose: a probability for a yes/no question, one of the choice IDs you defined, or a position on a scale you defined. To triage a support ticket, send the message as state to AIVAX's POST /api/v1/generations/decisions with three questions: a noul question for urgency, a choice question for the owning team, and a score question for frustration. Your code thresholds the numbers, and there is no prose from a chat model to parse.
A common setup asks a chat model for JSON and parses the reply. The prompt says "respond with JSON," the model usually complies, and then one day it adds an apology sentence before the brace, or invents a department that does not exist. Schema-constrained output fixes the shape of that reply, as the structured output post explains. It does not by itself give you a calibrated confidence for the routing branch, and it still spends output tokens on a step whose result is a branch in code. A decision model returns no prose, only typed answers, and the AIVAX documentation states that output tokens have no charge in the current decision-model catalog.
How do I triage a support ticket in one request?
A triage step needs three judgments about the same message: is it urgent, which team owns it, and how frustrated is the sender. With a chat model that is either three calls or one call with a fragile multi-field schema. With Decisions it is one request with three named questions, each with its own type:
curl -X POST https://inference.aivax.net/api/v1/generations/decisions \
-H "Authorization: Bearer YOUR_AIVAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "@typesafe/jev-1.13",
"state": "Help! My payouts have been failing for 3 days.",
"questions": {
"is_urgent": {
"type": "noul",
"instructions": "Does this message convey urgency?",
"criteria": {
"true": "Explicitly time-sensitive or blocking the customer",
"false": "No urgency expressed"
}
},
"department": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Payments, payouts, invoices, or charges",
"technical": "Bugs, integrations, or API errors",
"sales": "Plans, upgrades, or pricing questions"
}
},
"frustration": {
"type": "score",
"instructions": "How frustrated is the customer?",
"criteria": [
"Calm, just stating facts",
"Frustrated but civil",
"Very angry, strong language or threatening to leave"
]
}
}
}'
The three question types cover the shapes triage needs:
noulevaluates a yes/no proposition and returns one number, the probability that the answer is yes.criterianeeds non-emptytrueandfalsedescriptions.choiceselects one identifier from the map you provide, so the model cannot invent a fourth department.scoreplaces the state on your ordered scale and returns a fractional position computed from the per-level probabilities, plus the legend and the distribution so you can see how split the model was.
Every question carries its own instructions and criteria, which means the domain rules live in the request, not in a system prompt you hope the model respects.
The response below is the one this request returned on 2026-11-10. Each answer is keyed by the question name from the request and carries the fields that apply to its type.
{
"id": "d_xx4robu6er7jpo35gs5o",
"model": "@typesafe/jev-1.13",
"provider": "TypeSafe",
"answers": {
"is_urgent": { "type": "noul", "noul": 0.95 },
"department": {
"type": "choice",
"choice": "billing",
"probabilities": { "billing": 1, "technical": 0, "sales": 0 },
"confidence": 1
},
"frustration": {
"type": "score",
"score": 0.99,
"legend": {
"0": "Calm, just stating facts",
"1": "Frustrated but civil",
"2": "Very angry, strong language or threatening to leave"
},
"probabilities": { "0": 0.01, "1": 0.99, "2": 0 },
"confidence": 0.98
}
},
"usage": { "input_tokens": 448, "output_tokens": 73, "cost": 0.00001919232 }
}
The noul answer carries no separate confidence field: with only two outcomes, the single value already describes the distribution, so your code thresholds it directly. Treat confidence as optional on any answer type, because it may be present or absent depending on the model, and handle its absence instead of assuming it always appears. The score of 0.99 is not a label but the probability-weighted position between your levels (0.99 × 1 here). When the distribution splits, say 0.57 on level 1 and 0.39 on level 2, the score lands at 1.35 and the confidence drops, which is the case you want a person to confirm rather than a branch to decide silently.
How do I turn the answers into routing code?
Because the outputs are numbers, the policy is ordinary code. Threshold the noul value, switch on the choice identifier, and send low-confidence answers to a person:
function route({ is_urgent, department, frustration }) {
if ((department.confidence ?? 0) < 0.5 || (frustration.confidence ?? 0) < 0.5) {
return { queue: "human-review" };
}
return {
queue: department.choice,
priority: is_urgent.noul >= 0.8 || frustration.score >= 1.5 ? "high" : "normal",
};
}
The thresholds above are placeholders, not recommendations. Set them from labeled tickets of your own, and pick them by the cost of being wrong: paging someone on a false yes wants a high bar, and missing a genuine escalation wants a low one. TypeSafe's own guidance on confidence suggests three bands (act automatically at high confidence, proceed with confirmation in the middle, route to a human at low confidence) and uses a 0.5 floor in its example. Nothing about this requires a second model call or a judge prompt.
Run against the response above, the function returns billing with high priority, which exercises the code path and says nothing about the model.
A calibrated probability is not a guarantee. TypeSafe describes calibration as measured across groups of predictions, so a confidence of 0.91 on billing is a reason to route this ticket, and a score is the model's read of the customer, not a measured fact about them. Log the state, the criteria and the distribution as your audit trail.
Which decision model should I use for support triage?
The model field selects the model, so changing it is a one-line change. The public catalog at GET /api/v1/information/decisions-models.json lists every model with its price, context and image limit and needs no authentication. The table shows a subset, as read from that catalog on 2026-10-20.
| Model | Input price / 1M | Images | Worth knowing |
|---|---|---|---|
@supersonic-labs/julia-1 | $0.008 | None | 1,024-token context per question; eligible for the daily allowance on Free, Pro, Max and Ultimate |
@perplexity/pplx-decider-v1.2 | $0.02 | None | Perplexity's 27B decision model, succeeding Decider V1.1, with a 256K context |
@liquid/d1 | $0.02 | None | Described as built for routing, classification and policy checks |
@inception/mercury-decide | $0.04 | Up to 16 | Text and images |
@typesafe/jev-1.13 | $0.042 | None | Used in the example above; the @typesafe/jev alias now resolves to @typesafe/jev-1.14 |
@openai/gpt-6-luna-decisions | $0.10 | Up to 128 | The OpenAI Decisions model, discussed below |
@cloudflare/clef-omni | $0.15 | Up to 4 | Text and images |
Prices are base rates before account and plan adjustments, and the usage.cost in each response is the amount actually deducted. I have not compared the accuracy of these models, so the table says nothing about which one routes your tickets best. Build a labeled set of real tickets, including ambiguous and incomplete ones, and evaluate choice, noul and score separately, because success at routing does not establish reliable scoring.
Two practical differences matter for triage. Jev evaluates text only, and TypeSafe names English as its primary training language, where accuracy is currently best; test non-English tickets against your own content and watch confidence closely. If a ticket arrives with a screenshot, pick a model that accepts images and put each one in the top-level state array as an image_url part with a base64 data URL (image/png, image/jpeg or image/webp). Remote URLs are not fetched, and images nested inside objects are not read. The Semantic Decisions documentation covers the request format, per-model limits and error cases.
How does this compare with the OpenAI Decisions API?
OpenAI now has its own Decisions API. Its guide, as read on 2026-10-20, describes POST /v1/decisions as a public beta with gpt-6-luna as the only model, and says OpenAI expects general availability in the coming weeks. The idea is the same as above; the request shape is not.
| Aspect | OpenAI /v1/decisions | AIVAX /api/v1/generations/decisions |
|---|---|---|
| Evidence field | input: a string, or user messages with text and images | state: any JSON value |
| Questions | An array; each question has a name | An object keyed by question name |
| Yes/no type | predicate | noul, with true and false criteria |
| Choice options | A list of value and description pairs | A criteria map from choice ID to description |
| Image input | Base64 data URLs or public HTTP(S) URLs | Base64 data URLs only |
| Models | gpt-6-luna only | Several, listed in the public catalog; one is @openai/gpt-6-luna-decisions |
| Input price | $0.10 per 1M tokens for gpt-6-luna, with regional and long-context premiums | $0.10 base for that model, before account and plan adjustments |
The practical consequence is that an OpenAI Decisions request body does not work against AIVAX unchanged, and the reverse is also true. One endpoint with many models lets you run the same ticket set through several of them and compare answers before choosing, while OpenAI's endpoint accepts public image URLs and AIVAX's does not. OpenAI's guide also documents a refusal answer type that your code must handle before reading a probability; I did not verify how the AIVAX endpoint surfaces a refusal from the Luna model, so test that path before relying on it.
What does it cost, and what are the limits?
Usage is metered in input tokens against the catalog price of the model you select. With Jev at $0.042 per million input tokens, the three-question request above reported 448 input tokens for a one-line ticket, so 1,000 tickets of that size add up to roughly 448,000 input tokens, or about $0.019 before account and plan adjustments (its usage.cost was $0.0000192 on a Max account). Longer tickets cost more, and the questions' instructions and criteria count toward the input. Output is not priced for Jev, so the output_tokens value in the response does not add to the bill. Julia-1 counts the shared state again for every question, so three questions over one ticket use roughly three times the input of one question.
Two limits apply to every model. An authenticated API key and a positive account balance are required, even for a model with a zero base price. Requests are also capped per account across models and keys: 10 per minute on Free, 30 on Pro, 200 on Max, and no plan-imposed limit on Ultimate. A request with several questions counts once, which is another reason to put the three triage questions in one call. Above the limit the API returns 429, so retry with backoff.
What does a decision model not do?
It does not write replies, produce code, or explain its reasoning. A triage pipeline still needs a generator downstream for the customer-facing message and a human for the cases that confidence flags. For a generated object or a written explanation, use structured responses instead, and test the customer-facing conversation separately with multi-turn agent tests.
The request above and the response under it were run against the live AIVAX API on November 10, 2026, as were the catalog request, a @typesafe/jev-1.13 request with an image (a 400, "does not accept images") and a @supersonic-labs/julia-1 request (usage.cost of 0). The Perplexity row was updated from the same catalog that day, because the earlier v1.1-27b ID now returns a 400 ("Unknown semantic decision model"); the other prices and the OpenAI comparison were read on 2026-10-20. Check the response of your chosen model before you write code that depends on optional fields such as confidence.
FAQ
Can I send a screenshot with the ticket?
Yes, with a model that accepts images, such as @openai/gpt-6-luna-decisions, @inception/mercury-decide or @cloudflare/clef-omni. Jev and the other text-only models return a 400 if the state contains an image. Audio and video are not accepted, so transcribe or describe them into text first.
Is noul a boolean?
No. It is the probability that the answer is yes, a number from 0 to 1. Your application chooses the threshold and decides how to handle values near 0.5.
Can I try this without paying?
Julia-1 is eligible for a daily allowance on Free, Pro, Max and Ultimate, so small tests can cost nothing. The account still needs a positive balance. The Free plan post lists what the allowances cover.
Can one request classify several tickets?
No. Every question in a request applies to the same state, so each ticket needs its own request. The per-minute account limits are in the section above.