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.
Pre-this-PR, four handler files computed "should this request be
compressed?" inline at five sites with subtle drift. Three Gemini
sites silently ignored ``x-headroom-bypass: true``; one of those
also ignored the license gate. Anthropic and OpenAI got the full
conjunction right, but encoded it inline.
``CompressionDecision.decide(headers, config, usage_reporter,
messages)`` is the single canonical factory. Precedence:
1. ``bypass_header`` — user's explicit opt-out (highest)
2. ``compression_disabled`` — operator config.optimize=False
3. ``no_messages`` — nothing to compress
4. ``license_denied`` — commercial gate
The factory exposes every constituent boolean
(``bypass_header_set``, ``config_optimize_enabled``,
``license_allows``, ``has_messages``) so debug tooling can answer
"what did the decision see?" without re-running it.
Each migrated site now logs ``Compression skipped: reason=<X>`` on
passthrough — new structured observability.
Bug fixes that came with the consolidation:
* ``handlers/gemini.py:handle_gemini_generate_content`` — now
respects ``x-headroom-bypass``
* ``handlers/gemini.py:handle_google_cloudcode_stream`` — now
respects ``x-headroom-bypass``
* ``handlers/gemini.py:handle_gemini_count_tokens`` — now respects
``x-headroom-bypass`` AND the license gate (was missing both)