Reasoning models
import { Agent, anthropic } from "@fifthrevision/axle";
const agent = new Agent({
provider: anthropic(process.env.ANTHROPIC_API_KEY!),
model: "claude-opus-4-5",
reasoning: true,
});
agent.on((event) => {
switch (event.type) {
case "part:start":
if (event.part.type === "thinking") console.log("\n[thinking]");
if (event.part.type === "text") console.log("\n[answer]");
break;
case "thinking:delta":
process.stdout.write(event.delta);
break;
case "text:delta":
process.stdout.write(event.delta);
break;
}
});
const result = await agent.send("Prove that the square root of 2 is irrational.").final;
console.log(`\nreasoning tokens: ${result.usage.reasoningOut ?? 0}`);reasoning is a portable boolean that maps onto each provider's own controls. Set it per agent, or per send when one question doesn't need it:
await agent.send("Quick question.", { reasoning: false }).final;For provider-specific knobs — thinking budgets, effort levels — use providerOptions, which is applied after Axle's mapping and can override it:
const agent = new Agent({
provider,
model,
reasoning: true,
providerOptions: { thinking: { type: "enabled", budget_tokens: 10_000 } },
});Bear in mind that ties the agent to one provider, so keep it out of code you want to stay portable.
As always, that switch is fine for a terminal. In a UI, apply the events to a Transcript and render the thinking parts it assembles.
What you get back
Reasoning surfaces as thinking parts:
interface ThinkingPart {
id: string;
type: "thinking";
text?: string; // renderable reasoning, when the provider exposes it
summary?: string; // provider-supplied summary
redacted?: boolean; // provider withheld the content
continuity?: ThinkingContinuity; // opaque state — preserve it
}Your UI needs to handle three cases, and it's easiest to write all three up front:
textpresent — render it, usually collapsed by default.summarypresent, notext— some providers only expose a summary. Render that.redacted: true— the provider withheld the content for safety. Show that thinking happened; there is nothing to display.
case "thinking":
if (part.redacted) return <Note key={part.id}>Reasoning withheld</Note>;
return (
<details key={part.id}>
<summary>Thinking</summary>
<pre>{part.text ?? part.summary}</pre>
</details>
);Summaries stream on their own channel, by the way — thinking:summary-delta rather than thinking:delta.
Continuity across turns
continuity is opaque provider state — an encrypted blob, a signature, a thought signature — that lets a model continue reasoning across requests.
type ThinkingContinuity =
| { provider: "openai"; encrypted: string }
| { provider: "anthropic"; signature?: string; redactedData?: string }
| { provider: "gemini"; thoughtSignature: string };Axle carries it through agent.messages automatically, so normally you never think about it. But there are two places where you have to preserve it verbatim:
- Persistence. If you serialize and restore sessions, do not strip it.
agent.snapshot()keeps it; hand-rolled message filtering often does not. - Compaction. A compactor that rewrites assistant messages must preserve
continuityon any thinking part it keeps — or drop the whole part. Half a thinking part with a mangled signature is worse than none.
Reasoning tokens and cost
result.usage.reasoningOut; // included in usage.out — do not add it againReasoning tokens bill as output. A reasoning model can easily spend far more on thinking than on the answer itself — which is why maxOutputTokens may need to be much larger than the length of the answer would suggest. If a reasoning model keeps truncating, that's usually the cause.
Reasoning with tools
These compose naturally: the model thinks, calls tools, thinks about the results, and answers. Each step can produce its own thinking part, and they all accumulate into the same agent turn.
Reasoning with structured output
This works, with one caveat worth knowing in advance: reasoning models are more prone to prefixing their JSON with commentary. If parse errors climb after you enable reasoning, restate the bare-JSON requirement in your system prompt, or simplify the schema.
Turning it off
Some models reason by default, which isn't always what you want. reasoning: false turns it off wherever the provider supports that. It's worth setting explicitly on latency-sensitive paths and on compaction calls — PromptCompactor already does the latter for you.