Classify tree

Walk a nested label tree in one call and get the path, the compound numbers, and every level.

POST /v1/decision-machine-1/classify-tree runs classify once per level of a nested label tree, on the same text. Each level scores the children of the winner above it. The API returns the path from the root, the deepest label, the compound probability and confidence, and one entry per level with its own scores. The walk is a convenience: it is N classify calls in sequence, made for you. Every request needs an API key. See Authentication.

When to use it

  • Your labels form a taxonomy, and the leaves of far-apart branches steal probability from each other in one flat call.
  • You want the whole descent in one round trip, with the per-level numbers to audit it.
  • You need the input size and the model time of each level, not one total.
  • Use Classify instead when every label fits in one flat set. One call costs less than two.
  • Walk the tree in your own code instead when you need a stop rule per level. The API always walks to a leaf, so a client-side walk saves the calls below a level you would reject. Taxonomy classification shows both.

Three example decisions

  1. A support ticket arrives. Walk billing > subscription_change and open the matching workflow.
  2. A clinical note arrives. Walk a symptom taxonomy and store the full path, not only the leaf.
  3. A product description arrives. Walk a catalogue tree of 300 leaves, instead of scoring all 300 against each other in one flat call.

Request

FieldTypeRequiredMeaningLimits
textstringone of text / textsThe text to classify1 to 20,000 characters
textsstring[]one of text / textsA batch of texts1 to 32 items, each 1 to 20,000 characters
treeobjectyesMap of label name to description, or to a node2 to 64 entries per level, 8 levels deep

A tree value takes two forms. A string is a leaf with that description. An object is a node:

Node fieldTypeRequiredMeaning
descriptionstringnoWhat the parent level reads for this label. The bare name goes to the model when you omit it.
labelsobjectnoThe children of this label. The same rules apply to every child level.

A node without labels, or with an empty labels object, is a leaf. The walk stops there. Every level holds 2 to 64 entries, the top level included. The tree holds at most 8 levels.

curl -X POST https://api.milliseconds.ai/v1/decision-machine-1/classify-tree \
-H 'content-type: application/json' \
-H 'authorization: Bearer sk-ms-...' \
-d '{
"text": "62-year-old man with crushing substernal chest pain radiating to the left arm for 40 minutes, sweating, and ST elevation in leads II, III and aVF.",
"tree": {
"cardiovascular": {
"description": "heart and blood vessels: chest pain, arrhythmia, heart failure, hypertension",
"labels": {
"acute_coronary_syndrome": {
"description": "heart attack or angina: chest pain, ST elevation on the ECG, troponin rise",
"labels": {
"stemi": "ST-elevation myocardial infarction: ST elevation on the ECG",
"nstemi": "non-ST-elevation myocardial infarction: troponin rise without ST elevation",
"unstable_angina": "chest pain at rest without a troponin rise"
}
},
"arrhythmia": "abnormal heart rhythm: palpitations, atrial fibrillation, bradycardia",
"heart_failure": "fluid overload, breathlessness on exertion, swollen legs, reduced ejection fraction"
}
},
"pulmonary": {
"description": "lungs and airways: cough, breathlessness, wheeze, pneumonia",
"labels": {
"infection": "pneumonia or bronchitis: fever with a productive cough",
"obstructive": "asthma or COPD: wheeze, chronic cough, smoking history",
"embolism": "pulmonary embolism: sudden breathlessness, pleuritic pain, low oxygen"
}
},
"gastrointestinal": "digestive tract: abdominal pain, nausea, vomiting, bleeding",
"neurological": "brain and nerves: headache, seizure, weakness, numbness"
}
}'

Response

{
"path": [
"cardiovascular",
"acute_coronary_syndrome",
"stemi"
],
"label": "stemi",
"probability": 0.81,
"confidence": 0.524,
"levels": [
{
"label": "cardiovascular",
"probability": 0.982,
"confidence": 0.931,
"scores": {
"cardiovascular": 0.982,
"pulmonary": 0.001,
"gastrointestinal": 0.001,
"neurological": 0.016
},
"input_chars": 456,
"input_tokens": 112,
"inference_ms": 57
},
{
"label": "acute_coronary_syndrome",
"probability": 0.825,
"confidence": 0.564,
"scores": {
"acute_coronary_syndrome": 0.825,
"arrhythmia": 0.173,
"heart_failure": 0.003
},
"input_chars": 428,
"input_tokens": 105,
"inference_ms": 56
},
{
"label": "stemi",
"probability": 1,
"confidence": 0.998,
"scores": {
"stemi": 1,
"nstemi": 0,
"unstable_angina": 0
},
"input_chars": 357,
"input_tokens": 88,
"inference_ms": 59
}
]
}
  • path — the winning label of each level, top to bottom.
  • label — the deepest label, the last entry of path.
  • probability — the product of the level probabilities, rounded to 3 decimals.
  • confidence — the product of the level confidences, rounded to 3 decimals.
  • levels — one entry per level the walk ran, in order:
    • label — the winner at that level.
    • probability — the winner’s normalized share of that level’s labels.
    • confidence1 − normalized entropy over that level’s scores.
    • scores — one probability per label of that level. The values sum to 1, before rounding.
    • input_chars — the input characters that level sent: the text, plus that level’s names and descriptions.
    • input_tokens — the input tokens of that level, in the same unit as x-input-tokens.
    • inference_ms — the model processing time of that level, in milliseconds.

Three response headers describe the whole request. x-input-chars and x-input-tokens measure the request body once, as on every capability: the text plus the whole tree, branches the walk never entered included. x-input-tokens is the input tokens billed for this call. The per-level input_chars count what each level sent to the model, so their sum differs from the header. x-inference-ms is the sum of inference_ms. The capture above returned x-input-chars: 1183 and x-input-tokens: 289. See Pricing.

Reading the numbers

Read the levels first, then the compound number. Level 1 picked cardiovascular with a 0.982 share and a confidence of 0.931. Level 2 picked acute_coronary_syndrome with 0.825 and a confidence of 0.564. Level 3 picked stemi with a share of 1 and a confidence of 0.998. The compound numbers multiply those levels:

probability 0.982 × 0.825 × 1.000 = 0.810
confidence 0.931 × 0.564 × 0.998 = 0.524

Every level multiplies a number below 1, so both numbers only fall as the walk descends. Threshold the level you act on, not the compound number alone. Here level 2 is the weak one: arrhythmia took 0.173 of that level.

The inner node’s description decides that level. We first named this node ischemic and described it as reduced blood flow to the heart: angina, myocardial infarction. The same text then walked to arrhythmia at 0.753, and the stemi leaf, which scores 1 against its siblings, was never scored. Nothing in that description appears in the note, while “ST elevation in leads II, III and aVF” reads as a rhythm cue. Inner nodes need the cue words their leaves carry. The description above names chest pain, ST elevation and a troponin rise, and the level flips to 0.825.

Now read an ambiguous case. The ticket "I was charged twice for my annual subscription on 3 March and the second charge has not been refunded." over the support tree returns:

{
"path": ["billing", "refund_request"],
"label": "refund_request",
"probability": 0.432,
"confidence": 0.279,
"levels": [
{"label":"billing","probability":0.996,"confidence":0.98,"scores":{"billing":0.996,"shipping":0,"technical":0.004,"account":0},"input_chars":320,"input_tokens":79,"inference_ms":513},
{"label":"refund_request","probability":0.434,"confidence":0.285,"scores":{"duplicate_charge":0.434,"refund_request":0.434,"invoice_question":0.131,"subscription_change":0},"input_chars":367,"input_tokens":90,"inference_ms":561}
]
}

Level 1 is safe: billing holds 0.996 with a confidence of 0.98. Level 2 is a coin flip: duplicate_charge and refund_request both score 0.434, and the confidence falls to 0.285. The ticket describes both cases. Accept the partial path billing and send the leaf choice to a person.

The compound probability of 0.432 hides where the doubt sits. The level numbers name it. Log levels whenever you act on a deep path.

Batching

Send texts instead of text to walk up to 32 texts against one tree. The response is always {"results": [...]}, one entry per text, in input order. The texts walk concurrently. Each level of each text still costs one inference call.

curl -X POST https://api.milliseconds.ai/v1/decision-machine-1/classify-tree \
-H 'content-type: application/json' \
-H 'authorization: Bearer sk-ms-...' \
-d '{
"texts": [
"Please cancel my Pro plan at the end of the current billing period. I do not need the extra seats any more.",
"I was charged twice for my annual subscription on 3 March and the second charge has not been refunded."
],
"tree": {
"billing": {
"description": "payments, invoices, charges, refunds and subscriptions",
"labels": {
"duplicate_charge": "the customer was billed more than once for the same item",
"refund_request": "the customer asks for money back",
"invoice_question": "the customer asks about an invoice or receipt",
"subscription_change": "the customer wants to upgrade, downgrade or cancel a plan"
}
},
"shipping": {
"description": "delivery, tracking, lost or damaged parcels",
"labels": {
"lost_parcel": "the parcel is marked delivered but the customer never received it",
"damaged_parcel": "the parcel arrived broken or with missing items",
"late_delivery": "the parcel is still in transit and is past its promised date",
"address_change": "the customer wants to change the delivery address"
}
},
"technical": "bugs, errors, outages and login problems",
"account": "profile, password, plan and team members"
}
}'
{
"results": [
{"path":["billing","subscription_change"],"label":"subscription_change","probability":0.624,"confidence":0.336,"levels":[{"label":"billing","probability":0.766,"confidence":0.607,"scores":{"billing":0.766,"shipping":0,"technical":0,"account":0.234},"input_chars":325,"input_tokens":80,"inference_ms":659},{"label":"subscription_change","probability":0.814,"confidence":0.553,"scores":{"duplicate_charge":0.043,"refund_request":0.132,"invoice_question":0.011,"subscription_change":0.814},"input_chars":372,"input_tokens":91,"inference_ms":670}]},
{"path":["billing","refund_request"],"label":"refund_request","probability":0.432,"confidence":0.279,"levels":[{"label":"billing","probability":0.996,"confidence":0.98,"scores":{"billing":0.996,"shipping":0,"technical":0.004,"account":0},"input_chars":320,"input_tokens":79,"inference_ms":619},{"label":"refund_request","probability":0.434,"confidence":0.285,"scores":{"duplicate_charge":0.434,"refund_request":0.434,"invoice_question":0.131,"subscription_change":0},"input_chars":367,"input_tokens":90,"inference_ms":657}]}
]
}

That batch reported x-input-chars: 988 and x-input-tokens: 241: both texts and the tree, counted once. x-inference-ms is the sum over the four levels the two walks ran. See Batching for how batches spread across inference slots.

Limits and gotchas

  • Every level needs 2 to 64 labels, the top level included. A level with one label returns {"error":{"code":"invalid_request","message":"tree.billing.labels: each level needs 2 to 64 labels"}}. The path in the message names the node that broke the rule.
  • The tree holds at most 8 levels. A ninth level returns {"error":{"code":"invalid_request","message":"tree.a.labels.a.labels.a.labels.a.labels.a.labels.a.labels.a.labels.a.labels: tree is deeper than 8 levels"}}.
  • Both 400 cases arrive before any inference call, so the response carries no x-input-chars, x-input-tokens or x-inference-ms header.
  • text and texts are mutually exclusive. Sending both fails with body: provide text or texts, not both. Sending neither fails with the same message.
  • Each level re-sends the full text to the model, so latency grows with depth. A three-level walk makes three inference calls. The request bills its body once, whatever the depth.
  • A wrong branch at level 1 makes every level below it wrong. The response still returns a confident leaf inside that branch.
  • The model reads the label names and descriptions only. There is no per-level question and no instruction field. Write a description for every label.
  • The walk always runs to a leaf. The API applies no stop rule. Read levels and apply your own thresholds after the call, or walk the tree yourself to skip the calls below a weak level.
  • Sibling names must separate the cases at their own level. Two leaves that describe the same case tie, whatever the text says.

probability at each level is a share of that level’s label set, not a calibrated truth value. Adding or removing a sibling changes every number at that level. The compound probability multiplies those shares, so it falls with depth even on a correct path. Re-tune your thresholds after you change the tree.

Next