Models, Effort and Adaptive Thinking in Claude: Balancing Quality, Speed and Cost
In Claude, quality, speed and cost are set first by the model (Haiku 5.5, Sonnet 5.5, Opus 5.5, Fable 5.2) and then by output_config.effort; thinking is adaptive on these models and billed as output tokens. Prices as of 15 November 2026, default effort and which thinking can be turned off per model, fast mode, task budgets, and a model × effort evaluation tool for Bookstore's return classification.
Version note: The code in this post was built with .NET 10, Anthropic C# SDK 13.8.0 and Claude API version 2026-10-21; its unit tests and the price calculation mode of the evaluation tool were run. The model list, prices, default effort values and thinking behavior were taken on 15 November 2026 from the models overview, pricing, effort and thinking pages on platform.claude.com. This post is based on the official documentation; prices can change, so check the pricing page before relying on them.
Guess First
Selin, the Product Owner, wants to classify return requests automatically for Bookstore, the online bookstore used as the shared example across this site's series: six labels such as "arrived damaged", "wrong book arrived" and "changed my mind". The team is split. Ayşe says "let's use the best model, a wrong label is expensive". Emre says "it's a classification, the cheapest model is enough".
What is your guess? Is it more expensive to run Opus 5.5 at low effort or Sonnet 5.5 at high effort? And what does having thinking on change in a classification task? In this post we look at the current Claude models, the output_config.effort parameter and adaptive thinking, and then write a small evaluation tool that answers the question by measuring instead of guessing. The provider-neutral logic of cost and latency was covered in the sixteenth post of the LLM Fundamentals series.
The Current Model Family (15 November 2026)
The four current models on the models overview page:
| Model | API ID | Input / output (USD per MTok) | Default effort | Thinking | Relative latency |
|---|---|---|---|---|---|
| Claude Fable 5.2 | claude-fable-5-2 | 8 / 40 | high | Adaptive, always on | Moderate |
| Claude Opus 5.5 | claude-opus-5-5 | 4 / 20 | medium | Adaptive, always on | Moderate |
| Claude Sonnet 5.5 | claude-sonnet-5-5 | 2 / 10 | high | Adaptive | Fast |
| Claude Haiku 5.5 | claude-haiku-5-5 | 0.10 / 0.50 | medium | Adaptive | Fastest |
MTok means one million tokens. All four have a 1M token context window and a 128K token output limit on the synchronous Messages API. Haiku 5.5's price depends on prompt length: when the prompt is over 100,000 tokens, input goes up to 0.50 and output to 2.50 dollars. Tokens read from and written to the cache count toward the prompt length. The other models charge the same price across the whole 1M tokens; the pricing page says a 900K-token request pays the same per-token rate as a 9K-token request. Message Batches are 50 percent off. Cache reads are normally 10 percent of the input price; 5 percent on Opus 5.5 and Sonnet 5.5, and 2.5 percent on Fable 5.2.
The documentation's advice: if you are unsure, start with Opus 5.5 for most work. Fable 5.2 is for demanding reasoning and long-horizon agent work, or when Opus 5.5 falls short even at high effort. Haiku 5.5 is described for high-volume, latency-sensitive work such as classification, extraction and routing. The "Choosing a model" page offers two approaches: start with Haiku 5.5 and move up only when needed, or start with Opus 5.5 and lower the effort or the model over time. The page also says that tuning effort is often more effective than switching models. Our task is exactly the kind of work the first approach describes.
The schedule of older models matters too. According to the model deprecations page, Claude Sonnet 4.5 (claude-sonnet-4-5-20250929) retires on the Claude API on 30 November 2026, and the recommended replacement is claude-sonnet-5-5. If an old model ID is in your code, requests will fail after that date. The lifecycle and retires_at fields of the Models API let you track this in code.
Effort: Tuning Quality, Speed and Cost
Effort is a single value given to the request with output_config.effort: low, medium, high, xhigh or max. According to the effort page, the value affects not only thinking but every token of the answer: text, tool calls and their arguments, thinking. Lower effort means fewer and shorter tool calls. The documentation stresses that it is a behavioral signal, not a strict token budget: at low effort the model still thinks on a hard question, just less.
The first thing to watch is the defaults. On most models the default is high; on Opus 5.5 and Haiku 5.5 it is medium. So a request that moves from Opus 5 to Opus 5.5 without sending effort runs, in the documentation's words, one level lower. Sending the default value explicitly gives exactly the same behavior as not sending it. The second is xhigh and max: not every model has them. All five exist on the four current models, but if you move to an older model you need to check the capabilities.effort field in the Models API.
The per-model advice in brief. For Haiku 5.5 the documentation recommends starting with medium for most work and low for chat and simple high-volume work, but it also warns that in long agent prompts at low the model may skip a search or stop early. On Sonnet 5.5, medium or low for chat and latency-sensitive work. On Opus 5.5, if you use xhigh or max, give a large max_tokens; that limit is the hard limit on the total output, thinking included. The common advice in all of them is the same: do not copy the setting from the old model, run an effort sweep on your own evaluation set.
Adaptive Thinking: What Can You Turn Off on Which Model?
On the current models thinking is on by default and adaptive: the model decides how much to think, and effort steers that decision. The relevant part of the table on the thinking page for the four models:
| Model | adaptive | enabled + budget_tokens | between_tools | disabled |
|---|---|---|---|---|
| Opus 5.5 | Adaptive | 400 | 400 | 400 |
| Sonnet 5.5 | Adaptive | 400 | Up-front thinking off at high or below | 400 |
| Haiku 5.5 | Adaptive | 400 | 400 | Thinking off at high or below |
| Fable 5.2 | Adaptive | 400 | 400 | 400 |
Three conclusions follow. The old thinking: {type: "enabled", budget_tokens: ...} setting works on none of the four models; it returns a 400. On Opus 5.5 and Fable 5.2 thinking cannot be turned off; you control its cost and depth only with effort. Sonnet 5.5 has between_tools instead of disabled: it turns off the thinking before the answer, but the short progress notes between tool calls continue.
The display field decides whether the thinking text comes back to you. On the current models the default is omitted: a thinking block arrives but its thinking field is empty. With summarized you get a summary of the model's reasoning; the raw reasoning does not come back with any setting. Billing is the same for both. The documentation points out two more things: the full original thinking tokens are billed rather than the summary, so the text you see and the number on the bill do not match; and the benefit of omitted is not cost but the first text token arriving sooner when streaming.
In the SDK these settings are classes that convert implicitly to the ThinkingConfigParam union. We added an optional thinking field to the classifier's settings:
// Haiku 5.5: thinking can be turned off at high effort or below
ClassifierSettings haiku = new("claude-haiku-5-5", Effort.Low, new ThinkingConfigDisabled());
// Sonnet 5.5: "disabled" returns a 400; between_tools turns off up-front thinking
ClassifierSettings sonnet = new("claude-sonnet-5-5", Effort.Medium, new ThinkingConfigBetweenTools());
// Opus 5.5: thinking cannot be turned off; only display changes
ClassifierSettings opus = new("claude-opus-5-5", Effort.High, new ThinkingConfigAdaptive { Display = Display.Summarized });
The bodies of the three requests from RawBodyData (with the system and messages fields left out); the third also has fast mode on:
{"model":"claude-haiku-5-5","max_tokens":2048,"output_config":{"effort":"low"},"thinking":{"type":"disabled"}}
{"model":"claude-sonnet-5-5","max_tokens":2048,"output_config":{"effort":"medium"},"thinking":{"type":"between_tools"}}
{"model":"claude-opus-5-5","max_tokens":2048,"output_config":{"effort":"high"},"thinking":{"type":"adaptive","display":"summarized"},"speed":"fast"}
Fast Mode and Task Budgets
Fast mode runs the same model with a faster inference configuration. According to the fast mode page, output tokens per second go up by as much as 2.5 times; the gain is in output speed, not in time to first token. Among the current models it is available on Fable 5.2 and Opus 5.5. For Opus 5.5 the price is 8 dollars input and 40 dollars output, twice the normal price; no beta header is needed. Fast mode has its own rate limit, exceeding it returns a 429 with a retry-after header, and it cannot be used with the Batch API. In the SDK it is the request's Speed property:
MessageCreateParams fast = ReturnClassifier.BuildRequest("It arrived wet.", opus) with { Speed = Speed.Fast };
In Bookstore we would consider it only for work where the user waits for a long answer; a return classification does not need it.
Task budgets are in beta (with the task-budgets-2026-03-13 header). With output_config.task_budget you give the model a token budget for a whole agent loop; the model sees the remaining budget and finishes its work accordingly. The documentation says it does not replace effort but complements it: effort sets how carefully each step is reasoned, the task budget sets the limit of the total work. We will build the agent loop in the fifth post.
Bookstore: The Return Request Classifier
The classifier is a short system prompt, a single user message and an effort:
using Anthropic;
using Anthropic.Models.Messages;
using Bookstore.Api.Ai;
namespace Bookstore.Api.Returns;
public sealed record ClassifierSettings(string Model, Effort Effort, ThinkingConfigParam? Thinking = null);
public sealed record Classification(ReturnReason? Reason, string RawText, ReplyOutcome Outcome, TokenUsage Usage);
public sealed class ReturnClassifier(IAnthropicClient claude)
{
public const string SystemPrompt = """
You classify return requests for Bookstore, an online bookstore.
Reply with exactly one label and nothing else:
Damaged, WrongItem, NotAsDescribed, ChangedMind, LateDelivery, Other.
Damaged: the book arrived torn, wet or broken. WrongItem: a different title or edition arrived.
NotAsDescribed: the book differs from the product page. ChangedMind: the customer no longer wants it.
LateDelivery: it arrived too late to be useful. Other: anything else.
""";
public static MessageCreateParams BuildRequest(string requestText, ClassifierSettings settings)
{
MessageCreateParams request = new()
{
Model = settings.Model,
// Small answer, but leave room for thinking at higher effort.
MaxTokens = 2048,
System = SystemPrompt,
OutputConfig = new OutputConfig { Effort = settings.Effort },
Messages = [new UserMessageParam { Content = requestText }],
};
// Leave thinking out unless asked: each model has its own default.
return settings.Thinking is null ? request : request with { Thinking = settings.Thinking };
}
public async Task<Classification> ClassifyAsync(string requestText, ClassifierSettings settings, CancellationToken cancellationToken)
{
Message message = await claude.Messages.Create(BuildRequest(requestText, settings), cancellationToken);
ClaudeReply reply = ClaudeReplyReader.Read(message);
return new Classification(ParseLabel(reply.Text), reply.Text, reply.Outcome, reply.Usage);
}
public static ReturnReason? ParseLabel(string text) =>
Enum.TryParse(text.Trim().TrimEnd('.'), ignoreCase: true, out ReturnReason reason) ? reason : null;
}
We gave the labels with their definitions in the system prompt; we discussed why label descriptions matter in the classification post of the Prompt Engineering series. We take the answer with Enum.TryParse and return null if anything other than a single label comes back. The weak point of this approach is clear: if the model writes a sentence such as "The book is damaged", the classification is lost. In the sixth post we will tie the same task to a schema with structured outputs.
MaxTokens is 2048. The answer is one word, but Haiku 5.5 can think at medium too, and thinking counts toward max_tokens. Keeping the limit tight and seeing the answer cut off by max_tokens is one of the most common mistakes.
We put the price calculation into a class too:
namespace Bookstore.Api.Ai;
// USD per million tokens, from platform.claude.com/docs/en/about-claude/pricing (checked 2026-11-15).
public sealed record ModelPrice(decimal Input, decimal CacheWrite5m, decimal CacheRead, decimal Output);
public static class ClaudePricing
{
private const long HaikuLongPromptThreshold = 100_000;
private static readonly Dictionary<string, ModelPrice> Prices = new()
{
["claude-fable-5-2"] = new(8.00m, 10.00m, 0.20m, 40.00m),
["claude-opus-5-5"] = new(4.00m, 5.00m, 0.20m, 20.00m),
["claude-sonnet-5-5"] = new(2.00m, 2.50m, 0.10m, 10.00m),
["claude-haiku-5-5"] = new(0.10m, 0.125m, 0.01m, 0.50m),
};
// Haiku 5.5 has a higher price when the prompt (input + cache read + cache write) is over 100,000 tokens.
private static readonly ModelPrice HaikuLongPrompt = new(0.50m, 0.625m, 0.05m, 2.50m);
public static IReadOnlyCollection<string> KnownModels => Prices.Keys;
public static decimal EstimateUsd(string model, TokenUsage usage)
{
if (!Prices.TryGetValue(model, out ModelPrice? price))
{
throw new ArgumentException($"No price for model '{model}'.", nameof(model));
}
long promptTokens = usage.Input + usage.CacheRead + usage.CacheWrite;
if (model == "claude-haiku-5-5" && promptTokens > HaikuLongPromptThreshold)
{
price = HaikuLongPrompt;
}
// Thinking tokens are already inside Output; they have no separate price.
decimal total = usage.Input * price.Input
+ usage.CacheWrite * price.CacheWrite5m
+ usage.CacheRead * price.CacheRead
+ usage.Output * price.Output;
return total / 1_000_000m;
}
}
The tests pin both the label parsing and the price formula:
using System.Text.Json;
using Anthropic.Models.Messages;
using Bookstore.Api.Ai;
using Bookstore.Api.Returns;
namespace Bookstore.Api.Tests;
public sealed class ReturnClassifierTests
{
[Theory]
[InlineData("Damaged", ReturnReason.Damaged)]
[InlineData(" wrongitem.\n", ReturnReason.WrongItem)]
[InlineData("The book is damaged", null)]
public void ParseLabel_AcceptsOnlyOneLabel(string text, ReturnReason? expected) =>
Assert.Equal(expected, ReturnClassifier.ParseLabel(text));
[Fact]
public void BuildRequest_SendsEffortInOutputConfig()
{
MessageCreateParams request = ReturnClassifier.BuildRequest("It arrived wet.", new ClassifierSettings("claude-haiku-5-5", Effort.Low));
JsonElement body = JsonSerializer.SerializeToElement(request.RawBodyData);
Assert.Equal("low", body.GetProperty("output_config").GetProperty("effort").GetString());
Assert.False(body.TryGetProperty("thinking", out _));
}
[Fact]
public void BuildRequest_HaikuWithThinkingDisabled()
{
MessageCreateParams request = ReturnClassifier.BuildRequest("It arrived wet.",
new ClassifierSettings("claude-haiku-5-5", Effort.Low, new ThinkingConfigDisabled()));
JsonElement body = JsonSerializer.SerializeToElement(request.RawBodyData);
Assert.Equal("disabled", body.GetProperty("thinking").GetProperty("type").GetString());
}
}
public sealed class ClaudePricingTests
{
[Fact]
public void EstimateUsd_UsesBaseInputAndOutputPrices()
{
// 1M input + 1M output on Sonnet 5.5: $2 + $10.
decimal cost = ClaudePricing.EstimateUsd("claude-sonnet-5-5", new TokenUsage(1_000_000, 1_000_000, 0, 0, 0));
Assert.Equal(12.00m, cost);
}
[Fact]
public void EstimateUsd_HaikuPromptOver100kUsesTheHigherPrice()
{
decimal shortPrompt = ClaudePricing.EstimateUsd("claude-haiku-5-5", new TokenUsage(100_000, 0, 0, 0, 0));
decimal longPrompt = ClaudePricing.EstimateUsd("claude-haiku-5-5", new TokenUsage(100_001, 0, 0, 0, 0));
Assert.Equal(0.01m, shortPrompt);
Assert.Equal(0.0500005m, longPrompt);
}
}
Passed! - Failed: 0, Passed: 14, Skipped: 0, Total: 14, Duration: 53 ms - Bookstore.Api.Tests.dll (net10.0)
A Small Evaluation Harness
The answer to Ayşe and Emre's argument is not in the model's general quality but in its quality on this task. To measure it we prepared a small evaluation set of 12 examples (returns-eval.json) and a console tool that writes accuracy, median latency, output tokens and cost for each model and effort combination. We covered the general method of evaluation in the tenth post of the AI with .NET series and in the last post of the Prompt Engineering series.
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using Anthropic;
using Anthropic.Models.Messages;
using Bookstore.Api.Ai;
using Bookstore.Api.Returns;
// Usage:
// dotnet run -- estimate [inputTokens] [outputTokens] price math only, no API call
// dotnet run -- run needs ANTHROPIC_API_KEY, calls the API
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
string mode = args.Length > 0 ? args[0] : "estimate";
if (mode == "estimate")
{
long input = args.Length > 1 ? long.Parse(args[1]) : 450;
long output = args.Length > 2 ? long.Parse(args[2]) : 60;
TokenUsage perRequest = new(input, output, CacheRead: 0, CacheWrite: 0, Thinking: 0);
Console.WriteLine($"Assumed tokens per request: input={input} output={output}");
Console.WriteLine($"{"model",-20}{"per request",14}{"per 1,000",12}");
foreach (string model in ClaudePricing.KnownModels)
{
decimal one = ClaudePricing.EstimateUsd(model, perRequest);
Console.WriteLine($"{model,-20}{one,14:0.000000}{one * 1000,12:0.0000}");
}
return;
}
List<EvalCase> cases = JsonSerializer.Deserialize<List<EvalCase>>(
File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "returns-eval.json")),
new JsonSerializerOptions(JsonSerializerDefaults.Web))!;
// Each row is one configuration; Haiku 5.5 also runs once with thinking turned off.
(string Model, Effort Effort, ThinkingConfigParam? Thinking)[] grid =
[
("claude-haiku-5-5", Effort.Low, new ThinkingConfigDisabled()),
("claude-haiku-5-5", Effort.Low, null),
("claude-haiku-5-5", Effort.Medium, null),
("claude-sonnet-5-5", Effort.Low, null),
("claude-sonnet-5-5", Effort.Medium, null),
("claude-sonnet-5-5", Effort.High, null),
("claude-opus-5-5", Effort.Low, null),
("claude-opus-5-5", Effort.Medium, null),
("claude-opus-5-5", Effort.High, null),
];
using AnthropicClient client = new();
ReturnClassifier classifier = new(client);
Console.WriteLine($"{"model",-20}{"effort",-8}{"thinking",-10}{"accuracy",10}{"p50 ms",9}{"out tok",9}{"USD",10}");
foreach ((string model, Effort effort, ThinkingConfigParam? thinking) in grid)
{
int correct = 0;
long outputTokens = 0;
decimal cost = 0;
List<long> latencies = [];
// Sequential on purpose: a small eval should not hit the rate limit.
foreach (EvalCase item in cases)
{
Stopwatch watch = Stopwatch.StartNew();
Classification result = await classifier.ClassifyAsync(item.Text, new ClassifierSettings(model, effort, thinking), CancellationToken.None);
latencies.Add(watch.ElapsedMilliseconds);
correct += result.Reason?.ToString() == item.Expected ? 1 : 0;
outputTokens += result.Usage.Output;
cost += ClaudePricing.EstimateUsd(model, result.Usage);
}
latencies.Sort();
Console.WriteLine($"{model,-20}{effort.ToString().ToLowerInvariant(),-8}{(thinking is null ? "default" : "off"),-10}{(double)correct / cases.Count,10:P0}{latencies[latencies.Count / 2],9}{outputTokens,9}{cost,10:0.0000}");
}
internal sealed record EvalCase(string Text, string Expected);
The tool has two modes. The run mode runs the 9 configurations over the 12 examples, 108 requests in all, with your own ANTHROPIC_API_KEY, and writes accuracy, p50 latency, total output tokens and cost on each row. The estimate mode does not call the API at all: with the assumed token counts you give, it computes the price per request and per 1,000 requests for each model. The defaults are 450 input and 60 output tokens; these are our own assumptions for the calculation, not figures from the API:
cd tools/Bookstore.ClaudeEval
dotnet run -- estimate
dotnet run -- estimate 1200 300
Assumed tokens per request: input=450 output=60
model per request per 1,000
claude-fable-5-2 0.006000 6.0000
claude-opus-5-5 0.003000 3.0000
claude-sonnet-5-5 0.001500 1.5000
claude-haiku-5-5 0.000075 0.0750
Assumed tokens per request: input=1200 output=300
model per request per 1,000
claude-fable-5-2 0.021600 21.6000
claude-opus-5-5 0.010800 10.8000
claude-sonnet-5-5 0.005400 5.4000
claude-haiku-5-5 0.000270 0.2700
At the same token counts, Opus 5.5 costs 40 times Haiku 5.5 and Sonnet 5.5 20 times. But in the real table the token counts will not be the same: effort and thinking change the output tokens in particular from model to model. That is why the first column to look at in the output of the run mode is out tok. You will see two things there: how the output tokens of the Haiku row with thinking off compare with the other Haiku rows, and the difference between low and high on Opus.
Ask these questions when you read the table. How many examples behind the most expensive row is the cheapest one in accuracy? With 12 examples one mistake is 8 percent; grow the set before you decide. Which labels do the mistakes cluster in? If NotAsDescribed and WrongItem get mixed up, the problem is most likely in the label definition, and changing the model will not fix it. Does p50 latency matter in the user flow? For a classification that runs in the background after the return form is submitted, a difference of a few seconds usually does not matter.
Our first candidate before measuring is Haiku 5.5 at low effort with thinking off; the table from the run mode will either confirm it or move it up a row. The table should be run again as the evaluation set grows and examples from real requests are added.
In Production
Send effort explicitly. Defaults change from model to model, and when you move to a new model version you can quietly end up at a different effort. Keep the value in configuration and write it to the log.
Keep the model ID in configuration. That is why ClaudeOptions.Model exists in the first post. When a model's retirement date comes, the change should be a setting, not a deploy. Query the Models API regularly and raise a warning if you are using a model that is deprecated.
Compute cost from the log. A table like ClaudePricing lets you write a cost for every request from its usage. Because the price table is in code, you need to follow changes on the pricing page; in the last post we will compare it with the cost reports in the Console.
Version the evaluation set. Run the same set again before changing the model or the effort. The set should grow with real but anonymized examples from production; customer names and order numbers should not go into it.
Common Pitfalls
Sending budget_tokens. The old extended thinking setting works on none of the current models and returns a 400. Manage thinking depth with effort.
Trying to turn thinking off on Opus 5.5. disabled returns a 400. Sonnet 5.5 has no disabled either; the right value is between_tools.
Never setting effort. Opus 5.5 runs at medium, Sonnet 5.5 at high. If you compare the same code on two models without pinning effort, you compare two different settings.
Giving every job to the biggest model. Using Fable 5.2 for a classification costs 80 times Haiku 5.5 at the same token counts.
Choosing max_tokens by answer length. Thinking is included. Even a one-word answer may need a few thousand tokens of room.
Adding a date suffix to the model ID. Current IDs are dateless; on the models overview page Haiku 5.5's ID is only claude-haiku-5-5.
A Small Experiment
★☆☆ Run the estimate mode with your own assumptions: compute the per-1,000-request difference between Haiku 5.5 and Sonnet 5.5 for 3,000 input and 500 output tokens. Then raise the input to 120,000 and explain why Haiku 5.5's price jumps.
★★☆ Add a parameter for the Batch API discount (50 percent) to ClaudePricing and confirm it with a test. Add the fast mode price (8 / 40 for Opus 5.5) as a separate row.
★★★ Run the run mode with your own key. Add 20 more examples to the evaluation set that resemble Bookstore's real return requests (without customer data). Pick the row with the highest accuracy and the lowest cost in the table, and write up your decision in one paragraph.
Up to now every answer came in one piece. In the fourth post we move to streaming: the events from CreateStreaming, separating text and thinking deltas, collecting the final usage, and streaming these events from Bookstore.Api to the Angular client as Server-Sent Events.