Skip to content
Numes

Features

Reasoning

Control how much a model thinks, read its reasoning, and carry reasoning state through multi-turn and tool conversations.

On this page

Reasoning models can think before they answer. Numes offers one set of controls for every provider: request an effort level or a token budget, turn reasoning off, or hide it from the response. Controls are upstream hints by default. Set provider.require_parameters: true to require declared capabilities. Reasoning comes back in OpenRouter-compatible fields.

Request controls

Send a reasoning object with a chat completion:

curl -X POST "https://api.numes.ai/api/v1/chat/completions" \  -H "Authorization: Bearer $NUMES_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "model": "openai/gpt-6-luna",    "messages": [      {        "role": "user",        "content": "How many times does the letter r appear in \"strawberry\"?"      }    ],    "reasoning": {      "effort": "high"    },    "max_tokens": 4096  }'

reasoning

effortstring

Common values include none, minimal, low, medium, high, xhigh and max; providers may accept others. none turns reasoning off.

max_tokensinteger

Provider reasoning token budget. The upstream validates its range.

excludebooleanDefault false

Keep reasoning out of the response. The model can still reason; you do not receive it.

enabledboolean

true asks for reasoning without choosing a level; false turns it off.

Two shorthand fields are also accepted:

Other fields

reasoning_effortstring

OpenAI-style shorthand for reasoning.effort, with the same values. null is treated as omitted.

include_reasoningboolean

Deprecated. The inverse of reasoning.exclude.

Shorthand aliases take precedence when both forms are present. Unfamiliar effort values and reasoning attributes are forwarded; the upstream validates them.

Model capabilities

GET /models and the model endpoints list include a reasoning object for offers that support the reasoning parameter and have published reasoning details. Every field is optional. Missing catalogue facts do not exclude an endpoint unless you opt into strict matching.

curl "https://api.numes.ai/api/v1/models/openai/gpt-6-luna/endpoints" \  -H "Authorization: Bearer $NUMES_API_KEY"
Endpoint excerpt
{  "tag": "example-provider/primary",  "supported_parameters": ["max_tokens", "reasoning", "include_reasoning", "tools"],  "reasoning": {    "supported_efforts": ["low", "medium", "high"],    "default_effort": "medium",    "default_enabled": true,    "mandatory": false,    "supports_max_tokens": true  }}

reasoning capabilities

supported_effortsstring[]

Effort levels the endpoint accepts.

default_effortstring | null

Effort used when you send none.

default_enabledboolean

Whether the model reasons when you send no reasoning control.

mandatoryboolean

true when reasoning cannot be turned off.

supports_max_tokensboolean

Whether reasoning.max_tokens budgets are accepted.

A model row in GET /models describes one representative offer. Other endpoints of the same model can differ, so check the endpoints list when you depend on a specific control.

Routing requirements

Reasoning controls are forwarded by default, with one exception: an effort goes only to endpoints that list it in supported_efforts, or that don't publish supported_efforts at all. effort: "none" also reaches endpoints with mandatory: false. If no endpoint of the model accepts your effort, the request fails with 400 invalid_request on reasoning.effort and is never sent to a provider.

With provider.require_parameters: true, the following declarations narrow routing:

You sendThe endpoint must declare
Any reasoning control that does not disable reasoning, including exclude or include_reasoning on their ownreasoning in supported_parameters
effort other than noneThat level in supported_efforts, or no reasoning object at all
effort: "none" or enabled: falsenone in supported_efforts, or mandatory: false
max_tokenssupports_max_tokens: true

With provider.require_parameters: true, an endpoint that lists reasoning in supported_parameters but has no reasoning object hasn't published its effort levels. It still receives a request with an effort, passed on as a hint: the provider may use that level, use its own default, or reject it. Endpoints that publish a reasoning object get only the efforts they list.

With strict matching, if no eligible endpoint qualifies, the request fails with 503 provider_unavailable, unless the effort alone ruled out every endpoint, which returns 400 invalid_request. To use a model's defaults without narrowing routing, omit the reasoning fields entirely.

Response fields

Visible reasoning is returned next to the answer:

Response excerpt
{  "choices": [    {      "index": 0,      "finish_reason": "stop",      "message": {        "role": "assistant",        "content": "The letter r appears three times in \"strawberry\".",        "reasoning": "Spell it out: s, t, r, a, w, b, e, r, r, y. That is r at positions 3, 8 and 9.",        "reasoning_details": [          {            "type": "reasoning.text",            "text": "Spell it out: s, t, r, a, w, b, e, r, r, y. That is r at positions 3, 8 and 9.",            "signature": "EqQBCkgIBxABGAIqQ...",            "format": "anthropic-claude-v1"          }        ]      }    }  ]}
  • reasoning is the plain visible reasoning text, for display.

  • reasoning_details holds structured reasoning blocks as the provider returned them. It is present when the provider returns structured reasoning, such as signed blocks, summaries or encrypted state.

With exclude: true, both fields are omitted.

Detail typeCarriesMeaning
reasoning.texttext, optional signatureRaw reasoning text. A signature lets the provider verify the block when you send it back.
reasoning.summarysummary (or native text)A summary of the reasoning, for providers that summarize instead of exposing raw text.
reasoning.encrypteddataOpaque reasoning state you cannot read, only return.

Reasoning detail

typestring

reasoning.text, reasoning.summary or reasoning.encrypted.

textstring

Reasoning or summary text. Up to 4 MiB.

signaturestring

Provider signature for the block. Up to 1 MiB.

datastring

Encrypted state. Up to 8 MiB.

idstring

Provider block identifier.

formatstring

Provider-specific encoding of the block. Treat it as opaque. unknown marks plain reasoning without provider-specific state.

indexinteger

Provider block index.

Streaming

In a stream, each visible reasoning fragment arrives in one chunk as both delta.reasoning and a reasoning.text entry in delta.reasoning_details with the same text. Read one of the two, never both:

  • For display, concatenate delta.reasoning.

  • To send reasoning back later, collect the delta.reasoning_details entries in order and rebuild them as shown in From a stream.

Streamed details follow two further rules:

  • Signature-only fragments. When the provider signs a block after its text has streamed, a reasoning.text detail arrives with the signature (and id, format or index) but without the text, which you already have.

  • Finish. The finish chunk has an empty delta; no synthetic reasoning detail is added.

Event stream
data: {"id":"6f1c2b8e-3d4a-4f5b-9c7d-2e8a1b0c9d3f","object":"chat.completion.chunk","created":1790000000,"model":"openai/gpt-6-luna","choices":[{"index":0,"delta":{"role":"assistant","reasoning":"Spell it out: ","reasoning_details":[{"type":"reasoning.text","text":"Spell it out: "}]},"finish_reason":null}]}​data: {"id":"6f1c2b8e-3d4a-4f5b-9c7d-2e8a1b0c9d3f","object":"chat.completion.chunk","created":1790000000,"model":"openai/gpt-6-luna","choices":[{"index":0,"delta":{"reasoning":"s, t, r, a, w, b, e, r, r, y.","reasoning_details":[{"type":"reasoning.text","text":"s, t, r, a, w, b, e, r, r, y."}]},"finish_reason":null}]}​data: {"id":"6f1c2b8e-3d4a-4f5b-9c7d-2e8a1b0c9d3f","object":"chat.completion.chunk","created":1790000000,"model":"openai/gpt-6-luna","choices":[{"index":0,"delta":{"content":"Three."},"finish_reason":null}]}​data: {"id":"6f1c2b8e-3d4a-4f5b-9c7d-2e8a1b0c9d3f","object":"chat.completion.chunk","created":1790000000,"model":"openai/gpt-6-luna","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

With exclude: true, no reasoning appears in the stream at all. See Streaming for the complete event format.

Preserve reasoning across turns

Some providers need their earlier reasoning to continue a conversation correctly, most importantly after tool calls. Signed and encrypted details carry that state. Send it back on the assistant message it belongs to:

Next request, messages excerpt
[  { "role": "user", "content": "What is the weather in Lisbon?" },  {    "role": "assistant",    "content": null,    "reasoning_details": [      {        "type": "reasoning.text",        "text": "I need current data, so I will call the weather tool.",        "signature": "EqQBCkgIBxABGAIqQ...",        "format": "anthropic-claude-v1"      }    ],    "tool_calls": [      {        "id": "call_7f3a9c",        "type": "function",        "function": { "name": "get_weather", "arguments": "{\"city\":\"Lisbon\"}" }      }    ]  },  { "role": "tool", "tool_call_id": "call_7f3a9c", "content": "{\"temperature\":21}" }]

Rules for reasoning in request messages:

  • Return all reasoning details unchanged, including unfamiliar fields, formats, types and empty fragments.

  • The upstream validates continuation semantics. Aggregate request and JSON complexity limits still apply.

  • A native dialect adapter can return unsupported_capability when it cannot translate opaque state without losing data.

For requests without end-to-end encryption, sources using the OpenAI-compatible chat format handle these reasoning fields as follows:

You sendResult
Nonempty reasoning_content, with reasoning absent or emptyOpenRouter and PPQ send the text as reasoning; other compatible chat sources retain reasoning_content.
Both fields contain the same stringThe source sends one representation.
reasoning_content: ""The explicit empty string is retained.
reasoning: "", with no reasoning_contentThe empty field is omitted.
Other distinct string valuesBoth values stay independent.

From a buffered response

Copy message.reasoning_details as returned onto the assistant message you append. If the response only has a plain reasoning string, you can send that back as reasoning.

From a stream

Rebuild complete blocks from the streamed fragments before sending them back. Empty metadata fragments are accepted; no synthetic terminator is required.

  1. A reasoning.text fragment with text and no signature extends the previous detail if that is an unsigned reasoning.text; otherwise it starts a new one.

  2. A reasoning.text fragment with a signature but no text completes the most recent unsigned reasoning.text detail. If there is none, keep it as its own detail.

  3. A fragment with neither text nor signature only fills in missing fields, such as format, on the most recent reasoning.text detail.

  4. Every other fragment is its own detail.

def rebuild_reasoning_details(fragments: list[dict]) -> list[dict]:    """Merge streamed reasoning_details fragments into complete details."""    merged: list[dict] = []​    def latest_text(unsigned_only: bool) -> dict | None:        for detail in reversed(merged):            if detail["type"] == "reasoning.text" and not (unsigned_only and "signature" in detail):                return detail        return None​    for fragment in fragments:        has_text = bool(fragment.get("text"))        signed = "signature" in fragment        if fragment["type"] != "reasoning.text" or (has_text and signed):            merged.append(dict(fragment))        elif has_text:            last = merged[-1] if merged else None            if last and last["type"] == "reasoning.text" and "signature" not in last:                last["text"] = last.get("text", "") + fragment["text"]            else:                merged.append(dict(fragment))        else:            target = latest_text(unsigned_only=signed)            if target is None:                if signed:                    merged.append(dict(fragment))                continue            for key in ("signature", "id", "format", "index"):                if key in fragment and key not in target:                    target[key] = fragment[key]    return merged

SDKs built for OpenRouter reasoning details, such as the OpenRouter AI SDK provider, perform this accumulation for you.

Keep a conversation on one endpoint where possible: signed state is produced by one provider and is meant for that provider. A session ID keeps follow-up turns on the same endpoint while it remains eligible.

Output limits and billing

max_tokens (or max_completion_tokens) is passed to the provider, which applies it to the whole output. On most models it covers reasoning as well as the answer. reasoning.max_tokens is a separate budget for thinking; keep it below max_tokens so the answer has room.

When a native adapter converts an effort hint or default into a numeric thinking budget, it caps that generated budget below the total output limit. An explicit reasoning.max_tokens value is preserved for upstream validation.

Reasoning tokens are part of completion_tokens and are billed, including reasoning you exclude from the response. Where an endpoint prices reasoning separately, reasoning tokens use that rate instead of the completion rate, so each token is charged once, and a provider.max_price.completion ceiling also applies to it.

Usage excerpt
{  "prompt_tokens": 24,  "completion_tokens": 212,  "total_tokens": 236,  "completion_tokens_details": { "reasoning_tokens": 168 },  "token_count_basis": "provider_native",  "cost": 0.000765,  "cost_details": { "upstream_inference_cost": 0.000765 }}

See Usage accounting for every usage field.

Best practices

  • Check supported_efforts for guidance, or omit reasoning controls to use the model default. Strict matching is opt-in.

  • Budget output for thinking. Raise max_tokens for reasoning models and watch for finish_reason: "length".

  • Use exclude: true when you only need the answer, and expect a longer wait before the first chunk.

  • Always return reasoning details during tool loops for providers that sign or encrypt reasoning.

  • Read one streaming representation. Concatenating reasoning and the reasoning_details text doubles the output.

Next steps