Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Step classes

Three words that say what kind of model call this is. What each means, and what the proxy fills in when you leave it out.

ExplanationLast updated

On this page

An agent loop and a chat turn look identical over HTTP. Both are a POST with messages in it. They behave nothing alike: a chat turn is one call with a fresh prompt, and a tool loop is six calls that share almost all of their input and reuse the provider’s prompt cache across the whole run.

If your cost report cannot tell them apart, the most expensive shape in your traffic is invisible. x-tt-step-class is the header that tells them apart, and this page is about what the three values mean and what happens when you leave it out.

The three values

The vocabulary is closed. A value outside it is ignored rather than stored.

Value The call is Typical shape
user_turn a person or a caller asking something new one call, fresh prompt, no tool traffic
tool_loop one iteration of an agent working through tools many calls, near-identical prefix, tool results in the body
provider_state a call that maintains provider-side state rather than answering no user-visible output

provider_state is the one people ask about. It covers calls whose purpose is upkeep rather than an answer, and it exists so that housekeeping traffic can be separated from work when you split spend by class.

What happens when you leave it out

The class is always present on the row. When the header is absent the proxy guesses one from the shape of the request body, and records that it guessed.

The guess is a single rule: a request that carries a tools array or any tool-result content is classed tool_loop, and anything else is classed user_turn. provider_state cannot be seen in the body and is never guessed.

The row keeps the two facts apart:

Field Values Means
step_class one of the three the class in force for this row
step_class_source header or derived whether you said so or the proxy guessed

Here is that mix, counted over one lab run against a mock upstream:

  61  step_class='user_turn'   source='derived'  trace.source='none'
   3  step_class='tool_loop'   source='header'   trace.source='x-tt'
   1  step_class='user_turn'   source='header'   trace.source='x-tt'
   1  step_class='user_turn'   source='derived'  trace.source='x-tt'

The last line is the interesting one: that request carried a trace id, a step id and an agent name, and no step class. Identity came from the headers and the class came from the guess, which is a perfectly normal row and one you would misread if you assumed the two travel together.

The guess informs routing, and never identity

The derived class is a real input. Routing conditions can read trace.step_class, so a rule that sends tool loops to a cheaper tier fires on derived classes too.

What the guess cannot do is change who a row belongs to. Trace, step, parent and agent come from their own headers, and no amount of structural inference moves a row into a different trace. The split matters because the two carry different risk. A wrong routing input costs you one suboptimal decision that the ledger records and you can audit later. A wrong identity silently corrupts every total you group by it.

What tool_loop changes at request time

Marking a call tool_loop has one behavioural consequence beyond reporting, and it is worth knowing before you set it.

When routing.cache_affinity is enabled, a tool_loop step is pinned to its served tier from the first turn, rather than waiting for an observed cache read to trigger a pin. The reason is economic: a tool loop reuses a large cached prefix across its iterations, and letting per-step routing move the loop between upstreams forfeits the provider’s prompt-cache discount on every move. Traffic of any other class pins reactively, once a cache read is actually seen.

That is the only place the class changes what happens to the request. Everywhere else it is a label. Routing limits covers affinity in full.

Choosing between the header and the guess

Set the header when you know. It costs one line and it turns a guess into a statement.

Rely on the guess when the caller is out of your control. It is right about the common case, and step_class_source keeps it honest for anyone reading the numbers later.

Do not set the header to a value you are unsure of in order to avoid a derived marker. A mislabelled tool_loop pins affinity and skews the class split, and both are harder to notice than a derived marker you can filter on.