I’ve hit what looks like a default-injection gap in the Gateway’s OpenAI-compatible endpoint, affecting all three perplexity/* models. max_tokens is optional in the OpenAI Chat Completions spec, and the gateway defaults it correctly everywhere else, so this breaks any standard OpenAI client that doesn’t set it explicitly (I hit it through a GUI client that omits the field by default).
The important detail: it’s intermittent. Identical requests to the same model succeed and fail, which points at request routing rather than per-model config.
Summary
POST https://api.perplexity.ai/router/v1/chat/completions without max_tokens returns HTTP 400:
{"error":{"code":null,"param":null,"type":"invalid_request_error",
"message":"Anthropic Messages API requires `max_tokens`; set `SamplingParams.max_tokens` before routing to Anthropic"}}
Reproduction
for i in $(seq 1 25); do
curl -s -X POST 'https://api.perplexity.ai/router/v1/chat/completions' \
-H "Authorization: Bearer $PERPLEXITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"perplexity/kimi-k3","messages":[{"role":"user","content":"hi"}]}' \
| grep -q SamplingParams && echo -n F || echo -n .
done
A single request is not enough to see it on kimi-k3 / glm-5.2 — I initially sampled once per model and wrongly concluded glm-5.2 was unaffected.
Adding "max_tokens": 1024 makes the request succeed every time. Sending "max_tokens": null fails identically to omitting it.
"max_completion_tokens": 1024 also succeeds, so both spellings are honoured — only the complete absence of an output limit fails. That narrows this to a missing default rather than a field-name mismatch.
Worth stressing that omitting the field is valid OpenAI Chat Completions usage: max_tokens is optional in the spec. I hit this with an off-the-shelf GUI client that attaches the field only to chats where the user has explicitly set it, and sends this on any new chat:
{"model":"perplexity/deepseek-v4-flash-0731",
"messages":[{"role":"user","content":"hello"}],
"stream":true,"stream_options":{"include_usage":true}}
Nothing about that request is malformed, and it works against the other 12 models.
Scope
25 identical requests per model, with GET /router/v1/models enumerating all 15:
| Model | Failures (no max_tokens) |
|---|---|
perplexity/deepseek-v4-flash-0731 |
25/25 (100%) |
perplexity/kimi-k3 |
11/25 (44%) |
perplexity/glm-5.2 |
7/25 (28%) |
anthropic/claude-opus-5 |
0/25 |
openai/gpt-5.6-luna |
0/25 |
anthropic/*, openai/*, google/*, xai/* (single-shot sweep) |
0 failures |
Two observations that may help localise it:
- Only the
perplexity/*namespace is affected. Allanthropic/*models are clean, so this isn’t a general property of the Anthropic-compatible path. kimi-k3’s failures arrived as a run of 9 consecutive 400s inside an otherwise healthy sample (....FFFFFFFFF..F..F......), rather than scattered independently. That looks like sticky routing to a subset of backends that miss themax_tokensdefault, withdeepseek-v4-flash-0731apparently pinned to one.
The failure is independent of every other request parameter — it reproduces with and without temperature, stream: true, a system message, tools, and response_format. Only the presence of max_tokens matters.
Two smaller issues in the same area
1. The error message points at the wrong provider
It says “routing to Anthropic” for models under the perplexity/* namespace. I assume these are served via an Anthropic-compatible backend internally, but from the outside it reads as though the wrong model was requested — I initially spent a while debugging the anthropic/* models, which turned out to be fine. It also leaks an internal symbol (SamplingParams). Something like “max_tokens is required for model perplexity/kimi-k3” would point users straight at the fix.
2. Silent empty responses when max_tokens is low
The perplexity/* models are reasoners, and the reasoning tokens are billed in usage but never surfaced — there’s no reasoning_content field in the response or in streamed deltas. When the budget is consumed by reasoning, the call returns 200 OK with content: null and finish_reason: "length".
A request lands in one of four states, all observed live, and there is no max_tokens value that eliminates the bad ones:
finish_reason: stopwith content — the good casefinish_reason: stopwithcontent: null— clean stop, nothing returned (deepseek atmax_tokens: 1024, 104 completion tokens billed)finish_reason: lengthwithcontent: null— budget consumed by reasoningfinish_reason: lengthwith partial content — truncated mid-sentence
Rates on perplexity/deepseek-v4-flash-0731 at max_tokens: 4096 — a generous budget for a two-sentence answer, where successful runs finish in 129–362 tokens — for the prompt “Explain the CAP theorem in two sentences.”:
Model (at max_tokens: 4096) |
Unusable responses |
|---|---|
perplexity/deepseek-v4-flash-0731 |
4/20 and 2/12 across two samples — full 4096 spent on reasoning, nothing returned |
perplexity/kimi-k3 |
truncated at 1024 (1024 tokens, 517 chars partial) |
perplexity/glm-5.2 |
0/12 |
Two consequences for clients:
- Neither
finish_reasonnor a larger budget is a reliable guard. Raisingmax_tokenspast 4096 mainly raises the cost of each failure, since a runaway request is billed for the whole budget and returns nothing. - The partial-content case is the most dangerous: a 200 OK carrying what looks like a complete answer.
Exposing reasoning token counts in usage (as completion_tokens_details.reasoning_tokens, per the OpenAI schema) would at least make this diagnosable, and a server-side cap on reasoning length would stop the runaway case outright.
Environment
- Endpoint:
https://api.perplexity.ai/router/v1(OpenAI-compatible) - Observed via raw
curland theopenaiPython SDK 2.53.0 - Date: 2026-08-08
Happy to provide full request/response captures if useful.