A brass sorting machine on a wooden desk sliding three blank paper tickets down chutes into wooden trays
All posts

Stop parsing chat output: turn support messages into typed decisions

Support triage usually means prompting a chat model and parsing its prose back into fields your code can use. AIVAX's Decisions API takes the message once and returns urgency, department, and frustration as typed answers — billed from the same account balance as inference, RAG, voice, and images.

Most support pipelines ask a chat model to read the message and then ask their own code to read the model's 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, and the parser fails on the ticket that mattered. The failure is structural: a text generator is the wrong interface for a step whose output is a branch in code.

A decision model inverts the interface. You send the message once, together with the questions you need answered, and you get back values with fixed types: a probability, a choice identifier, a position on a scale you defined. No prose to parse, no tool names to validate against a registry. TypeSafe's Jev is built for exactly this job. It does not generate text at all; it evaluates a shared state against your questions in one pass and returns typed answers with probabilities. AIVAX exposes it through the Decisions API at POST /api/v1/generations/decisions, billed from the same account balance as the rest of the platform's AI services.

One state, three questions, 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 actually needs. A noul question evaluates a yes/no proposition and returns a single number: the probability the answer is yes, where 0 means no and 1 means yes. A choice question selects one identifier from the map you provide, so the model cannot invent a fourth department. A score question places the state on your ordered scale and returns a fractional position computed from the per-level probabilities, plus the legend and 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 illustrative, not a recorded run. It shows the shape your code consumes: each answer keyed by the question name from the request, with the fields that apply to its type.

{
  "id": "d_example000001",
  "model": "@typesafe/jev-1.13",
  "provider": "typesafe",
  "answers": {
    "is_urgent": { "type": "noul", "noul": 0.93 },
    "department": {
      "type": "choice",
      "choice": "billing",
      "confidence": 0.91,
      "probabilities": { "billing": 0.91, "technical": 0.07, "sales": 0.02 }
    },
    "frustration": {
      "type": "score",
      "score": 1.35,
      "confidence": 0.58,
      "legend": {
        "0": "Calm, just stating facts",
        "1": "Frustrated but civil",
        "2": "Very angry, strong language or threatening to leave"
      },
      "probabilities": { "0": 0.04, "1": 0.57, "2": 0.39 }
    }
  },
  "usage": { "input_tokens": 128, "output_tokens": 12, "cost": 0.0000054 }
}

Two details in that shape deserve attention. In this response, 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: it may be present or absent depending on the provider response, so handle its absence instead of assuming it never appears. The score of 1.35 is not a label but a weighted position between your levels, and the spread behind it is visible; a confidence of 0.58 says the model is split between "frustrated" and "very angry," which is precisely the case you want a person to confirm rather than a branch to decide silently.

Thresholds live in your code

Because the outputs are numbers, the policy is ordinary code. Threshold the noul value into a boolean, switch on the choice identifier, and gate the high-stakes branch on confidence. TypeSafe's own guidance (Confidence) suggests three bands: act automatically at high confidence, proceed with confirmation in the middle, and route to a human at low confidence, with the boundaries set by the cost of being wrong. Paging someone on a false yes wants a high bar; missing a genuine safety flag wants a low one. Nothing about this requires a second model call or a judge prompt. The uncertainty arrives with the answer, and the risk tolerance is a constant your team can read.

That division of labor is also what keeps the model honest about its limits. TypeSafe trains Jev for calibrated decisions, meaning probabilities optimized against outcomes across groups of predictions, which does not guarantee any individual answer is correct. A confidence of 0.91 on billing is a reason to route, not a proof the routing is right; the audit trail is the state, the criteria, and the distribution, all of which you logged. Treat the values as inputs to a policy you own, not as verdicts you appeal.

What Jev is, and what it is not

Jev is TypeSafe's "System One" model: fast, structured judgments for software, as opposed to the slow deliberative generation of a chat model. It accepts a text state string and returns decisions; structured content must be serialized into that string first, and images, audio, and video are not supported, so non-text inputs must be transcribed or described into text first. English is the primary training language and where accuracy is currently best, so non-English workloads need testing against your own content with close attention to confidence. There is no fine-tuning or per-account adaptation: the same weights serve every caller, and you shape behavior through state content, per-question instructions and criteria, and by decomposing broad judgments into atomic questions your code recombines. The upstream model is typesafe/jev-1.13 (32K context, released September 2026, served through OpenRouter's Decisions API), which is a chat-incompatible endpoint: chat completion SDKs will not work with it, which is why the AIVAX wrapper matters.

What Jev cannot do is as important as what it can. 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 in the loop for the cases confidence flags. The model also inherits the usual calibration caveat: its probabilities reflect uncertainty as trained, and published third-party workflow tests describe model-agreement checks rather than verified ground truth. Do not present a score as a measured fact about the customer; present it as the model's read, with the distribution attached.

One balance across services

The request above is authenticated with an AIVAX API key. Usage is metered in tokens against the catalog price for the selected model, currently $0.042 per million input tokens with output unpriced for this model, and the usage.cost in the response is the amount actually deducted after the account's tax multiplier and plan commission, not the raw upstream figure. Output pricing of zero is a property of this model entry, not a platform rule; other services price their own inputs and outputs separately.

The practical point is that the same balance pays for the whole pipeline around the decision. The message that needs triage may arrive through a voice call transcribed by the audio transcription service, get ranked against labels by the text classification service, have its evidence enriched by web search, and end with a drafted reply from an inference model or a generated illustration from image generation. Each step draws from one balance with its own usage category, so a support workflow that mixes decisions, retrieval, speech, and generation needs one funded account, not four vendor relationships. That is shared metering, not an unlimited plan: every call deducts, and the response tells you exactly what was deducted.

Build the branch, not the parser

Sorting, routing, flagging, and escalating are decisions, and decisions deserve typed outputs. Define the state once, ask named questions with explicit criteria, threshold the numbers in code, and let low confidence page a human instead of guessing. The parser you delete is the one that used to read your own model's prose back into the fields you already knew you needed.