fix(proxy): plumb CompressionPolicy through proxy + dispatchers (F2.1 c2/6)

Phase F2.1, commit 2 of 6. No behaviour change — wiring only.

Three things land:

1. **Proxy entry derives the policy alongside auth_mode** (proxy.rs).
   Both go into `req.extensions_mut()` so downstream stages can read
   either without re-classifying. New structured log event
   `policy_selected` fires once per request with auth_mode +
   live_zone_only + cache_aligner_enabled — gives F2.2 the bake-time
   data it'll need to tune. Pure addition; no existing log was
   removed.

2. **OpenAI live-zone dispatchers stop hard-coding `AuthMode::Payg`.**
   `compress_openai_chat_request` and `compress_openai_responses_request`
   already received `auth_mode` from the proxy caller (F1's plumbing),
   but the dispatcher invocation hard-coded `AuthMode::Payg` and
   ignored the parameter — c.f. the `_auth_mode` underscore-prefixed
   identifier in `compress_openai_chat_live_zone` upstream. Now the
   classified mode is forwarded via `auth_mode.into()` (uses the
   `From<>` impl added in c1/6 to bridge the two `AuthMode` enums).

3. **Anthropic live-zone dispatcher gets the same fix.**
   `compress_anthropic_request` was the closest to right — it threaded
   auth_mode all the way to `compress_anthropic_live_zone` already —
   but the inner call still had `AuthMode::Payg` hard-coded. Now it
   uses `auth_mode.into()` for symmetry and so the F2.1 dispatcher-
   level gate (added in c4/6) reads the real mode.

The dispatcher is unchanged in F2.1: it still runs the same
compression for every mode. The point of c2/6 is that *if* a future
commit (c4/6 in this PR, or anything post-F2.1) gates on the
mode-aware policy, the wiring is already in place. PAYG behaviour is
byte-for-byte identical because every mode currently dispatches
identically.

Two tiny cleanups: drop the `AuthMode` import from the three live-zone
dispatcher files now that the value flows in via `.into()` (the
`RequestAuthMode` alias remains, since it's still in the function
signatures).

Behaviour-change risk in c2/6: zero. Verified by running the full
`cargo test -p headroom-proxy` suite; existing tests pass without
modification because they're already shape-compatible (tests pass
`AuthMode::Payg` directly into the inner dispatcher; this commit
only changes how the *outer* compress_* fns invoke that dispatcher).
This commit is contained in:
chopratejas 2026-05-05 16:39:34 -07:00
parent 8376630062
commit 948c8f2069
4 changed files with 43 additions and 6 deletions

View file

@ -42,7 +42,7 @@ use bytes::Bytes;
use headroom_core::auth_mode::AuthMode as RequestAuthMode;
use headroom_core::transforms::live_zone::DEFAULT_MODEL;
use headroom_core::transforms::{
compress_anthropic_live_zone, AuthMode, BlockAction, ExclusionReason, LiveZoneError,
compress_anthropic_live_zone, BlockAction, ExclusionReason, LiveZoneError,
LiveZoneOutcome,
};
use serde_json::Value;
@ -326,7 +326,14 @@ pub fn compress_anthropic_request(
// `NoChange` otherwise (live zone empty, every compressor
// declined, or every compressor produced output whose token
// count was not strictly less than the input's).
match compress_anthropic_live_zone(&dispatch_body, frozen_count, AuthMode::Payg, model) {
// F2.1 c2/6: forward F1's classified auth_mode into the dispatcher
// instead of the hard-coded `Payg`. The dispatcher itself doesn't
// change behaviour by mode in F2.1 (live-zone compression runs for
// every mode — closing #327/#388 means subscription users keep
// getting compression, not losing it). The plumbing here lets
// F2.2 vary per-block thresholds by mode without touching this
// call site again.
match compress_anthropic_live_zone(&dispatch_body, frozen_count, auth_mode.into(), model) {
Ok(LiveZoneOutcome::NoChange { manifest }) => {
let block_count = manifest.block_outcomes.len();
let blocks_excluded = manifest

View file

@ -32,7 +32,7 @@ use bytes::Bytes;
use headroom_core::auth_mode::AuthMode as RequestAuthMode;
use headroom_core::transforms::live_zone::DEFAULT_MODEL;
use headroom_core::transforms::{
compress_openai_chat_live_zone, AuthMode, BlockAction, LiveZoneError, LiveZoneOutcome,
compress_openai_chat_live_zone, BlockAction, LiveZoneError, LiveZoneOutcome,
};
use serde_json::Value;
@ -132,7 +132,10 @@ pub fn compress_openai_chat_request(
let (dispatch_body, normalization_applied) =
normalize_tool_definitions_openai_chat(body, &parsed, auth_mode, request_id);
match compress_openai_chat_live_zone(&dispatch_body, AuthMode::Payg, model) {
// F2.1 c2/6: forward F1's classified auth_mode into the dispatcher
// instead of the hard-coded `Payg`. See live_zone_anthropic.rs for
// the rationale — same wiring on the OpenAI chat path.
match compress_openai_chat_live_zone(&dispatch_body, auth_mode.into(), model) {
Ok(LiveZoneOutcome::NoChange { manifest }) => {
tracing::info!(
event = "compression_decision",

View file

@ -35,7 +35,7 @@ use bytes::Bytes;
use headroom_core::auth_mode::AuthMode as RequestAuthMode;
use headroom_core::transforms::live_zone::DEFAULT_MODEL;
use headroom_core::transforms::{
compress_openai_responses_live_zone, AuthMode, BlockAction, LiveZoneError, LiveZoneOutcome,
compress_openai_responses_live_zone, BlockAction, LiveZoneError, LiveZoneOutcome,
};
use serde_json::Value;
@ -141,7 +141,10 @@ pub fn compress_openai_responses_request(
let (dispatch_body, normalization_applied) =
normalize_tool_definitions_responses(body, &parsed, auth_mode, request_id);
match compress_openai_responses_live_zone(&dispatch_body, AuthMode::Payg, model) {
// F2.1 c2/6: forward F1's classified auth_mode into the dispatcher
// instead of the hard-coded `Payg`. See live_zone_anthropic.rs for
// the rationale — same wiring on the OpenAI Responses path.
match compress_openai_responses_live_zone(&dispatch_body, auth_mode.into(), model) {
Ok(LiveZoneOutcome::NoChange { manifest }) => {
tracing::info!(
event = "compression_decision",

View file

@ -31,6 +31,7 @@ use crate::websocket::ws_handler;
// path for downstream handlers that read the value back out of
// `req.extensions()` (Phase F PR-F2/F3/F4).
use headroom_core::auth_mode::{classify as classify_auth_mode, AuthMode};
use headroom_core::compression_policy::CompressionPolicy;
/// Shared state passed to every handler.
///
@ -390,6 +391,17 @@ pub(crate) async fn forward_http(
let auth_mode = classify_auth_mode(req.headers());
req.extensions_mut().insert(auth_mode);
// Phase F PR-F2.1, c2/6: derive the per-mode CompressionPolicy at
// request entry and stash alongside auth_mode. Storing the policy
// (not just auth_mode) in extensions lets downstream stages read
// the gate they need directly — no per-stage `for_mode` call.
// F2.1 c2/6 only writes the policy; the dispatcher gates on
// `policy.live_zone_compression_enabled()` (currently always true,
// so no behaviour change). c4/6 flips the gate to read
// `live_zone_only` and `cache_aligner_enabled` for real.
let policy = CompressionPolicy::for_mode(auth_mode);
req.extensions_mut().insert(policy);
// Per PR-A1: structured entry log. The `auth_mode` field is now
// populated with the real classification result (Phase F PR-F1
// replaces the prior `auth_mode_placeholder = "unknown"`). Body
@ -406,6 +418,18 @@ pub(crate) async fn forward_http(
"request received"
);
// F2.1 c2/6: emit the policy that the request will run under so
// F2.2 has bake-time data to tune from. One log per request,
// structured fields so it joins on auth_mode + request_id.
tracing::debug!(
event = "policy_selected",
request_id = %request_id,
auth_mode = auth_mode.as_str(),
live_zone_only = policy.live_zone_only,
cache_aligner_enabled = policy.cache_aligner_enabled,
"compression policy resolved"
);
let upstream_url = build_upstream_url(&state.config.upstream, &uri)?;
// Forwarded-Host: prefer client's Host. Forwarded-Proto: assume http for