fn erase_unadopted_client_keys(
body: &mut Value,
discarded: &[String],
issues: &[FieldIssue],
)Expand description
Remove from the body the client keys gglib must not forward: what the trust gate binned, the budget alias gglib never emits, and one refused field.
The single place body keys leave this stage, so “what does gglib delete from a request” has one answer in one function.
§discarded — what the trust gate binned
The resolved patch is only ever inserted, and since ADR 0003 six modelled
fields resolve to nothing by design, so a gated key the ladder then stays
silent on rides the body to llama-server exactly like an unmodelled one.
Found live, not by review: an untrusted client’s frequency_penalty: 0.9
reached /slots intact, because no layer names that field and nothing
overwrote it. Before the deferral this could not happen — the floor emitted
every modelled key — which is why the gate never needed this until then.
Empty when the client is trusted.
§The budget alias — always, whatever the trust setting
llama-server reads THINKING_BUDGET_TOKENS_KEY as a second spelling of
reasoning_budget_tokens (ADR 0007 finding 7c). gglib reads it too, so its
value is already in the resolved ladder — but gglib emits the canonical
key only, and a surviving alias is a second answer to the same question
sitting next to the force-inserted first. Which one wins would then be
llama-server’s parse order rather than gglib’s ladder. Removing it is not a
trust decision, it is a consequence of gglib having one canonical spelling.
§issues — one field, and the asymmetry is upstream’s
A refused value never becomes Some, so it never enters the resolved patch
and never entered discarded either: the rejection stops at the layer, and
the client’s own text rides on. For nearly every field that is exactly
right — these readers reject what llama-server rejects, so the forwarded
value earns a clean HTTP 400 from the system that owns the field, which
tells the client more than a silent substitution would and keeps gglib no
stricter than upstream (the doctrine on
InferenceConfig::extract_client_sampling).
ADR 0007 finding 7c measured where that stops holding. Upstream governs
the budget — reasoning_budget_tokens: -2 comes back a 400 naming the
range — and does not govern effort at all. So the two reasoning
controls split:
REASONING_EFFORT_KEY, refused → deleted. There is no downstream 400 to inherit:"banana"is accepted upstream and rendered into the user’s prompt verbatim. Left in the body, gglib’s refusal would be a record inclient_fields_rejectedof a value the model then read. This is the one field where gglib’s “no” has to be the only “no” there is.REASONING_BUDGET_TOKENS_KEY, refused → left in place, like every other field. The client gets upstream’s honest 400. And if the ladder resolves a budget of its own, the force-insert overwrites the client’s text before it is ever sent, so the refusal costs nothing.
A FieldIssue::Normalised deletes nothing under either rule: the
substitute is either force-inserted over the client’s spelling or is an
absence that llama.cpp reads from the client’s own sentinel anyway
(max_tokens: -1).
A body that is not a JSON object is left alone, as everywhere else here.