File size: 7,553 Bytes
95cb372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
"""Tests for per-request compression segments capture and propagation.

Flow under test:
    ContentRouter observer β†’ `record_compression` β†’ thread-local
    accumulator (when inside `request_segments_scope`) β†’
    `TransformResult.compression_segments` β†’ `RequestLog.compression_segments`
    β†’ `/stats` `recent_requests`.
"""

from __future__ import annotations

from headroom.proxy.models import RequestLog
from headroom.proxy.prometheus_metrics import PrometheusMetrics
from headroom.transforms.observability import (
    current_request_segments,
    request_segments_scope,
)


def test_record_compression_appends_to_active_scope():
    """PrometheusMetrics.record_compression appends to the active
    per-request scope and still increments global counters."""
    m = PrometheusMetrics()
    # Pre-scope: accumulator is None, observer is a no-op for segments
    m.record_compression("smart_crusher", original_tokens=100, compressed_tokens=50)
    assert m.compressions_by_strategy["smart_crusher"] == 1
    assert m.tokens_original_by_strategy["smart_crusher"] == 100

    with request_segments_scope() as segs:
        assert current_request_segments() is segs
        m.record_compression("smart_crusher", original_tokens=300, compressed_tokens=80)
        m.record_compression("code_aware", original_tokens=200, compressed_tokens=120)
        assert len(segs) == 2
        assert segs[0] == {
            "strategy": "smart_crusher",
            "original_tokens": 300,
            "compressed_tokens": 80,
        }
        assert segs[1] == {
            "strategy": "code_aware",
            "original_tokens": 200,
            "compressed_tokens": 120,
        }

    # Global counters kept accumulating across the scope:
    assert m.compressions_by_strategy["smart_crusher"] == 2
    assert m.tokens_original_by_strategy["code_aware"] == 200

    # After scope, observer no longer appends anywhere:
    m.record_compression("diff", original_tokens=50, compressed_tokens=30)
    assert len(segs) == 2  # unchanged


def test_nested_scopes_isolate_per_request_state():
    """Nested scopes don't leak. The outer scope's list is restored
    after the inner scope exits."""
    with request_segments_scope() as outer:
        outer.append({"strategy": "outer", "original_tokens": 10, "compressed_tokens": 5})
        with request_segments_scope() as inner:
            assert current_request_segments() is inner
            inner.append({"strategy": "inner", "original_tokens": 20, "compressed_tokens": 10})
            assert len(outer) == 1  # inner append didn't touch outer
            assert len(inner) == 1
        # Inner scope exited β€” outer is restored as the current slot
        assert current_request_segments() is outer
        outer.append({"strategy": "outer2", "original_tokens": 30, "compressed_tokens": 15})
        assert len(outer) == 2


def test_exception_inside_scope_still_clears_slot():
    """Scope cleanup must run even if the body raises β€” otherwise a
    crashed request leaks segments into the next request on the same
    worker thread."""
    try:
        with request_segments_scope() as segs:
            segs.append({"strategy": "smart_crusher", "original_tokens": 5, "compressed_tokens": 2})
            raise RuntimeError("simulated pipeline failure")
    except RuntimeError:
        pass

    # Slot restored to None β€” no leak to whatever runs next on this thread.
    assert current_request_segments() is None


def test_request_log_carries_compression_segments():
    """RequestLog serialization roundtrip preserves the per-request
    routing log so `/stats` `recent_requests` consumers see it."""
    segments = [
        {"strategy": "smart_crusher", "original_tokens": 500, "compressed_tokens": 120},
        {"strategy": "code_aware", "original_tokens": 300, "compressed_tokens": 90},
    ]
    log = RequestLog(
        timestamp="2026-06-06T12:00:00Z",
        request_id="req-abc",
        provider="anthropic",
        model="claude-opus-4-7",
        input_tokens_original=800,
        input_tokens_optimized=210,
        output_tokens=0,
        tokens_saved=590,
        savings_percent=73.75,
        optimization_latency_ms=5.0,
        total_latency_ms=42.0,
        tags={},
        cache_hit=False,
        transforms_applied=["content_router"],
        compression_segments=segments,
    )

    from dataclasses import asdict

    d = asdict(log)
    assert d["compression_segments"] == segments
    # asdict is what request_logger uses for Redis serialization β†’
    # confirms the field survives the trip to `_merged_recent_requests`.


def test_request_log_default_compression_segments_is_none():
    """Legacy call sites that don't pass compression_segments get
    None β€” graceful degradation, no KeyError, no panic."""
    log = RequestLog(
        timestamp="2026-06-06T12:00:00Z",
        request_id="req-xyz",
        provider="openai",
        model="gpt-4",
        input_tokens_original=100,
        input_tokens_optimized=100,
        output_tokens=0,
        tokens_saved=0,
        savings_percent=0.0,
        optimization_latency_ms=1.0,
        total_latency_ms=10.0,
        tags={},
        cache_hit=False,
        transforms_applied=[],
    )
    assert log.compression_segments is None


def test_pipeline_apply_propagates_segments_through_transform_result():
    """End-to-end: TransformPipeline.apply wraps itself in
    request_segments_scope, so any observer-bearing transform that
    runs during apply() contributes segments to TransformResult."""
    from headroom.config import HeadroomConfig
    from headroom.transforms.pipeline import TransformPipeline

    # Build a minimal pipeline with a single ContentRouter that has
    # a real PrometheusMetrics observer attached. ContentRouter
    # emits one observer event per routing decision.
    cfg = HeadroomConfig()
    metrics = PrometheusMetrics()
    pipeline = TransformPipeline(cfg, observer=metrics)

    # Fabricate messages with enough content that the router will
    # actually compress (not passthrough). The exact strategy doesn't
    # matter β€” we just need at least one record_compression event.
    messages = [
        {"role": "system", "content": "You are helpful."},
        {
            "role": "user",
            "content": (
                "Here is a large JSON array of logs:\n"
                + "\n".join(f'{{"id": {i}, "level": "info", "msg": "event"}}' for i in range(200))
            ),
        },
    ]

    result = pipeline.apply(
        messages,
        model="claude-opus-4-7",
        model_limit=200000,
    )

    # If the router compressed anything, we should see segments both
    # on the result and in the global per-strategy counters. If the
    # router passthrough'd everything, both are empty β€” the invariant
    # is that they agree, not that compression must occur.
    assert result.compression_segments is None or isinstance(
        result.compression_segments, list
    )
    if result.compression_segments:
        # Every entry has the observer shape
        for entry in result.compression_segments:
            assert set(entry.keys()) == {
                "strategy",
                "original_tokens",
                "compressed_tokens",
            }
        # Global counters agree on totals
        total_original = sum(e["original_tokens"] for e in result.compression_segments)
        total_in_metrics = sum(metrics.tokens_original_by_strategy.values())
        assert total_original == total_in_metrics