OpenAI o-series Unsupported Parameters (temperature, top_p, penalties) — Fix Guide (2026)
Reasoning Models · Parameter Compatibility Severity: Medium 400

OpenAI o-series Unsupported Parameters (temperature, top_p, penalties)

o-series reasoning models reject nearly every sampling-control parameter that Chat Completions and Responses accept on standard models. Shared helper functions that worked for gpt-4.1 start failing with 400s when routed to o3. Here's the exact allow-list and what to do instead.

TL;DRo-series (o3, o4-mini) reject temperature, top_p, presence_penalty, frequency_penalty, logit_bias, logprobs, and (historically) system role. They also require max_completion_tokens instead of max_tokens. Fix: strip these params before routing to o-series, or use a model-aware wrapper that only passes what the target model accepts. GPT-5 reasoning models are more permissive but still lack some Chat Completions params.

Real error messages you'll see

BadRequestError — unsupported temperature
BadRequestError — unsupported temperature
openai.BadRequestError: Error code: 400 - {'error': {'message': "Unsupported parameter 'temperature' with value '0.7' for model 'o3'. Reasoning models do not support this parameter.", 'type': 'invalid_request_error'}}
# temperature, top_p, presence_penalty, frequency_penalty all rejected.
BadRequestError — max_tokens on reasoning model
BadRequestError — max_tokens on reasoning model
openai.BadRequestError: Error code: 400 - {'error': {'message': "Unsupported parameter 'max_tokens'. Use 'max_completion_tokens' for reasoning models.", 'type': 'invalid_request_error'}}
# Reasoning models require max_completion_tokens (Chat Completions) or max_output_tokens (Responses).
BadRequestError — logprobs / logit_bias
BadRequestError — logprobs / logit_bias
openai.BadRequestError: Error code: 400 - {'error': {'message': "The 'logprobs' parameter is not supported on reasoning models.", 'type': 'invalid_request_error'}}
# Also logit_bias, top_logprobs. Reasoning models don't expose token-level probabilities.

Parameter support: reasoning vs non-reasoning models

ParameterNon-reasoning (gpt-4.1)o3 / o4-miniGPT-5 reasoning
temperature✗ (400)✗ (400)
top_p✗ (400)✗ (400)
presence_penalty✗ (400)✗ (400)
frequency_penalty✗ (400)✗ (400)
logit_bias✗ (400)✗ (400)
logprobs, top_logprobs✗ (400)✗ (400)
max_tokens✗ — use max_completion_tokens✗ — use max_completion_tokens
stoplimited support
n (multiple completions)✗ (400)limited
system role✓ (treated as developer)
stream✓ (some rollout-gated)
reasoning_effort✓ (o3 only, not o4-mini)
verbosity✓ (GPT-5 only)

Root causes (ranked by frequency)

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

  • 25%
    Passing temperature via a shared helper. Utility functions built for gpt-4.1 include temperature=0.7 by default; routing to o3 raises 400.
  • 18%
    max_tokens instead of max_completion_tokens. The most common parameter-name migration miss on reasoning models.
  • 14%
    Copy-pasting from Chat Completions docs. Docs examples for older models include top_p, presence_penalty, etc. On o-series they all fail.
  • 12%
    Sampling-control code paths not stripped. A "diverse output" code path applies temperature=1.2 and frequency_penalty=1.0. Both must be dropped on o-series.
  • 10%
    Logprobs-based extraction code broken. Confidence extraction from logprobs silently unavailable on o-series; downstream logic gets empty arrays or 400s.
  • 9%
    Trying n=3 for multiple completions. Rejected on o-series. Loop the call instead.
  • 7%
    system role handling assumed unchanged. Older o1 rejected system messages; current o3 accepts them (treated as developer). Old defensive code that stripped system messages now removes needed instructions.
  • 5%
    Non-reasoning params sent to reasoning models via config-driven code. A YAML config sets temperature: 0.5 globally; specific model overrides missed.

How to fix it

Fix #1

Wrap in a model-aware helper that strips unsupported params

The most reliable fix for shared code paths.

Instead of maintaining two sets of helpers, write one wrapper that inspects the target model and strips unsupported params before the call. Whitelist by model family — reasoning models get a smaller allow-list.

model_aware_call.pypython
from openai import OpenAI

client = OpenAI()


REASONING_MODELS = {"o3", "o3-mini", "o4-mini"}
GPT5_REASONING   = {"gpt-5.4", "gpt-5.5", "gpt-5.4-nano"}

# Parameters that reasoning models REJECT
UNSUPPORTED_ON_REASONING = {
    "temperature", "top_p", "presence_penalty", "frequency_penalty",
    "logit_bias", "logprobs", "top_logprobs", "n",
    "max_tokens",           # they want max_completion_tokens
}


def call(model: str, messages: list, **params) -> str:
    """Model-aware Chat Completions caller — strips unsupported params."""
    is_reasoning = model in REASONING_MODELS or model in GPT5_REASONING

    if is_reasoning:
        # Migrate max_tokens → max_completion_tokens
        if "max_tokens" in params:
            params["max_completion_tokens"] = params.pop("max_tokens")

        # Strip everything reasoning models reject
        stripped = {k: v for k, v in params.items() if k not in UNSUPPORTED_ON_REASONING}

        # Log what we removed for observability
        removed = set(params) - set(stripped) - {"max_completion_tokens"}
        if removed:
            import logging
            logging.info("stripped unsupported params for %s: %s", model, removed)

        params = stripped

    resp = client.chat.completions.create(
        model=model,
        messages=messages,
        **params,
    )
    return resp.choices[0].message.content


# ✅ Now this works for both model families
call("gpt-4.1", [{"role": "user", "content": "Hi"}], temperature=0.7, max_tokens=200)
call("o3",      [{"role": "user", "content": "Hi"}], temperature=0.7, max_tokens=200)
# ↑ temperature stripped, max_tokens migrated to max_completion_tokens


# ✅ Config-driven — YAML with per-model overrides
import yaml

CONFIG = yaml.safe_load("""
defaults:
  temperature: 0.5
  top_p: 0.9
  max_tokens: 500

models:
  gpt-4.1:
    temperature: 0.7
  o3:
    reasoning_effort: medium
    max_completion_tokens: 20000
    # No temperature — it's not supported

  gpt-5.4:
    reasoning:
      effort: minimal
    max_completion_tokens: 3000
""")


def call_from_config(model_name: str, prompt: str) -> str:
    # Start with defaults, apply model overrides
    params = {**CONFIG["defaults"], **CONFIG["models"].get(model_name, {})}
    return call(model_name, [{"role": "user", "content": prompt}], **params)


# ✅ For Responses API — parallel wrapper
def call_responses(model: str, input_text: str, **params):
    is_reasoning = model in REASONING_MODELS or model in GPT5_REASONING

    if is_reasoning:
        if "max_tokens" in params:
            params["max_output_tokens"] = params.pop("max_tokens")

        # Strip Chat Completions-style effort, migrate to nested
        if "reasoning_effort" in params:
            effort = params.pop("reasoning_effort")
            params["reasoning"] = {"effort": effort}

        stripped = {k: v for k, v in params.items() if k not in UNSUPPORTED_ON_REASONING}
        params = stripped

    return client.responses.create(model=model, input=input_text, **params)
Note: Log the stripped parameters — that's how you find upstream code that's still trying to pass temperature to reasoning models. Fix at the source over time; the wrapper is safety net, not permanent hack.
Fix #2

Replace sampling-control patterns with prompt engineering

Fixes "no way to control creativity" concerns.

When you can't use temperature or top_p, control output style through the prompt. Reasoning models are highly instruction-following — a clear specification in the developer/system message replaces most sampling knobs. For diversity, use multiple calls with varied prompts instead of n=3.

prompt_replacements.pypython
from openai import OpenAI

client = OpenAI()


# ❌ OLD PATTERN — temperature controls creativity
resp = client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": "Write a poem about databases"}],
    temperature=1.2,                        # creative
)


# ✅ NEW PATTERN — instruction controls creativity
resp = client.chat.completions.create(
    model="o3",
    messages=[{
        "role": "developer",
        "content": (
            "You are a creative poet. Take unusual metaphors and unexpected imagery. "
            "Avoid safe/generic phrasing. Prefer specific concrete words."
        ),
    }, {
        "role": "user",
        "content": "Write a poem about databases",
    }],
    max_completion_tokens=5000,
)


# ❌ OLD — n=3 for diverse variants
# resp = client.chat.completions.create(model="gpt-4.1", messages=[...], n=3, temperature=1.0)
# [c.message.content for c in resp.choices]


# ✅ NEW — three parallel calls with varied instructions
import asyncio
from openai import AsyncOpenAI

async_client = AsyncOpenAI()

async def variant(style: str, prompt: str):
    resp = await async_client.responses.create(
        model="o3",
        input=prompt,
        instructions=f"Respond in a {style} style.",
        max_output_tokens=2000,
    )
    return resp.output_text

async def get_variants(prompt: str):
    styles = ["formal and technical", "casual and playful", "concise and blunt"]
    return await asyncio.gather(*[variant(s, prompt) for s in styles])


# ❌ OLD — presence_penalty to avoid repetition
# resp = client.chat.completions.create(
#     model="gpt-4.1", messages=[...],
#     presence_penalty=1.5,                 # discourage repeats
# )


# ✅ NEW — instruct against repetition
resp = client.chat.completions.create(
    model="o3",
    messages=[{
        "role": "developer",
        "content": (
            "Avoid repeating information, phrases, or transition words across sentences. "
            "Each sentence should introduce a distinct idea."
        ),
    }, {
        "role": "user",
        "content": prompt,
    }],
    max_completion_tokens=8000,
)


# ❌ OLD — logprobs for confidence scoring
# resp = client.chat.completions.create(
#     model="gpt-4.1", messages=[...], logprobs=True, top_logprobs=5,
# )
# confidence = compute_confidence_from_logprobs(resp.choices[0].logprobs)


# ✅ NEW — ask for confidence in structured output
from pydantic import BaseModel, Field

class Classification(BaseModel):
    label: str
    confidence: float = Field(ge=0.0, le=1.0, description="Self-reported confidence 0-1")
    reasoning: str

resp = client.responses.parse(
    model="o3",
    input=text_to_classify,
    text_format=Classification,
    max_output_tokens=15000,
    reasoning={"effort": "medium"},
)
result: Classification = resp.output_parsed
print(f"{result.label} (confidence: {result.confidence:.2f})")


# ✅ For deterministic output (temperature=0 replacement)
# Reasoning models are already fairly deterministic. Add:
#   - Explicit output format specification
#   - Rule: "If uncertain, output exactly 'UNKNOWN'"
#   - Structured output schema
# Then use seed if available (some models support it):
resp = client.responses.create(
    model="o3",
    input=prompt,
    max_output_tokens=5000,
    # seed=42,                              # verify seed support on your model
)
Note: The verbosity parameter on GPT-5 models ("low", "medium", "high") is a partial replacement for max_tokens-as-brevity-lever. It nudges response length without hard-capping. Not available on o-series.
Fix #3

Handle system → developer role migration cleanly

Fixes lost instructions on models with role changes.

Current o-series models accept system-role messages and internally treat them as developer messages. On the Responses API, the instructions field is the preferred vehicle. When migrating older defensive code that stripped system messages (originally needed for o1), leave the messages in place — they now work.

system_role_migration.pypython
from openai import OpenAI

client = OpenAI()


# ✅ CURRENT o-series — system role is accepted (treated as developer)
resp = client.chat.completions.create(
    model="o3",
    messages=[
        {"role": "system", "content": "You are a concise SQL expert."},
        {"role": "user",   "content": "Write a query for last month's top customers."},
    ],
    max_completion_tokens=15_000,
)


# ✅ Also works with explicit developer role
resp = client.chat.completions.create(
    model="o3",
    messages=[
        {"role": "developer", "content": "You are a concise SQL expert."},
        {"role": "user",      "content": "Write a query..."},
    ],
    max_completion_tokens=15_000,
)


# ✅ Responses API — instructions field is cleanest
resp = client.responses.create(
    model="o3",
    instructions="You are a concise SQL expert.",
    input="Write a query for last month's top customers.",
    max_output_tokens=15_000,
    reasoning={"effort": "medium"},
)


# ❌ ANTI-PATTERN — legacy defensive code that strips system on o-series
# This was needed for o1 (which rejected system) but now removes needed instructions.
def bad_strip(messages, model):
    if model.startswith("o"):
        return [m for m in messages if m["role"] != "system"]
    return messages


# ✅ NEW — leave system messages in place; they work
def messages_for_model(messages, model):
    if model.startswith("o1"):
        # o1 specifically still rejects system messages (verify per-model)
        return [{"role": "developer" if m["role"] == "system" else m["role"], "content": m["content"]} for m in messages]
    # o3, o4-mini, GPT-5 all accept system role
    return messages


# ✅ Don't use both a system and a developer message
# Some models are strict about only one high-priority instruction message.
# Pick one:
messages = [
    {"role": "system", "content": "You are a Sudoku solver."},
    # DO NOT ALSO include: {"role": "developer", "content": "..."}
    {"role": "user", "content": "Solve: ..."},
]
Note: The rule of thumb: if you're working with any model released in 2025 or later (o3, o4-mini, GPT-5), system messages work. For legacy o1 code paths only, migrate to developer. Never combine system and developer in the same request — some models reject that combination.

Prevention checklist

  • Never pass temperature, top_p, presence_penalty, frequency_penalty, logit_bias, or logprobs to reasoning models — all return 400.
  • Use max_completion_tokens (Chat Completions) or max_output_tokens (Responses) on reasoning models. max_tokens is rejected.
  • n (multiple completions) is not supported on o-series — loop the call or use async parallel invocations.
  • Wrap Chat Completions calls in a model-aware helper that strips unsupported params based on the target model family.
  • Replace sampling knobs with prompt-based controls: instruct style and variety instead of setting temperature.
  • For logprobs-based confidence, migrate to Pydantic structured output with an explicit confidence field.
  • Current o-series (o3+) accept system role; legacy defensive stripping code should be removed.

Frequently asked questions

Does o-series support seeding for deterministic output?

Some snapshots do, some don't — seed support varies by model. When available, it produces deterministic outputs for identical inputs (subject to fingerprint drift). When unavailable, the model still tends to be more deterministic than non-reasoning models because internal sampling defaults are conservative. Verify per model before relying on it.

How do I extract token-level confidence without logprobs?

Two approaches. First: ask the model to output confidence in a structured field (Pydantic schema with confidence: float). Reasoning models self-report reasonably well when explicitly prompted for it. Second: sample multiple calls (loop, since n isn't supported) and measure agreement across responses — the disagreement rate is a proxy for uncertainty.

Can I use response_format on o-series models?

Yes — response_format={"type": "json_schema", ...} works on o-series in Chat Completions, and text.format works in Responses. Structured output is a first-class feature on all reasoning models. What's not supported is response_format={"type": "json_object"} (the older "JSON mode") — use the strict json_schema variant.

What about stop sequences?

Reasoning models have limited stop support. It works in most cases but the model may occasionally emit tokens past the stop sequence due to how reasoning-model output is composed. If deterministic stopping is critical, add explicit end markers to your prompt (like “End your response with ###”) and post-process the output.

Are these restrictions the same for the Batch API?

Yes — the Batch API accepts the same requests as real-time, so the same restrictions apply. If your batch job includes temperature on o-series items, they'll 400 individually. Validate requests before submitting the batch; a single request-level error costs a whole item.

Related errors