Skip to Content
ModelsReasoning effort

Reasoning effort

Reasoning effort controls how much a model thinks before it answers: more effort usually means better answers on hard multi-step problems, at the cost of more output tokens and latency. Every lab ships this knob under a different name and shape: OpenAI’s reasoning_effort, Anthropic’s adaptive thinking with output_config.effort, older Claudes’ thinking.budget_tokens integer, LiteLLM’s normalized passthrough, Gemini’s own integer budget. Loomflow puts all of them behind one setting:

effort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max"

Pass it once; the framework picks the right provider-native shape for the model you’re talking to.

Quick start

from loomflow import Agent # Agent-level default. Every run thinks medium-hard. agent = Agent("...", model="claude-opus-4-7", effort="medium") # Per-call override. Wins over the agent default for this run. result = await agent.run("A hard reasoning question", effort="high")

Two ways to wire it on an Agent

Pick whichever reads better at your call site. They do the same thing.

Explicit kwargs, good for short inline construction:

agent = Agent("...", model="claude-opus-4-7", effort="high")

Dict form, good when you want everything model-related in one place:

agent = Agent( "...", model={ "name": "claude-opus-4-7", "effort": "high", "strict_effort": True, }, )

Same shape philosophy as audit_log={...}: one parameter, structured config. The framework normalises both forms to the same internal state, so they’re interchangeable.

If both are present, the explicit top-level kwarg wins. That’s useful when you’ve got a shared config dict but want to flip the dial per environment:

agent = Agent( "...", model={"name": "claude-opus-4-7", "effort": "low"}, effort="high", # this wins; the dict's "low" is shadowed )

Where each value lands

Each model accepts its own subset of the six levels. A level the model accepts is sent as is. Any other level is clamped to the highest level the model accepts below it, or to the model’s lowest level when you ask for less than all of them. So xhigh becomes high on a model whose top level is high, and minimal becomes low on every Claude that takes an effort level.

Anthropic. The adapter looks the model up by name (longest prefix wins, so dated snapshots match) and picks one of four request shapes:

ModelsRequest shapeEffort levels
Fable, Mythos 5.x, Opus 5.x, Opus 4.8, Opus 4.7, Sonnet 5.x, Haiku 5.xthinking={"type": "adaptive"} + output_config={"effort": ...}low medium high xhigh max
Opus 4.6, Sonnet 4.6, Mythos Previewsamelow medium high max (no xhigh)
Opus 4.5thinking={"type": "enabled", "budget_tokens": N} + output_config={"effort": ...}low medium high, plus the budget below
Opus 4 / 4.1, Sonnet 4 / 4.5, Sonnet 3.7, Haiku 4.5thinking={"type": "enabled", "budget_tokens": N} onlythe budget below

So per level:

effortFull setNo xhighOpus 4.5Budget only
minimaleffort="low"effort="low"budget_tokens=1024, effort="low"budget_tokens=1024
low"low""low"2048, "low"2048
medium"medium""medium"4096, "medium"4096
high"high""high"8192, "high"8192
xhigh"xhigh""high"16384, "high"16384
max"max""max"32768, "high"32768

OpenAI sends reasoning_effort, clamped to the levels each family accepts:

Modelsminimallowmediumhighxhighmax
o1, o3, o4lowlowmediumhighhighhigh
gpt-5, gpt-5-mini, gpt-5-nanominimallowmediumhighhighhigh
gpt-5.1lowlowmediumhighhighhigh
gpt-5.2 to gpt-5.5lowlowmediumhighxhighxhigh
gpt-5.6, gpt-6.xlowlowmediumhighxhighmax

LiteLLM sends reasoning_effort for every model, with xhigh and max mapped to high (see LiteLLM below).

Models that don’t support reasoning effort at all (GPT-4o, GPT-4.1, Claude 3 and 3.5) drop the kwarg and emit a one-time warning per (model, effort) pair.

An effort string that isn’t one of the six levels raises ValueError when the Agent is built or when run() is called, so a typo like "hgih" never goes out silently.

(Before 0.17, Claude 5.x, Fable, Opus 4 / 4.1 and Haiku 4.5 dropped effort with a warning, Opus 4.6 / Sonnet 4.6 clamped max to high, and an unknown effort string didn’t raise. Agents on those models that set effort= now think, and are billed for it.)

Constructor + run signatures

class Agent: def __init__( self, instructions: str, *, model: str | Model, effort: str | None = None, strict_effort: bool = False, # ... other params ) -> None: ... async def run( self, prompt: str, *, effort: str | None = None, # ... other params ) -> RunResult: ...

Resolution order (most specific wins):

  1. agent.run(..., effort=): per-call override
  2. Agent(effort=): agent default
  3. None: nothing is sent, so the provider’s own default applies (on Claude, no thinking)

strict_effort is agent-level only. There’s no per-call override. Whether a model can honour effort is a property of the adapter, not of any single call.

Warn-and-drop vs strict mode

By default, wiring effort= to a model that can’t honour it is a soft fallback. The kwarg is dropped, the model runs as it would have without effort, and a UserWarning is emitted exactly once per (model, effort) pair:

agent = Agent("...", model="gpt-4.1", effort="high") await agent.run("hi") # UserWarning: Model 'gpt-4.1' does not support effort='high': # OpenAI accepts reasoning_effort only on o-series, GPT-5.x and # GPT-6.x models. The kwarg has been dropped for this and future # calls; emitted once per (model, effort) pair.

This is the right default for production. A model swap shouldn’t break a working agent. But it’s the wrong default during development, where silently dropping the dial is exactly the bug you want surfaced.

Opt into strict mode to make the same situation a hard error:

from loomflow import Agent from loomflow.model._effort import EffortNotSupportedError agent = Agent( "...", model="gpt-4.1", effort="high", strict_effort=True, # raise instead of warning ) try: await agent.run("hi") except EffortNotSupportedError as exc: print(exc) # Model 'gpt-4.1' does not support effort='high': # ... Pass strict_effort=False on the Agent to downgrade # this to a warning + drop.

EffortNotSupportedError is a ValueError subclass. Strict mode only covers models that take no effort at all; a level the model doesn’t have is clamped, not raised.

Wire strict_effort=True in CI / pre-prod so model mismatches surface immediately (a misspelled level raises either way). Wire strict_effort=False (the default) in production so a vendor outage that pushes you onto a fallback model doesn’t take the whole agent down.

Provider-by-provider detail

OpenAI (o-series / GPT-5.x / GPT-6.x)

Maps directly to OpenAI’s native reasoning_effort request kwarg, clamped per family as in the table above. The family is found by longest prefix (o1, o3, o4, gpt-5, gpt-5.1, … gpt-6), so dated snapshots and variants (o3-mini-2025-…, gpt-5.4-mini) match automatically.

GPT-4o and GPT-4.1 don’t honour reasoning effort. They drop with a warning.

Tools change this on two GPT-6 models. GPT-6 Sol and GPT-6 Luna call tools through Chat Completions only with reasoning_effort="none", so on a request that offers tools the adapter sends "none" in place of your effort (warning once if you set one). See Providers for the GPT-6 models that can’t take tools at all.

Anthropic: four request shapes

The Anthropic adapter looks the model up by name and picks one of the four shapes in the table above: adaptive thinking with the full level set, adaptive without xhigh, Opus 4.5’s budget plus effort level, or a thinking budget alone. Claude models older than those (Claude 3.5, Claude 3) fall through to warn-and-drop / strict raise.

The budget mapping is minimal=1024, low=2048, medium=4096, high=8192, xhigh=16384, max=32768. These aren’t provider-documented constants. They’re defensible defaults tuned to match the perceived effort tiers. The adapter has no constructor option for an exact budget: effort= is the only way it sets thinking. If you need a precise integer (for a benchmark, say), subclass AnthropicModel and override its _effort_kwargs() method to return {"thinking": {"type": "enabled", "budget_tokens": N}}.

Behind a gateway

Gateway ids such as OpenRouter’s anthropic/claude-haiku-5.5 or openai/gpt-5-mini are looked up as the vendor’s model, so they get that model’s request shape and levels; the id sent to the gateway is unchanged. The adapter has to speak the vendor’s format, though: AnthropicModel for anthropic/... ids, OpenAIModel for openai/... ids. A Claude id on OpenAIModel has no reasoning_effort equivalent, so its effort is dropped with the usual warning. See Gateways. (Before 0.17.2, gateway ids dropped effort on both adapters.)

LiteLLM

LiteLLM normalises every supported provider to OpenAI’s reasoning_effort shape, so Loomflow forwards the dial via {"reasoning_effort": ...} and lets LiteLLM handle the provider-specific translation downstream. xhigh / max are sent as "high", and nothing is warn-dropped on this path: LiteLLM’s backend decides whether the provider takes the kwarg.

This means any LiteLLM-supported reasoning model (Vertex AI Gemini, Mistral reasoning models, and so on) picks up effort automatically, without per-adapter wiring.

Gemini (via LiteLLM)

Routes through LiteLLM’s reasoning_effort normalisation. For direct Gemini integration without LiteLLM, the integer thinking_budget kwarg is the native shape. Pass it to the provider client directly if you need precise control.

Example: see the dial in action

The framework ships examples/14_effort_dial.py running the same hard reasoning question (the 3L / 5L jug puzzle) at every tier on Claude Opus 4.7 and printing token usage at each:

effort=low tokens= 127+ 312 turns=1 effort=medium tokens= 127+ 564 turns=1 effort=high tokens= 127+1148 turns=1 effort=xhigh tokens= 127+2387 turns=1

The output token count is the visible signal. Higher effort spends more on internal reasoning, which is what you’re paying for.

Effort isn’t a magic correctness dial. xhigh makes the model spend more thinking budget; it does not make it solve a problem that’s outside its capability. Use higher effort for problems where the model’s first-attempt answer is close-but-wrong (multi-step arithmetic, constraint satisfaction, code review), not for out-of-distribution requests where no amount of thinking will help.

See also

Last updated on