> ## Documentation Index
> Fetch the complete documentation index at: https://gomodel-feat-anthropic-response-format.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 0011 field forwarding on translated paths

# ADR-0011: Field Forwarding on Translated Paths

## Status

Accepted

## Context

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_tokens` to `max_completion_tokens` and drops
  `temperature` for reasoning models (o-series, GPT-5).
* The shared cache-control helper strips Anthropic `cache_control` directives
  before a request goes to a provider that does not accept them.
* The Anthropic adapter rejects `verbosity` and any `response_format` other
  than type `text` with a 400, and silently accepts a `text` format because it
  changes nothing.

Each answer is reasonable on its own. Without a shared rule the next adapter
picks one at random, and clients cannot predict whether a field will be
honored, ignored, or refused.

## Decision

On translated paths, a field the client sends is handled by the first rule
that matches, in this order:

1. **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.
2. **Known, and the target accepts it.** Forward as-is.
3. **Known, and the target needs a different shape.** Translate. The client
   asked for something the provider can do; only the spelling differs.
4. **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.
5. **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.

"Known" means the gateway has a rule for the field, not that the field has a
typed struct member. `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.

| Field                                   | Target                                     | Rule     | Treatment                                    |
| --------------------------------------- | ------------------------------------------ | -------- | -------------------------------------------- |
| any unknown member                      | any                                        | 1        | forwarded untouched                          |
| `max_tokens`                            | OpenAI reasoning models                    | 3        | sent as `max_completion_tokens`              |
| `temperature`                           | OpenAI reasoning models                    | 4        | dropped                                      |
| `reasoning.effort`                      | Gemini, DeepSeek                           | 3        | sent as flat `reasoning_effort`              |
| `reasoning.budget_tokens`               | Gemini, DeepSeek                           | 4        | dropped                                      |
| `cache_control`                         | providers without Anthropic request shapes | 4        | dropped                                      |
| `response_format` of type `text`        | Anthropic                                  | 4        | dropped                                      |
| `response_format` of type `json_schema` | Anthropic                                  | 3        | sent as `output_config.format`               |
| `response_format` of type `json_object` | Anthropic                                  | 3        | sent as a system-prompt instruction          |
| `verbosity`                             | Anthropic                                  | 4        | dropped                                      |
| `temperature` together with `top_p`     | Anthropic                                  | 4        | `top_p` dropped, `temperature` kept          |
| `metadata`                              | xAI `/responses`                           | 4        | dropped                                      |
| `stream_options.include_usage`          | any                                        | reserved | set by the gateway when usage tracking is on |
| `provider`                              | any                                        | reserved | set by the gateway                           |

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's `x_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.

| Field                                                    | Source   | Rule | Treatment                                                                          |
| -------------------------------------------------------- | -------- | ---- | ---------------------------------------------------------------------------------- |
| any unknown member                                       | any      | 1    | forwarded untouched                                                                |
| `message.reasoning` / `delta.reasoning`                  | Groq     | 3    | returned as `reasoning_content`                                                    |
| `x_groq`, `queue_time`, `prompt_time`, `completion_time` | Groq     | 1    | forwarded untouched                                                                |
| `prompt_cache_hit_tokens`, `prompt_cache_miss_tokens`    | DeepSeek | 1    | forwarded untouched (`prompt_tokens_details.cached_tokens` carries the same count) |

`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 `ExtraFields` treatment requests already have,
  so rule 1 holds in both directions. `ChatResponse` and `Choice` gain it in
  \#859; `Usage` already keeps extras in `raw_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.
