Perplexity Gateway API: omitting `max_tokens` intermittently 400s on all `perplexity/*` models

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. All anthropic/* 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 the max_tokens default, with deepseek-v4-flash-0731 apparently 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: stop with content — the good case
  • finish_reason: stop with content: null — clean stop, nothing returned (deepseek at max_tokens: 1024, 104 completion tokens billed)
  • finish_reason: length with content: null — budget consumed by reasoning
  • finish_reason: length with 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_reason nor a larger budget is a reliable guard. Raising max_tokens past 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 curl and the openai Python SDK 2.53.0
  • Date: 2026-08-08

Happy to provide full request/response captures if useful.

Solid writeup. The sticky-routing theory fits the clustered failures well, that run of 9 consecutive 400s on kimi-k3 is pretty telling. The wrong-provider error message wasted a lot of my time too when I hit something similar. Honestly the silent empty response at finish_reason length is the scarier bug here, billing for reasoning tokens you never see feels rough.