Skip to content

Mapping System Prompt Handling Between APIs

9 min read · updated August 11, 2026

“Put the system prompt in the system field” is true on every API and tells you nothing, because the system field is a different kind of thing in each one. One is a message, one is a parameter, one is a structured object with cache semantics attached.

The same text, three shapes

Take one instruction — “You are a terse assistant. Answer in one sentence.” — and put it through each API.

As the first message in the array

{
  "model": "...",
  "messages": [
    { "role": "system", "content": "You are a terse assistant. Answer in one sentence." },
    { "role": "user", "content": "What is a token?" }
  ]
}

This is OpenAI’s Chat Completions shape. The instruction is an ordinary message with a privileged role, which means it obeys all the rules that apply to messages: it has an index, there can be more than one of it, and it can appear anywhere. OpenAI later added a developer role alongside system for the same purpose on newer models, so an adapter has to decide which one it emits.

As a top-level field

{
  "model": "...",
  "max_tokens": 1024,
  "system": "You are a terse assistant. Answer in one sentence.",
  "messages": [
    { "role": "user", "content": "What is a token?" }
  ]
}

Anthropic’s Messages API lifts it out of the array to a sibling of messages. It is not a message, it has no index, and it applies to the whole request. The field also accepts an array of text blocks rather than a bare string, which is what makes the caching behaviour in a later section possible.

As a differently named top-level field, or a parameter

Gemini uses systemInstruction, which takes a Content object rather than a string — the same shape as an entry in contents, but without a meaningful role. OpenAI’s newer Responses API uses a third spelling again: a top-level instructions string sitting beside input. Three providers, three names, and one of those providers uses two of them depending on which endpoint you call.

Position is a property, not a formality

The difference between a message and a parameter is not cosmetic, and it is the first thing that fails to translate. In the message shape, the instruction sits at an index, which means an application can insert a second one at turn twelve to change the model’s mode mid- conversation. In the parameter shape there is no index to insert at: the instruction covers the whole request or it does not exist.

So a transcript with instructions at positions 0 and 12 has no faithful translation to a single top-level field. You have three options and each costs something. Concatenating both into one system parameter loses the ordering, so the model sees the mode switch as though it had always been in effect. Moving the later instruction into the user turn at position 12 preserves the ordering but demotes its authority, and makes it forgeable by anything that can write user content. Newer Anthropic models accept a system entry inside messages for exactly this case, which is the faithful mapping where it is available — but it is model-gated, so an adapter that assumes it gets a 400 on the models that do not support it.

One instruction or several

Because the OpenAI shape treats instructions as messages, several are allowed and they compose by concatenation in order. Because the Anthropic and Gemini shapes treat the instruction as a field, there is exactly one of it. That asymmetry runs one way cleanly and one way badly.

Going from many to one is lossy but recoverable: join them with a separator and accept that any ordering signal the model was picking up on is gone. Going from one to many is trivial. The failure mode to watch for is an adapter that builds the OpenAI request by appending a system message per configuration layer — a product-level prompt, a tenant-level prompt, a per-request prompt — and then translates to Anthropic by taking only the first one. That is not a crash. It is a request that runs, costs the same, and quietly ignores two thirds of your instructions.

The cache boundary rides on the shape

The system prompt is usually the largest stable prefix in a request, which makes it the thing you most want cached, and the caching mechanism is attached to the shape rather than to the text.

Anthropic’s system accepts an array of text blocks and a block can carry a cache_control marker, so the boundary is expressed inside the field itself. OpenAI’s Chat Completions caching is automatic on a prefix-match basis with no explicit marker to translate. Those two facts together mean the mapping is asymmetric: going from Anthropic to OpenAI you drop the marker and the caching still mostly happens; going the other way you have to invent a marker the source request never had, and if your adapter emits a bare string instead, nothing is marked and every request pays full price for the prefix. The failure is invisible in the response body and visible only in the usage object — see the usage object mapping for which field to read.

Which models accept a mid-conversation system message, and whether a given provider marks cache boundaries explicitly or infers them, are both capability questions that change between model releases. Check the provider’s own reference for the model you are actually calling rather than assuming the family behaves uniformly.

Writing the adapter

The version that survives contact is one that keeps the instruction as a list of segments in its internal representation rather than as a string, with each segment carrying its intended position and whether it is a cache boundary. Rendering to the message shape emits one message per segment. Rendering to the field shape joins the segments that belong at the top, and routes any segment with a non-zero position through whichever mid-conversation mechanism the target supports, failing loudly if there is none.

The reason to hold segments rather than a joined string is that joining is a one-way operation. Once your intermediate representation is a single blob of text you cannot recover the boundaries, so you cannot place a cache marker, cannot preserve ordering, and cannot tell which part of the instruction came from the tenant. Every one of those is something you will want later, and none of them can be reconstructed from the blob. The same argument applies to tool schemas, for the same reason.