OpenAI o-series reasoning_effort Parameter Support & Errors
reasoning_effort lets you dial reasoning depth from low to high, but the parameter is not universal — o4-mini ignores it, GPT-5 accepts extra values, and the parameter shape differs between Chat Completions and the Responses API. Here's the exact support matrix and the errors each mismatch produces.
By Ahmed R. · Last updated Aug 14, 2026 · OpenAI · Page #141
reasoning_effort is supported on o3 and GPT-5 family reasoning models with values low, medium, high. GPT-5 adds minimal and none. o4-mini ignores the parameter (fixed internal budget). Chat Completions accepts reasoning_effort="medium" flat; Responses API nests it as reasoning={"effort": "medium"}. Wrong values return 400; wrong shape (flat on Responses, nested on Chat Completions) is often silently ignored.Real error messages you'll see
openai.BadRequestError: Error code: 400 - {'error': {'message': "Invalid value for parameter 'reasoning_effort': 'max'. Supported values are: low, medium, high.", 'type': 'invalid_request_error'}}
# Only low/medium/high are accepted on o3. GPT-5 models add minimal and none.
# reasoning_effort="high" passed to o4-mini — no error, but response looks the same as effort="low".
# o4-mini has a FIXED internal reasoning budget; the parameter is silently ignored.
# Fix: use o3 when you need per-call effort control.
# resp = client.chat.completions.create(
# model="o3",
# reasoning={"effort": "medium"}, # ← Responses-shape, wrong here
# )
# openai.BadRequestError: unknown parameter 'reasoning'.
# Fix: use flat reasoning_effort="medium" on Chat Completions.
reasoning_effort support by model & API
| Model | Accepted values | Chat Completions shape | Responses API shape |
|---|---|---|---|
o3 | low, medium, high | reasoning_effort="medium" | reasoning={"effort":"medium"} |
o3-mini | low, medium, high | flat | nested |
o4-mini | ignored | (silent no-op) | (silent no-op) |
gpt-5.4, gpt-5.5 | minimal, low, medium, high, none | flat | nested |
gpt-5.4-nano | minimal, low, medium, high, none | flat | nested |
| Non-reasoning models (gpt-4.1, etc.) | rejected | returns 400 | returns 400 |
Root causes (ranked by frequency)
Based on OpenAI developer reports; percentages sum to 100%.
- 23%Wrong parameter shape for the API. Flat
reasoning_effortworks on Chat Completions; nestedreasoning={"effort":"..."}works on Responses. Swapping produces either 400 or silent no-op. - 18%Passed to
o4-miniexpecting effect. No error, but no change in behavior. The model always uses its fixed budget. - 14%Invalid value. Typos like
"max","maximum", or numeric values. Only literal strings from the model's accepted set work. - 12%Passed to non-reasoning model.
reasoning_effortongpt-4.1returns 400. Route by model capability, not by hardcoded param. - 10%Defaulting to
mediumeverywhere. Cost inflates 5-10× on workloads that would work atlow. Route by task type. - 9%Using
highin latency-sensitive paths. Adds 20-60s per call. Chat UIs feel broken; batch endpoints time out. - 7%GPT-5
noneandminimalnot used when available. Teams stick withlowwhen a cheaper option exists. GPT-5 witheffort=nonebehaves as a non-reasoning model but keeps the same code path. - 7%Chained calls inherit effort. Setting effort on turn 1 doesn't stick — each subsequent
previous_response_idcall needs its own effort. Or misconception: effort applies per call, not per chain.
How to fix it
Use the correct shape per API surface
Fixes the 400/silent-no-op from shape mismatch.
reasoning_effort="medium" on Chat Completions; reasoning={"effort": "medium"} on Responses API. These are not interchangeable. Wrapping both in a small helper prevents cross-API bugs.
from openai import OpenAI
client = OpenAI()
# ✅ CHAT COMPLETIONS — flat reasoning_effort
resp_cc = client.chat.completions.create(
model="o3",
messages=[{"role": "user", "content": "Refactor this function..."}],
reasoning_effort="medium", # flat string
max_completion_tokens=20_000,
)
# ✅ RESPONSES API — nested reasoning object
resp_r = client.responses.create(
model="o3",
input="Refactor this function...",
reasoning={"effort": "medium"}, # nested dict
max_output_tokens=20_000,
)
# ✅ RESPONSES API — with summary opt-in
resp_r2 = client.responses.create(
model="o3",
input="Explain this stack trace...",
reasoning={
"effort": "high",
"summary": "auto", # request reasoning summary in output
},
max_output_tokens=60_000,
)
# ✅ Cross-API helper — one function, both surfaces
def with_effort(api: str, model: str, prompt: str, effort: str, budget: int):
if api == "chat_completions":
return client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
reasoning_effort=effort,
max_completion_tokens=budget,
)
elif api == "responses":
return client.responses.create(
model=model,
input=prompt,
reasoning={"effort": effort, "summary": "auto"},
max_output_tokens=budget,
)
raise ValueError(f"unknown api: {api}")
# ❌ WRONG — nested shape on Chat Completions
# resp = client.chat.completions.create(
# model="o3",
# messages=[...],
# reasoning={"effort": "medium"}, # 400: unknown parameter
# )
# ❌ WRONG — flat shape on Responses (silently accepted as unknown top-level, ignored)
# resp = client.responses.create(
# model="o3",
# input="...",
# reasoning_effort="medium", # rejected as unknown parameter
# )
reasoning is an object with multiple sub-fields (effort, summary, potentially more in future). Chat Completions kept the flat shape for backward compatibility.Route effort per task — never default to medium/high universally
Fixes 5-10× cost overruns on high-volume workloads.
Effort level is the single biggest lever for reasoning-model cost. A workload that runs happily at low costs 5-10× more at medium and 20-50× more at high. Route by task complexity: quick lookups → low or GPT-5 minimal; code review → medium; deep architecture / debugging → high.
from openai import OpenAI
from dataclasses import dataclass
client = OpenAI()
@dataclass
class ReasoningProfile:
model: str
effort: str | None
budget: int
max_latency_s: float
typical_cost_ratio: float # relative to non-reasoning baseline
PROFILES = {
"fast_classify": ReasoningProfile("gpt-5.4-nano", "minimal", 2_000, 1.0, 1.2),
"quick_answer": ReasoningProfile("gpt-5.4", "low", 5_000, 3.0, 2.0),
"code_review": ReasoningProfile("o3", "medium", 20_000, 10.0, 8.0),
"deep_debug": ReasoningProfile("o3", "high", 60_000, 45.0, 25.0),
"no_reasoning": ReasoningProfile("gpt-5.4", "none", 3_000, 2.0, 1.0),
}
def call(profile_name: str, prompt: str) -> str:
p = PROFILES[profile_name]
kwargs = {
"model": p.model,
"input": prompt,
"max_output_tokens": p.budget,
}
if p.effort:
kwargs["reasoning"] = {"effort": p.effort}
resp = client.responses.create(**kwargs)
return resp.output_text
# Use it
short = call("quick_answer", "What is OAuth PKCE in one paragraph?")
review = call("code_review", f"Review for concurrency bugs:\n{code}")
debug = call("deep_debug", f"Why does this test hang?\n{trace}")
# ✅ Latency-tiered — pick effort under a hard latency budget
def call_within_latency(prompt: str, max_seconds: float) -> str:
profile_by_latency = [
(1.0, "fast_classify"),
(3.0, "quick_answer"),
(10.0, "code_review"),
(45.0, "deep_debug"),
]
for max_s, name in profile_by_latency:
if max_s <= max_seconds:
chosen = name
return call(chosen, prompt)
# ✅ Cost-tiered — hard cap on relative cost vs baseline
def call_within_cost(prompt: str, max_cost_ratio: float) -> str:
ordered = sorted(PROFILES.items(), key=lambda kv: kv[1].typical_cost_ratio, reverse=True)
chosen = None
for name, p in ordered:
if p.typical_cost_ratio <= max_cost_ratio:
chosen = name
break
if chosen is None:
chosen = "no_reasoning"
return call(chosen, prompt)
# ✅ Escalation pattern — start low, retry higher if quality insufficient
def call_with_escalation(prompt: str, quality_check) -> str:
for profile_name in ["quick_answer", "code_review", "deep_debug"]:
answer = call(profile_name, prompt)
if quality_check(answer):
return answer
return answer # best effort
high to low calls is a leading cost indicator. Teams often discover 70% of their reasoning calls should have been at low effort or on a non-reasoning model.Route around o4-mini for effort-controlled workloads
Fixes "reasoning_effort has no effect" on o4-mini.
o4-mini uses a fixed internal reasoning budget; passing reasoning_effort is a silent no-op. When your workload needs per-call effort control, use o3. When you want cheap reasoning with adjustable depth, GPT-5 models (gpt-5.4, gpt-5.4-nano) accept the full effort range including minimal and none.
from openai import OpenAI
import warnings
client = OpenAI()
REASONING_EFFORT_SUPPORTED = {
"o3": {"low", "medium", "high"},
"o3-mini": {"low", "medium", "high"},
# o4-mini deliberately absent — parameter is a no-op
"gpt-5.4": {"minimal", "low", "medium", "high", "none"},
"gpt-5.5": {"minimal", "low", "medium", "high", "none"},
"gpt-5.4-nano": {"minimal", "low", "medium", "high", "none"},
}
def call_with_effort_check(model: str, prompt: str, effort: str, budget: int):
"""Warn if effort will be ignored; call anyway."""
supported = REASONING_EFFORT_SUPPORTED.get(model)
if supported is None:
warnings.warn(
f"Model {model!r} does not support reasoning_effort — parameter will be ignored. "
f"Use one of {list(REASONING_EFFORT_SUPPORTED.keys())} for effort control.",
UserWarning, stacklevel=2,
)
# Fall back to a call without the param
return client.responses.create(
model=model,
input=prompt,
max_output_tokens=budget,
)
if effort not in supported:
raise ValueError(
f"Model {model!r} accepts {sorted(supported)}, got {effort!r}"
)
return client.responses.create(
model=model,
input=prompt,
reasoning={"effort": effort},
max_output_tokens=budget,
)
# ✅ Router — pick the right model for the requested effort
def pick_model_for_effort(effort: str) -> str:
if effort == "minimal" or effort == "none":
return "gpt-5.4" # only GPT-5 accepts these
if effort in {"low", "medium", "high"}:
return "o3" # o3 for full control
raise ValueError(f"unknown effort: {effort}")
# ✅ Batch classifier — cheap, no reasoning
resp = client.responses.create(
model="gpt-5.4",
input=user_input,
reasoning={"effort": "none"}, # behaves as non-reasoning
max_output_tokens=200,
)
# ✅ Use o4-mini for its fixed-budget sweet spot, not for control
# o4-mini is best when you WANT the default reasoning depth without tuning.
resp = client.chat.completions.create(
model="o4-mini",
messages=[{"role": "user", "content": prompt}],
max_completion_tokens=15_000,
# Do NOT bother passing reasoning_effort — it's ignored.
)
effort="none" is subtly different from just using gpt-5.4-chat-latest — none stays inside the reasoning-capable model path and can be flipped back to low/medium/high without model swap. Useful for feature flags and A/B tests.Prevention checklist
- Chat Completions: flat
reasoning_effort="medium". Responses API: nestedreasoning={"effort":"medium"}. Never swap. - Don't pass
reasoning_efforttoo4-mini— it's silently ignored. Useo3if you need effort control. - Route effort per task: quick lookups →
low; code review →medium; deep debug →high. GPT-5 also supportsminimalandnone. - Never pass
reasoning_effortto non-reasoning models (gpt-4.1, etc.). Returns 400. - Track effort distribution in telemetry — sustained high-effort usage indicates workload mismatch.
- Use GPT-5
effort="none"when you want reasoning-model code paths but no reasoning cost. - Pair effort routing with adequate
max_completion_tokens/max_output_tokens— high effort needs 40K+ budget.
Frequently asked questions
Yes — every call sets its own effort independently. Chaining via previous_response_id doesn't inherit the parent's effort. This is useful: you can escalate effort for a hard sub-question in the middle of a conversation, then drop back down. Just pass the desired effort on each call.
It runs the model with a very small internal reasoning budget — typically 100-800 reasoning tokens. Fast, cheap, still slightly better on multi-step problems than none. Good default for classification, extraction, and short-form generation where you want a tiny reasoning boost without the latency of low.
Yes — Batch API calls accept the same reasoning_effort (or Responses-style reasoning.effort) as real-time calls. Batch is often the right home for high-effort workloads since latency doesn't matter and you get the 50% batch discount on top. Just size max_completion_tokens generously; batch retries on timeout are not free.
Conceptually similar but not identical. Anthropic uses thinking.budget_tokens (integer cap) on Claude reasoning models; OpenAI uses reasoning.effort (categorical). Anthropic exposes the raw thinking content; OpenAI only gives you a summary. If you're abstracting across providers, map effort levels to budget ranges: low≈2K, medium≈8K, high≈32K.
Yes — higher effort tends to produce more careful tool selection and fewer redundant calls. On agentic workloads (tools + multi-turn), the extra reasoning cost is often offset by fewer tool invocations. Measure end-to-end cost, not just per-call: a medium-effort call that makes 2 tool calls can be cheaper than a low-effort call that makes 6.