Summary
GET /v1/models reports modalities.input: ["text","image","pdf","video"] and the file-input tag for moonshotai/kimi-k3, but a chat/completions request carrying a {"type":"file"} content part fails on every provider in the routing chain, ending in a 422.
Clients use the modalities field to decide whether an attachment can be sent at all. When the field over-reports, the failure surfaces to the end user mid-conversation — and it is not recoverable by retrying, because every provider in the fallback list rejects the same payload.
Reproduction
PDF=$(printf '%%PDF-1.11 0 obj<</Type/Catalog>>endobjtrailer<</Root 1 0 R>>' | base64 -w0)
curl -s https://ai-gateway.vercel.sh/v1/chat/completions \ -H "authorization: Bearer $AI_GATEWAY_API_KEY" \ -H "content-type: application/json" \ -d "{ \"model\": \"moonshotai/kimi-k3\", \"max_tokens\": 8, \"messages\": [{ \"role\": \"user\", \"content\": [ {\"type\": \"text\", \"text\": \"what file is this?\"}, {\"type\": \"file\", \"file\": {\"filename\": \"t.pdf\", \"file_data\": \"data:application/pdf;base64,$PDF\"}} ] }] }"Observed
422, with error.message equal to the literal string "AI_APICallError" (empty inner message). The real reasons are only visible inside providerMetadata.gateway.routing.modelAttempts[0].providerAttempts[]:
| provider | status | message |
|---|---|---|
| modal | 503 | Service temporarily unavailable |
| baseten | 400 | (empty) |
| fireworks | 400 | 2 request validation errors: Input should be a valid string, field: 'messages[0].content.str' |
| digitalocean | 400 | failed to convert request |
| moonshotai | – | 'file part media type application/pdf' functionality not supported. |
| togetherai | 400 | Invalid JSON data: data did not match any variant of untagged enum MessageContent |
| wafer | 400 | The model request was rejected. |
| nebius | 422 | (empty) |
| morph | 503 | Service temporarily unavailable |
Same model, same request shape, image_url part → 200, and text-only → 200. So the model routes fine; only file parts break.
The request shape itself is correct: the identical payload sent to anthropic/claude-haiku-4.5 returns
messages.0.content.1.pdf.source.base64.data: The PDF specified was not valid. — i.e. the gateway translated the file part into the provider's document format and only rejected my deliberately malformed test PDF.
Also reproduced on
Same failure with a file part, all of which advertise pdf in modalities.input:
moonshotai/kimi-k2.7-code→ 422 (moonshotai, baseten 400, fireworks 400, deepinfra 422)alibaba/qwen3.7-plus→ 400 (alibaba, fireworks 400)alibaba/qwen3-vl-thinking→ 400 (alibaba, novita 400)
112 models in the catalog currently declare pdf input, so the mismatch may be broader than these four.
Expected
One of:
modalities.input(and thefile-inputtag) reflects what the routed providers actually accept, not the model card — a client that trusts the field should never build a request that cannot succeed; or- the gateway converts the
filepart into something the provider accepts (as it already does for Anthropic), or - the request fails fast with a typed, documented error (e.g.
model_does_not_support_file_input) instead of a422whosemessageis empty and whose cause is buried inproviderMetadata.
Options 1 or 3 would be enough for us: what hurts most is that the capability metadata is the only signal available before sending, and it currently says yes when the answer is no.
Notes on error surfacing
Independently of the metadata issue: when every provider attempt fails, the top-level error.message is "AI_APICallError" with an empty inner message. Surfacing the most informative providerAttempts[].error there (or at minimum a non-empty summary) would save a lot of guesswork — as it stands, the response looks like an internal error rather than a rejected request.
Environment
- Endpoint:
https://ai-gateway.vercel.sh/v1/{models,chat/completions} - Date of the runs: 2026-08-19
- Requests were sent with a plain API key, no
providerOptions.gateway.onlypin.