Home / AI & GenAI

AI & GenAITroubleshootingIntermediate

Reasoning models return 400 Unsupported parameter: fixing max_tokens, temperature and system messages

Calls that worked on gpt-4o fail with 400 after you point them at a reasoning model. Find the rejected parameters, remap them and keep one client that works with both model families.

Deepak Bhardwaj7 min read

Applies toAzure OpenAI and OpenAI API, Chat Completions and Responses APIs; reasoning models (o-series, gpt-5 family and later); openai Python SDK 1.x. Supported parameters differ per model, so check the model's page before you rely on this list.

Symptom

You change a deployment or model name from a GPT-4-class model to a reasoning model (an o-series or gpt-5 family model), and requests that worked yesterday fail at once with HTTP 400:

openai.BadRequestError: Error code: 400 - {'error': {'message': "Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.", 'type': 'invalid_request_error', 'param': 'max_tokens', 'code': 'unsupported_parameter'}}

Other versions of the same failure:

  • Unsupported value: 'temperature' does not support 0 with this model. Only the default (1) value is supported.
  • Unsupported parameter: 'top_p' ..., 'presence_penalty', 'frequency_penalty', 'logprobs', 'logit_bias'
  • On some older o1 deployments: an error saying the system role is not supported with this model.

The error is not intermittent. Every request carrying the parameter fails, which is why a model swap can take a feature down completely. The o-series retirements this month force this swap on a lot of teams.

Warning

If the model change happens through automatic version upgrade or a shared gateway alias, you can get this outage without anyone touching application code. Pin the model versions for critical deployments and test with the new model before you switch traffic.

Quick checks

  1. Read the param field. The 400 body names the exact parameter that was rejected. Fix that one, retry, and repeat. Reasoning models validate parameters strictly, so there may be several to fix.
  2. Confirm which model actually served the call. On Azure OpenAI the deployment name is not the model name. List what sits behind each deployment:
az cognitiveservices account deployment list \
  --name my-aoai --resource-group rg-ai \
  --query "[].{deployment:name, model:properties.model.name, version:properties.model.version}" -o table
  1. Check your SDK and API version. Old openai SDK releases don't know max_completion_tokens or reasoning_effort. Old Azure api-version values reject them too.
pip show openai | grep -i version
grep -rn "api_version\|api-version" --include=*.py --include=*.env --include=*.json .
  1. Find every place that sets the parameters. Defaults often hide in wrapper classes, config files and framework settings, not only at the call site:
grep -rnE "max_tokens|temperature|top_p|presence_penalty|frequency_penalty|logit_bias|logprobs|\"system\"" \
  --include=*.py --include=*.yaml --include=*.json src/ config/

What breaks and how to fix it

Rejected item Why Fix
max_tokens (Chat Completions) Reasoning models need a limit that covers hidden reasoning tokens as well as the visible answer Use max_completion_tokens
max_tokens / max_completion_tokens (Responses API) Different API, different name Use max_output_tokens
temperature, top_p Sampling is fixed for these models Remove them. Don't send temperature=1 as a workaround; just omit it
presence_penalty, frequency_penalty, logit_bias Not supported Remove them
logprobs, top_logprobs Not supported Remove them, and disable any feature that relies on token probabilities
system role on older o1 versions Older versions accepted neither role Use a developer message, or move to a newer model version
Both system and developer in one request Both roles do the same job Send only one
Old api-version (Azure) The version predates reasoning parameters Move to a current GA version or the v1 API

Tip

Microsoft documents developer messages as working the same way as system messages for reasoning models. Use one of them per request, not both.

Fix the call

Chat Completions

Before, written for gpt-4o:

resp = client.chat.completions.create(
    model="chat-prod",              # Azure: deployment name
    messages=[{"role": "system", "content": SYSTEM},
              {"role": "user", "content": question}],
    temperature=0,
    max_tokens=800,
)

After, for a reasoning model:

resp = client.chat.completions.create(
    model="chat-reasoning",
    messages=[{"role": "developer", "content": SYSTEM},
              {"role": "user", "content": question}],
    max_completion_tokens=4000,     # covers reasoning tokens + the answer
    reasoning_effort="low",         # allowed values vary by model
)
print(resp.choices[0].finish_reason, resp.usage.completion_tokens_details.reasoning_tokens)

Responses API

resp = client.responses.create(
    model="chat-reasoning",
    instructions=SYSTEM,
    input=question,
    max_output_tokens=4000,
    reasoning={"effort": "low"},
)
print(resp.status, resp.output_text)

If resp.status is incomplete, check resp.incomplete_details. A reason of max_output_tokens means the limit ran out, often before any visible text was produced.

Raise the token limit

Teams often move from 400 errors straight to the next problem: empty answers. The completion limit now includes reasoning tokens. An 800-token limit that was plenty for gpt-4o can be used up entirely by reasoning, so the call returns finish_reason: "length" with empty content. Start with a limit several times larger than before, then size it from real reasoning_tokens figures. Lowering reasoning_effort also reduces how many reasoning tokens the model spends.

One client for both model families

If a gateway or config switch can send the same code path to either a classic chat model or a reasoning model, put the parameter mapping in one place instead of spreading if checks through the code:

REASONING_PREFIXES = ("o1", "o3", "o4", "gpt-5")
UNSUPPORTED = {"temperature", "top_p", "presence_penalty",
               "frequency_penalty", "logit_bias", "logprobs", "top_logprobs"}

def chat_params(model_name: str, messages: list, max_out: int, **extra) -> dict:
    """model_name is the underlying model, not the Azure deployment name."""
    reasoning = model_name.startswith(REASONING_PREFIXES)
    params = {"messages": messages, **extra}
    if reasoning:
        params = {k: v for k, v in params.items() if k not in UNSUPPORTED}
        params["max_completion_tokens"] = max_out * 4   # headroom for reasoning
        params["messages"] = [
            {**m, "role": "developer"} if m["role"] == "system" else m
            for m in messages
        ]
    else:
        params["max_tokens"] = max_out
    return params

resp = client.chat.completions.create(
    model=DEPLOYMENT,
    **chat_params(MODEL_NAME, msgs, 800, temperature=0),
)

Keep MODEL_NAME in configuration next to the deployment name, and fill it from the deployment list in the quick checks above. A prefix list is crude, so check it every time a new model family arrives. Newer non-reasoning models also accept max_completion_tokens, so you can drop max_tokens entirely once every model you use supports the new name.

Caution

Silently dropping temperature=0 changes behaviour. Answers become less repeatable, and evaluation baselines recorded on the old model no longer apply. Re-run your evaluation set on the new model before you switch production traffic.

Frameworks and gateways

  • LangChain, Semantic Kernel, LlamaIndex: older releases send max_tokens and temperature from their own defaults even if your code doesn't. Upgrade the integration package. If the error continues, set the token limit through the framework's own field for completion tokens. Field names vary by version, so check its changelog.
  • API Management or LiteLLM in front: a policy or proxy config can inject max_tokens or temperature into every request. Check the inbound policy or proxy model settings as well as the app. LiteLLM has a drop_params option that removes parameters a provider doesn't support. It hides the problem, so log which parameters were dropped.
  • Evaluation and batch scripts: they often hard-code temperature=0. Check them as well as production code, or the first batch job after the switch fails.

Test before switching

Send one request with the exact parameter set your app uses, directly to the new deployment:

curl -s "https://my-aoai.openai.azure.com/openai/v1/chat/completions" \
  -H "Authorization: Bearer $(az account get-access-token --resource https://cognitiveservices.azure.com --query accessToken -o tsv)" \
  -H "Content-Type: application/json" \
  -d '{"model":"chat-reasoning","messages":[{"role":"developer","content":"Answer in one line."},{"role":"user","content":"ping"}],"max_completion_tokens":2000}' \
  | jq '{finish: .choices[0].finish_reason, text: .choices[0].message.content, usage: .usage}'

If your resource doesn't serve the v1 path yet, use /openai/deployments/<name>/chat/completions?api-version=<current GA version> instead.

Prevent it next time

  • Treat a model family change as a code change. It goes through the same pull request, evaluation and staged rollout as any other release.
  • Add a contract test to CI that sends your real parameter set to every model the app may route to and fails on any 400.
  • Keep request parameters in one function or config block per model family, as shown above.
  • Alert on a jump in HTTP 400 responses per deployment, not only on 429 and 5xx. A parameter mismatch shows up as a wall of 400s straight after the switch.
  • Pin model versions and set the upgrade option deliberately, so the model behind a deployment changes only when you decide.