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:
| Models | Request shape | Effort levels |
|---|---|---|
| Fable, Mythos 5.x, Opus 5.x, Opus 4.8, Opus 4.7, Sonnet 5.x, Haiku 5.x | thinking={"type": "adaptive"} + output_config={"effort": ...} | low medium high xhigh max |
| Opus 4.6, Sonnet 4.6, Mythos Preview | same | low medium high max (no xhigh) |
| Opus 4.5 | thinking={"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.5 | thinking={"type": "enabled", "budget_tokens": N} only | the budget below |
So per level:
| effort | Full set | No xhigh | Opus 4.5 | Budget only |
|---|---|---|---|---|
minimal | effort="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:
| Models | minimal | low | medium | high | xhigh | max |
|---|---|---|---|---|---|---|
o1, o3, o4 | low | low | medium | high | high | high |
gpt-5, gpt-5-mini, gpt-5-nano | minimal | low | medium | high | high | high |
gpt-5.1 | low | low | medium | high | high | high |
gpt-5.2 to gpt-5.5 | low | low | medium | high | xhigh | xhigh |
gpt-5.6, gpt-6.x | low | low | medium | high | xhigh | max |
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):
agent.run(..., effort=): per-call overrideAgent(effort=): agent defaultNone: 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=1The 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
Agent.effort: the constructor parameter reference- Providers: which adapter handles which model
- RetryPolicy: a separate dial for transient errors