GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-05-01Intermediate

Why 'contents must alternate between user and model' Won't Go Away in the Gemini API — and How to Fix It

A focused guide to the Gemini API's 'contents must alternate between user and model' error — what really triggers it, why role names from OpenAI break it, and how to fix Function Calling and system_instruction pitfalls with copy-pasteable code.

gemini-api277troubleshooting82chat4function-calling20python104javascript3

If you've ever wired up multi-turn chat with the Gemini API, there's a good chance you've stared at this line in your logs: google.api_core.exceptions.InvalidArgument: 400 Please ensure that multiturn requests alternate between user and model. I once spent the better part of a day on it while migrating a chatbot to Vertex AI, only to find the literal cause was nothing close to what the message implied.

The frustrating part is that the same error gets thrown for several very different mistakes — wrong role strings, malformed Function Calling history, even a stray system message — so reading the message literally sends you down the wrong rabbit hole. This article walks through the cases I've actually hit in production, with the smallest fix for each.

What Gemini Actually Requires

Two rules are non-negotiable on the generateContent endpoint. First, every entry in contents must use either role: "user" or role: "model", and the two must strictly alternate. Second, the array must end with a user turn — Gemini will not accept a trailing model message and ask itself to continue. Anything that isn't user or model (most commonly assistant carried over from OpenAI code) gets rejected on the spot.

It helps to remember that this contract is much stricter than what you might be used to from OpenAI-compatible APIs. Gemini does not silently coalesce same-role messages, and it does not accept a system role inside the conversation array. Once you internalize the user-led, strictly-alternating, user-trailing shape, every error in this family becomes a one-minute fix.

Here's the smallest valid example to keep as a mental anchor:

# Minimal working example (Python / google-genai SDK)
from google import genai
 
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
 
contents = [
    {"role": "user",  "parts": [{"text": "What's the weather in Tokyo?"}]},
    {"role": "model", "parts": [{"text": "Tokyo is sunny today."}]},
    {"role": "user",  "parts": [{"text": "How about tomorrow?"}]},  # must end with user
]
 
response = client.models.generate_content(
    model="gemini-2.5-pro",
    contents=contents,
)
print(response.text)

Anchor on that shape, and the cases below get much easier to spot.

Case 1: Two Same-Role Turns in a Row (the Most Common One)

The single most frequent cause I see is hard-coding a "greeting" message as a model turn at the start of history, then pushing the first real user input. As soon as the model replies, you accidentally end up with model → user → model → model — and the request blows up.

# Broken: roles repeat
contents = [
    {"role": "model", "parts": [{"text": "Hi! How can I help?"}]},  # starts with model
    {"role": "user",  "parts": [{"text": "Hello"}]},
    {"role": "model", "parts": [{"text": "Sure"}]},
    {"role": "model", "parts": [{"text": "What can I do for you?"}]},  # repeats
]

The cleanest fix is to move that opening line into system_instruction and keep contents strictly user-led. I prefer this approach in every project I maintain — it's easier to evolve the persona later without rewriting history merging logic. As a bonus, the system instruction is reapplied on every turn automatically, so you never lose the persona partway through a long conversation.

# Fixed: the greeting lives in system_instruction
config = genai.types.GenerateContentConfig(
    system_instruction="You are a friendly assistant. Greet the user in your first reply."
)
contents = [{"role": "user", "parts": [{"text": "Hello"}]}]
 
response = client.models.generate_content(
    model="gemini-2.5-pro",
    contents=contents,
    config=config,
)

If for some reason you really do need consecutive turns from the same speaker (for example, when reconstructing logs that arrived out of order), merge them into a single entry with multiple parts rather than sending separate items. That keeps the alternation rule intact while preserving the original message boundaries.

Case 2: You're Sending assistant (Common When Porting from OpenAI)

If you've ported code from OpenAI Chat Completions, it's easy to leave role: "assistant" in place. Gemini doesn't recognize it, and the resulting error often still says "alternate" — making the real cause hard to spot.

# Broken: OpenAI-style role
contents = [
    {"role": "user",      "parts": [{"text": "Hi"}]},
    {"role": "assistant", "parts": [{"text": "Hello!"}]},  # not valid for Gemini
]

When porting, add a small mapping helper at the boundary so the wrong role name never reaches the request body. The same idea applies if you're routing the same conversation between multiple providers — keep a single canonical schema in your domain, and translate at the edge.

ROLE_MAP = {"user": "user", "assistant": "model", "model": "model"}
 
def to_gemini_contents(messages):
    out = []
    for m in messages:
        role = ROLE_MAP.get(m["role"])
        if role is None:
            continue  # handle "system" separately
        out.append({"role": role, "parts": [{"text": m["content"]}]})
    return out

A quick TypeScript equivalent for Node.js or Edge runtimes looks essentially the same:

type ChatMessage = { role: "user" | "assistant" | "system"; content: string };
type GeminiContent = { role: "user" | "model"; parts: { text: string }[] };
 
const ROLE_MAP: Record<string, "user" | "model" | undefined> = {
  user: "user",
  assistant: "model",
  model: "model",
};
 
export function toGeminiContents(messages: ChatMessage[]): GeminiContent[] {
  const out: GeminiContent[] = [];
  for (const m of messages) {
    const role = ROLE_MAP[m.role];
    if (!role) continue; // pass system messages to systemInstruction instead
    out.push({ role, parts: [{ text: m.content }] });
  }
  return out;
}

Case 3: Function Calling Responses Sent with the Wrong Role

The first time you wire up tools, the role placement for functionResponse is genuinely surprising: Gemini expects the tool's result to live under role: "user", not under a function or tool role. Using either of the latter throws the same alternation error immediately, and because the message points to "user/model" rather than "tool", developers often spend a long time looking in the wrong place.

# Correct: functionResponse goes under role: "user"
from google.genai import types
 
contents = [
    {"role": "user",  "parts": [{"text": "Get the weather in Tokyo"}]},
    {"role": "model", "parts": [
        types.Part(function_call=types.FunctionCall(name="get_weather", args={"city": "Tokyo"}))
    ]},
    {"role": "user", "parts": [
        types.Part(function_response=types.FunctionResponse(
            name="get_weather",
            response={"weather": "sunny", "temp": 22},
        ))
    ]},
]

The mental model that helped me: from Gemini's perspective, the tool result is "input from outside the model" — and inputs always live under user. This catches a lot of developers off guard, but once you flip your mental schema it's hard to unsee. For a broader checklist when tools won't fire, the Function Calling troubleshooting guide covers schema and config issues that often hide behind this same error.

Case 4: A system Message Smuggled into contents

If your codebase still treats system prompts as the first entry in contents, Gemini reads it as a non-user opening turn and rejects the request. Move it to GenerateContentConfig.system_instruction and the error disappears. This is a subtle one because some compatibility shims silently strip system messages on other providers, so the bug only surfaces once you point your code at Gemini.

# Broken
contents = [
    {"role": "system", "parts": [{"text": "You are a polite support agent"}]},  # source of the error
    {"role": "user",   "parts": [{"text": "I'd like a refund"}]},
]
 
# Fixed
config = genai.types.GenerateContentConfig(
    system_instruction="You are a polite support agent"
)
contents = [{"role": "user", "parts": [{"text": "I'd like a refund"}]}]

If you're concatenating multiple system-style instructions (persona, safety policy, tool-use guidelines), join them with newlines and pass the merged string to system_instruction. Gemini accepts long instructions there without issue.

Keep a Validator Around for the Long Run

What pays off most over time is a tiny validator you call right before sending. I keep something like the snippet below in every project, with unit tests covering "two users in a row," "trailing model," and "leftover assistant role" — that combination has caught real regressions whenever I bumped the SDK or merged a teammate's PR.

def validate_contents(contents):
    if not contents:
        raise ValueError("contents is empty")
    if contents[0]["role"] != "user":
        raise ValueError("first entry must be 'user'")
    if contents[-1]["role"] != "user":
        raise ValueError("last entry must be 'user'")
    for i in range(len(contents) - 1):
        if contents[i]["role"] == contents[i + 1]["role"]:
            raise ValueError(
                f"roles repeat at index {i} and {i+1}: {contents[i]['role']}"
            )
    for c in contents:
        if c["role"] not in ("user", "model"):
            raise ValueError(f"unknown role: {c['role']}")
    return True

In CI, run this validator against fixture conversations that mimic your worst real-world cases — interleaved tool calls, retries that re-append the same model turn, and edits that delete a user entry without removing its corresponding model reply. Most production regressions in this area happen because someone modifies history mid-flight and ships before catching the broken alternation.

If you'd like to dig deeper into history compression and conversation design, the companion piece Gemini API chat history management pitfalls goes into how to keep multi-turn dialogues stable as they grow.

What to Do Next

In practice this error stops being scary the moment you internalize "user-led, strictly alternating, ends on user, no assistant or system." The single most useful next step you can take today is to drop the validate_contents helper into your project and add unit tests for the four shapes above — repeat-roles, OpenAI's assistant, trailing model, and a stray system entry. Future-you will quietly thank present-you the next time an SDK upgrade rearranges the message format.

If you're running tools heavily, also add a fixture for functionResponse placement so the surprise of "tool results under user" is encoded as a passing test instead of a midnight stack trace.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-05-28
Why per-turn generationConfig is ignored in Gemini API chat sessions
If you pass a different generationConfig (temperature, max_output_tokens, response_schema) to each send_message in a google-genai chat session and the behavior never changes, this walkthrough shows what is actually happening, why the SDK is designed that way, and three workarounds we use in production for review-summary and reply-draft pipelines.
API / SDK2026-04-19
Gemini API Multi-Turn Chat Breaking: Chat History Management Pitfalls and Fixes
When building multi-turn conversations with the Gemini API, longer chats cause token overflow, slowdowns, and context loss. Learn how to use ChatSession correctly with practical code examples for managing chat history.
API / SDK2026-06-21
Gemini API Implicit Caching Not Working — Troubleshooting Guide by Root Cause
Troubleshoot Gemini API implicit caching issues: cache not hitting, unexpectedly high costs, or low cache hit rates. Covers token thresholds, prompt structure, model version consistency, TTL expiry, and multimodal caching with code examples.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →