Mapping the Messages Array Between Chat APIs
9 min read · updated August 11, 2026
The transcript is the field every chat API agrees on and every chat API shapes differently. Renaming the array is trivial; the residue is the roles that have no counterpart, the fields that get dropped in translation, and the tool result that has to move from its own message into somebody else’s.
Three shapes for one transcript
OpenAI’s Chat Completions endpoint takes a flat array under messages. Every entry has a role and a content, and everything else — instructions, tool calls, tool results — is another entry in the same array with a different role. It is a single homogeneous list, which is why it became the shape everyone else gets compared against.
Anthropic’s Messages API also takes messages, but the array holds only two roles, and the system instruction is lifted out to a sibling parameter. Structure that OpenAI expresses with extra roles is expressed here with typed blocks inside content.
Google’s Gemini generateContent renames the array entirely. The transcript is contents, an entry is a Content object, and its text lives in a parts array rather than in a content field. The system instruction is a separate top-level systemInstruction, which the system prompt mapping covers on its own.
The role vocabulary does not line up
This is the first place a naive adapter loses information. The four vocabularies overlap but are not the same set:
- OpenAI Chat Completions —
system,developer,user,assistant,tool, and the deprecatedfunction. - Anthropic Messages —
userandassistantin the array. Newer models also accept asystementry mid-array as an operator channel, but there is notoolrole at all. - Gemini —
userandmodel. Notemodel, notassistant: this single rename is the most common cause of a translated request being rejected on the first call.
So the mapping for the assistant turn is a rename in one direction and a rename in the other, and the mapping for the tool turn is not a rename at all — there is nowhere for it to go, which is the subject of the section below.
There is a second, quieter divergence in what the array is allowed to look like. OpenAI accepts consecutive messages with the same role and treats them as one turn. Anthropic accepts them too and merges them, but it requires the first entry to be a user turn, so a transcript that begins with an assistant greeting — a very common shape for a product that opens the conversation — has to have that greeting either dropped or relocated. Neither behaviour is an error worth logging on its own, and both change what the model sees. An adapter that normalises the array before dispatch should be doing that normalisation explicitly rather than relying on whichever provider it happens to be talking to being forgiving about it.
Content: a string, or a list of typed parts
All three accept a plain string for a text-only turn, and all three accept a list when the turn is multimodal or structured. The list elements are where they diverge. OpenAI uses content parts with a type discriminator. Anthropic uses content blocks with the same idea but a different vocabulary and a much wider set of block types, because blocks are also how it represents tool calls, tool results and thinking. Gemini uses parts, where a part is an object with exactly one populated key.
Two fields on the OpenAI message object have no counterpart anywhere else and are the usual casualties. name, an optional participant label on a user or assistant message, is simply dropped: if your prompt relies on it to distinguish speakers in a group conversation, you have to fold it into the text before you translate. And an assistant message with a populated tool_calls array becomes, on the Anthropic side, an assistant message whose content list contains one tool_use block per call — the arguments move from a JSON string in function.arguments to a parsed object in input.
Tool results are the hard part
Here is one round trip in the OpenAI shape. Two tools were called in parallel, so two tool messages follow the assistant turn:
{
"messages": [
{ "role": "user", "content": "Weather in Paris and Berlin?" },
{ "role": "assistant", "content": null,
"tool_calls": [
{ "id": "call_a1", "type": "function",
"function": { "name": "get_weather",
"arguments": "{\"city\":\"Paris\"}" } },
{ "id": "call_b2", "type": "function",
"function": { "name": "get_weather",
"arguments": "{\"city\":\"Berlin\"}" } }
] },
{ "role": "tool", "tool_call_id": "call_a1", "content": "18C, rain" },
{ "role": "tool", "tool_call_id": "call_b2", "content": "22C, clear" }
]
}And the same exchange in the Anthropic shape. Four messages become three, the tool role disappears, and both results land inside a single user turn:
{
"messages": [
{ "role": "user", "content": "Weather in Paris and Berlin?" },
{ "role": "assistant", "content": [
{ "type": "tool_use", "id": "toolu_a1", "name": "get_weather",
"input": { "city": "Paris" } },
{ "type": "tool_use", "id": "toolu_b2", "name": "get_weather",
"input": { "city": "Berlin" } }
] },
{ "role": "user", "content": [
{ "type": "tool_result", "tool_use_id": "toolu_a1",
"content": "18C, rain" },
{ "type": "tool_result", "tool_use_id": "toolu_b2",
"content": "22C, clear" }
] }
]
}Three renames are visible: tool_call_id becomes tool_use_id, the arguments string becomes a parsed object, and the correlation identifier changes prefix. The structural change is the one that bites — n tool messages collapse into one user message with n blocks. An adapter that emits one user message per tool result produces a transcript that is accepted but that discourages the model from making parallel calls again, because the shape it sees is the shape of sequential calls. Gemini expresses the same thing again differently, with functionCall parts on the model turn and functionResponse parts on the user turn, correlated by function name rather than by an identifier.
What does not survive
- The
namefield. No counterpart. Fold it into the text or lose it. - An assistant turn in final position. OpenAI accepts a trailing assistant message as a prefill that the model continues. Anthropic’s current models reject it with a 400, so a prefill has to be re-expressed as a structured-output constraint or a system instruction. A trailing assistant turn is the one shape that fails loudly rather than quietly.
- Ordering freedom. OpenAI permits a system message at any index. Anthropic has one system parameter that applies to the whole request, so a mid-conversation instruction has to become something else — see the system prompt mapping.
- Cache breakpoints. Anthropic can mark an individual content block as a cache boundary. There is no per-message field to translate that into, so a round trip through a normalised intermediate representation silently discards it and the cache stops being read.
developer role and the mid-array system message both arrived after the shapes above were otherwise stable. Treat the vocabularies here as the documented sets at the time of writing and re-read the request reference before relying on a role you have not sent recently.