Skip to content

Commit 250ede2

Browse files
chopratejasTejas Chopraclaude
authored
fix(reporting): show net vs gross savings, real skip thresholds, and the effective profile (#3123)
## Description Six reporting/config defects found while investigating a user reporting ~1% savings on Claude Code. **None of these changes how much Headroom compresses** — all of them change whether an operator can tell what it did. Every one was found by reading that user's own 227,777 lines of proxy logs against the code. ## Changes Made - **`perf/analyzer`: parse and render `tok_inflated`.** Every PERF line carried it; nothing downstream read it. The report could print `321,239,562 -> 313,274,727` directly above `8,455,763 saved` — two figures that differ by exactly the 490,928 tokens of inflation it omitted. - **`content_router`: report the real skip thresholds.** The routing summary hardcoded `skipped (<50 words)` regardless of what was in force. Wrong number (the message gate is `min_tokens`, 10–250 by profile), wrong unit (tokens and characters, never words), and it merged two different gates under one label. - **`perf/analyzer`: disclose that Transform Effectiveness is partial.** It is built only from `pipeline.py`'s `Transform NAME:` lines. `compression_units.py` / `compression_batches.py` contain zero logging calls, so the table read `content_router: 189,783 saved` against a PERF total 44x larger. Reports the divergence rather than a coverage ratio — the two are different populations and neither contains the other (those lines carry no request_id, fire per stage, and are emitted before the forwarder decides). - **`perf/analyzer`: disclose the routing denominator.** Percentages were taken over 4 of the router's 17 outcome buckets, silently dropping buckets larger than several it displayed. - **`savings_tracker`: stop dropping tool-schema dollars.** `estimate_request_savings_usd` prices four buckets; `record_request` read three. `tool_schema` was computed and discarded, so a quarter of the token headline never reached "Cost saved". The two inputs are disjoint (verified at the call site), so this is additive, not double-counting. - **`agent_savings`: an unknown profile no longer degrades to `balanced`.** `balanced` is a different product posture from the default `coding`: cache→token mode, dedup off, tool-search off, user messages uncompressed, message floor 25x higher, block floor 20x higher. A typo in `HEADROOM_SAVINGS_PROFILE` silently reconfigured the whole proxy. Now degrades to `DEFAULT_PROFILE` and names the resolved profile in the warning. - **`agent_savings`: give `min_chars_for_block` a config-object path.** Every other router pipeline kwarg travels on the config object; this one alone was env-only, so an unseeded proxy applied every sibling `coding` knob while this floor stayed at 500 instead of 25. - **`server`: log the resolved compression posture at startup**, reading cross-turn dedup off the constructed router rather than the environment (the router resolves it as `config OR env`, so reading env alone would be a guess). ## Testing - [x] Unit tests pass, [x] ruff, [x] mypy, [x] new tests added ```text uv run pytest tests/ -k "content_router or agent_savings or perf or analyzer or savings or proxy_server or cli_perf or prometheus" 620 passed, 25 skipped uv run mypy headroom # Success uv run ruff check . && ruff format --check . # clean ``` ## Real behavior proof - **Setup:** macOS arm64, Python 3.12, this branch. Input: 60 MB / 227,777 lines of real proxy logs from the reporting user (6 rotated files, 2,792 PERF lines, 2026-08-17 → 2026-08-19). - **Steps:** pointed `headroom.perf.analyzer.LOG_DIR` at that directory and rendered the report before and after the patch. - **After-fix output (real data, unmodified):** ```text Requests: 2792 Tokens: 321,288,161 -> 313,323,326 (2.6% messages) Tokens saved: 11,158,901 (3.4% reduction) · inflated 490,928 (net message reduction 7,964,835) · messages 8,455,763 · tool schemas 2,703,138 ! stage-level total 190,641 != PERF message total 8,455,763 — this table sees only engines that emit a Transform line, counts per stage, and does not check whether the mutation shipped Skipped: 44641 (77%) — below size floor (shares are of these 4 buckets only, n=58319; see `[router] route_counts=` for the full outcome space) ``` The arithmetic now closes on the page: `8,455,763 - 490,928 = 7,964,835`, matching the token delta exactly. Before the patch none of the three annotated lines existed and the `Skipped` line claimed `<50 words`. - **Profile resolution verified by execution**, not inspection — subprocesses with controlled env: ```text vanilla (nothing set) mode=cache dedupe=1 tool_search=1 min_tokens=10 min_chars=25 HEADROOM_SAVINGS_PROFILE=coding mode=cache dedupe=1 tool_search=1 min_tokens=10 min_chars=25 unknown profile name (before) mode=token dedupe=0 tool_search=0 min_tokens=250 min_chars=500 unknown profile name (after) -> resolves to `coding`, warning names it coding, seeding never runs min_chars=25 (was 500 before this patch) ``` - **Not tested:** live paid Anthropic traffic. These are reporting/config surfaces; the wire path is untouched by this PR. ## Review readiness - [x] Self-reviewed. Three overclaims in my own first draft were corrected before this PR: a false subset claim in the Transform Effectiveness note, a comment asserting `min_chars_for_block` was the *only* env-only field (it is the only env-only *router pipeline kwarg*; `cross_turn_dedup`, `tool_search`, `protect_reads`, `code_aware`, `effort_router`, `lossless` remain env-only via a different mechanism and are **not** fixed here), and a money-path expression that relied on `a + b if c else d` grouping. ## Known remaining (deliberately out of scope) - `Requests: N` still overcounts: the Codex WS forwarder reuses one `request_id` across every turn (one observed 156x), plus ~18 duplicate PERF emissions. - `compression_units.py` / `compression_batches.py` remain unlogged — this PR *discloses* the blind spot rather than closing it. - The headline stays **gross**. True net is `11,158,901 - 490,928 = 10,667,973` (3.3%, not 3.4%). Making net the headline lowers every user's reported savings ~4.4%; that is a product call, not mine, so the inflation is surfaced beside it instead. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 05f5ef4 commit 250ede2

7 files changed

Lines changed: 458 additions & 21 deletions

File tree

docs/metrics-technical-guide.md

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Headroom Metrics — Dashboard Guide
2+
3+
What each metric shows, so you can build panels against it.
4+
5+
**Two endpoints.** Both are on the proxy (default `:8787`).
6+
7+
| Surface | How to get it | Use it for |
8+
|---|---|---|
9+
| **Prometheus**`GET /metrics` | Always on, no config | Everything below. Start here. |
10+
| **OpenTelemetry** — OTLP/HTTP | `HEADROOM_OTEL_METRICS_ENABLED=1` + `pip install "headroom-ai[proxy,otel]"` | Same data, dotted names, plus per-tenant labels |
11+
12+
Names differ between them: Prometheus uses `headroom_tokens_saved_total` (**milliseconds** for timings), OTel uses `headroom.proxy.tokens.saved` (**seconds**). Both are listed below.
13+
14+
---
15+
16+
## The savings panel — start here
17+
18+
**`headroom.proxy.tokens.saved`** is the headline number. It already combines compression + tool-schema deferral — no need to add anything to it.
19+
20+
| Metric | What it shows |
21+
|---|---|
22+
| **`headroom.proxy.tokens.saved`** *(OTel)* | **Total input tokens Headroom kept out of the request.** Compression + tool savings, combined. This is your hero number. |
23+
| `headroom.proxy.savings.usd{source}` *(OTel)* | **Dollars saved**, split by layer: `compression`, `tool_schema`, `output_shaping`, `provider_cache`. Sum for the total. |
24+
| `headroom_persistent_savings_tokens_saved_total` | Same tokens-saved number, but **survives proxy restarts**. Use for "lifetime saved" tiles. |
25+
| `headroom_persistent_savings_compression_savings_usd_total` | **Lifetime dollars saved**, durable across restarts. |
26+
| `headroom_tokens_input_total` | Input tokens actually sent upstream (post-compression). The denominator for a reduction %. |
27+
| `headroom_tokens_output_total` | Output tokens returned by the provider. |
28+
29+
```promql
30+
# Hero tile: tokens saved per second
31+
rate(headroom_tokens_saved_total[5m])
32+
+ sum(rate(headroom_savings_attributed_tokens_total{source="tool_search",realized="true"}[5m]))
33+
34+
# Context reduction %
35+
100 * rate(headroom_tokens_saved_total[5m])
36+
/ clamp_min(rate(headroom_tokens_input_total[5m]) + rate(headroom_tokens_saved_total[5m]), 1)
37+
38+
# Lifetime tiles (survive restart)
39+
headroom_persistent_savings_tokens_saved_total
40+
headroom_persistent_savings_compression_savings_usd_total
41+
```
42+
43+
> **One catch on the Prometheus side.** `headroom_tokens_saved_total` is compression **only** — it leaves out tool-schema deferral. The OTel `headroom.proxy.tokens.saved` includes both. That's why the query above adds the `tool_search` term back in. On tool-heavy workloads the gap is large.
44+
45+
---
46+
47+
## Latency panel
48+
49+
All Prometheus timings are in **milliseconds**, exposed as `_sum` / `_count` / `_min` / `_max`. Build means with `rate(sum)/rate(count)`.
50+
51+
| Metric | What it shows |
52+
|---|---|
53+
| **`headroom_overhead_ms_*`** | **Latency Headroom itself adds.** Handler entry → end of compression. Excludes the LLM call. This is the "what does this cost us" number. |
54+
| `headroom_latency_ms_*` | Total request duration, including the provider. |
55+
| `headroom_ttfb_ms_*` | Time to first byte from upstream. Streaming requests only. |
56+
| `headroom_stage_timing_ms_*{path,stage}` | Where time went inside the handler — `compression_first_stage`, `upstream_connect`, `memory_context`, etc. |
57+
| `headroom_transform_timing_ms_*{transform}` | Time per compression transform. Use to find a slow transform. |
58+
59+
```promql
60+
# Headroom's added overhead, mean ms
61+
rate(headroom_overhead_ms_sum[5m]) / rate(headroom_overhead_ms_count[5m])
62+
63+
# End-to-end, mean ms
64+
rate(headroom_latency_ms_sum[5m]) / rate(headroom_latency_ms_count[5m])
65+
66+
# Slowest stages
67+
topk(5, rate(headroom_stage_timing_ms_sum[5m]) / rate(headroom_stage_timing_ms_count[5m]))
68+
```
69+
70+
> **No percentiles are available.** There are no histogram buckets on `/metrics`, and the OTel histograms ship with default buckets that put every request into one bucket, so `histogram_quantile()` returns nonsense. **Means work fine.** For real p95/p99 today, use the `headroom perf` CLI.
71+
>
72+
> Also: divide each `_sum` by **its own** `_count`. Overhead and TTFB are only sampled when > 0, so their counts are smaller than the latency count.
73+
74+
---
75+
76+
## Cache panel
77+
78+
| Metric | What it shows |
79+
|---|---|
80+
| `headroom_provider_cache_hit_requests_total{provider}` | Requests that read from the provider's prompt cache. |
81+
| `headroom_provider_cache_requests_total{provider}` | Requests with any cache activity. **The correct denominator for hit rate.** |
82+
| `headroom_cache_read_tokens_total{provider}` | Tokens served from cache (the discounted ones). |
83+
| `headroom_cache_write_tokens_total{provider}` | Tokens written into cache (these carry a premium). |
84+
| `headroom_cache_write_ttl_tokens_total{provider,ttl}` | Cache writes split by TTL — `5m` vs `1h`. |
85+
| `headroom_uncached_input_tokens_total{provider}` | Input tokens that missed cache entirely. |
86+
| `headroom_cache_bust_total` | Requests where compression broke a cached prefix. **Should stay near zero.** |
87+
| `headroom_cache_miss_attribution_total{provider,reason}` | Why a cached prefix missed — `ttl_expiry`, `prefix_change`, `unknown`. |
88+
89+
```promql
90+
# Cache hit rate by provider
91+
sum by (provider) (rate(headroom_provider_cache_hit_requests_total[5m]))
92+
/ sum by (provider) (rate(headroom_provider_cache_requests_total[5m]))
93+
94+
# Compression breaking cache — alert if this rises
95+
rate(headroom_cache_bust_total[5m])
96+
```
97+
98+
> **Don't use `headroom_requests_cached_total` as a hit rate.** It mixes the provider's prompt cache with Headroom's own response cache into one boolean, so it measures neither.
99+
100+
---
101+
102+
## Traffic & health panel
103+
104+
| Metric | What it shows |
105+
|---|---|
106+
| `headroom_requests_total` | Requests handled. Unlabelled. |
107+
| `headroom_requests_by_provider{provider}` | Traffic split by provider — `anthropic`, `openai`, `gemini`, `bedrock`|
108+
| `headroom_requests_by_model{model}` | Traffic split by model. Capped at 1024 distinct; overflow lands in `model="other"`. |
109+
| `headroom_requests_failed_total` | Upstream 5xx errors. |
110+
| `headroom_requests_rate_limited_total` | Requests **Headroom** rejected via its own rate limiter (not upstream 429s). |
111+
| `headroom_compression_failed_total{reason}` | Compression failures — `timeout` or `error`. Fails open, so traffic keeps flowing but savings quietly stop. **Worth an alert.** |
112+
| `headroom_compression_quarantine_total{event}` | Compression disabled after repeated timeouts — `activated`, `skipped`, `released`. |
113+
| `headroom_inbound_requests_active` | In-flight requests, gauge. Counts all HTTP including `/metrics`. |
114+
| `headroom_active_ws_sessions` | Live Codex WebSocket sessions, gauge. |
115+
116+
```promql
117+
# Failure rate
118+
rate(headroom_requests_failed_total[5m])
119+
/ clamp_min(rate(headroom_requests_total[5m]) + rate(headroom_requests_failed_total[5m]), 1)
120+
121+
# Savings silently stopped
122+
sum by (reason) (rate(headroom_compression_failed_total[5m]))
123+
124+
# Traffic mix
125+
sum by (provider) (rate(headroom_requests_by_provider[5m]))
126+
```
127+
128+
---
129+
130+
## Anthropic subscription panel
131+
132+
Only if you're on an Anthropic OAuth/subscription plan. OTel only, gauges, no labels.
133+
134+
| Metric | What it shows |
135+
|---|---|
136+
| `headroom.subscription.5h_utilization_pct` | How much of the 5-hour rate-limit window is used (0–100). |
137+
| `headroom.subscription.7d_utilization_pct` | Same for the 7-day window. |
138+
| `headroom.subscription.5h_seconds_to_reset` | Seconds until the 5-hour window resets. |
139+
| `headroom.subscription.7d_seconds_to_reset` | Seconds until the 7-day window resets. |
140+
| `headroom.subscription.overage_usd` | Extra-usage credits consumed, in dollars. |
141+
142+
---
143+
144+
## Attribution — where savings came from
145+
146+
| Metric | What it shows |
147+
|---|---|
148+
| `headroom_savings_attributed_tokens_total{source,realized}` | Tokens saved, broken out by named source. `source="tool_search"` is tool-schema deferral. |
149+
| `headroom_savings_attributed_usd_total{source,realized}` | Dollars saved by source. **Gauge, can go negative** — don't `rate()` it. |
150+
| `headroom_savings_attribution_events_total{source,realized}` | How often each source contributed. |
151+
| `headroom_waste_signal_tokens_total{signal}` | Wasteful patterns *detected* in the input — `json_bloat`, `base64`, `repetition`, `reread`… This is diagnosis, **not savings**. |
152+
153+
These rows *explain* the headline total — they are never added to it.
154+
155+
---
156+
157+
## Compression internals
158+
159+
| Metric | What it shows |
160+
|---|---|
161+
| `headroom.compression.tokens.input` *(OTel)* | Tokens going into the compression pipeline. |
162+
| `headroom.compression.tokens.output` *(OTel)* | Tokens coming out. |
163+
| `headroom.compression.tokens.saved` *(OTel)* | The difference. Pipeline-level view of compression only. |
164+
| `headroom.compression.runs` *(OTel)* | Pipeline executions. Note: **per pipeline run, not per request.** |
165+
| `headroom.compression.pipeline.duration` *(OTel, seconds)* | How long the pipeline took. |
166+
| `headroom.compression.transforms{transform}` *(OTel)* | Which transforms fired. **High cardinality — drop or aggregate at the collector.** |
167+
168+
---
169+
170+
## Five things that will break a dashboard
171+
172+
1. **Only savings counters survive a restart.** 55 of 60 Prometheus families reset to zero when the proxy restarts. Only `headroom_persistent_savings_*` is durable, and it needs `HEADROOM_WORKSPACE_DIR` on a persistent volume — otherwise it resets on every deploy.
173+
174+
2. **No percentiles anywhere.** Use means. See the latency section.
175+
176+
3. **`headroom_latency_ms` measures differently for streaming.** On streaming requests the timer starts *after* compression, so end-to-end is `latency + overhead`. On non-streaming it's just `latency`. Don't mix both in one panel.
177+
178+
4. **A 5xx erases its own savings.** Requests that fail upstream are dropped from every savings and token counter. During a provider incident, savings rates look artificially clean while throughput falls.
179+
180+
5. **`/metrics` needs auth if you set a proxy token.** With `HEADROOM_PROXY_TOKEN` set, any non-loopback scraper must send `Authorization: Bearer <token>`. Loopback is always exempt.
181+
182+
---
183+
184+
## Metrics the docs mention that don't exist
185+
186+
If panels came back empty, this is probably why. These names appear in the published docs but not in the code:
187+
188+
`headroom_compression_ratio` · `headroom_latency_seconds` (and `_bucket`) · `headroom_cache_hits_total` · `headroom_cache_misses_total` · `headroom_cost_usd_total` · the `mode="optimize"` label on `headroom_requests_total`
189+
190+
The shipped `examples/grafana/headroom-dashboard.json` also filters every panel on `pool` and `hook` labels that no metric emits — the dropdowns will be permanently empty. Its metric names are otherwise correct.
191+
192+
---
193+
194+
## Setup reference
195+
196+
```bash
197+
# Prometheus — nothing to do, GET /metrics is always on
198+
199+
# OpenTelemetry
200+
pip install "headroom-ai[proxy,otel]"
201+
export HEADROOM_OTEL_METRICS_ENABLED=1
202+
export HEADROOM_OTEL_METRICS_ENDPOINT=https://otel.corp.example/v1/metrics
203+
export HEADROOM_OTEL_METRICS_HEADERS="authorization=Bearer XXX"
204+
export HEADROOM_OTEL_RESOURCE_ATTRIBUTES="service.instance.id=$HOSTNAME"
205+
```
206+
207+
| Variable | Default | Notes |
208+
|---|---|---|
209+
| `HEADROOM_OTEL_METRICS_ENABLED` | `0` | Master switch |
210+
| `HEADROOM_OTEL_METRICS_EXPORTER` | `otlp_http` | Or `console`. No gRPC exporter exists. |
211+
| `HEADROOM_OTEL_METRICS_ENDPOINT` | unset | Passed verbatim — `/v1/metrics` is **not** appended |
212+
| `HEADROOM_OTEL_METRICS_HEADERS` | unset | `k=v,k2=v2` |
213+
| `HEADROOM_OTEL_METRICS_EXPORT_INTERVAL_MS` | `10000` | |
214+
| `HEADROOM_OTEL_SERVICE_NAME` | `headroom-proxy` | |
215+
| `HEADROOM_OTEL_RESOURCE_ATTRIBUTES` | unset | **Set `service.instance.id` here** — Headroom doesn't, and replicas will collide |
216+
217+
Verify with `curl -s localhost:8787/stats | jq .otel`.
218+
219+
**Multi-tenant labels:** `register_otel_metric_attribute_provider()` adds request-scoped attributes (tenant, team, cost centre) to every OTel datapoint. Max 16 attributes, 256 chars each.
220+
221+
**Air-gapped deployments:** `HEADROOM_OFFLINE=1` disables all outbound traffic — the anonymous usage beacon (which is **on by default**), the update check, and model downloads.
222+
223+
---

headroom/agent_savings.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -207,27 +207,43 @@ def apply_proxy_env_defaults(self, env: MutableMapping[str, str]) -> MutableMapp
207207
def get_agent_savings_profile(name: str | None = None) -> AgentSavingsProfile:
208208
"""Return a named agent savings profile.
209209
210-
An unrecognized name falls back to the ``balanced`` profile with a warning
211-
instead of raising. The savings profile is a soft config knob, but it is
212-
resolved during proxy startup (``proxy_pipeline_kwargs`` -> ``create_app``),
213-
so raising here takes the whole proxy down before it can open its port. That
214-
happens on desktop/runtime version skew: a newer client requests a profile
215-
(e.g. ``coding``) that an older pinned or fallback runtime predates. Degrade
216-
to ``balanced`` rather than leaving the user with no proxy at all.
210+
An unrecognized name degrades with a warning instead of raising. The savings
211+
profile is a soft config knob, but it is resolved during proxy startup
212+
(``proxy_pipeline_kwargs`` -> ``create_app``), so raising here takes the
213+
whole proxy down before it can open its port. That happens on desktop/runtime
214+
version skew: a newer client requests a profile (e.g. ``coding``) that an
215+
older pinned or fallback runtime predates. Degrade rather than leaving the
216+
user with no proxy at all.
217+
218+
**Where it degrades to matters.** This used to land on ``balanced``
219+
unconditionally, which is a drastically different posture from the
220+
out-of-box default: cache->token mode, cross-turn dedup off, tool-search
221+
off, user messages uncompressed, the message floor 25x higher (250 vs 10)
222+
and the block floor 20x higher (500 vs 25). A single typo in
223+
``HEADROOM_SAVINGS_PROFILE`` therefore silently reconfigured the whole
224+
proxy, and the only trace was one WARNING at startup that operators read
225+
past. Prefer :data:`DEFAULT_PROFILE` — the documented out-of-box posture and
226+
the same thing an unset variable resolves to, so a typo now costs nothing.
227+
``balanced`` remains the last resort for the genuine version-skew case,
228+
where an older runtime has no ``DEFAULT_PROFILE`` entry to fall back to.
217229
"""
218230

219231
key = (name or DEFAULT_PROFILE).strip().lower()
220232
profile = _PROFILES.get(key)
221233
if profile is not None:
222234
return profile
235+
fallback_name = DEFAULT_PROFILE if DEFAULT_PROFILE in _PROFILES else FALLBACK_PROFILE
223236
valid = ", ".join(sorted(_PROFILES))
224237
logger.warning(
225-
"unknown savings profile %r; falling back to %r (known: %s)",
238+
"unknown savings profile %r; falling back to %r (known: %s). "
239+
"Set HEADROOM_SAVINGS_PROFILE to one of the known names, or unset it to "
240+
"get %r explicitly.",
226241
name,
227-
FALLBACK_PROFILE,
242+
fallback_name,
228243
valid,
244+
DEFAULT_PROFILE,
229245
)
230-
return _PROFILES[FALLBACK_PROFILE]
246+
return _PROFILES[fallback_name]
231247

232248

233249
def apply_agent_savings_env_defaults(
@@ -300,6 +316,28 @@ def proxy_pipeline_kwargs(config: object) -> dict[str, object]:
300316
# unset → Kompress decides / ambient default applies).
301317
if profile.target_ratio is not None:
302318
kwargs["target_ratio"] = profile.target_ratio
319+
# Block-compression char floor. Every OTHER router pipeline kwarg in this
320+
# function travels on the config object; this one alone was populated
321+
# only from ``HEADROOM_MIN_CHARS_FOR_BLOCK`` (read below), so a proxy
322+
# whose config carries ``savings_profile="coding"`` but whose process env
323+
# was never seeded applied every sibling coding knob while this floor
324+
# silently stayed at ``ContentRouterConfig.min_chars_for_block_compression``
325+
# (500) instead of the profile's 25 — a 20x gap on the gate that governs
326+
# tool_result blocks, the dominant content type in agent traffic.
327+
#
328+
# NOTE: this does not make the profile fully config-deliverable. The
329+
# profile's ``cross_turn_dedup`` / ``tool_search`` / ``lossless_then_lossy``
330+
# / ``protect_reads`` / ``code_aware`` / ``effort_router`` / ``lossless``
331+
# fields are still env-only, but by a different mechanism: their consumers
332+
# read ``os.environ`` directly (ContentRouter.__init__ for HEADROOM_DEDUPE,
333+
# the Anthropic handler for HEADROOM_TOOL_SEARCH) and never pass through
334+
# this function at all. Those remain seed-dependent and are the reason a
335+
# profile can still be half-applied; fixing them means threading each
336+
# consumer, which is a larger change than this one.
337+
#
338+
# The env read below still wins, since it is an explicit operator override.
339+
if profile.min_chars_for_block is not None:
340+
kwargs["min_chars_for_block_compression"] = profile.min_chars_for_block
303341

304342
if getattr(config, "compress_user_messages", False):
305343
kwargs["compress_user_messages"] = True

0 commit comments

Comments
 (0)