@@ -643,13 +643,26 @@ class ContentRouter(Transform):
643643
644644 name : str = "content_router"
645645
646- def __init__ (self , config : ContentRouterConfig | None = None ):
646+ def __init__ (
647+ self ,
648+ config : ContentRouterConfig | None = None ,
649+ observer : Any = None ,
650+ ):
647651 """Initialize content router.
648652
649653 Args:
650654 config: Router configuration. Uses defaults if None.
655+ observer: Optional `CompressionObserver` (see
656+ `headroom.transforms.observability`) called once per
657+ routing decision after `compress()` finishes. The
658+ proxy's `PrometheusMetrics` is the production
659+ implementation — it increments per-strategy counters
660+ so silent regressions become visible. `None` disables
661+ observation; pick one explicitly per the no-fallback
662+ rule in the audit doc.
651663 """
652664 self .config = config or ContentRouterConfig ()
665+ self ._observer = observer
653666
654667 # Lazy-loaded compressors
655668 self ._code_compressor : Any = None
@@ -766,20 +779,46 @@ def compress(
766779 RouterCompressionResult with compressed content and routing metadata.
767780 """
768781 if not content or not content .strip ():
769- return RouterCompressionResult (
782+ result = RouterCompressionResult (
770783 compressed = content ,
771784 original = content ,
772785 strategy_used = CompressionStrategy .PASSTHROUGH ,
773786 routing_log = [],
774787 )
788+ else :
789+ # Determine strategy from content analysis
790+ strategy = self ._determine_strategy (content )
775791
776- # Determine strategy from content analysis
777- strategy = self ._determine_strategy (content )
792+ if strategy == CompressionStrategy .MIXED :
793+ result = self ._compress_mixed (content , context , question , bias = bias )
794+ else :
795+ result = self ._compress_pure (content , strategy , context , question , bias = bias )
778796
779- if strategy == CompressionStrategy .MIXED :
780- return self ._compress_mixed (content , context , question , bias = bias )
781- else :
782- return self ._compress_pure (content , strategy , context , question , bias = bias )
797+ # One observer call per routing decision; the observer is the
798+ # forcing function for catching strategy-level regressions.
799+ # Empty routing_log (passthrough fast path) → no calls.
800+ self ._observe (result )
801+ return result
802+
803+ def _observe (self , result : RouterCompressionResult ) -> None :
804+ """Forward each `RoutingDecision` in `result.routing_log` to the
805+ configured `CompressionObserver`. No-op when no observer is set.
806+
807+ Observers MUST NOT raise per the protocol contract; if one does
808+ anyway, swallow at debug level. Compression already succeeded;
809+ a buggy observer must not turn a 200 into a 500.
810+ """
811+ if self ._observer is None :
812+ return
813+ for d in result .routing_log :
814+ try :
815+ self ._observer .record_compression (
816+ strategy = d .strategy .value ,
817+ original_tokens = d .original_tokens ,
818+ compressed_tokens = d .compressed_tokens ,
819+ )
820+ except Exception as e : # pragma: no cover - defensive
821+ logger .debug ("CompressionObserver raised (non-fatal): %s" , e )
783822
784823 def _determine_strategy (self , content : str ) -> CompressionStrategy :
785824 """Determine the compression strategy from content analysis.
0 commit comments