ADR-0011: Field Forwarding on Translated Paths
Status
AcceptedContext
GoModel exposes an OpenAI-compatible API (/v1/*) and an Anthropic Messages
ingress, and routes each request to a provider whose native API may differ
from what the client sent. ADR-0002 already decided that unknown JSON fields
are preserved rather than dropped, and ExtraFields on the request types
implements that for requests.
What happens to a field the gateway does know about, when the selected
provider or model cannot take it, was never written down. The adapters answer
the same question three different ways today:
- The OpenAI adapter maps
max_tokenstomax_completion_tokensand dropstemperaturefor reasoning models (o-series, GPT-5). - The shared cache-control helper strips Anthropic
cache_controldirectives before a request goes to a provider that does not accept them. - The Anthropic adapter rejects
verbosityand anyresponse_formatother than typetextwith a 400, and silently accepts atextformat because it changes nothing.
Decision
On translated paths, a field the client sends is handled by the first rule that matches, in this order:- Unknown to the gateway. Forward untouched, in both directions. This is the ADR-0002 forward-compatibility promise and is how clients reach provider-native extras (for example OpenRouter routing preferences) without waiting for a GoModel release.
- Known, and the target accepts it. Forward as-is.
- Known, and the target needs a different shape. Translate. The client asked for something the provider can do; only the spelling differs.
- Known, the target cannot honor it, and ignoring it does not change what the client asked for. Drop it. A hint the provider would not have acted on anyway is not worth a failed request.
- Known, the target cannot honor it, and dropping it would change the result. Reject with a 400 that names the field and the provider. A silent drop here returns a wrong answer under a 200.
cache_control travels in ExtraFields and is still a
known field.
A known field with no rule for the selected target is forwarded as-is (rule 2
is the default). The provider is the authority on what it accepts and answers
with its own error when it does not. Add a rule for a target only when that
answer is wrong or unhelpful: the provider silently ignores the field, needs
another spelling, or returns an error a client cannot act on.
Gateway-reserved fields are outside the ladder. Members the gateway itself
sets or interprets, such as provider on requests and responses and the
usage options it injects into streams, are never forwarded from the client
and never taken from the provider.
Scope
The ladder applies to translated paths only:/v1/* and the Anthropic
Messages ingress, where the client speaks the gateway’s dialect and the
gateway promises to adapt it.
Passthrough routes (/p/{provider}/...) are exempt. The client speaks the
provider’s dialect and chose the provider by name, so the body reaches the
provider byte for byte. The gateway substitutes credentials and records audit
and usage, and it does not edit the body. The reverse also holds: a translated
endpoint must not be served with passthrough semantics, because every gateway
invariant then has to be re-implemented on the shortcut.
Where the rules live
Each provider adapter owns its own field decisions, table-driven and tested per field, as the OpenAI adapter does today. There is no central field-capability registry; ADR-0004’s capability model describes routes and features, not fields. Revisit if three adapters end up with the same table shape. A rule that translates or drops a field should log the decision at debug level so operators can see why an outbound request differs from the inbound one. The adapters that already translate or drop (OpenAI reasoning models, cache-control stripping) do not log today; adding that is a follow-up, not a prerequisite for this ADR. Nothing is added to the response.Example ladder for /v1/chat/completions
This table records the current decisions so they can be reviewed and updated
as providers change. Add a row when an adapter gains a rule; change a row when
a provider starts accepting a field.
Two request rows deserve a note. Anthropic rejects a request that carries both
temperature and top_p on every current model, and xAI rejects metadata on
its native /responses endpoint; both are rule 4 because the dropped half does
not change what the caller asked for, and rule 5 would lock OpenAI SDKs that
fill in both sampling defaults out of Anthropic entirely. The dropped value is
logged. metadata is removed only from the outbound provider request, so the
gateway can still echo the member back to the client.
Response direction
Rule 1 applies in both directions, so a member the gateway has no rule for comes back to the client exactly as the provider sent it. That is deliberate: it is how a client reads provider-native extras, and it is why vendor members such as Groq’sx_groq and its queue_time/prompt_time usage timings, or
DeepSeek’s prompt_cache_hit_tokens/prompt_cache_miss_tokens, are relayed
rather than stripped.
reasoning is rule 3 rather than rule 1 because the gateway does have a rule
for the field: reasoning_content is the spelling every other adapter emits
and the one the Responses and Messages translation layers and the dashboard
read, so relaying Groq’s spelling would make identical client code work on
DeepSeek and silently lose the reasoning on Groq. It is renamed rather than
duplicated: two copies of the same chain of thought would double the payload
of every reasoning response.
On the streaming path the rename is gated on a byte scan for "reasoning", so
only the reasoning deltas of a reasoning model are decoded and re-encoded;
every other line keeps the upstream bytes. Anything that would force a decode
of every chunk needs the same justification, because the verbatim relay of
chat SSE is a deliberate hot-path property.
Consequences
- Clients get one predictable answer per field and provider, and a 400 only when honoring the request is impossible.
- Adapters gain an explicit place to record their decisions, and reviewers have a rule to check a new mapping against.
- Response types need the same
ExtraFieldstreatment requests already have, so rule 1 holds in both directions.ChatResponseandChoicegain it in #859;Usagealready keeps extras inraw_usage. - Some existing behavior may move between rungs once reviewed against the ladder; each move is a behavior change and gets its own PR and changelog entry.