mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
Merge pull request #403 from chopratejas/realign-F2_2-policy-tuning
fix(proxy): F2.2 — per-mode CompressionPolicy tuning fields
This commit is contained in:
commit
294df2b894
8 changed files with 770 additions and 57 deletions
|
|
@ -1,9 +1,11 @@
|
|||
//! Per-auth-mode compression policy — Phase F PR-F2.1.
|
||||
//! Per-auth-mode compression policy — Phase F PR-F2.1, extended in F2.2.
|
||||
//!
|
||||
//! F1 (`auth_mode.rs`) classifies each inbound request into one of
|
||||
//! `{Payg, OAuth, Subscription}`. Phase F2.1 turns that classification
|
||||
//! into a `CompressionPolicy` that downstream pipeline stages read to
|
||||
//! decide whether they run.
|
||||
//! decide whether they run. F2.2 extends the same struct with per-mode
|
||||
//! tuning fields so the same call sites also read *how aggressively*
|
||||
//! to run.
|
||||
//!
|
||||
//! Why a struct instead of `match auth_mode { ... }` everywhere?
|
||||
//! Two reasons:
|
||||
|
|
@ -22,13 +24,10 @@
|
|||
//! end-to-end behaviour against the dispatcher requires a full
|
||||
//! request fixture.
|
||||
//!
|
||||
//! Phase F2.2 will add fields to this struct (per-mode volatile
|
||||
//! threshold, per-mode max lossy ratio, per-mode TOIN read-only flag).
|
||||
//! F2.1 keeps the field set minimal — only the two flags load-bearing
|
||||
//! for closing the cache-instability complaints in issues #327 / #388.
|
||||
//!
|
||||
//! ## Field semantics
|
||||
//!
|
||||
//! ### F2.1 fields (load-bearing for closing #327 / #388)
|
||||
//!
|
||||
//! - **`live_zone_only`**: when `true`, downstream stages MUST NOT
|
||||
//! modify bytes outside the post-cache-marker live zone. Phase B's
|
||||
//! Rust dispatcher is *already* live-zone-only by construction, so
|
||||
|
|
@ -46,15 +45,47 @@
|
|||
//! which is what destabilised Subscription users' prompt caches.
|
||||
//! Disabling it for Subscription is the user-visible win of F2.1.
|
||||
//!
|
||||
//! ## Per-mode F2.1 values
|
||||
//! ### F2.2 tuning fields (CONSERVATIVE defaults pending bake telemetry)
|
||||
//!
|
||||
//! | Mode | live_zone_only | cache_aligner_enabled |
|
||||
//! |--------------|----------------|-----------------------|
|
||||
//! | Payg | false | true |
|
||||
//! | OAuth | false | true (= PAYG today) |
|
||||
//! | Subscription | true | false |
|
||||
//! - **`volatile_token_threshold`**: per-mode token-count threshold
|
||||
//! below which content is treated as cache-stable (i.e. not flagged
|
||||
//! as volatile). Subscription is conservative (low threshold → flag
|
||||
//! more aggressively → keep prompts stable) while PAYG is aggressive
|
||||
//! (higher threshold → tolerate more volatile noise before warning).
|
||||
//! F2.1 had no such threshold; F2.2 introduces the field plumbed
|
||||
//! through the struct so future detector code can pick it up. NOTE:
|
||||
//! no current detector consumes this value — it lands plumbed-but-
|
||||
//! unconsumed in F2.2 (intentional; the volatile detector in
|
||||
//! `cache_aligner.py` is shape-based, not token-count-based, and
|
||||
//! wiring it would force a detector refactor outside F2.2 scope).
|
||||
//!
|
||||
//! OAuth starts identical to PAYG. F2.2 will divide them once
|
||||
//! - **`max_lossy_ratio`**: per-mode upper bound on how aggressive
|
||||
//! lossy compression can be, expressed as the fraction of original
|
||||
//! tokens that may be dropped (`0.0` = no lossy compression allowed,
|
||||
//! `1.0` = unlimited). Subscription is conservative (`0.25`) so cache
|
||||
//! prefixes stay stable, PAYG aggressive (`0.45`). NOTE: no current
|
||||
//! compressor consumes this value — it lands plumbed-but-unconsumed
|
||||
//! in F2.2 (the `target_ratio` runtime kwarg in `content_router.py`
|
||||
//! is a separate, caller-driven knob; wiring `max_lossy_ratio` as a
|
||||
//! policy-driven cap is F2.2-followup once telemetry decides whether
|
||||
//! to gate lossy paths or just observe them).
|
||||
//!
|
||||
//! - **`toin_read_only`**: when `true`, TOIN serves cached
|
||||
//! recommendations but never *writes* new pattern observations from
|
||||
//! this request. Subscription requests pay for prompt-cache stability,
|
||||
//! so we don't want their compression events to mutate the global
|
||||
//! learning pool — consistency over learning. PAYG/OAuth still write
|
||||
//! so the network effect keeps growing.
|
||||
//!
|
||||
//! ## Per-mode F2.2 values (CONSERVATIVE; F2.2-followup will tune)
|
||||
//!
|
||||
//! | Mode | live_zone_only | cache_aligner_enabled | volatile_token_threshold | max_lossy_ratio | toin_read_only |
|
||||
//! |--------------|----------------|-----------------------|--------------------------|-----------------|----------------|
|
||||
//! | Payg | false | true | 128 | 0.45 | false |
|
||||
//! | OAuth | false | true (= PAYG today) | 128 (= PAYG today) | 0.45 (= PAYG) | false (= PAYG) |
|
||||
//! | Subscription | true | false | 32 | 0.25 | true |
|
||||
//!
|
||||
//! OAuth starts identical to PAYG. F2.2-followup will divide them once
|
||||
//! telemetry from F2.1's bake on `main` shows what each mode actually
|
||||
//! costs / saves.
|
||||
//!
|
||||
|
|
@ -69,12 +100,42 @@
|
|||
|
||||
use crate::auth_mode::AuthMode;
|
||||
|
||||
// ── F2.2 per-mode default values (CONSERVATIVE pending bake telemetry) ──
|
||||
// Centralised constants instead of inlining in the match arms so a
|
||||
// follow-up tune lands in one place. Each constant is `pub(crate)` so
|
||||
// the unit tests can assert against the same source of truth — if a
|
||||
// caller drifts the per-mode value, the assertion fails.
|
||||
//
|
||||
// Per the realignment build constraints (project memory
|
||||
// `feedback_realignment_build_constraints.md`): "configurable / no
|
||||
// hardcoded values". The configuration *is* the per-mode default — we
|
||||
// deliberately do NOT add a separate env var per field. Operators tune
|
||||
// by editing these constants and shipping a new build, which is the
|
||||
// same pattern the F2.1 fields use.
|
||||
|
||||
/// PAYG: aggressive — let volatile content noise up to ~128 tokens slip
|
||||
/// before flagging. Higher than Subscription because PAYG users opt in
|
||||
/// to aggressive compression.
|
||||
pub(crate) const VOLATILE_TOKEN_THRESHOLD_PAYG: u32 = 128;
|
||||
|
||||
/// Subscription: conservative — flag volatile content earlier (32
|
||||
/// tokens) so cache prefixes stay stable.
|
||||
pub(crate) const VOLATILE_TOKEN_THRESHOLD_SUBSCRIPTION: u32 = 32;
|
||||
|
||||
/// PAYG: cap lossy compression at 45% of original tokens. Aggressive
|
||||
/// but bounded — F2.1 had no cap (effectively `1.0`), F2.2 introduces
|
||||
/// one.
|
||||
pub(crate) const MAX_LOSSY_RATIO_PAYG: f32 = 0.45;
|
||||
|
||||
/// Subscription: conservative cap at 25%. Cache stability over savings.
|
||||
pub(crate) const MAX_LOSSY_RATIO_SUBSCRIPTION: f32 = 0.25;
|
||||
|
||||
/// Per-auth-mode policy that downstream compression stages consult.
|
||||
///
|
||||
/// `Copy` because the struct is two `bool`s — passing by value is
|
||||
/// cheaper than passing a reference and the call sites all want
|
||||
/// owned copies anyway.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// `Copy` because the struct is small POD (two `bool`s + a `u32` + an
|
||||
/// `f32` + a `bool`) — passing by value is cheaper than passing a
|
||||
/// reference and the call sites all want owned copies anyway.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CompressionPolicy {
|
||||
/// When `true`, transforms MUST NOT modify bytes outside the
|
||||
/// post-cache-marker live zone. See module docs.
|
||||
|
|
@ -83,29 +144,73 @@ pub struct CompressionPolicy {
|
|||
/// When `false`, the `CacheAligner` transform MUST be skipped.
|
||||
/// See module docs.
|
||||
pub cache_aligner_enabled: bool,
|
||||
|
||||
/// F2.2: per-mode threshold (in tokens) below which content is
|
||||
/// treated as cache-stable. Subscription is conservative
|
||||
/// (`32`); PAYG aggressive (`128`). See module docs.
|
||||
///
|
||||
/// NOT consumed by any detector in F2.2 — plumbed through the
|
||||
/// struct so the volatile detector refactor in a follow-up PR
|
||||
/// has a stable hook to read from.
|
||||
pub volatile_token_threshold: u32,
|
||||
|
||||
/// F2.2: per-mode upper bound on lossy compression aggressiveness,
|
||||
/// expressed as the fraction of original tokens that may be
|
||||
/// dropped (`0.0`–`1.0`). Subscription `0.25`, PAYG `0.45`.
|
||||
/// See module docs.
|
||||
///
|
||||
/// NOT consumed by any compressor in F2.2 — plumbed through the
|
||||
/// struct as a stable hook for a follow-up PR that gates lossy
|
||||
/// paths on the cap. Distinct from the caller-driven
|
||||
/// `target_ratio` kwarg in the Python ContentRouter.
|
||||
pub max_lossy_ratio: f32,
|
||||
|
||||
/// F2.2: when `true`, TOIN serves cached recommendations but
|
||||
/// never writes new pattern observations from this request.
|
||||
/// Subscription `true` (consistency over learning), PAYG/OAuth
|
||||
/// `false` (network effect keeps growing).
|
||||
pub toin_read_only: bool,
|
||||
}
|
||||
|
||||
// `f32` doesn't impl `Eq`, so the derived `Eq` would be invalid. Two
|
||||
// `f32`s in this struct are never NaN by construction (we only set
|
||||
// them from finite literal constants), so `PartialEq` is sufficient.
|
||||
// The unit tests assert structural equality via `assert_eq!`.
|
||||
|
||||
impl CompressionPolicy {
|
||||
/// Resolve the F2.1 policy for an auth mode. See module docs for
|
||||
/// per-mode rationale.
|
||||
/// Resolve the F2.1+F2.2 policy for an auth mode. See module docs
|
||||
/// for per-mode rationale.
|
||||
pub fn for_mode(mode: AuthMode) -> Self {
|
||||
match mode {
|
||||
AuthMode::Payg => Self {
|
||||
live_zone_only: false,
|
||||
cache_aligner_enabled: true,
|
||||
volatile_token_threshold: VOLATILE_TOKEN_THRESHOLD_PAYG,
|
||||
max_lossy_ratio: MAX_LOSSY_RATIO_PAYG,
|
||||
toin_read_only: false,
|
||||
},
|
||||
// OAuth identical to PAYG in F2.1. F2.2 may diverge once
|
||||
// telemetry shows what OAuth users actually need.
|
||||
// OAuth identical to PAYG in F2.1+F2.2. F2.2-followup may
|
||||
// diverge once telemetry shows what OAuth users actually
|
||||
// need.
|
||||
AuthMode::OAuth => Self {
|
||||
live_zone_only: false,
|
||||
cache_aligner_enabled: true,
|
||||
volatile_token_threshold: VOLATILE_TOKEN_THRESHOLD_PAYG,
|
||||
max_lossy_ratio: MAX_LOSSY_RATIO_PAYG,
|
||||
toin_read_only: false,
|
||||
},
|
||||
// The user-visible win of F2.1: subscription users stop
|
||||
// seeing cache instability because CacheAligner no longer
|
||||
// touches their prefix.
|
||||
// touches their prefix. F2.2 extends that protection: the
|
||||
// volatile threshold is tighter, the lossy cap is lower,
|
||||
// and TOIN won't mutate the learning pool from these
|
||||
// requests.
|
||||
AuthMode::Subscription => Self {
|
||||
live_zone_only: true,
|
||||
cache_aligner_enabled: false,
|
||||
volatile_token_threshold: VOLATILE_TOKEN_THRESHOLD_SUBSCRIPTION,
|
||||
max_lossy_ratio: MAX_LOSSY_RATIO_SUBSCRIPTION,
|
||||
toin_read_only: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -133,15 +238,38 @@ mod tests {
|
|||
assert!(p.live_zone_compression_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payg_tuning_fields_aggressive() {
|
||||
// F2.2: per-mode tuning fields. PAYG values are the aggressive
|
||||
// end of the conservative-defaults spectrum — F2.2-followup may
|
||||
// raise them once bake telemetry confirms savings.
|
||||
let p = CompressionPolicy::for_mode(AuthMode::Payg);
|
||||
assert_eq!(
|
||||
p.volatile_token_threshold, 128,
|
||||
"PAYG volatile threshold is the relaxed default; F2.2-followup will tune"
|
||||
);
|
||||
assert!(
|
||||
(p.max_lossy_ratio - 0.45).abs() < f32::EPSILON,
|
||||
"PAYG max_lossy_ratio caps lossy paths at 0.45; F2.2-followup will tune"
|
||||
);
|
||||
assert!(
|
||||
!p.toin_read_only,
|
||||
"PAYG keeps TOIN write-enabled — network effect feeds on PAYG traffic"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_matches_payg_today() {
|
||||
// Canary: when F2.2 diverges OAuth from PAYG, this test fails
|
||||
// and forces a deliberate update — which is the point.
|
||||
// Canary: when F2.2-followup diverges OAuth from PAYG, this test
|
||||
// fails and forces a deliberate update — which is the point.
|
||||
// Covers ALL fields (F2.1 + F2.2) so a future field-level
|
||||
// divergence (e.g. OAuth gets stricter `max_lossy_ratio` than
|
||||
// PAYG) trips the assertion just as loudly as a flag flip.
|
||||
let oauth = CompressionPolicy::for_mode(AuthMode::OAuth);
|
||||
let payg = CompressionPolicy::for_mode(AuthMode::Payg);
|
||||
assert_eq!(
|
||||
oauth, payg,
|
||||
"F2.1 ships OAuth=PAYG; F2.2 will diverge based on telemetry"
|
||||
"F2.1+F2.2 ship OAuth=PAYG; F2.2-followup will diverge based on telemetry"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -158,4 +286,40 @@ mod tests {
|
|||
"Subscription still gets live-zone compression — closing the cache complaint must NOT mean shipping zero compression"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subscription_tuning_fields_conservative() {
|
||||
// F2.2: per-mode tuning fields. Subscription is the conservative
|
||||
// end — tighter threshold, lower lossy cap, TOIN read-only — so
|
||||
// cache prefixes stay stable and the learning pool isn't
|
||||
// mutated from cache-stability-sensitive traffic.
|
||||
let p = CompressionPolicy::for_mode(AuthMode::Subscription);
|
||||
assert_eq!(
|
||||
p.volatile_token_threshold, 32,
|
||||
"Subscription volatile threshold flags content earlier (cache stability)"
|
||||
);
|
||||
assert!(
|
||||
(p.max_lossy_ratio - 0.25).abs() < f32::EPSILON,
|
||||
"Subscription max_lossy_ratio caps lossy paths at 0.25 (conservative)"
|
||||
);
|
||||
assert!(
|
||||
p.toin_read_only,
|
||||
"Subscription MUST be TOIN read-only — load-bearing for keeping the learning pool consistent across cache-sensitive traffic"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_lossy_ratio_in_unit_interval() {
|
||||
// Defensive: every per-mode `max_lossy_ratio` MUST be in `[0.0,
|
||||
// 1.0]` because it expresses a fraction. A tune that drifts
|
||||
// outside the unit interval is a bug — catch it cheaply here
|
||||
// rather than at the eventual consumer site.
|
||||
for mode in [AuthMode::Payg, AuthMode::OAuth, AuthMode::Subscription] {
|
||||
let r = CompressionPolicy::for_mode(mode).max_lossy_ratio;
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&r),
|
||||
"max_lossy_ratio for {mode:?} = {r} is outside [0.0, 1.0]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -435,6 +435,13 @@ pub(crate) async fn forward_http(
|
|||
// c3/6 adds `enforcement` so the dashboard can split "policy
|
||||
// resolved as PAYG because mode is PAYG" from "policy resolved as
|
||||
// PAYG because the enforcement flag is off."
|
||||
//
|
||||
// F2.2 c2/3: extend the structured fields with the three new
|
||||
// tuning fields so the bake dashboard has per-mode observability
|
||||
// for the F2.2-followup tune. ``volatile_token_threshold`` /
|
||||
// ``max_lossy_ratio`` are plumbed-but-unconsumed today, so the
|
||||
// log lines are the only signal that the values are flowing
|
||||
// correctly through the proxy → handlers → transforms path.
|
||||
tracing::debug!(
|
||||
event = "policy_selected",
|
||||
request_id = %request_id,
|
||||
|
|
@ -442,6 +449,9 @@ pub(crate) async fn forward_http(
|
|||
enforcement = state.config.auth_mode_policy_enforcement.as_str(),
|
||||
live_zone_only = policy.live_zone_only,
|
||||
cache_aligner_enabled = policy.cache_aligner_enabled,
|
||||
volatile_token_threshold = policy.volatile_token_threshold,
|
||||
max_lossy_ratio = policy.max_lossy_ratio,
|
||||
toin_read_only = policy.toin_read_only,
|
||||
"compression policy resolved"
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Per-auth-mode compression policy — Phase F PR-F2.1, Python parity.
|
||||
"""Per-auth-mode compression policy — Phase F PR-F2.1, extended in F2.2 (Python parity).
|
||||
|
||||
Hand-mirrored port of `headroom_core::compression_policy::CompressionPolicy`
|
||||
(Rust). The Rust crate is the source of truth; this module exists so
|
||||
|
|
@ -10,10 +10,10 @@ A parity test (`tests/test_compression_policy.py`) instantiates one of
|
|||
each variant and asserts the field map matches what the Rust unit
|
||||
tests assert. F2.2 should consider exposing the Rust struct via PyO3
|
||||
to retire this hand-mirror — that's deliberately out of scope here so
|
||||
F2.1 can ship.
|
||||
F2.1/F2.2 can ship.
|
||||
|
||||
See `crates/headroom-core/src/compression_policy.rs` for the canonical
|
||||
docstring.
|
||||
docstring (per-mode rationale, why-a-struct, etc.).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -23,15 +23,58 @@ from dataclasses import dataclass
|
|||
|
||||
from headroom.proxy.auth_mode import AuthMode
|
||||
|
||||
# ── F2.2 per-mode default values (CONSERVATIVE pending bake telemetry) ──
|
||||
# Mirrors the Rust ``pub(crate) const`` block in
|
||||
# ``crates/headroom-core/src/compression_policy.rs``. Centralised here so
|
||||
# a follow-up tune lands in one place per language. The Rust tests assert
|
||||
# the values directly; the Python parity tests assert the *fields* match
|
||||
# (not the values — that would double-pin against drift in a way that
|
||||
# masks real divergence).
|
||||
#
|
||||
# Per the realignment build constraints (project memory
|
||||
# ``feedback_realignment_build_constraints.md``): "configurable / no
|
||||
# hardcoded values". The configuration *is* the per-mode default — we
|
||||
# deliberately do NOT add a separate env var per field. Operators tune
|
||||
# by editing these constants and shipping a new build, mirroring the
|
||||
# Rust pattern exactly.
|
||||
|
||||
#: PAYG: aggressive — let volatile content noise up to ~128 tokens slip
|
||||
#: before flagging. Higher than Subscription because PAYG users opt in
|
||||
#: to aggressive compression.
|
||||
_VOLATILE_TOKEN_THRESHOLD_PAYG: int = 128
|
||||
|
||||
#: Subscription: conservative — flag volatile content earlier (32
|
||||
#: tokens) so cache prefixes stay stable.
|
||||
_VOLATILE_TOKEN_THRESHOLD_SUBSCRIPTION: int = 32
|
||||
|
||||
#: PAYG: cap lossy compression at 45% of original tokens. Aggressive
|
||||
#: but bounded — F2.1 had no cap (effectively ``1.0``), F2.2 introduces one.
|
||||
_MAX_LOSSY_RATIO_PAYG: float = 0.45
|
||||
|
||||
#: Subscription: conservative cap at 25%. Cache stability over savings.
|
||||
_MAX_LOSSY_RATIO_SUBSCRIPTION: float = 0.25
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CompressionPolicy:
|
||||
"""Per-auth-mode policy that downstream compression stages consult.
|
||||
|
||||
Two fields in F2.1 — `live_zone_only` and `cache_aligner_enabled`.
|
||||
F2.2 will add tuning fields (per-mode volatile threshold,
|
||||
max-lossy-ratio cap, per-mode TOIN read-only flag) once telemetry
|
||||
from F2.1's bake on `main` shows what each mode actually costs/saves.
|
||||
Five fields after F2.2:
|
||||
|
||||
- ``live_zone_only`` (F2.1) — gate for the Python TransformPipeline
|
||||
to skip pre-cache-marker mutation.
|
||||
- ``cache_aligner_enabled`` (F2.1) — gate for the Python
|
||||
``CacheAligner`` transform.
|
||||
- ``volatile_token_threshold`` (F2.2) — per-mode token threshold
|
||||
below which content is treated as cache-stable. Plumbed through
|
||||
the struct; no current detector consumes it (intentional — the
|
||||
detector refactor is a follow-up).
|
||||
- ``max_lossy_ratio`` (F2.2) — per-mode upper bound on lossy
|
||||
compression aggressiveness (fraction in ``[0.0, 1.0]``). Plumbed
|
||||
through the struct; no current compressor consumes it.
|
||||
- ``toin_read_only`` (F2.2) — TOIN learning gate. ``True`` =
|
||||
serve cached patterns but never write new observations from
|
||||
this request (Subscription).
|
||||
"""
|
||||
|
||||
live_zone_only: bool
|
||||
|
|
@ -46,33 +89,76 @@ class CompressionPolicy:
|
|||
return False — that's the load-bearing F2.1 gate for the cache-
|
||||
instability complaints in #327 / #388."""
|
||||
|
||||
volatile_token_threshold: int
|
||||
"""F2.2: per-mode token-count threshold below which content is
|
||||
treated as cache-stable. Subscription is conservative
|
||||
(low → flag aggressively → keep prompts stable); PAYG aggressive
|
||||
(high → tolerate more volatile noise). Plumbed but unconsumed in
|
||||
F2.2 — the volatile detector in ``cache_aligner.py`` is shape-
|
||||
based, not token-count-based; wiring it is a follow-up."""
|
||||
|
||||
max_lossy_ratio: float
|
||||
"""F2.2: per-mode upper bound on lossy compression aggressiveness,
|
||||
expressed as the fraction of original tokens that may be dropped
|
||||
(``0.0`` = no lossy, ``1.0`` = unlimited). Subscription ``0.25``;
|
||||
PAYG ``0.45``. Plumbed but unconsumed in F2.2 — distinct from the
|
||||
caller-driven ``target_ratio`` kwarg in the Python ContentRouter."""
|
||||
|
||||
toin_read_only: bool
|
||||
"""F2.2: when True, TOIN serves cached recommendations but
|
||||
never writes new pattern observations from this request.
|
||||
Subscription True (consistency over learning); PAYG/OAuth False
|
||||
(network effect keeps growing). The gate is read by
|
||||
``smart_crusher.py`` and ``content_router.py`` at the
|
||||
``record_compression`` call sites."""
|
||||
|
||||
|
||||
def policy_for_mode(mode: AuthMode) -> CompressionPolicy:
|
||||
"""Resolve the F2.1 policy for an auth mode.
|
||||
"""Resolve the F2.1+F2.2 policy for an auth mode.
|
||||
|
||||
PAYG and OAuth are identical in F2.1 (aggressive: live-zone-not-
|
||||
only, cache-aligner on). Subscription is the user-visible win:
|
||||
live-zone-only with cache aligner disabled.
|
||||
PAYG and OAuth are identical (aggressive: live-zone-not-only,
|
||||
cache-aligner on, relaxed thresholds, TOIN write-enabled).
|
||||
Subscription is the user-visible win: live-zone-only with cache
|
||||
aligner disabled, tighter thresholds, TOIN read-only.
|
||||
|
||||
F2.2 may diverge OAuth from PAYG once telemetry is collected.
|
||||
F2.2-followup may diverge OAuth from PAYG once telemetry is
|
||||
collected.
|
||||
"""
|
||||
if mode == AuthMode.PAYG:
|
||||
return CompressionPolicy(live_zone_only=False, cache_aligner_enabled=True)
|
||||
return CompressionPolicy(
|
||||
live_zone_only=False,
|
||||
cache_aligner_enabled=True,
|
||||
volatile_token_threshold=_VOLATILE_TOKEN_THRESHOLD_PAYG,
|
||||
max_lossy_ratio=_MAX_LOSSY_RATIO_PAYG,
|
||||
toin_read_only=False,
|
||||
)
|
||||
if mode == AuthMode.OAUTH:
|
||||
# Identical to PAYG in F2.1. The parity test in
|
||||
# `tests/test_compression_policy.py` is the canary that
|
||||
# catches a future divergence and forces a deliberate
|
||||
# update there + in the Rust crate.
|
||||
return CompressionPolicy(live_zone_only=False, cache_aligner_enabled=True)
|
||||
# Identical to PAYG in F2.1/F2.2. The parity test in
|
||||
# ``tests/test_compression_policy.py`` is the canary that
|
||||
# catches a future divergence and forces a deliberate update
|
||||
# there + in the Rust crate.
|
||||
return CompressionPolicy(
|
||||
live_zone_only=False,
|
||||
cache_aligner_enabled=True,
|
||||
volatile_token_threshold=_VOLATILE_TOKEN_THRESHOLD_PAYG,
|
||||
max_lossy_ratio=_MAX_LOSSY_RATIO_PAYG,
|
||||
toin_read_only=False,
|
||||
)
|
||||
if mode == AuthMode.SUBSCRIPTION:
|
||||
return CompressionPolicy(live_zone_only=True, cache_aligner_enabled=False)
|
||||
return CompressionPolicy(
|
||||
live_zone_only=True,
|
||||
cache_aligner_enabled=False,
|
||||
volatile_token_threshold=_VOLATILE_TOKEN_THRESHOLD_SUBSCRIPTION,
|
||||
max_lossy_ratio=_MAX_LOSSY_RATIO_SUBSCRIPTION,
|
||||
toin_read_only=True,
|
||||
)
|
||||
raise ValueError(f"Unhandled AuthMode variant: {mode!r}")
|
||||
|
||||
|
||||
def policy_default_payg() -> CompressionPolicy:
|
||||
"""The PAYG-equivalent policy used when the
|
||||
``HEADROOM_PROXY_AUTH_MODE_POLICY_ENFORCEMENT`` flag is disabled
|
||||
(default in F2.1 c1-c4; flips to enabled in c5/5).
|
||||
(default in F2.1 c1-c4; flipped to enabled in c5/5).
|
||||
|
||||
Centralised so the proxy handlers do not duplicate the constant,
|
||||
and so a future change to PAYG semantics propagates to both the
|
||||
|
|
|
|||
|
|
@ -743,6 +743,17 @@ class ContentRouter(Transform):
|
|||
# TOIN integration for cross-strategy learning
|
||||
self._toin: Any = None
|
||||
|
||||
# F2.2: per-request CompressionPolicy, set from
|
||||
# ``kwargs["compression_policy"]`` at the start of ``apply()``
|
||||
# and read by ``_record_to_toin`` to gate TOIN writes when
|
||||
# ``policy.toin_read_only`` is true (Subscription mode).
|
||||
# Defaults to ``None`` so direct ``compress()`` callers (e.g.
|
||||
# tests, hand-written pipelines that don't go through the
|
||||
# proxy) keep pre-F2.2 behaviour: TOIN writes are not gated.
|
||||
# Same pattern the existing ``_runtime_target_ratio`` /
|
||||
# ``_runtime_kompress_model`` fields below use.
|
||||
self._runtime_compression_policy: Any = None
|
||||
|
||||
self._cache = CompressionCache()
|
||||
|
||||
def _record_to_toin(
|
||||
|
|
@ -778,6 +789,22 @@ class ContentRouter(Transform):
|
|||
if original_tokens <= compressed_tokens:
|
||||
return
|
||||
|
||||
# F2.2 gate: when the active CompressionPolicy says
|
||||
# ``toin_read_only=True`` (Subscription auth mode), don't
|
||||
# mutate the TOIN learning pool from this request. Direct
|
||||
# ``compress()`` callers don't go through ``apply()`` and
|
||||
# have ``self._runtime_compression_policy is None`` — those
|
||||
# keep their pre-F2.2 write-enabled behaviour.
|
||||
policy = self._runtime_compression_policy
|
||||
if policy is not None and policy.toin_read_only:
|
||||
logger.debug(
|
||||
"ContentRouter: skipping TOIN record_compression for %s "
|
||||
"— policy.toin_read_only=True (auth_mode resolved as "
|
||||
"Subscription, F2.2 gate)",
|
||||
strategy.value,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
# Lazy load TOIN
|
||||
if self._toin is None:
|
||||
|
|
@ -1827,6 +1854,12 @@ class ContentRouter(Transform):
|
|||
# Store runtime options on self for access by _route_and_compress_block
|
||||
self._runtime_target_ratio: float | None = kwargs.get("target_ratio")
|
||||
self._runtime_kompress_model: str | None = kwargs.get("kompress_model")
|
||||
# F2.2: capture the per-request CompressionPolicy so
|
||||
# ``_record_to_toin`` can gate TOIN writes on
|
||||
# ``policy.toin_read_only``. ``None`` when the caller didn't
|
||||
# pass a policy — ``_record_to_toin`` treats that as "no gate"
|
||||
# to preserve pre-F2.2 behaviour for non-proxy callers.
|
||||
self._runtime_compression_policy = kwargs.get("compression_policy")
|
||||
|
||||
tokens_before = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages)
|
||||
context = kwargs.get("context", "")
|
||||
|
|
|
|||
|
|
@ -230,6 +230,18 @@ class SmartCrusher(Transform):
|
|||
self._toin: Any = None
|
||||
self._toin_load_failed = False
|
||||
|
||||
# F2.2: per-request CompressionPolicy, set from
|
||||
# ``kwargs["compression_policy"]`` at the start of ``apply()``
|
||||
# and read by ``_record_to_toin`` to gate TOIN writes when
|
||||
# ``policy.toin_read_only`` is true (Subscription mode).
|
||||
# Defaults to ``None`` so the direct ``crush()`` / ``crush_array_json()``
|
||||
# / ``compact_document_json()`` entry points (which don't go
|
||||
# through ``apply()``) keep their pre-F2.2 behaviour: TOIN
|
||||
# writes are not gated. Same pattern as the existing
|
||||
# ``_runtime_target_ratio`` / ``_runtime_kompress_model``
|
||||
# fields in ContentRouter.
|
||||
self._runtime_compression_policy: Any = None
|
||||
|
||||
# Build the Rust crusher with every field from the Python
|
||||
# config, plus the relevance_threshold default (0.3) — the
|
||||
# Python dataclass doesn't carry that field; it lives on
|
||||
|
|
@ -457,9 +469,29 @@ class SmartCrusher(Transform):
|
|||
implementation used. The router doesn't pass a tokenizer down
|
||||
this far, and re-tokenizing here would dominate the recording
|
||||
cost. Rough estimates are fine for learning aggregates.
|
||||
|
||||
F2.2: when the active ``CompressionPolicy`` (set by
|
||||
``apply()`` from ``kwargs["compression_policy"]``) has
|
||||
``toin_read_only=True``, the write is skipped — Subscription
|
||||
users keep prompt-cache stability AND don't mutate the global
|
||||
TOIN learning pool from cache-sensitive traffic. Direct
|
||||
``crush()`` / ``crush_array_json()`` callers don't set the
|
||||
policy, so they keep their pre-F2.2 write-enabled behaviour.
|
||||
"""
|
||||
if self._toin_load_failed:
|
||||
return
|
||||
# F2.2 gate. Read the per-request policy set by ``apply()``;
|
||||
# ``None`` means we are not running under the Transform
|
||||
# protocol (direct caller via ``crush()``) and the legacy
|
||||
# write-enabled behaviour applies.
|
||||
policy = self._runtime_compression_policy
|
||||
if policy is not None and policy.toin_read_only:
|
||||
logger.debug(
|
||||
"SmartCrusher: skipping TOIN record_compression — "
|
||||
"policy.toin_read_only=True (auth_mode resolved as "
|
||||
"Subscription, F2.2 gate)"
|
||||
)
|
||||
return
|
||||
try:
|
||||
try:
|
||||
items = json.loads(original)
|
||||
|
|
@ -782,6 +814,15 @@ class SmartCrusher(Transform):
|
|||
markers_inserted: list[str] = []
|
||||
warnings: list[str] = []
|
||||
|
||||
# F2.2: capture the per-request CompressionPolicy so
|
||||
# ``_record_to_toin`` can gate TOIN writes on
|
||||
# ``policy.toin_read_only``. Same one-liner pattern the
|
||||
# ContentRouter uses for ``_runtime_target_ratio``. ``None``
|
||||
# when the caller didn't pass a policy (e.g. legacy direct-
|
||||
# apply callers in tests) — ``_record_to_toin`` treats that
|
||||
# as "no gate", matching pre-F2.2 behaviour.
|
||||
self._runtime_compression_policy = kwargs.get("compression_policy")
|
||||
|
||||
query_context = self._extract_context_from_messages(result_messages)
|
||||
crushed_count = 0
|
||||
frozen_message_count = kwargs.get("frozen_message_count", 0)
|
||||
|
|
|
|||
|
|
@ -205,6 +205,12 @@ def test_should_apply_false_when_policy_disables_aligner(tokenizer: Tokenizer) -
|
|||
``self._previous_prefix_hash`` and emitting volatility warnings,
|
||||
which are the exact log lines #327/#388 reporters complained
|
||||
about.
|
||||
|
||||
F2.2 added three per-mode tuning fields to CompressionPolicy
|
||||
(``volatile_token_threshold``, ``max_lossy_ratio``,
|
||||
``toin_read_only``); the policy here uses the Subscription
|
||||
defaults from ``policy_for_mode(AuthMode.SUBSCRIPTION)`` so the
|
||||
fixture mirrors a real subscription request.
|
||||
"""
|
||||
from headroom.transforms.compression_policy import CompressionPolicy
|
||||
|
||||
|
|
@ -213,18 +219,35 @@ def test_should_apply_false_when_policy_disables_aligner(tokenizer: Tokenizer) -
|
|||
# Sanity: without a policy, the detector opts in.
|
||||
assert aligner.should_apply(messages, tokenizer)
|
||||
# F2.1 gate: with the subscription policy, the detector opts out.
|
||||
sub_policy = CompressionPolicy(live_zone_only=True, cache_aligner_enabled=False)
|
||||
sub_policy = CompressionPolicy(
|
||||
live_zone_only=True,
|
||||
cache_aligner_enabled=False,
|
||||
volatile_token_threshold=32,
|
||||
max_lossy_ratio=0.25,
|
||||
toin_read_only=True,
|
||||
)
|
||||
assert not aligner.should_apply(messages, tokenizer, compression_policy=sub_policy)
|
||||
|
||||
|
||||
def test_should_apply_true_when_policy_enables_aligner(tokenizer: Tokenizer) -> None:
|
||||
"""F2.1 c4/5: ``compression_policy.cache_aligner_enabled=True``
|
||||
must NOT short-circuit. PAYG/OAuth keep current behaviour."""
|
||||
must NOT short-circuit. PAYG/OAuth keep current behaviour.
|
||||
|
||||
F2.2 added three per-mode tuning fields; the policy here uses the
|
||||
PAYG defaults from ``policy_for_mode(AuthMode.PAYG)`` so the
|
||||
fixture mirrors a real PAYG request.
|
||||
"""
|
||||
from headroom.transforms.compression_policy import CompressionPolicy
|
||||
|
||||
messages = _system_user_messages("Session: 550e8400-e29b-41d4-a716-446655440000")
|
||||
aligner = CacheAligner(CacheAlignerConfig(enabled=True))
|
||||
payg_policy = CompressionPolicy(live_zone_only=False, cache_aligner_enabled=True)
|
||||
payg_policy = CompressionPolicy(
|
||||
live_zone_only=False,
|
||||
cache_aligner_enabled=True,
|
||||
volatile_token_threshold=128,
|
||||
max_lossy_ratio=0.45,
|
||||
toin_read_only=False,
|
||||
)
|
||||
assert aligner.should_apply(messages, tokenizer, compression_policy=payg_policy)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ The Python module is a hand-mirror of
|
|||
pin both halves: that the per-mode values are right, and that the
|
||||
Python and Rust sides agree on the field map. F2.2 will likely retire
|
||||
the hand-mirror via PyO3 — until then, this file is the canary.
|
||||
|
||||
F2.2 extends the F2.1 surface with three tuning fields:
|
||||
``volatile_token_threshold``, ``max_lossy_ratio``, ``toin_read_only``.
|
||||
Per-mode value tests below mirror the Rust unit tests in
|
||||
``crates/headroom-core/src/compression_policy.rs``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -20,7 +25,7 @@ from headroom.transforms.compression_policy import (
|
|||
|
||||
|
||||
class TestCompressionPolicyForMode:
|
||||
"""Per-mode field assertions. Mirrors the three Rust unit tests in
|
||||
"""Per-mode field assertions. Mirrors the Rust unit tests in
|
||||
`crates/headroom-core/src/compression_policy.rs`.
|
||||
"""
|
||||
|
||||
|
|
@ -29,13 +34,31 @@ class TestCompressionPolicyForMode:
|
|||
assert p.live_zone_only is False, "PAYG can touch outside live zone"
|
||||
assert p.cache_aligner_enabled is True, "PAYG runs cache aligner"
|
||||
|
||||
def test_payg_tuning_fields_aggressive(self):
|
||||
# F2.2: per-mode tuning fields. Values are the conservative
|
||||
# defaults pending bake telemetry (see PR body and module
|
||||
# docstring).
|
||||
p = policy_for_mode(AuthMode.PAYG)
|
||||
assert p.volatile_token_threshold == 128, (
|
||||
"PAYG volatile threshold is the relaxed default; F2.2-followup will tune"
|
||||
)
|
||||
assert p.max_lossy_ratio == pytest.approx(0.45), (
|
||||
"PAYG max_lossy_ratio caps lossy paths at 0.45; F2.2-followup will tune"
|
||||
)
|
||||
assert p.toin_read_only is False, (
|
||||
"PAYG keeps TOIN write-enabled — network effect feeds on PAYG traffic"
|
||||
)
|
||||
|
||||
def test_oauth_matches_payg_today(self):
|
||||
# Canary: when F2.2 diverges OAuth from PAYG, this test fails
|
||||
# and forces a deliberate update on BOTH sides (Rust + Python).
|
||||
# Canary: when F2.2-followup diverges OAuth from PAYG, this test
|
||||
# fails and forces a deliberate update on BOTH sides (Rust +
|
||||
# Python). Covers ALL fields (F2.1 + F2.2) so a future field-
|
||||
# level divergence trips the assertion just as loudly as a flag
|
||||
# flip.
|
||||
oauth = policy_for_mode(AuthMode.OAUTH)
|
||||
payg = policy_for_mode(AuthMode.PAYG)
|
||||
assert oauth == payg, (
|
||||
"F2.1 ships OAuth=PAYG; F2.2 will diverge based on telemetry. "
|
||||
"F2.1+F2.2 ship OAuth=PAYG; F2.2-followup will diverge based on telemetry. "
|
||||
"If you are reading this assertion failure: also update "
|
||||
"crates/headroom-core/src/compression_policy.rs "
|
||||
"::oauth_matches_payg_today, otherwise the Rust + Python "
|
||||
|
|
@ -49,6 +72,32 @@ class TestCompressionPolicyForMode:
|
|||
"Subscription MUST skip cache aligner — load-bearing for issues #327 / #388"
|
||||
)
|
||||
|
||||
def test_subscription_tuning_fields_conservative(self):
|
||||
# F2.2: per-mode tuning fields. Subscription is the conservative
|
||||
# end — tighter threshold, lower lossy cap, TOIN read-only — so
|
||||
# cache prefixes stay stable and the learning pool isn't
|
||||
# mutated from cache-stability-sensitive traffic.
|
||||
p = policy_for_mode(AuthMode.SUBSCRIPTION)
|
||||
assert p.volatile_token_threshold == 32, (
|
||||
"Subscription volatile threshold flags content earlier (cache stability)"
|
||||
)
|
||||
assert p.max_lossy_ratio == pytest.approx(0.25), (
|
||||
"Subscription max_lossy_ratio caps lossy paths at 0.25 (conservative)"
|
||||
)
|
||||
assert p.toin_read_only is True, (
|
||||
"Subscription MUST be TOIN read-only — load-bearing for keeping the "
|
||||
"learning pool consistent across cache-sensitive traffic"
|
||||
)
|
||||
|
||||
def test_max_lossy_ratio_in_unit_interval(self):
|
||||
# Defensive: every per-mode `max_lossy_ratio` MUST be in
|
||||
# ``[0.0, 1.0]`` because it expresses a fraction. A tune that
|
||||
# drifts outside the unit interval is a bug — catch it cheaply
|
||||
# here rather than at the eventual consumer site.
|
||||
for mode in (AuthMode.PAYG, AuthMode.OAUTH, AuthMode.SUBSCRIPTION):
|
||||
r = policy_for_mode(mode).max_lossy_ratio
|
||||
assert 0.0 <= r <= 1.0, f"max_lossy_ratio for {mode!r} = {r} is outside [0.0, 1.0]"
|
||||
|
||||
|
||||
class TestPolicyDefaultPayg:
|
||||
"""The constant used when the enforcement flag is disabled."""
|
||||
|
|
@ -68,20 +117,38 @@ class TestImmutability:
|
|||
# CPython 3.10+). Catch both for compatibility.
|
||||
p.live_zone_only = True # type: ignore[misc]
|
||||
|
||||
def test_f22_tuning_fields_also_frozen(self):
|
||||
# Each F2.2 field gets its own immutability assertion — a
|
||||
# future refactor that accidentally drops `frozen=True` on the
|
||||
# dataclass would silently allow per-request mutation. The
|
||||
# F2.1 test only covered ``live_zone_only``; explicit per-
|
||||
# field coverage prevents quiet regressions.
|
||||
p = policy_for_mode(AuthMode.PAYG)
|
||||
for attr_name in ("volatile_token_threshold", "max_lossy_ratio", "toin_read_only"):
|
||||
with pytest.raises((AttributeError, Exception)):
|
||||
setattr(p, attr_name, 0) # type: ignore[misc]
|
||||
|
||||
|
||||
class TestRustParityFieldMap:
|
||||
"""The Python policy must have the same fields as the Rust struct.
|
||||
|
||||
The canonical Rust struct lives at
|
||||
``crates/headroom-core/src/compression_policy.rs``. When you add a
|
||||
field there for F2.2, add it here AND update this test. Otherwise
|
||||
the parity silently drifts.
|
||||
field there for a future PR, add it here AND update this test.
|
||||
Otherwise the parity silently drifts.
|
||||
"""
|
||||
|
||||
def test_field_set_matches_rust(self):
|
||||
# Hard-coded set — when Rust grows fields, this test fails until
|
||||
# Python catches up.
|
||||
expected_fields = {"live_zone_only", "cache_aligner_enabled"}
|
||||
# Python catches up. F2.2 added three: volatile_token_threshold,
|
||||
# max_lossy_ratio, toin_read_only.
|
||||
expected_fields = {
|
||||
"live_zone_only",
|
||||
"cache_aligner_enabled",
|
||||
"volatile_token_threshold",
|
||||
"max_lossy_ratio",
|
||||
"toin_read_only",
|
||||
}
|
||||
actual_fields = {f.name for f in CompressionPolicy.__dataclass_fields__.values()}
|
||||
assert actual_fields == expected_fields, (
|
||||
f"Python CompressionPolicy fields drifted from Rust. "
|
||||
|
|
|
|||
289
tests/test_compression_policy_toin_gate.py
Normal file
289
tests/test_compression_policy_toin_gate.py
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
"""F2.2: TOIN write-gate tests for the per-mode CompressionPolicy.
|
||||
|
||||
When ``CompressionPolicy.toin_read_only`` is ``True`` (Subscription
|
||||
auth mode), TOIN must serve cached recommendations but NEVER write new
|
||||
pattern observations from this request. PAYG / OAuth keep writing so
|
||||
the network effect keeps growing. The gate is read at the
|
||||
``record_compression`` call site in ``smart_crusher.py`` and
|
||||
``content_router.py``.
|
||||
|
||||
These tests mirror the structure of
|
||||
``tests/test_smart_crusher_toin_attachment.py`` (the F2.1-era TOIN
|
||||
re-attachment regression suite) so a future contributor can locate the
|
||||
expected behaviour by name.
|
||||
|
||||
Behaviour matrix:
|
||||
|
||||
| Mode | toin_read_only | record_compression called? |
|
||||
|--------------|----------------|----------------------------|
|
||||
| Payg | False | yes |
|
||||
| OAuth | False | yes |
|
||||
| Subscription | True | NO |
|
||||
|
||||
Direct callers (those that call ``crush()`` / ``crush_array_json()``
|
||||
without going through ``apply()``) don't set
|
||||
``self._runtime_compression_policy``, so they keep their pre-F2.2
|
||||
write-enabled behaviour. That's a deliberate compatibility decision —
|
||||
non-proxy callers have no auth context.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from headroom.proxy.auth_mode import AuthMode
|
||||
from headroom.telemetry.toin import TOINConfig, get_toin, reset_toin
|
||||
from headroom.tokenizer import Tokenizer
|
||||
from headroom.tokenizers import EstimatingTokenCounter
|
||||
from headroom.transforms.compression_policy import policy_for_mode
|
||||
|
||||
|
||||
def _has_core() -> bool:
|
||||
"""Match the pattern in ``test_smart_crusher_rust_parity.py``.
|
||||
|
||||
SmartCrusher's __init__ hard-imports ``headroom._core`` (the Rust
|
||||
PyO3 wheel). On dev machines or CI lanes that haven't run
|
||||
``scripts/build_rust_extension.sh``, the wheel is absent. Skip the
|
||||
SmartCrusher-touching tests rather than fail loudly — the
|
||||
ContentRouter tests don't need the wheel and exercise the same
|
||||
F2.2 gate code path.
|
||||
"""
|
||||
try:
|
||||
from headroom._core import SmartCrusher # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
_skip_no_core = pytest.mark.skipif(
|
||||
not _has_core(),
|
||||
reason="headroom._core wheel not installed (run `scripts/build_rust_extension.sh`)",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fresh_toin():
|
||||
"""Per-test TOIN instance backed by a tempdir to avoid global drift."""
|
||||
reset_toin()
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
storage = str(Path(tmpdir) / "toin.json")
|
||||
toin = get_toin(
|
||||
TOINConfig(
|
||||
storage_path=storage,
|
||||
auto_save_interval=0,
|
||||
)
|
||||
)
|
||||
yield toin
|
||||
reset_toin()
|
||||
|
||||
|
||||
def _bigger_array(n: int = 60) -> str:
|
||||
"""JSON array of `n` dicts, sized to trigger crushing.
|
||||
|
||||
Mirrors the helper in ``test_smart_crusher_toin_attachment.py`` so
|
||||
these tests use the same shape and any "didn't trigger compression"
|
||||
skip lines up with the existing suite.
|
||||
"""
|
||||
items = [{"status": "ok", "tag": "x", "n": i} for i in range(n)]
|
||||
return json.dumps(items)
|
||||
|
||||
|
||||
def _wrap_in_tool_message(payload: str) -> list[dict]:
|
||||
"""Build the OpenAI-style ``role=tool`` message ``apply()`` walks."""
|
||||
return [{"role": "tool", "content": payload, "tool_call_id": "t1"}]
|
||||
|
||||
|
||||
def _tokenizer() -> Tokenizer:
|
||||
return Tokenizer(EstimatingTokenCounter()) # type: ignore[arg-type]
|
||||
|
||||
|
||||
# ─── SmartCrusher: apply() with policy ──────────────────────────────────
|
||||
|
||||
|
||||
@_skip_no_core
|
||||
def test_smart_crusher_payg_policy_writes_to_toin(fresh_toin):
|
||||
"""PAYG: ``toin_read_only=False`` → record_compression IS called."""
|
||||
from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig
|
||||
|
||||
crusher = SmartCrusher(SmartCrusherConfig())
|
||||
messages = _wrap_in_tool_message(_bigger_array(60))
|
||||
pre = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
|
||||
policy = policy_for_mode(AuthMode.PAYG)
|
||||
assert policy.toin_read_only is False # baseline sanity
|
||||
result = crusher.apply(messages, _tokenizer(), compression_policy=policy)
|
||||
|
||||
if not result.transforms_applied:
|
||||
pytest.skip("payload didn't trigger compression — bump the size")
|
||||
post = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
assert post > pre, "PAYG should write to TOIN (network effect)"
|
||||
|
||||
|
||||
@_skip_no_core
|
||||
def test_smart_crusher_oauth_policy_writes_to_toin(fresh_toin):
|
||||
"""OAuth: identical to PAYG in F2.2 — writes enabled."""
|
||||
from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig
|
||||
|
||||
crusher = SmartCrusher(SmartCrusherConfig())
|
||||
messages = _wrap_in_tool_message(_bigger_array(60))
|
||||
pre = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
|
||||
policy = policy_for_mode(AuthMode.OAUTH)
|
||||
assert policy.toin_read_only is False
|
||||
result = crusher.apply(messages, _tokenizer(), compression_policy=policy)
|
||||
|
||||
if not result.transforms_applied:
|
||||
pytest.skip("payload didn't trigger compression — bump the size")
|
||||
post = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
assert post > pre, "OAuth (matches PAYG today) should write to TOIN"
|
||||
|
||||
|
||||
@_skip_no_core
|
||||
def test_smart_crusher_subscription_policy_skips_toin_write(fresh_toin):
|
||||
"""Subscription: ``toin_read_only=True`` → record_compression is NOT called.
|
||||
|
||||
This is THE behaviour change of F2.2 — keep the learning pool
|
||||
consistent for cache-stability-sensitive traffic.
|
||||
"""
|
||||
from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig
|
||||
|
||||
crusher = SmartCrusher(SmartCrusherConfig())
|
||||
messages = _wrap_in_tool_message(_bigger_array(60))
|
||||
pre = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
|
||||
policy = policy_for_mode(AuthMode.SUBSCRIPTION)
|
||||
assert policy.toin_read_only is True # baseline sanity
|
||||
result = crusher.apply(messages, _tokenizer(), compression_policy=policy)
|
||||
|
||||
# Compression itself should still complete — this gate is on the
|
||||
# learning side only, not the compression path.
|
||||
if not result.transforms_applied:
|
||||
pytest.skip("payload didn't trigger compression — bump the size")
|
||||
post = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
assert post == pre, (
|
||||
"Subscription MUST NOT write to TOIN — load-bearing for keeping "
|
||||
"the learning pool consistent across cache-sensitive traffic"
|
||||
)
|
||||
|
||||
|
||||
@_skip_no_core
|
||||
def test_smart_crusher_no_policy_keeps_legacy_write_behaviour(fresh_toin):
|
||||
"""Direct ``apply()`` call without ``compression_policy`` keeps
|
||||
pre-F2.2 behaviour: TOIN writes are not gated.
|
||||
|
||||
Many test fixtures and non-proxy callers don't pass a policy; they
|
||||
must continue to feed the learning pool exactly as they did
|
||||
before F2.2.
|
||||
"""
|
||||
from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig
|
||||
|
||||
crusher = SmartCrusher(SmartCrusherConfig())
|
||||
messages = _wrap_in_tool_message(_bigger_array(60))
|
||||
pre = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
|
||||
# No `compression_policy` kwarg.
|
||||
result = crusher.apply(messages, _tokenizer())
|
||||
|
||||
if not result.transforms_applied:
|
||||
pytest.skip("payload didn't trigger compression — bump the size")
|
||||
post = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
assert post > pre, "no policy → legacy write-enabled behaviour"
|
||||
|
||||
|
||||
# ─── ContentRouter: apply() captures the policy ─────────────────────────
|
||||
|
||||
|
||||
def test_content_router_apply_stores_runtime_policy():
|
||||
"""``ContentRouter.apply()`` must populate
|
||||
``self._runtime_compression_policy`` from kwargs so
|
||||
``_record_to_toin`` can read it.
|
||||
|
||||
We don't assert TOIN behaviour here (the router routes most JSON
|
||||
arrays to SmartCrusher, which has its own gate already covered
|
||||
above); the load-bearing thing for the parity guard is that the
|
||||
field is wired through.
|
||||
"""
|
||||
from headroom.transforms.content_router import ContentRouter
|
||||
|
||||
router = ContentRouter()
|
||||
# Sanity: the field exists on a fresh instance and starts None.
|
||||
assert router._runtime_compression_policy is None
|
||||
|
||||
policy = policy_for_mode(AuthMode.SUBSCRIPTION)
|
||||
# Empty-message apply is fine — the field assignment happens
|
||||
# before the message walk, so we don't need a payload that
|
||||
# actually compresses.
|
||||
router.apply([], _tokenizer(), compression_policy=policy)
|
||||
assert router._runtime_compression_policy is policy, (
|
||||
"ContentRouter.apply() must capture the policy onto self so _record_to_toin can read it"
|
||||
)
|
||||
|
||||
|
||||
def test_content_router_subscription_skips_toin_record(fresh_toin):
|
||||
"""ContentRouter._record_to_toin returns early when
|
||||
policy.toin_read_only is True.
|
||||
|
||||
We exercise the gate directly rather than building a fixture that
|
||||
routes to a non-SmartCrusher compressor — both are equivalent
|
||||
coverage for the gate, and the direct call avoids the routing
|
||||
flake from ``test_smart_crusher_toin_attachment.py``'s comments.
|
||||
"""
|
||||
from headroom.transforms.content_router import (
|
||||
CompressionStrategy,
|
||||
ContentRouter,
|
||||
)
|
||||
|
||||
router = ContentRouter()
|
||||
router._runtime_compression_policy = policy_for_mode(AuthMode.SUBSCRIPTION)
|
||||
|
||||
pre = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
# Pick TEXT strategy (not SMART_CRUSHER, which has its own
|
||||
# early-return). With Subscription policy, the F2.2 gate fires
|
||||
# and the call returns before ever loading TOIN.
|
||||
router._record_to_toin(
|
||||
strategy=CompressionStrategy.TEXT,
|
||||
content="some text content",
|
||||
compressed="compressed",
|
||||
original_tokens=100,
|
||||
compressed_tokens=50,
|
||||
)
|
||||
post = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
assert post == pre, "Subscription policy must skip ContentRouter TOIN write"
|
||||
|
||||
|
||||
def test_content_router_payg_records_to_toin(fresh_toin):
|
||||
"""PAYG policy → ContentRouter._record_to_toin proceeds to the
|
||||
real TOIN call. Asserts the gate doesn't accidentally fire when
|
||||
``toin_read_only=False``.
|
||||
"""
|
||||
from headroom.transforms.content_router import (
|
||||
CompressionStrategy,
|
||||
ContentRouter,
|
||||
)
|
||||
|
||||
router = ContentRouter()
|
||||
router._runtime_compression_policy = policy_for_mode(AuthMode.PAYG)
|
||||
|
||||
pre = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
router._record_to_toin(
|
||||
strategy=CompressionStrategy.TEXT,
|
||||
content="some text content with structure that learns",
|
||||
compressed="compressed shorter",
|
||||
original_tokens=100,
|
||||
compressed_tokens=50,
|
||||
)
|
||||
post = sum(p.total_compressions for p in fresh_toin._patterns.values())
|
||||
# Real TOIN write should happen unless _create_content_signature
|
||||
# returns None (it can for malformed inputs). We accept either
|
||||
# "post > pre" (signature succeeded) OR "post == pre with a
|
||||
# signature-None path"; the load-bearing assertion is that the
|
||||
# F2.2 gate did NOT fire (which it would with toin_read_only=True
|
||||
# regardless of signature).
|
||||
assert post >= pre, (
|
||||
"PAYG must not be blocked by the F2.2 gate — write should happen or fall through naturally"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue