The rename is the easy half. The half that catches people is what the new field counts — max_completion_tokens covers reasoning plus visible output, so a budget sized for the answer alone comes back empty with finish_reason "length", and billed.
The error
{
"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"
}
}Causes and fixes at a glance
| Cause | Fix |
|---|---|
| Reasoning-model families replaced the field | Send max_completion_tokens instead of max_tokens on those models. |
| An SDK or wrapper pinned to the old field | Upgrade it, or set the field explicitly rather than through the helper. |
| One code path fanning out to several providers | Normalize once at the edge instead of branching per model. |
| Empty answer after fixing it | The budget includes reasoning tokens — raise it well above your expected output. |
Rename the field
At the call site it is a straight substitution. Everything else about the request is unchanged.
# Before
resp = client.chat.completions.create(
model="gpt-5-6-sol", max_tokens=1024, messages=msgs)
# After
resp = client.chat.completions.create(
model="gpt-5-6-sol", max_completion_tokens=1024, messages=msgs)Budget for the reasoning you cannot see
max_completion_tokens caps reasoning tokens plus visible output together. If a model spends 900 tokens thinking under a 1,024 cap, you get 124 tokens of answer — or an empty message with finish_reason "length", and a bill for all of it. Size the budget for both, and check finish_reason before trusting an empty response.
Normalize once instead of branching per model
A single helper at the edge of your code keeps the rest provider-agnostic, and stops the next model family from being another round of edits.
def token_budget(model: str, n: int) -> dict:
"""One place that knows which spelling a model wants."""
if model.startswith("claude-"):
return {"max_tokens": n}
return {"max_completion_tokens": n}
resp = client.chat.completions.create(
model=model, messages=msgs, **token_budget(model, 4096))Expect the sibling rejections
The same model families that dropped max_tokens often reject temperature and top_p too. Fixing this one usually surfaces the next; remove unsupported sampling parameters rather than setting them to their defaults.
If you’re calling through Kunavo
Kunavo's /v1/chat/completions accepts max_tokens on the GPT-5.x reasoning family: the translator reads whichever of the two spellings you send and maps it onto the upstream field, and /v1/responses does the mirror image. So on those models you do not have to rename anything. One asymmetry stated plainly rather than smoothed over: on claude-* models the chat translator currently reads max_tokens only, so send that spelling for Claude — which is what the helper above does.
FAQ
Is max_completion_tokens just a rename?
At the call site yes, in meaning no — it caps reasoning tokens plus output together, where max_tokens capped visible output only.
Why is my response empty after fixing this?
The budget was spent on reasoning. Check finish_reason: "length" with empty content means raise the cap.
Do I have to branch per model?
Not on Kunavo for the GPT family — both spellings are accepted. Branch only for claude-* models, or use one normalizing helper everywhere.
Related guides
- context_length_exceeded / prompt is too long — fixes that don't lobotomize your app
- OpenAI-compatible API returning 401/403 — base_url and header pitfalls
More error semantics live in the error reference; getting a key takes a minute via sign up and the authentication docs.