Features
Reasoning
Control how much a model thinks, read its reasoning, and carry reasoning state through multi-turn and tool conversations.
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
effortstringCommon values include
none,minimal,low,medium,high,xhighandmax; providers may accept others.noneturns reasoning off.max_tokensintegerProvider reasoning token budget. The upstream validates its range.
excludebooleanDefaultfalseKeep reasoning out of the response. The model can still reason; you do not receive it.
enabledbooleantrueasks for reasoning without choosing a level;falseturns it off.
Two shorthand fields are also accepted:
Other fields
reasoning_effortstringOpenAI-style shorthand for
reasoning.effort, with the same values.nullis treated as omitted.include_reasoningbooleanDeprecated. 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"{ "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 | nullEffort used when you send none.
default_enabledbooleanWhether the model reasons when you send no reasoning control.
mandatorybooleantruewhen reasoning cannot be turned off.supports_max_tokensbooleanWhether
reasoning.max_tokensbudgets 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 send | The endpoint must declare |
|---|---|
Any reasoning control that does not disable reasoning, including exclude or include_reasoning on their own | reasoning in supported_parameters |
effort other than none | That level in supported_efforts, or no reasoning object at all |
effort: "none" or enabled: false | none in supported_efforts, or mandatory: false |
max_tokens | supports_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:
{ "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" } ] } } ]}reasoningis the plain visible reasoning text, for display.reasoning_detailsholds 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 type | Carries | Meaning |
|---|---|---|
reasoning.text | text, optional signature | Raw reasoning text. A signature lets the provider verify the block when you send it back. |
reasoning.summary | summary (or native text) | A summary of the reasoning, for providers that summarize instead of exposing raw text. |
reasoning.encrypted | data | Opaque reasoning state you cannot read, only return. |
Reasoning detail
typestringreasoning.text,reasoning.summaryorreasoning.encrypted.textstringReasoning or summary text. Up to 4 MiB.
signaturestringProvider signature for the block. Up to 1 MiB.
datastringEncrypted state. Up to 8 MiB.
idstringProvider block identifier.
formatstringProvider-specific encoding of the block. Treat it as opaque.
unknownmarks plain reasoning without provider-specific state.indexintegerProvider 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_detailsentries 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.textdetail arrives with thesignature(andid,formatorindex) but without the text, which you already have.Finish. The finish chunk has an empty delta; no synthetic reasoning detail is added.
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:
[ { "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_capabilitywhen 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 send | Result |
|---|---|
Nonempty reasoning_content, with reasoning absent or empty | OpenRouter and PPQ send the text as reasoning; other compatible chat sources retain reasoning_content. |
| Both fields contain the same string | The source sends one representation. |
reasoning_content: "" | The explicit empty string is retained. |
reasoning: "", with no reasoning_content | The empty field is omitted. |
| Other distinct string values | Both 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.
A
reasoning.textfragment withtextand nosignatureextends the previous detail if that is an unsignedreasoning.text; otherwise it starts a new one.A
reasoning.textfragment with asignaturebut notextcompletes the most recent unsignedreasoning.textdetail. If there is none, keep it as its own detail.A fragment with neither
textnorsignatureonly fills in missing fields, such asformat, on the most recentreasoning.textdetail.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 mergedSDKs 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.
{ "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_effortsfor guidance, or omit reasoning controls to use the model default. Strict matching is opt-in.Budget output for thinking. Raise
max_tokensfor reasoning models and watch forfinish_reason: "length".Use
exclude: truewhen 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
reasoningand thereasoning_detailstext doubles the output.