Reasoning
Control, read, hide, and preserve model reasoning.
Reasoning models think before they answer. Aivene returns that thinking alongside the answer in a single, consistent shape for every model, lets you control how much the model thinks, hide it from your users, and carry it across turns so multi-step tool use keeps its train of thought.
This page covers the Chat Completions API (/v1/chat/completions). The
Messages and Responses APIs keep their own
native reasoning shapes, summarized at the end.
Quick example
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://api.aivene.com/v1',
apiKey: process.env.AIVENE_API_KEY
});
const response = await client.chat.completions.create({
model: 'claude-sonnet-5',
messages: [{ role: 'user', content: 'Which is larger, 9.11 or 9.9?' }],
// @ts-expect-error - `reasoning` is an Aivene extension
reasoning: { effort: 'high' }
});
const message = response.choices[0].message;
console.log('THINKING:', 'reasoning' in message ? message.reasoning : null);
console.log('ANSWER:', message.content);Controlling reasoning
Send a reasoning object with the request:
{
"model": "claude-sonnet-5",
"messages": [{ "role": "user", "content": "..." }],
"reasoning": {
"effort": "high",
"max_tokens": 4000,
"enabled": true,
"exclude": false
}
}| Field | Type | What it does |
|---|---|---|
effort | max | xhigh | high | medium | low | minimal | none | How much the model should think. none turns reasoning off on models that allow it. |
max_tokens | integer | A token budget for thinking. See Effort vs. token budget. |
enabled | boolean | true turns reasoning on at medium effort when you don't pass effort or max_tokens. false turns it off. |
exclude | boolean | true keeps reasoning out of the response. The model still thinks. See Hiding reasoning. |
All fields are optional. Use either effort or max_tokens, not both. When a
model reasons by default, sending nothing keeps its default behavior.
reasoning_effort is accepted as a top-level shorthand for reasoning.effort:
{ "model": "gpt-5.5", "messages": [], "reasoning_effort": "low" }Not every model can stop thinking
Some models always reason, so effort: "none" or enabled: false may still
produce a short amount of thinking. Use exclude if your goal is to keep
reasoning out of your app's output.
Effort vs. token budget
Models take reasoning settings in one of two ways, and Aivene translates between them for you:
| Model family | effort | max_tokens |
|---|---|---|
| Claude | Native adaptive thinking at that effort | Used directly as the thinking budget (min 1,024, max 128,000) |
| Gemini | Mapped to the model's thinking level | Used directly as the thinking budget |
| GPT, Grok, DeepSeek and others | Native | Converted to an effort level (below) |
For effort-only models, reasoning.max_tokens becomes an effort level based on
its share of the request's max_tokens:
reasoning.max_tokens / max_tokens | Effort |
|---|---|
| 80% or more | high |
| 50% to 79% | medium |
| below 50% | low |
request has no max_tokens | medium |
Claude needs room to answer
With a token budget, Claude requires the request's max_tokens to be
strictly greater than reasoning.max_tokens, otherwise there is no room left
for the answer.
Reading reasoning
Every reasoning model returns two fields on the assistant message:
| Field | Type | Use it for |
|---|---|---|
reasoning | string | The readable thinking text. Show it in your UI or log it. |
reasoning_details | array | The structured reasoning blocks. Send them back unchanged to keep reasoning across turns. |
Non-streaming response
{
"choices": [
{
"message": {
"role": "assistant",
"content": "9.9 is larger. Compare the tenths digit: 9 is greater than 1.",
"reasoning": "Both numbers start with 9. Compare the decimal parts: 0.11 vs 0.90...",
"reasoning_details": [
{
"type": "reasoning.text",
"text": "Both numbers start with 9. Compare the decimal parts: 0.11 vs 0.90...",
"signature": "EqQBCkgIBxABGAIqQH...",
"format": "anthropic-claude-v1",
"index": 0
}
]
},
"finish_reason": "stop"
}
]
}Streaming response
Reasoning arrives in delta.reasoning and delta.reasoning_details, usually
before the answer text:
{
"choices": [
{
"delta": {
"reasoning": "Let me check",
"reasoning_details": [
{
"type": "reasoning.text",
"text": "Let me check",
"signature": null,
"format": "anthropic-claude-v1",
"index": 0
}
]
}
}
]
}- Concatenate
delta.reasoningacross chunks to get the full thinking text. - Chunks that belong to the same reasoning block share the same
index(andid, when present). Concatenate theirtextorsummaryin order. - A block's
signaturearrives in its own chunk, usually with an emptytext. reasoning.encryptedblocks arrive whole, in a single chunk.
You can also keep every reasoning_details entry exactly as streamed and send
the list back as-is. Aivene merges the fragments for you.
Reasoning details
Each entry in reasoning_details has a type and these common fields:
| Field | Type | Description |
|---|---|---|
type | string | reasoning.text, reasoning.summary, or reasoning.encrypted |
format | string | null | Which model family produced the block (see below) |
index | integer | Position of the block. Streamed fragments of one block share it. |
id | string | null | Block identifier, when the model provides one |
reasoning.text
Raw thinking text, optionally with a signature that lets the model verify the
block when you send it back.
{
"type": "reasoning.text",
"text": "First, list the constraints...",
"signature": "EqQBCkgIBxABGAIqQH...",
"format": "anthropic-claude-v1",
"index": 0
}reasoning.summary
A condensed summary of the model's thinking. Models that don't expose their raw thinking return summaries instead.
{
"type": "reasoning.summary",
"summary": "Compared both options on cost and latency.",
"id": "rs_0a1b2c3d",
"format": "azure-openai-responses-v1",
"index": 0
}reasoning.encrypted
Thinking the model keeps private. data is opaque: you can't read it, but
sending it back lets the model continue from where it left off.
{
"type": "reasoning.encrypted",
"data": "gAAAAABo9x...",
"id": "rs_0a1b2c3d",
"format": "azure-openai-responses-v1",
"index": 1
}Formats
format | Produced by |
|---|---|
anthropic-claude-v1 | Claude |
openai-responses-v1 | GPT models |
azure-openai-responses-v1 | GPT models |
bedrock-openai-responses-v1 | GPT models |
bedrock-xai-responses-v1 | Grok models |
xai-responses-v1 | Grok |
meta-responses-v1 | Meta models |
google-gemini-v1 | Gemini |
unknown | Any other reasoning model (DeepSeek, Qwen, GLM, MiMo, ...) |
null | Format not specified |
What each model family returns
| Model family | reasoning text | reasoning_details |
|---|---|---|
| Claude | Full thinking | reasoning.text with signature, and reasoning.encrypted for redacted thinking |
| GPT | Summaries | reasoning.summary plus one reasoning.encrypted per reasoning step |
| Gemini | Thought summaries | reasoning.text, plus a reasoning.encrypted per tool call (its id is the tool call id) |
| Grok, Meta | Usually none | reasoning.encrypted |
| DeepSeek, Qwen, GLM, MiMo and others | Full thinking | reasoning.text |
Preserving reasoning across turns
When you send the conversation back, include each assistant message's
reasoning_details exactly as you received them. This matters most for tool
calling: the model pauses mid-thought to call a tool, and sending its reasoning
back lets it continue that thought once the tool result arrives instead of
starting over.
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://api.aivene.com/v1',
apiKey: process.env.AIVENE_API_KEY
});
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get the current weather for a city',
parameters: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city']
}
}
}
];
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{ role: 'user', content: 'What should I wear in Jakarta today?' }
];
// Turn 1: the model thinks, then calls the tool.
const first = await client.chat.completions.create({
model: 'claude-sonnet-5',
messages,
tools,
// @ts-expect-error - `reasoning` is an Aivene extension
reasoning: { max_tokens: 2000 },
max_tokens: 4000
});
const assistant = first.choices[0].message;
const toolCall = assistant.tool_calls![0];
// Send the assistant message back unchanged, including `reasoning_details`.
messages.push(assistant);
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({ temperature: 31, condition: 'rain' })
});
// Turn 2: the model picks up its earlier reasoning and answers.
const second = await client.chat.completions.create({
model: 'claude-sonnet-5',
messages,
tools,
// @ts-expect-error - `reasoning` is an Aivene extension
reasoning: { max_tokens: 2000 },
max_tokens: 4000
});
console.log(second.choices[0].message.content);Pushing the SDK's message object back as-is keeps every field, including
reasoning and reasoning_details. If you rebuild messages yourself, copy
both fields over.
Accepted on assistant messages in the history:
| Field | Notes |
|---|---|
reasoning_details | Preferred. Carries signatures and encrypted blocks, which some models need to continue reasoning. |
reasoning | Plain text. Enough for models that only return readable thinking. |
Rules for opaque values
- Never edit
signatureordata. They are verified by the model and any change invalidates them. Keep the whole entry, includingid,format, andindex. - Keep tool call ids unchanged. Gemini binds its reasoning to the tool call id, so a rewritten id loses that context.
- Don't move reasoning between models. Blocks from one model family are ignored by another.
Hiding reasoning
If your app shows everything the model returns (chat front-ends, workflow tools, roleplay clients), hide reasoning with any one of these:
| Option | Where |
|---|---|
"reasoning": { "exclude": true } | Request body |
"include_reasoning": false | Request body |
x-strip-reasoning: true | Request header |
All three remove reasoning and reasoning_details from the response,
streaming and non-streaming. The model still reasons and those tokens are still
billed. To stop the reasoning itself, use effort: "none" or
enabled: false on models that allow it.
{
"model": "deepseek-v4-flash",
"messages": [{ "role": "user", "content": "Write a haiku about rain." }],
"reasoning": { "exclude": true }
}Third-party apps
Most chat front-ends and workflow tools let you add extra body parameters to
OpenAI-compatible requests (often called "Additional parameters", "Extra
body", or "Custom JSON"). Add {"reasoning": {"exclude": true}} there. If
the app only lets you add headers, use x-strip-reasoning: true.
Tokens and billing
Reasoning tokens are output tokens and are billed at the model's output price, whether or not you hide them.
{
"usage": {
"prompt_tokens": 42,
"completion_tokens": 1180,
"completion_tokens_details": {
"reasoning_tokens": 1024
}
}
}completion_tokensincludes reasoning. Subtractreasoning_tokensto get the visible answer length.- The request's
max_tokenscovers reasoning and the answer. If thinking uses the whole budget, the response ends withfinish_reason: "length"and little or nocontent. Raisemax_tokens, or lowereffort/reasoning.max_tokens.
Messages and Responses APIs
The other text APIs keep their native shapes:
- Messages API (
/v1/messages) - Usethinking({ "type": "enabled", "budget_tokens": 2000 }or{ "type": "adaptive" }) andoutput_config.effort. Thinking comes back asthinking/redacted_thinkingcontent blocks. Send them back unchanged in the next assistant message. - Responses API (
/v1/responses) - Usereasoning: { "effort": "high", "summary": "auto" }and add"include": ["reasoning.encrypted_content"]to receive encrypted reasoning. Reasoning comes back asreasoningoutput items. Pass them back unchanged in the next request'sinput.
The same rule applies: don't edit opaque values.