Mapping Stop Sequences Between Provider APIs
9 min read · updated August 11, 2026
Stop sequences look like the most portable parameter in a chat API: a list of strings, and generation halts when one appears. The list is portable. The limit on its length is not, the return signal is not, and on one of these APIs you cannot tell afterwards whether a stop sequence fired at all.
Four names for one parameter
The concept survives the translation intact; only the spelling and the ceiling change.
- OpenAI Chat Completions —
stop. Accepts either a single string or an array, and the specification documents the limit as “up to 4 sequences”. Some newer reasoning models do not accept the parameter at all. - Anthropic Messages —
stop_sequences. Always an array; there is no bare-string form. - Gemini —
generationConfig.stopSequences. Nested inside the generation configuration object rather than sitting at the top level, and camelCase like every other field there.
Three renames, one nesting change, one type widening, and one hard numeric ceiling. The ceiling is the only one that can silently truncate your intent: an adapter translating a five-element list into a stop array will be rejected rather than truncated, but an adapter that helpfully slices to the first four ships a request that runs and no longer stops where you asked. Do not slice. If the target cannot express the list, that is an error to surface, not a detail to smooth over. The published limit for OpenAI is documented on the stop parameter limit page.
What counts as a match
The mechanism is the same everywhere and it is worth stating precisely, because the mental model people carry is usually wrong. The model does not know about your stop sequences. It emits tokens; the serving layer decodes them into text and checks the accumulated text for your strings. That means matching happens on decoded characters, not on tokens.
Take the test string </answer> as a stop sequence and suppose the model is generating the text ...done.</answer>Next section. The tokenizer may well split that closing tag across several tokens, and it may produce a token whose decoded form is >Next — spanning the boundary. Because matching is on characters, the halt still occurs at the right place. But it also means a stop sequence can fire in the middle of a token, and it means a stop sequence that appears inside a longer word will match: end as a stop sequence halts generation in the middle of “calendar”. There is no word-boundary option on any of these APIs. If you want one, the stop sequence has to carry the boundary itself — a leading newline, a closing angle bracket, something the model will not produce mid-word.
The matched text is not returned
On all three APIs, the matched sequence is excluded from the returned content. This is a consistent behaviour and it is also the one that most often surprises people writing a parser on the far side of an adapter.
If your prompt asks the model to wrap its answer in <answer>...</answer> and you set </answer> as a stop sequence, the text you receive has an opening tag and no closing tag. A strict XML parser rejects it. This is not a mapping problem — it behaves the same way on every provider — but it becomes one the moment your adapter is doing double duty as a normaliser, because a normaliser that re-appends the matched sequence on one provider and not on another has invented a difference that the APIs did not have.
Only one API tells you which one fired
This is the genuinely lossy part of the mapping, and it runs in the direction people usually migrate.
Anthropic reports stop_reason: "stop_sequence" when a sequence halted generation, and it populates a separate top-level stop_sequence field with the string that matched. Two pieces of information: that a sequence fired, and which one.
OpenAI reports finish_reason: "stop". The documented enumeration for that field is stop, length, tool_calls, content_filter and the deprecated function_call — and the same stop value covers both “the model finished its turn naturally” and “one of your stop sequences matched”. Gemini has the same collapse: finishReason: STOP covers both cases, with MAX_TOKENS, SAFETY, RECITATION and others for the abnormal terminations.
So mapping Anthropic to OpenAI throws information away, and mapping OpenAI to Anthropic cannot manufacture it. If your application branches on “did the model stop because it was done, or because it hit my delimiter?”, that branch works on one provider and is undecidable on the other two. The workaround is to recover the distinction from the content rather than the metadata: if you know the model was going to emit the delimiter and the returned text ends exactly where the delimiter would have begun, a sequence fired. That is a heuristic, not a signal, and it fails when the model legitimately ends its turn at the same place. The full comparison of terminal vocabularies is on the finish reason mapping.
Stop sequences and streaming
There is one more consequence that only shows up under streaming. The serving layer cannot emit a fragment until it knows the fragment is not the beginning of a stop sequence. If </answer> is a stop sequence and the model has just produced </ans, that text cannot go out yet, because it may be about to become a match.
The practical effect is a small, sequence-length-dependent delay in the stream that is invisible without stop sequences and appears when you add them. Long stop sequences buffer more than short ones. This is a property of the mechanism rather than of any one vendor, so it does not create a mapping difference — but it does mean that a latency regression appearing right after you added stop sequences to a migration is probably not the new provider being slower. See the streaming event mapping for what the wire actually looks like on each side.