Your Model Started Showing Its Work. Now You Have to Handle It.
Reasoning traces are the feature everyone turned on this month and nobody planned for. Handled wrong, they quietly triple your token bill, dump intermediate reasoning full of customer data into your logs, and break your tool loop in a way that looks like the model got dumber. Here is how to treat extended thinking as something you manage at the boundary, not something you print.
Sometime in the last few weeks you probably flipped on extended thinking across your product. The models got a lot better at hard reasoning when you let them think first, the flag was one line, and the demo looked great. Then the invoice came in higher than you expected, someone found a customer's email address sitting in a log line that was never supposed to hold one, and a tool-using agent that worked fine last month started making a specific kind of dumb mistake that you could not reproduce locally.
None of that is the model getting worse. It is what happens when a reasoning trace, which used to be an internal thing the model did and threw away, becomes a first-class part of the response that you are now responsible for handling. Most teams turned the feature on and changed nothing else about how they build the request, read the response, and write their logs. That gap is where the three problems live: cost you did not budget, data landing where it should not, and a correctness bug in your tool loop that only shows up under load.
This is a boundary problem, not a prompt problem. The fix is to decide, in code, what happens to the thinking on the way in and on the way out. Let me walk through the three failure modes and the handling that makes each one go away.
What a reasoning trace actually is in the response
Start with the shape of the thing, because the handling follows from it. When you enable thinking, the model's answer is no longer a single block of text. The response content is now an ordered list of blocks, and some of them are thinking blocks that come before the text block with the final answer. On the current model family you ask for it with adaptive thinking and you steer the depth with an effort level, not a fixed token budget.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
// Adaptive thinking lets the model decide how much to reason.
// display: "summarized" returns a readable trace; the default is
// "omitted", which still reasons and still bills, but hands back
// empty thinking text. Ask for what you actually intend to use.
thinking: { type: "adaptive", display: "summarized" },
output_config: { effort: "high" }, // low | medium | high | xhigh | max
messages: [{ role: "user", content: "..." }],
});
for (const block of response.content) {
if (block.type === "thinking") {
// Internal reasoning. Useful for debugging, dangerous in logs.
handleTrace(block.thinking);
} else if (block.type === "text") {
// The answer your user is meant to see.
deliver(block.text);
}
}
// Thinking is counted here, in output_tokens, no matter the display setting.
console.log(response.usage.output_tokens);
Two things in that snippet matter more than they look. First, the display setting is not a cost control. Whether you ask for the summarized trace or leave it omitted, the model does the same reasoning and you pay for the same tokens. Second, those thinking tokens land in output_tokens, which are the expensive ones. That is the whole cost story in one line, and it is why the first failure mode sneaks up on people.
Failure one: the bill you did not budget for
When you turn thinking on globally, every request starts paying for reasoning, including the ones that do not need any. A classification call that used to be a hundred cheap tokens now spends several hundred output tokens thinking about a decision that was never hard. Multiply that across your traffic and the increase is real, and because it hides inside normal output tokens it does not show up as a new line item you would notice.
The lever is effort, and the move is to stop treating it as a global default. Route it per task. Simple, mechanical, high-volume steps get low effort. The genuinely hard reasoning that made you want the feature in the first place gets the high tiers.
// Effort is the spend dial. Match it to how hard the step actually is,
// instead of paying "high" for a router decision a regex could make.
type Effort = "low" | "medium" | "high" | "xhigh" | "max";
function effortFor(taskKind: string): Effort {
switch (taskKind) {
case "classify":
case "route":
case "extract-field":
return "low"; // fewer thinking tokens, terser output
case "summarize":
case "draft":
return "medium";
case "plan":
case "debug":
case "multi-step-tool-use":
return "high"; // the work that justified turning thinking on
default:
return "medium";
}
}
async function run(taskKind: string, messages: Anthropic.MessageParam[]) {
const res = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
thinking: { type: "adaptive" },
output_config: { effort: effortFor(taskKind) },
messages,
});
// Track spend by task so you can see where the thinking budget goes.
meter.record(taskKind, res.usage.output_tokens);
return res;
}
The meter.record line is not decoration. The reason thinking cost surprises people is that they cannot see it broken down. Tag your usage by task kind and the expensive categories become obvious in a day, and you can pull effort down on the ones that were never worth it. If you already run per-tenant or per-feature budgets, this is the same idea one level deeper: reasoning is now a cost you meter, and the same cost guardrails you put on completions should count thinking tokens too.
Failure two: the trace in your logs
Here is the one that turns into an incident. A reasoning trace is the model restating your input in its own words and working through it. That means whatever the user handed you tends to reappear inside the thinking: the account number they pasted, the name in their support message, the contents of the document you asked the model to reason over. It is, functionally, a second copy of your sensitive input, generated fresh and dropped into the response.
Now think about where your thinking blocks go by default. Most teams log the raw model response for debugging. If that logger now serializes the full content array, you have just written intermediate reasoning full of customer data into a log store that your redaction pipeline probably does not cover, that gets shipped to a third-party observability vendor, and that is retained for months. Nobody decided to do that. It happened because the response grew a new field and the logger kept logging the whole thing.
The fix is a boundary. Strip thinking blocks off the response before anything is persisted, emitted to a client, or forwarded to a log sink. Keep a hash if you want to correlate, keep a short summary if you genuinely debug with it, but do not let the raw trace flow downstream by default.
import { createHash } from "node:crypto";
// Everything that leaves the service goes through here first.
// Thinking never survives the boundary in raw form.
function sanitizeForEgress(content: Anthropic.ContentBlock[]) {
const forClient: Anthropic.ContentBlock[] = [];
const forLogs: Array<Record<string, unknown>> = [];
for (const block of content) {
if (block.type === "thinking") {
// Do not persist or ship the raw reasoning. Keep a fingerprint
// so you can tell "was there a trace and did it change" without
// storing what it said.
forLogs.push({
type: "thinking",
chars: block.thinking.length,
digest: createHash("sha256").update(block.thinking).digest("hex").slice(0, 16),
});
continue; // dropped from what the user sees
}
forClient.push(block);
forLogs.push({ type: block.type });
}
return { forClient, forLogs };
}
This is the same discipline as any other output guardrail at the egress boundary: the model can produce something you do not want leaving the building, so you gate the exit rather than trusting every call site to remember. The difference with thinking is that it is easy to forget the trace is even there, because for months the model's reasoning was never something you had to handle at all.
Failure three: the tool loop that quietly breaks
The last one is the subtle bug, and it is a correctness bug, not a cost or privacy one. It only appears in agents that use tools, and it looks like the model suddenly reasoning worse mid-conversation.
Here is the mechanism. When the model wants to call a tool, it stops with stop_reason: "tool_use", and the assistant turn it just produced contains its thinking blocks followed by the tool_use block. You run the tool, and to continue you append that assistant turn plus a user turn with the tool result, then call again. The mistake is rebuilding that assistant turn from the pieces you extracted, usually just the text and the tool_use, and dropping the thinking blocks on the floor. When the thinking is missing, the model has lost the reasoning it had already started, and it has to reconstruct its plan from nothing on every single tool round. That is the "it got dumber" symptom.
The rule is small and absolute: within a tool-use turn on the same model, append the response content verbatim. Do not reassemble it.
async function agentLoop(
initial: Anthropic.MessageParam[],
tools: Anthropic.Tool[],
) {
const messages = [...initial];
while (true) {
const res = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
thinking: { type: "adaptive" },
tools,
messages,
});
// THE load-bearing line. Append the whole content array, thinking
// blocks included, exactly as it came back. Rebuilding this turn
// from just the text + tool_use silently strips the reasoning and
// the model starts each tool round from scratch.
messages.push({ role: "assistant", content: res.content });
if (res.stop_reason !== "tool_use") {
return res; // turn is over; now the thinking is safe to prune
}
const results = res.content
.filter((b): b is Anthropic.ToolUseBlock => b.type === "tool_use")
.map((call) => ({
type: "tool_result" as const,
tool_use_id: call.id,
content: runTool(call.name, call.input),
}));
messages.push({ role: "user", content: results });
}
}
Two clarifications that save you from over-correcting. First, this only matters on the same model. If you route the next call to a different model, the thinking blocks are ignored and not billed, so you do not need to strip them by hand for a model switch. Second, "preserve within the turn" is not "carry forever." Once a turn ends, old thinking blocks from earlier turns are dead weight in your context, and you pay to resend them every request. That is exactly the kind of stale context worth pruning between turns, and the API gives you a context edit that clears thinking blocks for you so you keep the live reasoning and drop the history.
// Clear old thinking blocks from history so you stop resending
// reasoning the model no longer needs. Preserves within the active
// turn; drops the accumulated dead weight from earlier ones.
const res = await client.beta.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
betas: ["context-management-2025-06-27"],
thinking: { type: "adaptive" },
context_management: { edits: [{ type: "clear_thinking_20251015" }] },
tools,
messages,
});
The through line
Extended thinking is not a switch you flip, it is a new kind of content in your responses, and content has to be handled. Read it deliberately instead of logging it by reflex. Meter it, because it spends output tokens whether or not you ever look at it, and set effort to match how hard the task actually is. Gate it at the boundary, because the trace is a fresh copy of your input and it should not walk out into your logs. Preserve it verbatim inside a tool turn and prune it between turns, because the model needs the reasoning it started and pays nothing to forget the reasoning it finished. Do those four things and the feature does what the demo promised without the three surprises the demo did not mention.
If you turned on thinking across your stack and you are not sure what your logs are now holding or what your tool loop is dropping, book a consultation call and we will trace one real request end to end and put the right handling at each boundary.
Viral Ruparel
Generative AI consultant helping teams ship reliable LLM and agent systems in production.
Contact Viral about your AI project →Frequently Asked Questions
Do I need to send the model's thinking blocks back on the next request?+
Only within an unbroken tool-use turn on the same model. When the model stops with a tool_use and you run the tool, the assistant turn you append before sending results must include the thinking blocks exactly as they came back, because the model uses them to continue the reasoning it had already started. The safe rule is to append the whole content array from the response rather than rebuilding the turn from the text you extracted. Once a turn has ended normally you are free to drop old thinking from the history, and if you route the next request to a different model the thinking blocks are ignored and not billed, so you do not have to strip them by hand.
Does extended thinking cost extra even if I never display it?+
Yes. Thinking tokens are billed as output tokens whether the display is set to summarized or omitted. Display controls what you can see, not whether the model reasons or what it costs. The lever that actually changes spend is the effort setting, which scales how much the model thinks. Use a lower effort for routine or mechanical steps and reserve the higher tiers for the requests where correctness is worth the tokens.
Is it safe to log the raw reasoning text?+
Treat it as sensitive by default. A reasoning trace restates the input in the model's own words, so anything the user gave it, including names, account numbers, and free text, tends to reappear inside the trace. If you pipe raw thinking into your log pipeline you have created a second copy of that data in a place your redaction usually does not cover. Strip thinking blocks at the boundary before anything is persisted or shown, and keep at most a hash or a short summary for debugging.
Related Articles
As of This Month, EU Law Says Your AI Output Has to Mark Itself
Article 50 of the EU AI Act took effect on August 2, and it says any image, audio, video, or text your system generates has to carry a machine-readable mark that it was made by AI. A checkbox in your UI does not satisfy that. The mark has to live inside the file, survive a re-upload, and be verifiable. Here is how to build that into your generation pipeline as a gate, not a bolt-on.
Your Agent Called the Same Tool Seventy Times and Billed You for It
A ReAct-style agent calls the same search tool, gets the same unhelpful result, decides another identical call will help, and does it again. Seventy times. It never crashes and never finishes, it just burns tokens going nowhere until a timeout or your bill catches it. A hard step ceiling is a backstop, not a fix. What you want is a guard that notices the agent has stopped making progress and breaks the cycle in seconds.
You Installed a Skill and Your Agent Will Run Whatever Is Inside
You pulled a skill off a registry, your agent read its instructions, and it is now ready to execute whatever code shipped in the folder. Nobody signed it, nobody diffed it, and the manifest that says "read-only" is just a text file the author wrote. Treat every third-party skill as untrusted code and put a gate in front of it: pin and hash it, check what it actually does against what it claims, and run it in a box that can only reach what it declared.