---
url: 'https://adk.nht.io/batteries/validation/api-surface-scope.md'
---

# Which API surface a rule applies to

> **The one-line version.** An ordering rule is a claim about **a model reached through a specific
> API**, not about the model. Enable a recipe only when you are dispatching through the API that
> recipe was derived from. Through any other surface it will, at best, check nothing; at worst it
> will block turn state that the surface you are actually using accepts.

## Why this page exists

Every other page in this section is organised by model — "dispatching to Nova? enable `'nova'`."
That framing is incomplete, and the omission is load-bearing.

A vendor's ordering requirements are enforced by an **endpoint**, not by weights. The same model
reached two different ways enforces two different sets of rules:

| The model                | Reached via                          | What enforces ordering                                             |
| ------------------------ | ------------------------------------ | ------------------------------------------------------------------ |
| Amazon Nova              | Bedrock **Converse**                 | Converse's own content-block grammar — text before `toolUse`, strict alternation |
| Amazon Nova              | an OpenAI-compatible gateway         | whatever that gateway normalises to before it calls Converse       |
| Claude                   | Anthropic **Messages**               | Anthropic's block ordering, conditioned on `anthropic-beta` headers |
| Claude                   | Bedrock Converse                     | Converse's grammar **plus** Anthropic's                            |
| Gemini                   | **`generateContent`**                | `functionCall` / `functionResponse` part ordering, `thoughtSignature` |
| Gemini                   | an OpenAI-compatible gateway         | the gateway's translation of `tool_calls` into parts               |

The recipes in [Recipes](./recipes.md) were derived from vendor documentation describing the
**first** row of each pair. Applied to the second, they are describing a wire format the request
never takes.

## A gateway may already be doing the guard's job

This is the failure mode that is hardest to see, because everything appears to work.

An OpenAI-compatible gateway in front of a non-OpenAI vendor cannot forward your messages verbatim
— it has to translate them into the vendor's native shape. That translation routinely **includes
the very normalisations these rules check for**. A gateway fronting Bedrock will merge consecutive
same-role messages (and insert a filler turn if merging is not possible) because Converse rejects
them. A gateway fronting Gemini will inject a `thoughtSignature` sentinel on historical
`functionCall` parts because Gemini 3 rejects requests without one.

Both of those are correct behaviour for the gateway. Both also mean that if you enable
`'strict_alternation'` or `'thought-signature-required'` while dispatching through such a gateway:

* the guard evaluates turn state the vendor **will never see in that shape**;
* a violation the guard blocks would have been repaired in transit anyway;
* and a passing dispatch tells you nothing about the vendor, because you measured the gateway.

We hit exactly this while auditing these rules against live models. Cells dispatched through an
OpenAI-compatible endpoint returned "the vendor accepts this" for `strict_alternation` on three
separate families — until reading the gateway's source showed it merging consecutive same-role
messages before the request left the building. The vendor never received the violation. The result
was an artefact of the path, not a fact about the model.

## What this means for you

**Match the recipe to the API you dispatch through.**

| If you dispatch through…                                        | Then…                                                                                                                      |
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| the vendor's **native** API (Anthropic Messages, Gemini `generateContent`, Bedrock Converse) | The recipes apply as documented. This is the case they were written for.                                                    |
| an **OpenAI-compatible gateway** in front of another vendor      | Assume the gateway normalises shape. Prefer `'permissive'` or a recipe narrowed to rules the gateway demonstrably does not touch, and verify against the gateway's own behaviour rather than the vendor's docs. |
| a gateway you do **not** control                                 | Treat every ordering rule as unverified until you have observed the request the vendor actually received.                    |

**Rules that are surface-specific by construction.** These name an API in their own description and
are meaningless anywhere else:

* `converse_text_before_tool_use` — Bedrock **Converse** content blocks. There is no `toolUse`
  block on an OpenAI-shaped request, so through any other surface this rule cannot fire on the
  thing it describes.
* `thought_signature_required` / `thought_signature_advisory` — Gemini `generateContent`
  `thoughtSignature` parts. An OpenAI-shaped `tool_calls` entry has no such field; whether one
  reaches Gemini is decided by the translator, not by your turn state.
* `harmony_commentary_channel` — the Harmony `commentary` channel, a prompt-format concept. Whether
  it survives depends on how the serving stack renders the conversation.
* `role_remap_split_tool_roles` / `role_remap_inline_tool_call` — a **consumer-supplied** wire-role
  tag. These check the consistency of your own renderer against your own tag; see
  [Writing a profile](./writing-a-profile.md). Nothing in the ADK writes that tag, so without a
  renderer that reads it, both legs of any test render identically.

**How to check, rather than assume.** Capture the request as assembled. Every LLM battery exposes
an `onPromptAssembled` hook that hands back the exact body about to be dispatched — observe it,
compare it to what the vendor's documentation describes, and only then decide whether a rule is
checking something real. If your traffic passes through a gateway, that hook shows what the
gateway *received*, not what the vendor did; for that you need the gateway's own logs.

## Measured: the same rule, two surfaces

Amazon Nova, `strict_alternation`. The violating shape is two consecutive `user` turns.

| Path                                             | Result                                                                    |
| ------------------------------------------------ | ------------------------------------------------------------------------- |
| OpenAI-compatible gateway → Bedrock               | 200 — but the gateway **merged the two turns** before Converse saw them    |
| **Bedrock Converse directly** (no gateway)        | **200 — Nova accepted the consecutive user turns as sent**                 |

Only the second row is evidence about Nova. The first measured the gateway's repair. Both look
identical from the client.

Same model, `converse_text_before_tool_use` — the rule that says text blocks must precede `toolUse`
within one assistant turn. Sent natively, `toolUse` first and text second:

| Shape                                            | Result       |
| ------------------------------------------------ | ------------ |
| `[toolUse, text]` — what the rule forbids         | 200 `end_turn` |
| `[text, toolUse]` — what the rule demands         | 200 `end_turn` |

Converse accepted both. That does not make the rule worthless — a vendor may still degrade quality,
and behaviour differs across models on the same API — but it does mean the rule is not enforcing a
hard wire constraint on this model, and should not gate dispatch as though it were.

## Honest limits of the shipped recipes

The recipes encode vendor **documentation**. Documentation describes the native API. We have
verified a subset against live traffic and found that several rules do not reproduce through an
OpenAI-compatible surface — which is a statement about surfaces, not a refutation of the vendor
docs.

Treat a recipe as: *this is what the vendor says its native API requires*. It is not: *this is what
will happen to your request on the path you are actually using*. Only observation settles the
second, and this page exists because the difference between the two is easy to miss and expensive
to discover in production.
