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
systemrole 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
- Read the
paramfield. 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. - 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
- Check your SDK and API version. Old
openaiSDK releases don't knowmax_completion_tokensorreasoning_effort. Old Azureapi-versionvalues reject them too.
pip show openai | grep -i version
grep -rn "api_version\|api-version" --include=*.py --include=*.env --include=*.json .
- 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
developermessages as working the same way assystemmessages 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=0changes 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_tokensandtemperaturefrom 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_tokensortemperatureinto every request. Check the inbound policy or proxy model settings as well as the app. LiteLLM has adrop_paramsoption 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.