Drex guides

Reading answers

What each field in a Drex response means, why answers are probabilities, and how to turn them into decisions.

Drex does not return a verdict. It returns a probability for each question, and your code turns that into a decision. This page explains each field in a response and the reasoning behind choosing thresholds, rounding, or using the whole distribution. For how to shape the questions, see Questions.

The response shape

A successful POST /v1/systemone returns HTTP 200 with one answer per question id. This is illustrative output for a support ticket, not a measured Drex 2.0 response. Actual probabilities, usage and timings vary.

200 response
{
	"model": "drex-v2.0",
	"answers": {
		"category": {
			"type": "choice",
			"choice": "billing",
			"confidence": 0.986,
			"probabilities": { "billing": 0.9895, "technical": 0.005, "account": 0.0006, "other": 0.0049 }
		},
		"sentiment": {
			"type": "score",
			"score": 1.5306,
			"legend": ["Calm", "Mildly annoyed", "Frustrated", "Angry"],
			"probabilities": { "0": 0.1572, "1": 0.3516, "2": 0.2946, "3": 0.1966 },
			"confidence": 0.7183
		},
		"wants_refund": {
			"type": "noul",
			"noul": 0.9868,
			"confidence": 0.9868
		}
	},
	"usage": { "input_tokens": 106, "output_tokens": 212 },
	"evaluation_time_ms": 142.6,
	"request_id": "req_<32 hex characters>"
}

The state was "I was charged twice for my March invoice. Please refund one of the charges." Each answer repeats its type, so you can switch on it without looking up the question.

A noul answer is the probability of yes

noul is a number from 0 to 1. It is the model's probability that the answer to your question is yes.

There is no built-in cutoff. You choose a threshold, and the right one depends on what each kind of mistake costs you.

  • If acting on a false yes is cheap and missing a true yes is expensive, set a low threshold. Tagging a ticket maybe_refund for a human to glance at costs a few seconds. Missing a refund request costs a customer.
  • If acting on a false yes is expensive, set a high threshold. Auto-issuing a refund at 0.6 will refund people who did not ask for one.
  • If both mistakes are expensive, use two thresholds. Act above the high one, ignore below the low one, and send the band in between to a person.

A threshold is a business decision, not a property of the model. Log the raw noul value with each decision, so you can move the threshold later and replay past traffic against the new one.

A noul answer also carries confidence, the probability of the likelier side: max(noul, 1 - noul). It is 0.9868 when noul is 0.9868 and 0.93 when noul is 0.07, and it is never below 0.5. It is there so gateways and frameworks that route on confidence read every answer type the same way. For your own decisions, threshold noul, which keeps the direction.

A choice answer is a label and a distribution

A choice answer has three fields.

  • choice is the selected label, one of the keys you sent in criteria.
  • probabilities has one entry per label. In these examples they sum to 1 within rounding. The category answer above sums to 1.0000.
  • confidence is the average of the confidence (probability) scores the model produced for this answer. It measures how sure the model was overall, so it tracks the top probability but is a different number.

choice alone is enough when one label dominates. When the top two are close, probabilities tells you that the model could not separate them. For example, a moderation question on a vague threat might pick flag, with remove close behind. Code that reads only choice sees a clean flag. Code that reads probabilities sees how close remove was, and can send that comment to a reviewer.

A score answer is an expected level

A score answer places the state on your ordered scale.

  • legend is the score question's criteria, the level labels in the order you sent them, so index 0 is the first label. It is an array by default; see legend as an array or an object.
  • probabilities is keyed by index as a string, "0" to "n-1", one entry per level.
  • score is the expected level, the sum of each index times its probability. For sentiment above, 0 × 0.1572 + 1 × 0.3516 + 2 × 0.2946 + 3 × 0.1966 = 1.5306, the score returned. It can fall between two levels. The examples on this page illustrate this.
  • confidence is the average of the confidence (probability) scores the model produced for this answer. A lower value means the model was less sure. sentiment above, with its mass spread over four levels, has confidence 0.7183.

Rounding score to the nearest level loses information. 1.5306 rounds to 2, Frustrated. But the distribution puts 0.5088 of the mass on Calm and Mildly annoyed together, so "Frustrated" describes this ticket less than half the time. Three ways to read the same answer, each right for a different job:

You wantReadFor sentiment above
One label for displayThe index with the highest probabilityMildly annoyed (0.3516)
A number to average across many ticketsscore1.5306
The chance the customer is at least frustratedprobabilities["2"] + probabilities["3"]0.4912

In the Python SDK, ScoreAnswer.probabilities is keyed by the level as an integer and legend is a tuple, so the last row reads p[2] + p[3] with p = result.scores["sentiment"].probabilities. The TypeScript SDK keeps the JSON's string keys.

Use score when you average scores or chart them over time. It keeps the fraction, and rounding each score to a label throws that away. When most of the probability sits on one level, the rounded score and the most likely level agree, so either one works for display.

legend as an array or an object

legend is an array of labels by default, as in the sentiment answer above. Some clients expect an object keyed by level instead, and the same answer then reads:

"legend": { "0": "Calm", "1": "Mildly annoyed", "2": "Frustrated", "3": "Angry" }

The keys are the level indexes as strings, like the keys of probabilities, and the labels are the same. Only the shape of legend changes. The score, probabilities, confidence, billing and every other field are identical.

  • Send X-Nace-Legend: object or X-Nace-Legend: array to choose. This header wins over everything below, in both directions. Any other value is ignored.
  • Without it, a request from the TypeSafe SDK gets the object, because that SDK reads legend as one. Drex recognizes it by an X-TypeSafe-SDK header with any non-empty value, or by a User-Agent that starts with typesafe-sdk/ or langchainjs-typesafe/. langchain-typesafe validates legend the same way but sends no X-TypeSafe-SDK, so only its User-Agent identifies it (that comes from its source, not a live call). A User-Agent that only contains those names later doesn't count. Every other client gets the array, and can send X-Nace-Legend: object.

The choice is made for each request, so two identical requests can get different shapes. The Drex SDKs and the OpenAI-compatible POST /v1/decisions don't send the header: the SDKs return the same list either way, and /v1/decisions has no legend.

usage, evaluation_time_ms, and request_id

usage.input_tokens is the number of tokens in state and questions, as the model counts them, plus any images on Drex Max. A long state counts in full even when the model reads only the passages that bear on your questions. This is what Drex bills. On an image request, usage.image_tokens says how many of those tokens the images took; it is already part of input_tokens. usage.output_tokens is what the model produced and is not billed. Rates are on the pricing reference.

evaluation_time_ms is the time the model spent on your request. It is not your round trip. Network time, queueing, and Drex's own validation and rate limiting are on top of it. The timing values on this page are illustrative. Under load, Drex waits up to 55 seconds for the model before it gives up, so set your client timeout with that in mind. See Errors and retries.

request_id is a string like req_ followed by 32 hex characters. It is also sent as the x-request-id and x-typesafe-request-id headers, and it is present on error responses too. The SDKs give it to you as request_id on the result, and as requestId (TypeScript) or request_id (Python) on an error. Log it with every call. When you write to support@nace.ai, include it, and Drex can join your report to every log line for that request.

On this page