The Claude thinking parameter: why Haiku 4.5 returns a 400, and how to gate it per model
Adaptive thinking runs on Opus 4.8 and Sonnet 5, Haiku 4.5 rejects it with a 400, and omitting it turns thinking off on Opus. Gate it in one helper.
Everything works in development on Opus. Then a free-plan user sends a message, the route clamps them to Haiku, and the request dies with a 400. The cause is one field: thinking: { type: "adaptive" }. Claude's thinking parameter is not uniform across models, and the differences go both ways — the setting one model requires is the setting another rejects. If your app routes between models, which any plan-gated app does, you need to choose the field per model, in one place.
No single value works on every model
Here is how the three models in the kit's registry treat the parameter, per the current API reference:
- Opus 4.8 and Sonnet 5:
{ type: "adaptive" }is the way to turn thinking on — the model decides when and how much to think. The older fixed-budget form,{ type: "enabled", budget_tokens: N }, has been removed on these models and returns a 400. - Haiku 4.5: does not accept adaptive thinking — the request fails with a 400. Haiku 4.5 still uses the budget form if you want thinking at all.
So a hard-coded adaptive field breaks Haiku, and a hard-coded budget field breaks Opus and Sonnet. There is no literal you can paste into every call.
The quieter trap: leaving it out
The obvious workaround — just omit thinking everywhere — trades a loud failure for a silent one. Omitting the field does not mean the same thing on every model. On Sonnet 5, a request with no thinking field still runs adaptive thinking. On Opus 4.8, it runs without thinking. Drop the field to fix Haiku and your default, most expensive model quietly stops reasoning on hard prompts, and nothing errors to tell you.
A capability flag and one helper
The fix is to make "does this model take adaptive thinking?" data, not a guess at each call site. The model registry carries a flag, and one helper returns the right fields to spread into a request:
// In the model registry: Opus 4.8 and Sonnet 5 set supportsThinking: true; Haiku 4.5 sets false.
export function adaptiveThinking(
model: ModelId,
): { thinking: { type: "adaptive" } } | Record<string, never> {
return MODELS[model].supportsThinking
? { thinking: { type: "adaptive" } }
: {};
}
// At every call site:
anthropic().messages.stream({
model,
max_tokens: opts.maxTokens ?? 64_000,
...adaptiveThinking(model),
messages: opts.messages,
});
The kit spreads this into the text, streaming, and tool-loop helpers, so a model swap — or a plan clamp from Opus down to Haiku — never needs a call-site change. This is the same move as gating models by plan: put the per-model rule in the registry, and let every route read it.
Why plan gating makes this urgent
In a single-model app you would hit this once, in development, and fix it. In a plan-gated app the model changes per user. The free tier routes to Haiku because it is cheap, which means the requests most likely to hit the 400 are the ones from users who have not paid you yet — and a broken first impression on the free tier is the conversion you lose. Gate the parameter and the clamp is safe in both directions.
Things that bite
- Effort is gated too.
output_config.effortalso errors on Haiku 4.5. If you add effort tuning, route it through the same capability check rather than adding a second set of per-model conditionals. - Decide per helper, deliberately. In the kit, the structured-output helper sends no
thinkingfield — so on Opus 4.8 those calls run without thinking. That can be the right trade for extraction, but it should be a decision, not an accident of omission. - Thinking is billed even when you cannot see it. On these models, thinking text is omitted from responses by default — the blocks come back empty unless you request a summary — but the reasoning still happens and its tokens are still billed as output. Meter output tokens, not just visible text.
- The budget form has rules if you use it on Haiku.
budget_tokensmust be at least 1,024 and less thanmax_tokens, or the request fails.
Shipwright — the Next.js and Claude starter kit this blog documents — ships the registry flag and the one helper every model call spreads, so routing a free user to Haiku is a pricing decision, not a production incident. Try the free tier in the live demo.