Nearly every INVALID_ARGUMENT of this shape comes from posting OpenAI-style messages to Google's native generateContent endpoint. It has different role names, a separate home for system instructions, and no tolerance for an empty turn.
The error
{"error":{"code":400,
"message":"Please use a valid role: user, model.",
"status":"INVALID_ARGUMENT"}}
{"error":{"code":400,
"message":"* GenerateContentRequest.contents: contents is not specified\n",
"status":"INVALID_ARGUMENT"}}Causes and fixes at a glance
| Cause | Fix |
|---|---|
| OpenAI-shaped messages sent to the native endpoint | assistant → model, and system moves to systemInstruction. Nothing else is legal. |
| Empty contents array | Every message got filtered out — often a tool turn with content: null. |
| A turn with an empty parts array | Substitute an empty text part rather than emitting a part-less turn. |
| Inline image data without a mimeType | inline_data needs both mime_type and base64 data. |
Work out which API you are actually on
The same SDK name fronts two incompatible schemas: Google's native generateContent, which takes contents with user and model roles, and an OpenAI-compatible /v1/chat/completions, which takes messages with system, user and assistant. Every error in this family is a payload written for one and sent to the other.
Map the roles if you are on the native one
system and developer messages fold into a separate systemInstruction field. assistant becomes model. user stays user. Any other role has no equivalent and must be dropped or merged before sending.
def to_gemini(messages):
system, contents = [], []
for m in messages:
if m["role"] in ("system", "developer"):
system.append(m["content"])
continue
if m["role"] not in ("user", "assistant"):
continue # no Gemini equivalent
parts = [{"text": m["content"] or ""}] # never emit empty parts
contents.append({
"role": "model" if m["role"] == "assistant" else "user",
"parts": parts,
})
req = {"contents": contents}
if system:
req["systemInstruction"] = {"parts": [{"text": "\n\n".join(system)}]}
return reqNever emit a turn with no parts
A message whose content was null or filtered to nothing produces a part-less turn, which is rejected. Substituting an empty text part keeps the turn valid and preserves the alternation the model expects.
Validate before sending
Assert that contents is non-empty, that every role is user or model, and that every turn has at least one part. Three lines at the call site, versus a 400 from the network.
If you’re calling through Kunavo
Calling Gemini through Kunavo's OpenAI-compatible /v1/chat/completions means you never hand-build Google's contents array at all: you send OpenAI-shaped messages and the gateway does the mapping — system and developer messages folded into systemInstruction, assistant rewritten to model, roles with no equivalent dropped, and an empty text part substituted rather than a part-less turn emitted. So this whole class of INVALID_ARGUMENT does not arise from message shape. What it cannot save you from is an argument that is genuinely invalid — an unsupported response_format, or a malformed inline image — which will still be rejected on its merits. Per-token rates for the Gemini shelf are in our Gemini pricing guide.
FAQ
What are Gemini's valid roles?
On the native API, exactly two: user and model. There is no system role — system instructions go in the separate systemInstruction field.
Can I send a system message?
Not as a turn. Move it to systemInstruction, or use an OpenAI-compatible endpoint that does that for you.
Why does the identical payload work against OpenAI?
Because OpenAI's schema defines system and assistant roles, and Gemini's native schema does not. The payload is valid — for the other API.
Related guides
- Gemini API 429 RESOURCE_EXHAUSTED — quota vs rate limit, fixed properly
- Gemini API key not working — API_KEY_INVALID and its five causes
More error semantics live in the error reference; getting a key takes a minute via sign up and the authentication docs.