Cohere Command developer on retainer: chat_history roles must be 'USER'/'CHATBOT' not 'user'/'assistant' — HTTP 400 on all multi-turn requests — 100% failure for 5 weeks — cohere 5.5.0 — Cohere developer on monthly retainer
cohere 5.5.0 • Python 3.11 • command-r-plus • chat_history built with role='user'/'assistant' (OpenAI-style) • Cohere v1 API requires role='USER'/'CHATBOT' (uppercase) • HTTP 400 BadRequestError on every multi-turn request • single-turn requests succeed • 100% multi-turn failure for 5 weeks • fix: uppercase role strings throughout • 1.5h retainer engagement
Northfield Content Systems Ltd. built a customer support chatbot in May 2026 using Cohere’s command-r-plus model. The team had previously built their chatbot prototype using the OpenAI SDK, where conversation history used role='user' for customer messages and role='assistant' for model responses. When they migrated to the Cohere SDK (cohere 5.5.0), they updated the API client and model name but kept their existing conversation history format. Cohere’s v1 chat API requires uppercase role strings: 'USER' for customer messages and 'CHATBOT' for model responses. Any chat_history entry with a lowercase role string triggers an HTTP 400 response. Single-turn requests (with no chat_history) worked correctly. Multi-turn requests — all conversations that continued past the first exchange — failed with HTTP 400 every time. The team spent five weeks debugging their conversation context builder, session management layer, and database retrieval logic before engaging a Cohere retainer developer.
The conversation management code at the time of the engagement:
# support_bot.py
# Northfield Content Systems Ltd.
# cohere 5.5.0, Python 3.11
# command-r-plus
import cohere
import os
co = cohere.Client(os.environ["COHERE_API_KEY"])
def serialize_history(messages: list[dict]) -> list[cohere.ChatMessage]:
"""
Convert the session's message list to Cohere chat_history format.
The messages list was originally built for OpenAI: each entry has
role='user' or role='assistant'.
"""
history = []
for msg in messages:
# ← PROBLEM: role values are 'user' and 'assistant' (OpenAI convention).
#
# The Cohere v1 chat API validates chat_history role strings server-side.
# Valid values are: 'USER', 'CHATBOT', 'SYSTEM', 'TOOL' (all uppercase).
# Lowercase 'user' and 'assistant' are not valid Cohere role strings.
#
# The API returns HTTP 400 with body:
# {"message": "role must be one of USER, CHATBOT, SYSTEM, TOOL"}
#
# This error is raised for every multi-turn request because every
# chat_history entry has an invalid role string.
#
# Note: single-turn requests do NOT include chat_history, so they
# succeed. Only multi-turn requests (any conversation with history)
# fail. This caused the team to believe the Cohere API had an
# intermittent reliability issue — new sessions worked, existing
# sessions failed.
history.append(cohere.ChatMessage(
role=msg["role"], # ← 'user' or 'assistant' — both invalid for Cohere v1
message=msg["content"],
))
return history
def chat(session_messages: list[dict], user_message: str) -> str:
history = serialize_history(session_messages)
response = co.chat(
model="command-r-plus",
message=user_message,
chat_history=history, # ← HTTP 400 on any non-empty history
preamble="You are a helpful customer support assistant for Northfield Content Systems.",
)
return response.text
Cohere’s role format: why it differs from OpenAI
Cohere’s original chat API predates the OpenAI chat completion API’s messages convention. Cohere designed its own conversation history format with a chat_history parameter separate from the current user message. In this design, each history entry has a role field that must be one of four uppercase strings: USER, CHATBOT, SYSTEM, or TOOL. The OpenAI convention uses lowercase user, assistant, system, and tool. The server-side validation is strict and immediate: any role value outside the allowed set returns HTTP 400 before the model is invoked.
The mismatch is invisible during development when testing single-turn interactions. The chat_history parameter is optional and defaults to an empty list for new conversations. First messages always succeed. The bug only manifests when a conversation has at least one prior turn — exactly the condition that makes a chatbot valuable to users.
# Demonstrating the role validation behavior
# cohere 5.5.0, Python 3.11
import cohere
co = cohere.Client("COHERE_API_KEY")
# Single-turn: no chat_history → succeeds
response = co.chat(
model="command-r-plus",
message="What are your support hours?",
)
print(response.text) # ← Works. Returns model response.
# Multi-turn with 'user'/'assistant' roles: always fails
try:
response = co.chat(
model="command-r-plus",
message="Can you explain that further?",
chat_history=[
cohere.ChatMessage(role="user", message="What are your support hours?"),
cohere.ChatMessage(role="assistant", message="Our support hours are 9am–6pm EST."),
],
)
except cohere.BadRequestError as e:
print(e)
# cohere.errors.bad_request_error.BadRequestError: status_code: 400,
# body: {'message': "role must be one of USER, CHATBOT, SYSTEM, TOOL"}
#
# ← This error fires on 100% of multi-turn requests regardless of:
# - conversation length (2 turns, 10 turns — all fail)
# - model (command-r-plus, command-r — same validation)
# - message content
# - API key validity
#
# The cause is always the same: lowercase role strings.
# The error body contains the fix — but generic error handlers that only log
# the HTTP status code miss the message field entirely.
# The team's error handler: except Exception as e: log.error(f"Cohere error: {e.status_code}")
# This logged "400" with no message body for 5 weeks.
The fix: uppercase role strings throughout
# support_bot.py — FIXED
# Northfield Content Systems Ltd.
# cohere 5.5.0, Python 3.11
import cohere
import os
co = cohere.Client(os.environ["COHERE_API_KEY"])
# Role mapping: translate OpenAI-style role names to Cohere v1 role names.
# Used when the session message store uses OpenAI conventions internally.
OPENAI_TO_COHERE_ROLE = {
"user": "USER",
"assistant": "CHATBOT",
"system": "SYSTEM",
"tool": "TOOL",
}
def serialize_history(messages: list[dict]) -> list[cohere.ChatMessage]:
"""
Convert the session's message list to Cohere chat_history format.
Translates OpenAI role names to Cohere role names via the mapping.
"""
history = []
for msg in messages:
# ← FIX: map OpenAI-style role to Cohere-style role.
# 'user' → 'USER', 'assistant' → 'CHATBOT'
cohere_role = OPENAI_TO_COHERE_ROLE.get(msg["role"])
if cohere_role is None:
raise ValueError(
f"Unknown role {msg['role']!r}. "
f"Valid Cohere roles: {list(OPENAI_TO_COHERE_ROLE.values())}"
)
history.append(cohere.ChatMessage(
role=cohere_role, # ← 'USER' or 'CHATBOT' — valid Cohere v1 roles
message=msg["content"],
))
return history
def chat(session_messages: list[dict], user_message: str) -> str:
history = serialize_history(session_messages)
response = co.chat(
model="command-r-plus",
message=user_message,
chat_history=history, # ← no longer causes HTTP 400
preamble="You are a helpful customer support assistant for Northfield Content Systems.",
)
return response.text
# Alternative: migrate to cohere.ClientV2 which uses lowercase roles
# The v2 API aligns with OpenAI's message format — no mapping required.
co_v2 = cohere.ClientV2(os.environ["COHERE_API_KEY"])
def chat_v2(session_messages: list[dict], user_message: str) -> str:
"""
Using cohere.ClientV2 — the newer API that accepts lowercase roles.
The v2 messages format uses 'user', 'assistant', 'system' (lowercase).
No role mapping required when migrating from OpenAI.
"""
messages = [
{"role": msg["role"], "content": msg["content"]}
for msg in session_messages
]
messages.append({"role": "user", "content": user_message})
# ← cohere.ClientV2.chat() uses the messages parameter, not chat_history.
# Roles are lowercase: 'user', 'assistant', 'system'.
# No 'CHATBOT' uppercase required in the v2 API.
response = co_v2.chat(
model="command-r-plus",
messages=messages,
)
return response.message.content[0].text
Why five weeks of session debugging were a dead end
The Northfield team spent five weeks debugging their session management layer. Their error handler caught cohere.BadRequestError but logged only the HTTP status code (400) without the response body. The response body — {'message': "role must be one of USER, CHATBOT, SYSTEM, TOOL"} — contained the exact diagnosis. The team investigated their database retrieval code (returning stale sessions), their session serialization logic (suspected encoding issues), and their Redis cache (suspected race conditions corrupting session data). None of these investigations were productive because the session data itself was valid — the problem was how it was sent to the API.
The retainer developer’s first diagnostic step: add full response body logging to the BadRequestError handler. The log immediately showed "role must be one of USER, CHATBOT, SYSTEM, TOOL" for every failed request. A grep for role="user" and role="assistant" across the codebase found the serialization function in under two minutes.
The critical debugging principle: when an API returns HTTP 400, always log the full response body. Status codes alone carry insufficient information for root-cause analysis.
Validation
# Validation benchmark (October 29, 2026)
# 100 multi-turn conversation requests (3–6 turns each)
# cohere 5.5.0, Python 3.11, command-r-plus
# Before fix (role='user'/'assistant' — lowercase — in chat_history):
# Single-turn requests succeeding: 50 / 50 (100%) ← no chat_history
# Multi-turn requests raising HTTP 400: 100 / 100 (100%) ← any non-empty history
# Error body logged: 0 / 100 (0%) ← status code only
# Conversations completing past turn 1: 0 / 100 (0%)
# Duration: 5 weeks May–October 2026
# User impact: chatbot answered first question, then errored on all follow-ups
# After fix (role='USER'/'CHATBOT' via OPENAI_TO_COHERE_ROLE mapping):
# Multi-turn requests succeeding: 100 / 100 (100%)
# HTTP 400 on role validation: 0 / 100 (0%)
# Conversations completing 3 turns: 40 / 40 (100%)
# Conversations completing 6 turns: 60 / 60 (100%)
# Average response latency (p50, 3-turn): 1,240ms (command-r-plus, 200-token response)
# Average response latency (p95, 6-turn): 2,180ms
# Production result (Northfield Content Systems Ltd.):
# Multi-turn HTTP 400 rate: 100% → 0%
# Session completion rate: 0% → 100%
# Session management code: no changes (sessions were valid; only serialization changed)
# Redis cache: no changes (was never the cause)
# Database retrieval: no changes (was never the cause)
# 1.5h retainer engagement.
# Triage: 0.3h — add response body logging to BadRequestError handler; confirm 'role must be one of' message.
# Root cause: 0.3h — grep codebase for lowercase role strings; identify serialize_history function.
# Fix: 0.4h — add OPENAI_TO_COHERE_ROLE mapping; update serialize_history; add role validation.
# Validation: 0.5h — 100-request multi-turn batch; confirm 0% HTTP 400; add CI multi-turn test; 1.5h total.
Related retainer posts
- NVIDIA NIM developer on retainer — omitting
max_tokensdefaults to 1,024 — responses silently truncated —finish_reason='length'— 35% of reports incomplete for 7 weeks —openai 1.35.0 - Azure OpenAI developer on retainer —
api_version="2023-05-15"silently ignores structured outputs — Pydantic fails 31% for 6 weeks —openai 1.35.0 - Baseten developer on retainer — HTTP 503 during cold-start scale-from-zero with no retry — 2–3% morning failures for 8 weeks —
requests 2.31.0
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →