OpenAI o-series reasoning_effort Parameter Support & Errors — Fix Guide (2026)
Reasoning Models · reasoning_effort Severity: Medium 400

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.

TL;DRreasoning_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

BadRequestError — invalid effort value
BadRequestError — invalid effort value
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.
Parameter accepted but has no effect (o4-mini)
Parameter accepted but has no effect (o4-mini)
# 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.
Nested shape used on Chat Completions
Nested shape used on Chat Completions
# 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

ModelAccepted valuesChat Completions shapeResponses API shape
o3low, medium, highreasoning_effort="medium"reasoning={"effort":"medium"}
o3-minilow, medium, highflatnested
o4-miniignored(silent no-op)(silent no-op)
gpt-5.4, gpt-5.5minimal, low, medium, high, noneflatnested
gpt-5.4-nanominimal, low, medium, high, noneflatnested
Non-reasoning models (gpt-4.1, etc.)rejectedreturns 400returns 400

Root causes (ranked by frequency)

Based on OpenAI developer reports; percentages sum to 100%.

  • 23%
    Wrong parameter shape for the API. Flat reasoning_effort works on Chat Completions; nested reasoning={"effort":"..."} works on Responses. Swapping produces either 400 or silent no-op.
  • 18%
    Passed to o4-mini expecting 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_effort on gpt-4.1 returns 400. Route by model capability, not by hardcoded param.
  • 10%
    Defaulting to medium everywhere. Cost inflates 5-10× on workloads that would work at low. Route by task type.
  • 9%
    Using high in latency-sensitive paths. Adds 20-60s per call. Chat UIs feel broken; batch endpoints time out.
  • 7%
    GPT-5 none and minimal not used when available. Teams stick with low when a cheaper option exists. GPT-5 with effort=none behaves 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_id call needs its own effort. Or misconception: effort applies per call, not per chain.

How to fix it

Fix #1

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.

effort_by_api.pypython
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
# )
Note: The Responses API shape is nested because reasoning is an object with multiple sub-fields (effort, summary, potentially more in future). Chat Completions kept the flat shape for backward compatibility.
Fix #2

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.

effort_routing_practical.pypython
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
Note: Track effort distribution in your telemetry — the ratio of 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.
Fix #3

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.

model_routing_by_capability.pypython
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.
)
Note: GPT-5 effort="none" is subtly different from just using gpt-5.4-chat-latestnone 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: nested reasoning={"effort":"medium"}. Never swap.
  • Don't pass reasoning_effort to o4-mini — it's silently ignored. Use o3 if you need effort control.
  • Route effort per task: quick lookups → low; code review → medium; deep debug → high. GPT-5 also supports minimal and none.
  • Never pass reasoning_effort to 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

Can I change reasoning_effort mid-chain?

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.

What does effort="minimal" actually do on GPT-5?

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.

Is reasoning_effort supported on the Batch API?

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.

How does effort compare to Anthropic's thinking budget?

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.

Does effort affect tool-calling behavior?

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.

Related errors