Skip to main content

Module apply

Module apply 

Source
Expand description

The ordered request-shaping pipeline, and the one statement of its order.

§The stages

#StageLives inReads
1Strip prior reasoningsuper::messagesmessages
2Coalesce for capabilitiessuper::messagesmessages
2bStrip unsupported toolssuper::toolstools, capabilities
3Truncate stale historysuper::truncationmessages, payload size
4Resolve the sampling hierarchysuper::samplingtop-level keys
5Pin cache_promptsuper::samplingtop-level keys
5bSuppress an unreadable reasoning_effortsuper::effort_gateresolved effort, template caps
Log the sampling decisionsuper::sampling_logthe decision
6Constrain dialect tool callssuper::constraintools, tool_choice, tags

§The order is load-bearing

1 before 2. Coalescing merges message content. Stripping afterwards would have to find and excise <think> blocks inside text that has already been concatenated with "\n\n" separators from other turns.

2 before 3. Both stages 1 and 2 only ever shrink the body, and stage 3 measures it. Truncating first would size its budget against bytes that were about to be discarded anyway, and trim history that did not need trimming. The same goes for stage 2b: a stripped tools array must not count against the truncation budget.

3 before 4. Stage 3 measures the payload; stage 4 inserts up to seven sampling keys. Resolving sampling first would have truncation size its budget against keys the client never sent. The margin is small, but it is the difference between measuring the conversation and measuring our own additions to it.

4 before 5. cache_prompt is not an InferenceConfig field, so pinning it last means the resolved sampling patch can never overwrite it.

5b after 4. Stage 5b deletes a reasoning_effort the model’s observed template never reads, and the value it has to catch is usually not the client’s. It arrives from the ladder — a :high profile, a per-model default, a global setting — which does not exist until stage 4 has folded it. Placed at 2b beside the tool strip, where the capability shape is otherwise identical, the gate would delete the client’s key and stage 4 would then force-insert gglib’s own resolved level straight past it (the patch is inserted, not merged), so the case that matters most would sail through untouched while the tests still passed on a client-sent level. The stage runs after 4 for the same reason it takes &mut SamplingDecision: it can only suppress a value once something has resolved one, and it must correct that decision’s own record when it does. a_ladder_supplied_effort_is_suppressed_not_just_a_client_one fails if this ever moves.

6 after 3. The grammar stage 6 adds a top-level key, so it runs after the measurement for the same reason sampling does: the truncation budget measures the client’s conversation, not our own additions to it.

§Why the seam is &mut Value and not a typed request struct

The proxy forwards requests from arbitrary external clients — IDE extensions, gateways — which send OpenAI parameters this workspace has never heard of. Round-tripping through a typed ChatRequest would silently drop every field the struct does not model: a passthrough regression that is invisible in tests and painful in the field. Mutating a Value in place preserves them by construction. The adapter builds its body with json! and already holds a Value, so this is also the cheaper side for it.

§One pipeline, two callers, no second route

Every request path calls apply. The proxy used to run the stages by hand with its own truncation pass spliced between them, because truncation gated on the payload’s size in wire bytes and could reject the request with an axum response — neither of which fits here. Measuring the serialized Value and returning a domain error removed both obstacles, so there is now exactly one implementation of the order above and nothing to keep in sync.

Structs§

PipelineReport
What the pipeline did, for the caller that has to report or verify it.

Functions§

apply
Apply every request-shaping transform, in order, in place.