Reasoning tokens are output tokens, and five APIs count them differently

Reasoning tokens are output tokens on LM Studio, OpenAI, Anthropic, Gemini and OpenRouter, and each names and caps them differently. One client reads all five.

0:00
Reasoning tokens are output tokens, and five APIs count them differently

Five APIs agree that reasoning tokens are output tokens and disagree about everything else: what to call them, where to put the count, how to cap them, and what to return when the cap hits. A client written for one of them reads the wrong field on the next and reports a blank answer as a model failure.

This came out of a local measurement on a studio test machine, where an empty reply from LM Studio turned out to be 4,093 reasoning tokens and a cap of 4,096. Here are the five contracts side by side, from each provider’s own documentation, and the small client that reads all of them.

Where each API reports reasoning tokens

The count lives in the usage object everywhere, under a different name each time:

ProviderFieldDocumented as
LM Studio (OpenAI compatible)usage.completion_tokens_details.reasoning_tokensmeasured on this stack; not described in the endpoint docs
OpenAIusage.output_tokens_details.reasoning_tokens“visible in the usage object of the response object, under output_tokens_details
Anthropicusage.output_tokens_details.thinking_tokens“reports how many of the billed output tokens were internal reasoning”
Google Gemini SDKthoughts_token_countas reported in a bug thread on the SDK
OpenRoutercounted inside output tokens“Reasoning tokens are counted as output tokens for billing purposes”

The word changes, reasoning for two of them and thinking or thoughts for the other two, and so does the parent key, completion_tokens_details on the OpenAI compatible shape that LM Studio implements and output_tokens_details on the current OpenAI and Anthropic responses. A client that hard codes one path will find None on the others and conclude the model did not think.

How each API caps reasoning tokens

All five draw the reasoning from the same pool as the answer, and they say so in nearly the same words. OpenAI: reasoning tokens “still occupy space in the model’s context window and are billed as output tokens.” Anthropic: “Thinking tokens count toward the max_tokens limit for the turn, so the budget must leave room for the final response.” OpenRouter: “max_tokens must be strictly higher than the reasoning budget to ensure there are tokens available for the final response after thinking.”

Where they differ is whether you can steer the amount. Anthropic’s manual mode takes a budget_tokens value, minimum 1,024 and “less than max_tokens“, and describes it as “a target rather than a strict cap”; its newer models replace the budget with an adaptive mode and an effort setting. OpenAI takes a reasoning_effort with seven levels from none to max, and says “Lower effort favors speed and lower token usage.” LM Studio accepts a reasoning_effort field on the same endpoint, does not document it, and on the model tested here low produced twice the reasoning of high. Same parameter name, opposite behaviour, and only one of the two is written down.

The safe assumption on any local server is that the cap is the only control that works, and it has to be set from a measurement rather than from a parameter that sounds like a budget.

What each API returns when reasoning tokens hit the cap

This is where clients break, because the signals are all different and all quiet.

LM Studio returns HTTP 200, finish_reason: "length", an empty content, and a reasoning count equal to the cap minus a few tokens. Nothing in the status code says anything went wrong.

OpenAI’s Responses API, per its docs, returns “a response with a status of incomplete and incomplete_details with reason set to max_output_tokens“. A different field, and a status rather than a finish reason.

Anthropic’s docs say “max_tokens remains the hard ceiling on total output” and that the model “may stop reasoning well before the budget is exhausted”; the response stop reason carries the max tokens case.

Google’s SDK, per the issue that documents it, gives “a MAX_TOKENS finish reason” when “thoughts_token_count + output_token_count > max_output_tokens“, and “the response text is empty”.

Four shapes for one event. A harness has to check the provider’s own truncation signal before it looks at the content, because in every one of the four the content is empty and only one of them raises anything.

Why LM Studio’s reasoning_effort is not OpenAI’s

The OpenAI compatible endpoint is a shape, not a contract. LM Studio implements the request and response format so that clients written for OpenAI work unchanged, which is why reasoning_effort is accepted without error. What the server does with the value is up to the server and the model behind it, and LM Studio’s endpoint documentation does not mention the field at all.

The measurement on this stack is the only documentation there is for that combination: on gemma-4-31b-qat, low thought for exactly 4,096 tokens, medium for 2,774, high for 1,556, and leaving the field unset gave 2,136. Whether that is the model, a mapping in the server, or a default budget being applied, the parameter cannot be used the way OpenAI’s docs describe theirs. Treat any parameter that arrives through a compatibility layer as unverified until a run on your own hardware says otherwise.

How to write a client that reads reasoning tokens from any of them

Read three things from every response, in this order, and never the content first.

The truncation signal, by provider: finish_reason == "length" on OpenAI compatible servers including LM Studio, status == "incomplete" with incomplete_details.reason on OpenAI’s Responses API, the stop reason on Anthropic, MAX_TOKENS on Gemini. If it fired, the content is meaningless and the run needs a bigger cap or a smaller input.

The reasoning count, from whichever of the paths in the first table exists, falling back to zero if none does. Log it on every call; it is the number that tells you what the task costs, and it is the number a control ladder reads to tell a slow model from a stuck one.

Only then the content, and for tool calls the tool_calls array instead, since a required tool call also returns an empty content with a finish reason of tool_calls, which is the third way an empty answer can be entirely healthy.

A response object that is empty in the one field you were looking at is not a failed model. On a reasoning model it is, far more often, a model that ran out of room to finish a sentence it had spent four thousand tokens preparing, and every one of these APIs will tell you so if you read the field they put it in.

Share this