From e4e28b65f459df6879b38384842faaeba6357a2a Mon Sep 17 00:00:00 2001 From: chopratejas Date: Fri, 15 May 2026 14:26:01 -0700 Subject: [PATCH] fix(proxy): surface CompressionDecision.passthrough_reason in tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ``CompressionDecision.apply_to_tags(tags)`` — a one-liner mutator that stamps the passthrough reason into a tags dict for downstream observability. Each migrated handler now calls ``_decision.apply_to_tags(tags)`` immediately after ``CompressionDecision.decide(...)``. The tags dict flows unchanged into every downstream ``RequestOutcome(tags=tags, ...)`` construction, which the funnel surfaces in ``RequestLog.tags`` — same mechanism the funnel already uses for ``client``. Dashboards can now slice passthrough traffic by cause: * tags["passthrough_reason"] == "bypass_header" * tags["passthrough_reason"] == "compression_disabled" * tags["passthrough_reason"] == "no_messages" * tags["passthrough_reason"] == "license_denied" No-op when ``should_compress=True`` — compressing requests don't carry the tag, so absence vs presence is itself the signal. Bonus fix: ``handle_gemini_count_tokens`` was the one Gemini handler that never pulled tags out of headers, so its emitted ``RequestOutcome`` reached the dashboard without any of the per- request slicing keys. Added the missing ``tags = self._extract_tags (request.headers)`` and threaded ``tags=tags`` into its outcome. Closes the observability loop opened by PR #477: the four Gemini- bypass-bug fixes are now visible in the request-log feed the moment they fire. --- .claude-plugin/marketplace.json | 4 +- .github/plugin/marketplace.json | 4 +- headroom/proxy/compression_decision.py | 21 ++++ headroom/proxy/handlers/anthropic.py | 1 + headroom/proxy/handlers/gemini.py | 9 ++ headroom/proxy/handlers/openai.py | 1 + .../.claude-plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- tests/test_compression_decision.py | 109 ++++++++++++++++++ 9 files changed, 147 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 9ac7d0c7f..d321c0a7c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.", - "version": "0.21.38" + "version": "0.22.0" }, "plugins": [ { "name": "headroom", "source": "./plugins/headroom-agent-hooks", "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.", - "version": "0.21.38", + "version": "0.22.0", "author": { "name": "Headroom Contributors", "url": "https://github.com/chopratejas/headroom" diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 9ac7d0c7f..d321c0a7c 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.", - "version": "0.21.38" + "version": "0.22.0" }, "plugins": [ { "name": "headroom", "source": "./plugins/headroom-agent-hooks", "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.", - "version": "0.21.38", + "version": "0.22.0", "author": { "name": "Headroom Contributors", "url": "https://github.com/chopratejas/headroom" diff --git a/headroom/proxy/compression_decision.py b/headroom/proxy/compression_decision.py index 7d72a58b1..628caf2ee 100644 --- a/headroom/proxy/compression_decision.py +++ b/headroom/proxy/compression_decision.py @@ -145,3 +145,24 @@ class CompressionDecision: license_allows=license_ok, has_messages=has_msgs, ) + + def apply_to_tags(self, tags: dict[str, str]) -> None: + """Stamp the passthrough reason into a tags dict for downstream + observability. + + Mutates ``tags`` in place. No-op when ``should_compress=True`` + (compressing requests don't carry a ``passthrough_reason`` tag — + absence vs presence is itself the signal). + + Handler call pattern, after ``CompressionDecision.decide(...)``:: + + tags = self._extract_tags(headers) + _decision = CompressionDecision.decide(...) + _decision.apply_to_tags(tags) + # ... tags now carries passthrough_reason if applicable; + # every downstream RequestOutcome(tags=tags, ...) inherits + # it for free, which flows through emit_request_outcome() + # → RequestLog.tags → dashboard. + """ + if self.passthrough_reason is not None: + tags["passthrough_reason"] = self.passthrough_reason diff --git a/headroom/proxy/handlers/anthropic.py b/headroom/proxy/handlers/anthropic.py index 4b9ceefb8..7262186c7 100644 --- a/headroom/proxy/handlers/anthropic.py +++ b/headroom/proxy/handlers/anthropic.py @@ -893,6 +893,7 @@ class AnthropicHandlerMixin: usage_reporter=self.usage_reporter, messages=messages, ) + _decision.apply_to_tags(tags) if not _decision.should_compress: logger.info( f"[{request_id}] Compression skipped: reason={_decision.passthrough_reason}" diff --git a/headroom/proxy/handlers/gemini.py b/headroom/proxy/handlers/gemini.py index ec8bbf320..ef847a5f5 100644 --- a/headroom/proxy/handlers/gemini.py +++ b/headroom/proxy/handlers/gemini.py @@ -330,6 +330,7 @@ class GeminiHandlerMixin: usage_reporter=self.usage_reporter, messages=messages, ) + _decision.apply_to_tags(tags) if not _decision.should_compress: logger.info( f"[{request_id}] Compression skipped: reason={_decision.passthrough_reason}" @@ -642,6 +643,7 @@ class GeminiHandlerMixin: usage_reporter=self.usage_reporter, messages=messages, ) + _decision.apply_to_tags(tags) if not _decision.should_compress: logger.info( f"[{request_id}] Compression skipped: reason={_decision.passthrough_reason}" @@ -876,12 +878,18 @@ class GeminiHandlerMixin: transforms_applied: list[str] = [] optimized_messages = messages + # countTokens is the one Gemini handler that didn't pull tags + # out of headers; sibling handlers do and thread them into the + # outcome. Extract here so apply_to_tags below has a dict to + # mutate and the outcome at end-of-call inherits the tag. + tags = self._extract_tags(request.headers) _decision = CompressionDecision.decide( headers=request.headers, config=self.config, usage_reporter=self.usage_reporter, messages=messages, ) + _decision.apply_to_tags(tags) if not _decision.should_compress: logger.info( f"[{request_id}] Compression skipped: reason={_decision.passthrough_reason}" @@ -960,6 +968,7 @@ class GeminiHandlerMixin: attempted_input_tokens=compressed_tokens + tokens_saved, total_latency_ms=total_latency, transforms_applied=tuple(transforms_applied), + tags=tags, client=client, ) ) diff --git a/headroom/proxy/handlers/openai.py b/headroom/proxy/handlers/openai.py index 57915decf..ffb4df40a 100644 --- a/headroom/proxy/handlers/openai.py +++ b/headroom/proxy/handlers/openai.py @@ -1409,6 +1409,7 @@ class OpenAIHandlerMixin: usage_reporter=self.usage_reporter, messages=messages, ) + _decision.apply_to_tags(tags) if not _decision.should_compress: logger.info( f"[{request_id}] Compression skipped: reason={_decision.passthrough_reason}" diff --git a/plugins/headroom-agent-hooks/.claude-plugin/plugin.json b/plugins/headroom-agent-hooks/.claude-plugin/plugin.json index ab49bb9a8..218caf453 100644 --- a/plugins/headroom-agent-hooks/.claude-plugin/plugin.json +++ b/plugins/headroom-agent-hooks/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "headroom", - "version": "0.21.38", + "version": "0.22.0", "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.", "author": { "name": "Headroom Contributors", diff --git a/plugins/headroom-agent-hooks/.github/plugin/plugin.json b/plugins/headroom-agent-hooks/.github/plugin/plugin.json index 34c3be518..0116ff27b 100644 --- a/plugins/headroom-agent-hooks/.github/plugin/plugin.json +++ b/plugins/headroom-agent-hooks/.github/plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "headroom", - "version": "0.21.38", + "version": "0.22.0", "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.", "author": { "name": "Headroom Contributors", diff --git a/tests/test_compression_decision.py b/tests/test_compression_decision.py index 708a89409..95d16286d 100644 --- a/tests/test_compression_decision.py +++ b/tests/test_compression_decision.py @@ -380,3 +380,112 @@ def test_decide_with_missing_messages_field_on_body() -> None: d = CompressionDecision.decide(headers={}, config=_config(), usage_reporter=None, messages=None) assert d.should_compress is False assert d.passthrough_reason == "no_messages" + + +# ── apply_to_tags: thread passthrough_reason into RequestOutcome.tags ─ +# +# Handlers compute ``tags = self._extract_tags(headers)`` at entry and +# pass that dict through to every downstream ``RequestOutcome`` +# construction. ``decision.apply_to_tags(tags)`` is a one-liner mutation +# at the post-decision point that gives every downstream outcome the +# ``passthrough_reason`` for free — no need to thread the decision +# through five layers of helper calls. The outcome funnel then surfaces +# ``tags["passthrough_reason"]`` in ``RequestLog.tags`` (dashboard +# slicing) — same mechanism the funnel already uses for ``client``. + + +def test_apply_to_tags_stamps_reason_when_passthrough() -> None: + """On a passthrough decision, ``apply_to_tags`` mutates the supplied + tags dict in place with ``passthrough_reason = ``. This + is the single integration point between the input-side decision and + the output-side ``RequestOutcome``.""" + d = CompressionDecision.decide( + headers={"x-headroom-bypass": "true"}, + config=_config(), + usage_reporter=None, + messages=_msgs(), + ) + tags: dict[str, str] = {} + d.apply_to_tags(tags) + assert tags == {"passthrough_reason": "bypass_header"} + + +def test_apply_to_tags_is_a_noop_when_compressing() -> None: + """When the decision is "compress" (``passthrough_reason is None``), + the tags dict must be left untouched — no spurious + ``passthrough_reason=None`` string entry, which would mislead any + dashboard that filters on tag presence.""" + d = CompressionDecision.decide( + headers={}, config=_config(), usage_reporter=None, messages=_msgs() + ) + assert d.should_compress is True + tags: dict[str, str] = {"client": "codex"} + d.apply_to_tags(tags) + assert tags == {"client": "codex"} # untouched + assert "passthrough_reason" not in tags + + +def test_apply_to_tags_preserves_pre_existing_entries() -> None: + """``apply_to_tags`` is a mutator over the existing tags dict, not + a replacement. Pre-existing entries (``client``, custom routing + tags, etc.) must survive unchanged.""" + d = CompressionDecision.decide( + headers={}, config=_config(optimize=False), usage_reporter=None, messages=_msgs() + ) + tags: dict[str, str] = {"client": "aider", "route": "alpha"} + d.apply_to_tags(tags) + assert tags == { + "client": "aider", + "route": "alpha", + "passthrough_reason": "compression_disabled", + } + + +def test_apply_to_tags_for_every_passthrough_reason() -> None: + """Every passthrough reason name must round-trip through + ``apply_to_tags`` exactly — these strings are the dashboard's + slicing keys; a typo would silently break filtering.""" + reason_to_inputs: dict[str, dict[str, Any]] = { + "bypass_header": { + "headers": {"x-headroom-bypass": "true"}, + "config": _config(), + "usage_reporter": None, + "messages": _msgs(), + }, + "compression_disabled": { + "headers": {}, + "config": _config(optimize=False), + "usage_reporter": None, + "messages": _msgs(), + }, + "no_messages": { + "headers": {}, + "config": _config(), + "usage_reporter": None, + "messages": [], + }, + "license_denied": { + "headers": {}, + "config": _config(), + "usage_reporter": _usage_reporter(should_compress=False), + "messages": _msgs(), + }, + } + for expected_reason, decide_kwargs in reason_to_inputs.items(): + d = CompressionDecision.decide(**decide_kwargs) + tags: dict[str, str] = {} + d.apply_to_tags(tags) + assert tags.get("passthrough_reason") == expected_reason, expected_reason + + +def test_apply_to_tags_overwrites_a_pre_existing_passthrough_reason() -> None: + """If a tag with the same key existed before (a contrived case — + handlers don't write this tag elsewhere), the decision overwrites + it. The decision is the canonical source of truth for this tag; + anything earlier was stale or wrong.""" + d = CompressionDecision.decide( + headers={}, config=_config(optimize=False), usage_reporter=None, messages=_msgs() + ) + tags: dict[str, str] = {"passthrough_reason": "stale_value"} + d.apply_to_tags(tags) + assert tags["passthrough_reason"] == "compression_disabled"