feat: add dashboard agent usage stats (#814)

## Description

Add a clear dashboard view for per-agent token usage so end users can
see Cursor, Claude, Codex, and other detected clients with before/after
token counts, tokens saved, and savings percentages. The stats API now
exposes a stable `agent_usage` object that the dashboard renders near
the top of the session view.

Fixes #

## Type of Change

- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

### New Files

**Tests:**
- `tests/test_dashboard_agent_usage.py` — Covers agent classification,
exact per-request aggregation, and aggregate fallback behavior.

### Modified Files

- `headroom/proxy/server.py` — Adds per-agent usage aggregation to
`/stats` with before tokens, after tokens, output tokens, saved tokens,
savings percentage, source, providers, and models.
- `headroom/dashboard/templates/dashboard.html` — Adds a prominent Agent
Usage panel with totals, coverage status, per-agent token-flow bars,
request counts, before/after tokens, saved tokens, and share of savings.

## Testing

- [x] Unit tests pass: `.venv312/bin/pytest
tests/test_dashboard_agent_usage.py`
- [x] Linting passes: `.venv312/bin/ruff check headroom/proxy/server.py
tests/test_dashboard_agent_usage.py`
- [x] Diff whitespace check passes: `git diff --check
origin/main...HEAD`
- [x] Dashboard smoke render: local proxy on `127.0.0.1:8790`, captured
Chrome headless screenshot of `/dashboard`
- [x] New tests added for new functionality

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing relevant unit tests pass locally with my changes
- [ ] I have made corresponding changes to the documentation
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

The agent usage panel uses exact request-log data when available. If
detailed request logs are empty, it falls back to aggregate
provider/model request counts and labels the coverage as aggregate
fallback so users are not misled.
This commit is contained in:
Michael Sam 2026-06-12 22:12:22 +03:00 committed by GitHub
parent dff6a19946
commit 6d3f39f213
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1424 additions and 51 deletions

View file

@ -195,8 +195,19 @@ jobs:
run: |
pytest tests scripts/tests \
--splits 4 --group ${{ matrix.shard }} \
--cov=headroom --cov-branch \
--cov-report=xml:coverage-${{ matrix.shard }}.xml \
--cov-report= \
--tb=short -q
- name: Upload coverage shard ${{ matrix.shard }} to Codecov
uses: codecov/codecov-action@v5
with:
files: coverage-${{ matrix.shard }}.xml
flags: python
name: python-shard-${{ matrix.shard }}
fail_ci_if_error: true
test-extras:
needs: [changes, build-wheel]
if: needs.changes.outputs.code == 'true'

View file

@ -15,5 +15,6 @@ ignore:
- "scripts/tests/**"
- ".github/**"
- ".claude-plugin/**"
- "headroom/dashboard/templates/**"
- "plugins/headroom-agent-hooks/.claude-plugin/**"
- "plugins/headroom-agent-hooks/.github/**"

View file

@ -783,12 +783,6 @@ def proxy(
optimize=not no_optimize,
cache_enabled=not no_cache,
rate_limit_enabled=not no_rate_limit,
# CCR opt-outs for compression-only deployments (streaming / non-MCP
# clients that can't resolve the injected retrieve tool). Defaults keep
# CCR fully on; each flag flips one dataclass default to False.
ccr_inject_tool=not no_ccr_inject_tool,
ccr_inject_marker=not no_ccr_marker,
ccr_proactive_expansion=not no_ccr_proactive_expansion,
compress_user_messages=_get_env_bool("HEADROOM_COMPRESS_USER_MESSAGES", False),
min_tokens_to_crush=_get_env_int_optional("HEADROOM_MIN_TOKENS") or 500,
max_items_after_crush=_get_env_int_optional("HEADROOM_MAX_ITEMS") or 50,
@ -799,6 +793,12 @@ def proxy(
protect_recent=_get_env_int_optional("HEADROOM_PROTECT_RECENT"),
protect_analysis_context=_get_env_bool_optional("HEADROOM_PROTECT_ANALYSIS_CONTEXT"),
accuracy_guard=os.environ.get("HEADROOM_ACCURACY_GUARD") or None,
# CCR opt-outs for compression-only deployments (streaming / non-MCP
# clients that can't resolve the injected retrieve tool). Defaults keep
# CCR fully on; each flag flips one dataclass default to False.
ccr_inject_tool=not no_ccr_inject_tool,
ccr_inject_marker=not no_ccr_marker,
ccr_proactive_expansion=not no_ccr_proactive_expansion,
# Flatten repeat-flag tuple AND any comma-separated values inside it.
# `--proxy-extension a,b --proxy-extension c` and `HEADROOM_PROXY_EXTENSIONS=a,b,c`
# both yield ["a", "b", "c"]. None when nothing was supplied.

View file

@ -38,6 +38,9 @@ if sys.platform == "win32" and hasattr(sys.stdout, "buffer"):
import click
from headroom._version import __version__ as _HEADROOM_VERSION
from headroom.agent_savings import (
apply_agent_savings_env_defaults,
)
from headroom.copilot_auth import (
has_oauth_auth,
resolve_client_bearer_token,
@ -95,6 +98,7 @@ _CONTEXT_TOOL_ENV = "HEADROOM_CONTEXT_TOOL"
_CONTEXT_TOOL_RTK = "rtk"
_CONTEXT_TOOL_LEAN_CTX = "lean-ctx"
_VALID_CONTEXT_TOOLS = {_CONTEXT_TOOL_RTK, _CONTEXT_TOOL_LEAN_CTX}
_AGENT_SAVINGS_TARGET_AGENTS = {"claude", "codex", "cursor"}
_WRAP_PROXY_TIMEOUT_ENV = "HEADROOM_WRAP_PROXY_TIMEOUT"
_WRAP_PROXY_TIMEOUT_DEFAULT_SECONDS = 45
_WRAP_PROXY_TIMEOUT_ML_DEFAULT_SECONDS = 90
@ -360,6 +364,8 @@ def _start_proxy(
# Ensure proxy subprocess uses UTF-8 (Windows defaults to cp1252)
proxy_env = os.environ.copy()
proxy_env["PYTHONIOENCODING"] = "utf-8"
if agent_type in {"claude", "codex", "cursor"}:
apply_agent_savings_env_defaults(proxy_env)
# Tell the proxy which agent is being wrapped (for traffic learning output)
if agent_type != "unknown":
@ -367,8 +373,6 @@ def _start_proxy(
proxy_env.setdefault("HEADROOM_STACK", f"wrap_{agent_type}")
savings_profile = _wrap_agent_savings_profile(agent_type)
if savings_profile is not None:
from headroom.agent_savings import apply_agent_savings_env_defaults
apply_agent_savings_env_defaults(proxy_env, savings_profile)
if openai_api_url:
proxy_env["OPENAI_TARGET_API_URL"] = openai_api_url
@ -1553,6 +1557,77 @@ def _proxy_health_config(payload: dict[str, Any] | None) -> dict[str, Any] | Non
return config if isinstance(config, dict) else None
def _env_bool_value(value: str) -> bool:
return value.strip().lower() in {"1", "true", "yes", "on"}
def _agent_savings_config_mismatches(
running_config: dict[str, Any],
agent_type: str,
) -> list[str]:
"""Return restart reasons when a running proxy lacks target agent savings."""
if agent_type not in _AGENT_SAVINGS_TARGET_AGENTS:
return []
desired_env = os.environ.copy()
apply_agent_savings_env_defaults(desired_env)
checks: tuple[tuple[str, str, str, str], ...] = (
("HEADROOM_SAVINGS_PROFILE", "savings_profile", "savings-profile", "str"),
("HEADROOM_TARGET_RATIO", "target_ratio", "target-ratio", "float"),
(
"HEADROOM_COMPRESS_USER_MESSAGES",
"compress_user_messages",
"compress-user-messages",
"bool",
),
(
"HEADROOM_COMPRESS_SYSTEM_MESSAGES",
"compress_system_messages",
"compress-system-messages",
"bool",
),
("HEADROOM_PROTECT_RECENT", "protect_recent", "protect-recent", "int"),
(
"HEADROOM_PROTECT_ANALYSIS_CONTEXT",
"protect_analysis_context",
"protect-analysis-context",
"bool",
),
("HEADROOM_MIN_TOKENS", "min_tokens_to_crush", "min-tokens", "int"),
("HEADROOM_MAX_ITEMS", "max_items_after_crush", "max-items", "int"),
(
"HEADROOM_SMART_CRUSHER_COMPACTION",
"smart_crusher_with_compaction",
"smart-crusher-compaction",
"bool",
),
("HEADROOM_ACCURACY_GUARD", "accuracy_guard", "accuracy-guard", "str"),
)
mismatches: list[str] = []
for env_key, config_key, label, value_type in checks:
expected = desired_env.get(env_key)
if expected is None:
continue
actual = running_config.get(config_key)
try:
if value_type == "float":
matches = actual is not None and abs(float(actual) - float(expected)) < 1e-9
elif value_type == "int":
matches = actual is not None and int(actual) == int(expected)
elif value_type == "bool":
matches = actual is not None and bool(actual) is _env_bool_value(expected)
else:
matches = str(actual or "").strip().lower() == expected.strip().lower()
except (TypeError, ValueError):
matches = False
if not matches:
mismatches.append(label)
return mismatches
def _proxy_active_session_count(payload: dict[str, Any] | None) -> int:
"""Return active session count from /health runtime metadata."""
if payload is None:
@ -4310,6 +4385,6 @@ def unwrap_codex(port: int, no_stop_proxy: bool) -> None:
click.echo()
click.echo("✓ Codex is no longer routed through the Headroom proxy.")
if not no_stop_proxy:
if not no_stop_proxy and status != "noop":
_echo_unwrap_proxy_stop_status(_stop_local_proxy_for_unwrap(port), port)
click.echo()

View file

@ -58,9 +58,10 @@ from __future__ import annotations
import logging
import threading
from dataclasses import dataclass, field
from dataclasses import dataclass, field, replace
from typing import Any
from .agent_savings import apply_agent_savings_profile
from .observability import get_otel_metrics
from .pipeline import PipelineExtensionManager, PipelineStage, summarize_routing_markers
from .utils import extract_user_query as _extract_user_query
@ -133,6 +134,9 @@ class CompressConfig:
Set to 'disabled' to skip ML compression entirely
(only SmartCrusher + CacheAligner will run)."""
savings_profile: str | None = None
"""Named high-savings profile, e.g. 'agent-90' for Codex/Claude/Cursor."""
@dataclass
class CompressResult:
@ -204,6 +208,9 @@ def compress(
for key, value in kwargs.items():
if key in config_fields:
setattr(cfg, key, value)
if cfg.savings_profile:
cfg = replace(cfg)
apply_agent_savings_profile(cfg, cfg.savings_profile)
pipeline = _get_pipeline()
pipeline_extensions = PipelineExtensionManager(hooks=hooks, discover=False)

View file

@ -133,6 +133,18 @@
<span class="text-xs text-amber-400 font-medium">Anon Telemetry</span>
</div>
</template>
<template x-if="stats.config && stats.config.savings_profile">
<div class="inline-flex items-center gap-1.5 rounded-full border border-cyan-500/40 bg-cyan-500/10 px-2.5 py-1"
:title="'Current proxy profile: ' + stats.config.savings_profile">
<span class="w-1.5 h-1.5 rounded-full bg-cyan-400"></span>
<span class="text-xs text-cyan-100">
<span x-text="stats.config.savings_profile"></span>
<template x-if="stats.config.target_savings_percent !== null">
<span x-text="' · target ' + stats.config.target_savings_percent + '%'"></span>
</template>
</span>
</div>
</template>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500">Status</span>
<span class="flex items-center gap-1.5">
@ -229,6 +241,106 @@
</div>
</div>
<!-- Agent Usage -->
<div class="bg-surface rounded-lg border border-border overflow-hidden mb-6">
<div class="px-4 py-3 border-b border-border flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div>
<div class="text-sm font-medium text-gray-300">Agent Usage</div>
<div class="text-xs text-gray-500">Before and after token usage by detected client</div>
</div>
<div class="flex flex-wrap items-center gap-3 text-xs">
<span class="text-gray-500" x-text="'Coverage: ' + agentCoverageLabel"></span>
<span class="px-2 py-0.5 rounded border border-border bg-[#141414] font-mono text-gray-400"
x-text="formatNumber(stats.agent_usage?.totals?.requests || 0) + ' requests'"></span>
</div>
</div>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-5">
<div class="rounded-lg border border-border bg-[#141414] p-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Before</div>
<div class="text-2xl font-light tabular-nums" x-text="formatNumber(stats.agent_usage?.totals?.before_tokens || 0)"></div>
</div>
<div class="rounded-lg border border-border bg-[#141414] p-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">After</div>
<div class="text-2xl font-light tabular-nums text-gray-200" x-text="formatNumber(stats.agent_usage?.totals?.after_tokens || 0)"></div>
</div>
<div class="rounded-lg border border-border bg-[#141414] p-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Saved</div>
<div class="text-2xl font-light tabular-nums text-accent" x-text="formatNumber(stats.agent_usage?.totals?.tokens_saved || 0)"></div>
</div>
<div class="rounded-lg border border-border bg-[#141414] p-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Savings</div>
<div class="text-2xl font-light tabular-nums text-emerald-400" x-text="(stats.agent_usage?.totals?.savings_percent || 0).toFixed(1) + '%'"></div>
</div>
</div>
<template x-if="agentRows.length > 0">
<div class="space-y-3">
<template x-for="agent in agentRows" :key="agent.agent">
<div class="rounded-lg border border-border bg-[#141414] p-3">
<div class="grid grid-cols-1 gap-3 lg:grid-cols-[minmax(160px,0.9fr)_minmax(260px,1.5fr)_minmax(260px,1.2fr)] lg:items-center">
<div class="min-w-0">
<div class="flex items-center gap-2">
<span class="h-2.5 w-2.5 rounded-full" :class="agentDotClass(agent.agent)"></span>
<span class="text-sm font-medium text-gray-200 truncate" x-text="agent.label"></span>
</div>
<div class="mt-1 text-xs text-gray-500">
<span x-text="formatNumber(agent.requests || 0) + ' requests'"></span>
<span class="mx-1 text-gray-700">/</span>
<span x-text="agent.source"></span>
</div>
</div>
<div>
<div class="flex items-center justify-between text-xs mb-1">
<span class="text-gray-500">Token flow</span>
<span class="font-mono text-emerald-400" x-text="(agent.savings_percent || 0).toFixed(1) + '% saved'"></span>
</div>
<div class="h-3 w-full rounded-full bg-border overflow-hidden flex">
<div class="h-full bg-emerald-500 transition-all duration-500"
:style="'width:' + agentSavedWidth(agent) + '%'"></div>
<div class="h-full bg-accent/60 transition-all duration-500"
:style="'width:' + agentAfterWidth(agent) + '%'"></div>
</div>
<div class="mt-1 flex flex-wrap gap-3 text-xs text-gray-500">
<span class="inline-flex items-center gap-1"><span class="h-2 w-2 rounded-full bg-emerald-500"></span>Saved</span>
<span class="inline-flex items-center gap-1"><span class="h-2 w-2 rounded-full bg-accent/60"></span>Sent</span>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 text-right">
<div>
<div class="text-[11px] uppercase tracking-wide text-gray-500">Before</div>
<div class="font-mono text-sm" x-text="formatNumber(agent.before_tokens || 0)"></div>
</div>
<div>
<div class="text-[11px] uppercase tracking-wide text-gray-500">After</div>
<div class="font-mono text-sm" x-text="formatNumber(agent.after_tokens || 0)"></div>
</div>
<div>
<div class="text-[11px] uppercase tracking-wide text-gray-500">Saved</div>
<div class="font-mono text-sm text-accent" x-text="formatNumber(agent.tokens_saved || 0)"></div>
</div>
<div>
<div class="text-[11px] uppercase tracking-wide text-gray-500">Share</div>
<div class="font-mono text-sm text-gray-300" x-text="(agent.share_of_saved_percent || 0).toFixed(1) + '%'"></div>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
<template x-if="agentRows.length === 0">
<div class="rounded-lg border border-dashed border-border p-6 text-center text-sm text-gray-500">
Agent usage appears after Cursor, Claude, Codex, or another client sends traffic through this proxy.
</div>
</template>
</div>
</div>
<!-- Savings Breakdown -->
<template x-if="(stats.cost?.savings_usd || 0) > 0 || (stats.cost?.cache_savings_usd || 0) > 0">
<div class="bg-surface rounded-lg p-4 border border-border mb-6">
@ -1763,6 +1875,48 @@
.substring(0, 20);
},
// --- Agent Usage ---
get agentRows() {
return this.stats.agent_usage?.agents || [];
},
get agentCoverageLabel() {
const coverage = this.stats.agent_usage?.coverage || {};
if (coverage.mode === 'request_logs') {
return this.formatNumber(coverage.logged_requests || 0) + ' logged requests';
}
return 'aggregate fallback';
},
agentSavedWidth(agent) {
const before = agent.before_tokens || 0;
if (before <= 0) return 0;
return Math.min(100, Math.max(0, (agent.tokens_saved || 0) / before * 100)).toFixed(1);
},
agentAfterWidth(agent) {
const before = agent.before_tokens || 0;
if (before <= 0) return 0;
return Math.min(100, Math.max(0, (agent.after_tokens || 0) / before * 100)).toFixed(1);
},
agentDotClass(agent) {
const colors = {
'claude-code': 'bg-orange-400',
claude: 'bg-orange-400',
codex: 'bg-emerald-400',
cursor: 'bg-cyan-400',
copilot: 'bg-violet-400',
openai: 'bg-sky-400',
anthropic: 'bg-orange-400',
gemini: 'bg-rose-400',
aider: 'bg-amber-400',
unknown: 'bg-gray-500',
};
return colors[agent] || 'bg-gray-400';
},
// --- Historical View ---
get historyGranularityOptions() {

View file

@ -11,6 +11,7 @@ Anthropic), not the full input price. This prevents overstating dollar savings.
from __future__ import annotations
import logging
import os
import re
from dataclasses import asdict, dataclass, field
from datetime import datetime, timedelta
@ -134,6 +135,7 @@ class PerfRecord:
timestamp: str
request_id: str
model: str = ""
client: str = ""
num_messages: int = 0
tokens_before: int = 0
tokens_after: int = 0
@ -143,7 +145,6 @@ class PerfRecord:
cache_hit_pct: int = 0
optimization_ms: float = 0
transforms: list[str] = field(default_factory=list)
client: str = ""
@dataclass
@ -234,7 +235,7 @@ def parse_log_files(last_n_hours: float = 168.0) -> PerfReport:
report = PerfReport()
report.requested_hours = last_n_hours
log_dir = _paths.log_dir()
log_dir = _paths.log_dir() if os.environ.get("HEADROOM_WORKSPACE_DIR") else LOG_DIR
if not log_dir.exists():
return report
@ -299,6 +300,7 @@ def parse_log_files(last_n_hours: float = 168.0) -> PerfReport:
timestamp=ts,
request_id=m.group("rid"),
model=kv.get("model", ""),
client=kv.get("client", ""),
num_messages=int(kv.get("msgs", 0)),
tokens_before=int(kv.get("tok_before", 0)),
tokens_after=int(kv.get("tok_after", 0)),
@ -308,7 +310,6 @@ def parse_log_files(last_n_hours: float = 168.0) -> PerfReport:
cache_hit_pct=int(kv.get("cache_hit_pct", 0)),
optimization_ms=float(kv.get("opt_ms", 0)),
transforms=transforms,
client=kv.get("client", ""),
)
)
continue

View file

@ -217,7 +217,7 @@ CLIENT_UA_MAP: tuple[tuple[str, str], ...] = (
)
def classify_client(headers: Mapping[str, Any] | Any) -> str | None:
def classify_client(headers: Mapping[str, Any] | Any, *, default: str | None = None) -> str | None:
"""Identify the client harness (Codex / Claude Code / aider / etc).
Decision order:
@ -250,7 +250,7 @@ def classify_client(headers: Mapping[str, Any] | Any) -> str | None:
for needle, name in CLIENT_UA_MAP:
if needle in ua_lower:
return name
return None
return default
__all__ = [

View file

@ -23,6 +23,7 @@ if TYPE_CHECKING:
import httpx
from headroom.agent_savings import proxy_pipeline_kwargs
from headroom.copilot_auth import build_copilot_upstream_url
from headroom.pipeline import PipelineStage, summarize_routing_markers
from headroom.proxy.auth_mode import classify_auth_mode, classify_client
@ -663,7 +664,7 @@ class AnthropicHandlerMixin:
# Identify the harness (codex / claude-code / aider / etc.)
# from User-Agent or X-Client. Surfaced via the funnel into
# PERF logs and RequestLog.tags — see RequestOutcome.client.
client = classify_client(headers)
client = classify_client(headers, default="claude")
# PR-A5 (P5-49): strip internal x-headroom-* from upstream-bound
# headers AFTER `_extract_tags` reads them. Inbound bypass gating
# uses `request.headers.get(...)` directly above; memory user-id
@ -1066,6 +1067,7 @@ class AnthropicHandlerMixin:
biases=biases,
request_id=request_id,
compression_policy=compression_policy,
**proxy_pipeline_kwargs(self.config),
),
timeout=COMPRESSION_TIMEOUT_SECONDS,
)
@ -1106,6 +1108,7 @@ class AnthropicHandlerMixin:
biases=biases,
request_id=request_id,
compression_policy=compression_policy,
**proxy_pipeline_kwargs(self.config),
),
timeout=COMPRESSION_TIMEOUT_SECONDS,
)
@ -1137,6 +1140,7 @@ class AnthropicHandlerMixin:
biases=biases,
request_id=request_id,
compression_policy=compression_policy,
**proxy_pipeline_kwargs(self.config),
),
timeout=COMPRESSION_TIMEOUT_SECONDS,
)
@ -2550,7 +2554,7 @@ class AnthropicHandlerMixin:
headers = dict(request.headers.items())
headers.pop("host", None)
headers.pop("content-length", None)
client = classify_client(headers)
client = classify_client(headers, default="claude")
tags = extract_tags(headers)
# PR-A5 (P5-49): strip internal x-headroom-* before forwarding upstream.
from headroom.proxy.helpers import _strip_internal_headers, log_outbound_headers
@ -2802,7 +2806,7 @@ class AnthropicHandlerMixin:
headers = dict(request.headers.items())
headers.pop("host", None)
client = classify_client(headers)
client = classify_client(headers, default="claude")
tags = extract_tags(headers)
# PR-A5 (P5-49): strip internal x-headroom-* before forwarding upstream.
from headroom.proxy.helpers import _strip_internal_headers, log_outbound_headers
@ -2925,7 +2929,7 @@ class AnthropicHandlerMixin:
headers = dict(request.headers.items())
headers.pop("host", None)
client = classify_client(headers)
client = classify_client(headers, default="claude")
tags = extract_tags(headers)
# PR-A5 (P5-49): strip internal x-headroom-* before forwarding upstream.
from headroom.proxy.helpers import _strip_internal_headers, log_outbound_headers

View file

@ -41,6 +41,7 @@ if TYPE_CHECKING:
import httpx
from headroom.agent_savings import proxy_pipeline_kwargs
from headroom.copilot_auth import apply_copilot_api_auth, build_copilot_upstream_url
from headroom.pipeline import PipelineStage, summarize_routing_markers
from headroom.proxy.auth_mode import classify_auth_mode, classify_client
@ -139,7 +140,12 @@ def _openai_responses_unit_executor() -> ThreadPoolExecutor:
return _OPENAI_RESPONSES_UNIT_EXECUTOR
def _openai_responses_unit_cache_key(unit: Any, *, model: str) -> str:
def _openai_responses_unit_cache_key(
unit: Any,
*,
model: str,
target_ratio: float | None = None,
) -> str:
text_hash = hashlib.sha256(unit.text.encode("utf-8", errors="replace")).hexdigest()
key_payload = {
"version": _OPENAI_RESPONSES_UNIT_CACHE_VERSION,
@ -155,6 +161,7 @@ def _openai_responses_unit_cache_key(unit: Any, *, model: str) -> str:
"question": unit.question,
"bias": unit.bias,
"metadata": unit.metadata,
"target_ratio": target_ratio,
"text_sha256": text_hash,
}
serialized = json.dumps(key_payload, sort_keys=True, separators=(",", ":"), default=str)
@ -646,6 +653,10 @@ class OpenAIHandlerMixin:
if router is None:
logger.debug("[%s] OpenAI Responses ContentRouter unavailable", request_id)
return payload, False, 0, [], {}, [], 0
profile_kwargs = proxy_pipeline_kwargs(getattr(self, "config", None))
unit_target_ratio = profile_kwargs.get("target_ratio")
if unit_target_ratio is not None:
unit_target_ratio = float(unit_target_ratio)
try:
tokenizer = self.openai_provider.get_token_counter(model)
@ -877,7 +888,12 @@ class OpenAIHandlerMixin:
# `elapsed_ms=60000+` in production logs even though they did
# no work. With the semaphore deleted, this timer is honest.
unit_started = time.perf_counter()
result = compress_unit_with_router(routed.unit, router=router, tokenizer=tokenizer)
result = compress_unit_with_router(
routed.unit,
router=router,
tokenizer=tokenizer,
target_ratio=unit_target_ratio,
)
elapsed_ms = (time.perf_counter() - unit_started) * 1000.0
return routed.slot, result, elapsed_ms
@ -886,7 +902,11 @@ class OpenAIHandlerMixin:
cache_misses: list[tuple[int, str, RoutedCompressionUnit]] = []
cache_miss_followers: dict[str, list[int]] = {}
for unit_idx, routed in enumerate(routed_units):
cache_key = _openai_responses_unit_cache_key(routed.unit, model=model)
cache_key = _openai_responses_unit_cache_key(
routed.unit,
model=model,
target_ratio=unit_target_ratio,
)
cached = self._get_openai_responses_cached_unit(cache_key)
if cached is not None:
routed_results[unit_idx] = (routed.slot, cached, 0.0)
@ -2975,6 +2995,8 @@ class OpenAIHandlerMixin:
)
headers, is_chatgpt_auth = _resolve_codex_routing_headers(headers)
if is_chatgpt_auth:
client = "codex"
# Route to correct endpoint based on auth mode.
# ChatGPT session auth (codex login) uses chatgpt.com, not api.openai.com.
@ -4876,7 +4898,8 @@ class OpenAIHandlerMixin:
f"cache_write={_perf_cache_write} "
f"cache_hit_pct={_perf_cache_hit_pct} "
f"opt_ms={overhead_delta_ms:.0f} "
f"transforms={_summarize_transforms(transforms_applied)}"
f"transforms={_summarize_transforms(transforms_applied)} "
f"client={client or ''}"
)
ws_recorded_input_tokens_total = ws_input_tokens_total
@ -5816,7 +5839,10 @@ class OpenAIHandlerMixin:
protect_recent = compress_config.get("protect_recent")
protect_analysis_context = compress_config.get("protect_analysis_context")
pipeline_kwargs: dict = {"model_limit": context_limit}
pipeline_kwargs: dict = {
"model_limit": context_limit,
**proxy_pipeline_kwargs(self.config),
}
if compress_user_messages:
pipeline_kwargs["compress_user_messages"] = True
if target_ratio is not None:

View file

@ -191,6 +191,240 @@ _build_session_summary = build_session_summary
_merge_cost_stats = merge_cost_stats
_AGENT_LABELS: dict[str, str] = {
"claude": "Claude",
"claude-code": "Claude",
"claude_cli": "Claude",
"claude-code-cli": "Claude",
"codex": "Codex",
"codex-cli": "Codex",
"cursor": "Cursor",
"copilot": "GitHub Copilot",
"github-copilot": "GitHub Copilot",
"aider": "Aider",
"zed": "Zed",
"opencode": "OpenCode",
"openclaw": "OpenClaw",
"gemini": "Gemini",
"google": "Gemini",
"vertex:google": "Gemini",
"anthropic": "Claude",
"openai": "OpenAI",
"unknown": "Unidentified",
}
_AGENT_SOURCE_PRIORITY: dict[str, int] = {
"unknown": 0,
"provider": 1,
"model": 2,
"stack": 3,
"client": 4,
}
def _normalize_agent_key(raw: Any) -> str | None:
if raw is None:
return None
value = str(raw).strip().lower()
if not value:
return None
value = value.replace(" ", "-").replace("_", "-")
if value.startswith("wrap-"):
value = value.removeprefix("wrap-")
if value in {"claude-cli", "claude-code", "claude-code-cli"}:
return "claude-code"
if value in {"codex-cli", "codex"}:
return "codex"
if value in {"github-copilot", "copilot"}:
return "copilot"
if value in {"google", "vertex-google", "vertex:google"}:
return "gemini"
return value
def _agent_label(agent_key: str) -> str:
if agent_key in _AGENT_LABELS:
return _AGENT_LABELS[agent_key]
return agent_key.replace("-", " ").replace("_", " ").title()
def _classify_agent_from_log(entry: dict[str, Any]) -> tuple[str, str, str]:
raw_tags = entry.get("tags")
tags = raw_tags if isinstance(raw_tags, dict) else {}
for source, candidate in (
("client", tags.get("client")),
("stack", tags.get("stack") or tags.get("headroom-stack")),
):
key = _normalize_agent_key(candidate)
if key:
return key, _agent_label(key), source
model = str(entry.get("model") or "").lower()
if "codex" in model:
return "codex", _agent_label("codex"), "model"
if "claude" in model:
return "claude-code", _agent_label("claude-code"), "model"
if "gemini" in model:
return "gemini", _agent_label("gemini"), "model"
key = _normalize_agent_key(entry.get("provider"))
if key:
return key, _agent_label(key), "provider"
return "unknown", _agent_label("unknown"), "unknown"
def _build_agent_usage_summary(
logs: list[dict[str, Any]],
*,
requests_by_provider: dict[str, int],
requests_by_model: dict[str, int],
global_before_tokens: int,
global_after_tokens: int,
global_tokens_saved: int,
global_output_tokens: int,
) -> dict[str, Any]:
agents: dict[str, dict[str, Any]] = {}
def _agent_row(agent_key: str, label: str, source: str) -> dict[str, Any]:
row = agents.setdefault(
agent_key,
{
"agent": agent_key,
"label": label,
"source": source,
"requests": 0,
"before_tokens": 0,
"after_tokens": 0,
"output_tokens": 0,
"tokens_saved": 0,
"models": {},
"providers": {},
"has_exact_tokens": False,
},
)
if _AGENT_SOURCE_PRIORITY.get(source, 0) > _AGENT_SOURCE_PRIORITY.get(
str(row.get("source") or "unknown"), 0
):
row["source"] = source
return row
for entry in logs:
agent_key, label, source = _classify_agent_from_log(entry)
row = _agent_row(agent_key, label, source)
before = max(0, int(entry.get("input_tokens_original") or 0))
after = max(0, int(entry.get("input_tokens_optimized") or 0))
saved = max(0, int(entry.get("tokens_saved") or 0))
output = max(0, int(entry.get("output_tokens") or 0))
provider = str(entry.get("provider") or "unknown")
model = str(entry.get("model") or "unknown")
row["requests"] += 1
row["before_tokens"] += before
row["after_tokens"] += after
row["output_tokens"] += output
row["tokens_saved"] += saved
row["providers"][provider] = int(row["providers"].get(provider, 0)) + 1
row["models"][model] = int(row["models"].get(model, 0)) + 1
if before > 0 or after > 0 or saved > 0:
row["has_exact_tokens"] = True
if not agents:
inferred_model_counts: dict[str, int] = {}
for model, count in requests_by_model.items():
model_lower = str(model).lower()
if "codex" in model_lower:
key = "codex"
elif "claude" in model_lower:
key = "claude-code"
elif "gemini" in model_lower:
key = "gemini"
else:
continue
inferred_model_counts[str(model)] = int(count)
provider_request_count = sum(max(0, int(count)) for count in requests_by_provider.values())
inferred_request_count = sum(max(0, count) for count in inferred_model_counts.values())
use_model_fallback = (
inferred_request_count > 0 and inferred_request_count == provider_request_count
)
if not use_model_fallback:
for provider, count in requests_by_provider.items():
key = _normalize_agent_key(provider) or "unknown"
row = _agent_row(key, _agent_label(key), "provider")
row["requests"] += int(count)
row["providers"][provider] = int(row["providers"].get(provider, 0)) + int(count)
for model, count in requests_by_model.items():
model_lower = str(model).lower()
if "codex" in model_lower:
key = "codex"
elif "claude" in model_lower:
key = "claude-code"
elif "gemini" in model_lower:
key = "gemini"
else:
continue
if not use_model_fallback:
continue
row = _agent_row(key, _agent_label(key), "model")
row["requests"] += int(count)
row["models"][str(model)] = int(row["models"].get(str(model), 0)) + int(count)
rows: list[dict[str, Any]] = []
for row in agents.values():
before = int(row["before_tokens"])
saved = int(row["tokens_saved"])
after = int(row["after_tokens"])
if before == 0 and (after > 0 or saved > 0):
before = after + saved
savings_percent = round((saved / before) * 100.0, 2) if before else 0.0
row["before_tokens"] = before
row["savings_percent"] = savings_percent
row["after_percent"] = round((after / before) * 100.0, 2) if before else 0.0
row["share_of_saved_percent"] = (
round((saved / global_tokens_saved) * 100.0, 2) if global_tokens_saved else 0.0
)
row["share_of_requests_percent"] = 0.0
rows.append(row)
total_requests = sum(int(row["requests"]) for row in rows)
for row in rows:
row["share_of_requests_percent"] = (
round((int(row["requests"]) / total_requests) * 100.0, 2) if total_requests else 0.0
)
rows.sort(
key=lambda row: (
int(row.get("tokens_saved", 0)),
int(row.get("before_tokens", 0)),
int(row.get("requests", 0)),
),
reverse=True,
)
return {
"agents": rows,
"totals": {
"requests": total_requests,
"before_tokens": global_before_tokens,
"after_tokens": global_after_tokens,
"output_tokens": global_output_tokens,
"tokens_saved": global_tokens_saved,
"savings_percent": (
round((global_tokens_saved / global_before_tokens) * 100.0, 2)
if global_before_tokens
else 0.0
),
},
"coverage": {
"logged_requests": len(logs),
"exact_token_rows": sum(1 for row in rows if row.get("has_exact_tokens")),
"mode": "request_logs" if logs else "aggregate_fallback",
},
}
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
@ -372,7 +606,6 @@ class HeadroomProxy(
enable_code_aware=config.code_aware_enabled,
tool_profiles=config.tool_profiles,
read_lifecycle=ReadLifecycleConfig(enabled=config.read_lifecycle),
ccr_inject_marker=config.ccr_inject_marker,
smart_crusher_max_items_after_crush=cast(
int | None,
profile_kwargs.get("max_items_after_crush"),
@ -381,6 +614,7 @@ class HeadroomProxy(
bool,
profile_kwargs.get("smart_crusher_with_compaction", True),
),
ccr_inject_marker=config.ccr_inject_marker,
)
if config.disable_kompress:
router_config.enable_kompress = False
@ -2020,6 +2254,35 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
_stats_snapshot_lock = asyncio.Lock()
_stats_snapshot: dict[str, Any] = {"expires_at": 0.0, "value": None}
RECENT_REQUEST_LOG_WINDOW = 100
def _build_recent_request_payload(limit: int = RECENT_REQUEST_LOG_WINDOW) -> dict[str, Any]:
recent_request_logs = proxy.logger.get_recent(limit) if proxy.logger else []
dashboard_recent_requests = [
{
"request_id": log.get("request_id"),
"timestamp": log.get("timestamp"),
"provider": log.get("provider"),
"model": log.get("model"),
"input_tokens_original": log.get("input_tokens_original"),
"input_tokens_optimized": log.get("input_tokens_optimized"),
"output_tokens": log.get("output_tokens"),
"tokens_saved": log.get("tokens_saved"),
"savings_percent": log.get("savings_percent"),
"optimization_latency_ms": log.get("optimization_latency_ms"),
"total_latency_ms": log.get("total_latency_ms"),
"transforms_applied": log.get("transforms_applied", []),
"waste_signals": log.get("waste_signals"),
}
for log in recent_request_logs
if log.get("input_tokens_original") is not None
and log.get("input_tokens_optimized") is not None
][-10:]
return {
"request_logs": recent_request_logs[-10:],
"recent_requests": dashboard_recent_requests,
}
async def _build_stats_payload() -> dict[str, Any]:
"""Build the full `/stats` response payload.
@ -2167,9 +2430,21 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
total_tokens_all_layers = all_layers_tokens_saved
persistent_savings = m.savings_tracker.stats_preview()
display_session = persistent_savings.get("display_session", {})
recent_request_logs = proxy.logger.get_recent(10_000) if proxy.logger else []
recent_request_payload = _build_recent_request_payload()
agent_usage = _build_agent_usage_summary(
recent_request_logs,
requests_by_provider=dict(m.requests_by_provider),
requests_by_model=dict(m.requests_by_model),
global_before_tokens=proxy_total_before_compression,
global_after_tokens=m.tokens_input_total,
global_tokens_saved=proxy_compression_tokens,
global_output_tokens=m.tokens_output_total,
)
return {
"summary": summary,
"agent_usage": agent_usage,
"savings": {
"total_tokens": total_tokens_all_layers,
"per_project": persistent_savings.get("projects", {}),
@ -2434,11 +2709,43 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
"proxy_inbound": proxy.metrics.inbound_snapshot(),
"cache": await proxy.cache.stats() if proxy.cache else None,
"rate_limiter": await proxy.rate_limiter.stats() if proxy.rate_limiter else None,
"recent_requests": proxy.logger.get_recent(10) if proxy.logger else [],
**recent_request_payload,
"log_full_messages": proxy.config.log_full_messages if proxy else False,
**get_quota_registry().get_all_stats(),
}
def _dashboard_config_payload() -> dict[str, Any]:
profile_kwargs = proxy_pipeline_kwargs(config)
target_ratio = profile_kwargs.get("target_ratio", config.target_ratio)
target_savings_percent = None
if isinstance(target_ratio, (int, float)):
target_savings_percent = round(max(0.0, min(1.0, 1.0 - float(target_ratio))) * 100, 1)
return {
"savings_profile": config.savings_profile,
"target_ratio": target_ratio,
"target_savings_percent": target_savings_percent,
"compress_user_messages": bool(
profile_kwargs.get("compress_user_messages", config.compress_user_messages)
),
"compress_system_messages": bool(
profile_kwargs.get("compress_system_messages", config.compress_system_messages)
),
"protect_recent": profile_kwargs.get("read_protection_window", config.protect_recent),
"protect_analysis_context": config.protect_analysis_context,
"min_tokens_to_crush": profile_kwargs.get(
"min_tokens_to_compress", config.min_tokens_to_crush
),
"max_items_after_crush": profile_kwargs.get(
"max_items_after_crush", config.max_items_after_crush
),
"smart_crusher_with_compaction": profile_kwargs.get(
"smart_crusher_with_compaction",
config.smart_crusher_with_compaction,
),
"force_kompress": bool(profile_kwargs.get("force_kompress", False)),
"accuracy_guard": config.accuracy_guard,
}
async def _get_cached_stats_payload() -> dict[str, Any]:
"""Return a short-TTL cached `/stats` snapshot for dashboard polling."""
now = time.monotonic()
@ -2474,8 +2781,13 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
snapshot to avoid rebuilding the full payload on every UI poll.
"""
if cached:
return await _get_cached_stats_payload()
return await _build_stats_payload()
payload = dict(await _get_cached_stats_payload())
payload.update(_build_recent_request_payload())
payload["config"] = _dashboard_config_payload()
return payload
payload = await _build_stats_payload()
payload["config"] = _dashboard_config_payload()
return payload
@app.post("/stats/reset", dependencies=[Depends(_require_loopback)])
async def stats_reset():
@ -3610,6 +3922,11 @@ if __name__ == "__main__":
optimize=optimize,
min_tokens_to_crush=_get_env_int("HEADROOM_MIN_TOKENS", args.min_tokens),
max_items_after_crush=_get_env_int("HEADROOM_MAX_ITEMS", args.max_items),
smart_crusher_with_compaction=(
_get_env_bool("HEADROOM_SMART_CRUSHER_COMPACTION", False)
if "HEADROOM_SMART_CRUSHER_COMPACTION" in os.environ
else None
),
cache_enabled=cache_enabled,
cache_ttl_seconds=_get_env_int("HEADROOM_CACHE_TTL", args.cache_ttl),
rate_limit_enabled=rate_limit_enabled,
@ -3632,6 +3949,28 @@ if __name__ == "__main__":
mode=normalize_proxy_mode(_get_env_str("HEADROOM_MODE", PROXY_MODE_TOKEN)),
compress_user_messages=args.compress_user_messages
or _get_env_bool("HEADROOM_COMPRESS_USER_MESSAGES", False),
savings_profile=os.environ.get("HEADROOM_SAVINGS_PROFILE") or None,
target_ratio=(
float(os.environ["HEADROOM_TARGET_RATIO"])
if os.environ.get("HEADROOM_TARGET_RATIO")
else None
),
compress_system_messages=(
_get_env_bool("HEADROOM_COMPRESS_SYSTEM_MESSAGES", False)
if "HEADROOM_COMPRESS_SYSTEM_MESSAGES" in os.environ
else None
),
protect_recent=(
int(os.environ["HEADROOM_PROTECT_RECENT"])
if os.environ.get("HEADROOM_PROTECT_RECENT")
else None
),
protect_analysis_context=(
_get_env_bool("HEADROOM_PROTECT_ANALYSIS_CONTEXT", False)
if "HEADROOM_PROTECT_ANALYSIS_CONTEXT" in os.environ
else None
),
accuracy_guard=os.environ.get("HEADROOM_ACCURACY_GUARD") or None,
)
# Get worker and concurrency settings

View file

@ -15,6 +15,7 @@ from headroom.agent_savings import (
proxy_pipeline_kwargs,
with_target_savings,
)
from headroom.cli import wrap as wrap_module
from headroom.cli.main import main
from headroom.compress import CompressConfig, compress
from headroom.proxy.models import ProxyConfig
@ -137,6 +138,110 @@ def test_compress_applies_agent_savings_profile_to_pipeline(monkeypatch) -> None
assert captured["min_tokens_to_compress"] == 120
def test_compress_savings_profile_does_not_mutate_supplied_config(monkeypatch) -> None:
captured: dict[str, object] = {}
messages = [{"role": "user", "content": "x" * 500}]
config = CompressConfig(
compress_user_messages=False,
compress_system_messages=False,
protect_recent=9,
protect_analysis_context=False,
target_ratio=None,
min_tokens_to_compress=999,
)
class Pipeline:
def apply(self, **kwargs):
captured.update(kwargs)
return SimpleNamespace(
messages=messages,
tokens_before=1000,
tokens_after=100,
transforms_applied=["test"],
)
monkeypatch.setattr(compress_module, "_get_pipeline", lambda: Pipeline())
compress(messages, config=config, savings_profile=AGENT_90_PROFILE)
assert captured["target_ratio"] == 0.10
assert captured["min_tokens_to_compress"] == 120
assert config.compress_user_messages is False
assert config.compress_system_messages is False
assert config.protect_recent == 9
assert config.protect_analysis_context is False
assert config.target_ratio is None
assert config.min_tokens_to_compress == 999
def test_agent_savings_config_mismatches_returns_specific_labels() -> None:
profile = get_agent_savings_profile(AGENT_90_PROFILE)
running_config = {
"savings_profile": profile.name,
"target_ratio": 0.20,
"compress_user_messages": profile.compress_user_messages,
"compress_system_messages": profile.compress_system_messages,
"protect_recent": profile.protect_recent,
"protect_analysis_context": profile.protect_analysis_context,
"min_tokens_to_crush": profile.min_tokens_to_compress,
"max_items_after_crush": profile.max_items_after_crush,
"smart_crusher_with_compaction": profile.smart_crusher_with_compaction,
"accuracy_guard": profile.accuracy_guard,
}
assert wrap_module._agent_savings_config_mismatches(running_config, "codex") == ["target-ratio"]
def test_agent_savings_config_mismatches_ignores_non_target_agents() -> None:
assert wrap_module._agent_savings_config_mismatches({}, "openhands") == []
def test_agent_savings_config_mismatches_accepts_matching_runtime_config() -> None:
profile = get_agent_savings_profile(AGENT_90_PROFILE)
running_config = {
"savings_profile": profile.name,
"target_ratio": "0.10",
"compress_user_messages": True,
"compress_system_messages": True,
"protect_recent": "2",
"protect_analysis_context": True,
"min_tokens_to_crush": "120",
"max_items_after_crush": "8",
"smart_crusher_with_compaction": False,
"accuracy_guard": "strict",
}
assert wrap_module._agent_savings_config_mismatches(running_config, "cursor") == []
def test_agent_savings_config_mismatches_reports_unparseable_values() -> None:
running_config = {
"savings_profile": None,
"target_ratio": "not-a-float",
"compress_user_messages": None,
"compress_system_messages": None,
"protect_recent": "not-an-int",
"protect_analysis_context": None,
"min_tokens_to_crush": object(),
"max_items_after_crush": object(),
"smart_crusher_with_compaction": None,
"accuracy_guard": None,
}
assert wrap_module._agent_savings_config_mismatches(running_config, "claude") == [
"savings-profile",
"target-ratio",
"compress-user-messages",
"compress-system-messages",
"protect-recent",
"protect-analysis-context",
"min-tokens",
"max-items",
"smart-crusher-compaction",
"accuracy-guard",
]
def test_agent_90_profile_applies_to_proxy_config_runtime_kwargs() -> None:
config = ProxyConfig(savings_profile="agent-90")

View file

@ -16,6 +16,7 @@ from headroom.proxy.auth_mode import (
SUBSCRIPTION_UA_PREFIXES,
AuthMode,
classify_auth_mode,
classify_client,
)
@ -186,3 +187,9 @@ def test_classify_under_100us_per_call() -> None:
per_call_us = (elapsed / iters) * 1_000_000
assert per_call_us < 100, f"classify_auth_mode took {per_call_us:.2f} us/call (limit: 100 us)"
def test_classify_client_uses_default_when_no_client_signal():
headers = {"user-agent": "anthropic/0.42.0"}
assert classify_client(headers, default="claude") == "claude"

View file

@ -517,6 +517,67 @@ def test_start_proxy_uses_separate_session_for_signal_isolation(
assert popen_kwargs["start_new_session"] == (wrap_mod.os.name == "posix")
@pytest.mark.parametrize("agent_type", ["claude", "codex", "cursor"])
def test_start_proxy_applies_agent_90_defaults(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, agent_type: str
) -> None:
"""Wrapped coding agents should start the proxy with high-savings defaults."""
popen_kwargs: dict[str, object] = {}
class FakeProc:
returncode = None
def poll(self) -> None:
return None
def fake_popen(*args: object, **kwargs: object) -> FakeProc:
popen_kwargs.update(kwargs)
return FakeProc()
monkeypatch.setattr(wrap_mod, "_get_log_path", lambda: tmp_path / "proxy.log")
monkeypatch.setattr(wrap_mod, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_mod.subprocess, "Popen", fake_popen)
wrap_mod._start_proxy(8787, agent_type=agent_type)
env = popen_kwargs["env"]
assert isinstance(env, dict)
assert env["HEADROOM_SAVINGS_PROFILE"] == "agent-90"
assert env["HEADROOM_TARGET_RATIO"] == "0.10"
assert env["HEADROOM_MAX_ITEMS"] == "8"
assert env["HEADROOM_SMART_CRUSHER_COMPACTION"] == "0"
def test_start_proxy_preserves_explicit_savings_overrides(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""User-provided savings env vars should override wrapper defaults."""
popen_kwargs: dict[str, object] = {}
class FakeProc:
returncode = None
def poll(self) -> None:
return None
def fake_popen(*args: object, **kwargs: object) -> FakeProc:
popen_kwargs.update(kwargs)
return FakeProc()
monkeypatch.setenv("HEADROOM_TARGET_RATIO", "0.20")
monkeypatch.setenv("HEADROOM_MAX_ITEMS", "12")
monkeypatch.setattr(wrap_mod, "_get_log_path", lambda: tmp_path / "proxy.log")
monkeypatch.setattr(wrap_mod, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_mod.subprocess, "Popen", fake_popen)
wrap_mod._start_proxy(8787, agent_type="codex")
env = popen_kwargs["env"]
assert isinstance(env, dict)
assert env["HEADROOM_TARGET_RATIO"] == "0.20"
assert env["HEADROOM_MAX_ITEMS"] == "12"
def test_launch_tool_ignores_sigint_in_wrapper(
monkeypatch: pytest.MonkeyPatch,
) -> None:

View file

@ -156,6 +156,35 @@ def test_perf_json_raw_is_array(runner, monkeypatch):
assert data[0]["request_id"] == "hr_1"
def test_perf_json_raw_preserves_client_field(runner, monkeypatch):
report = _sample_report()
report.perf_records[0].client = "codex"
_patch_report(monkeypatch, report)
result = runner.invoke(main, ["perf", "--format", "json", "--raw"])
assert result.exit_code == 0, result.output
data = json.loads(result.output)
assert data[0]["client"] == "codex"
def test_parse_perf_line_preserves_client_field(monkeypatch, tmp_path):
log_dir = tmp_path / "logs"
log_dir.mkdir()
(log_dir / "proxy.log").write_text(
"2026-06-10 10:00:00,000 - headroom.proxy - INFO - "
"[hr_codex] PERF model=gpt-5 msgs=3 tok_before=1000 "
"tok_after=90 tok_saved=910 cache_read=0 cache_write=0 "
"cache_hit_pct=0 opt_ms=12 transforms=content_router client=codex\n"
)
monkeypatch.setattr(analyzer, "LOG_DIR", log_dir)
report = analyzer.parse_log_files(last_n_hours=0)
assert len(report.perf_records) == 1
assert report.perf_records[0].client == "codex"
def test_perf_csv_by_model(runner, monkeypatch):
_patch_report(monkeypatch, _sample_report())
result = runner.invoke(main, ["perf", "--format", "csv"])
@ -191,3 +220,22 @@ def test_perf_rejects_unknown_format(runner, monkeypatch):
_patch_report(monkeypatch, _sample_report())
result = runner.invoke(main, ["perf", "--format", "xml"])
assert result.exit_code != 0
def test_parse_perf_line_preserves_blank_client_field(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
logs_dir = tmp_path / "logs"
logs_dir.mkdir()
monkeypatch.setattr(analyzer, "LOG_DIR", logs_dir)
(logs_dir / "proxy.log").write_text(
"2026-06-10 10:00:00,000 - headroom.proxy - INFO - [req-blank] PERF "
"model=gpt-5 msgs=1 tok_before=100 tok_after=50 tok_saved=50 "
"cache_read=0 cache_write=0 cache_hit_pct=0 opt_ms=1 transforms=test client=\n",
encoding="utf-8",
)
report = analyzer.parse_log_files(last_n_hours=0)
assert len(report.perf_records) == 1
assert report.perf_records[0].client == ""

View file

@ -156,6 +156,24 @@ class TestCLIProxyEnvVars:
assert result.exit_code == 0, result.output
assert captured_config["config"].port == 9797
def test_headroom_min_tokens_from_env(self, runner):
"""HEADROOM_MIN_TOKENS env var should be passed to ProxyConfig."""
captured_config = {}
def mock_run_server(config, **kwargs):
captured_config["config"] = config
with patch("headroom.proxy.server.run_server", mock_run_server):
result = runner.invoke(
main,
["proxy"],
env={"HEADROOM_MIN_TOKENS": "120"},
catch_exceptions=False,
)
assert result.exit_code == 0, result.output
assert captured_config["config"].min_tokens_to_crush == 120
def test_headroom_budget_from_env(self, runner):
"""HEADROOM_BUDGET env var should be passed to ProxyConfig."""
captured_config = {}

View file

@ -0,0 +1,276 @@
from headroom.proxy.server import (
_agent_label,
_build_agent_usage_summary,
_classify_agent_from_log,
_normalize_agent_key,
)
def test_agent_usage_groups_exact_logged_requests_by_client() -> None:
summary = _build_agent_usage_summary(
[
{
"provider": "openai",
"model": "gpt-5.2-codex",
"tags": {"client": "codex"},
"input_tokens_original": 1000,
"input_tokens_optimized": 650,
"output_tokens": 100,
"tokens_saved": 350,
},
{
"provider": "anthropic",
"model": "claude-sonnet-4-6",
"tags": {"client": "claude-code"},
"input_tokens_original": 800,
"input_tokens_optimized": 500,
"output_tokens": 80,
"tokens_saved": 300,
},
{
"provider": "anthropic",
"model": "claude-sonnet-4-6",
"tags": {"client": "cursor"},
"input_tokens_original": 500,
"input_tokens_optimized": 400,
"output_tokens": 60,
"tokens_saved": 100,
},
],
requests_by_provider={},
requests_by_model={},
global_before_tokens=2300,
global_after_tokens=1550,
global_tokens_saved=750,
global_output_tokens=240,
)
rows = {row["agent"]: row for row in summary["agents"]}
assert rows["codex"]["label"] == "Codex"
assert rows["codex"]["before_tokens"] == 1000
assert rows["codex"]["after_tokens"] == 650
assert rows["codex"]["tokens_saved"] == 350
assert rows["codex"]["savings_percent"] == 35.0
assert rows["claude-code"]["label"] == "Claude"
assert rows["claude-code"]["savings_percent"] == 37.5
assert rows["cursor"]["label"] == "Cursor"
assert rows["cursor"]["share_of_saved_percent"] == 13.33
assert summary["coverage"] == {
"logged_requests": 3,
"exact_token_rows": 3,
"mode": "request_logs",
}
def test_agent_usage_falls_back_to_inferred_model_counts_when_complete() -> None:
summary = _build_agent_usage_summary(
[],
requests_by_provider={"anthropic": 2, "openai": 3},
requests_by_model={"claude-sonnet-4-6": 2, "gpt-5.2-codex": 3},
global_before_tokens=1000,
global_after_tokens=700,
global_tokens_saved=300,
global_output_tokens=90,
)
rows = {row["agent"]: row for row in summary["agents"]}
assert set(rows) == {"claude-code", "codex"}
assert rows["claude-code"]["label"] == "Claude"
assert rows["claude-code"]["source"] == "model"
assert rows["claude-code"]["requests"] == 2
assert rows["claude-code"]["models"] == {"claude-sonnet-4-6": 2}
assert rows["codex"]["label"] == "Codex"
assert rows["codex"]["requests"] == 3
assert rows["codex"]["models"] == {"gpt-5.2-codex": 3}
assert summary["totals"]["savings_percent"] == 30.0
assert summary["coverage"]["mode"] == "aggregate_fallback"
def test_agent_usage_fallback_does_not_duplicate_provider_and_model_rows() -> None:
summary = _build_agent_usage_summary(
[],
requests_by_provider={"anthropic": 2, "openai": 3},
requests_by_model={"claude-sonnet-4-6": 2, "gpt-5.2-codex": 3},
global_before_tokens=1000,
global_after_tokens=700,
global_tokens_saved=300,
global_output_tokens=90,
)
rows = {row["agent"]: row for row in summary["agents"]}
assert set(rows) == {"claude-code", "codex"}
assert all(row["requests"] > 0 for row in rows.values())
assert summary["totals"]["requests"] == 5
def test_agent_usage_skips_partial_model_fallback_counts() -> None:
summary = _build_agent_usage_summary(
[],
requests_by_provider={"anthropic": 2, "openai": 3},
requests_by_model={"claude-sonnet-4-6": 2},
global_before_tokens=1000,
global_after_tokens=700,
global_tokens_saved=300,
global_output_tokens=90,
)
rows = {row["agent"]: row for row in summary["agents"]}
assert set(rows) == {"anthropic", "openai"}
assert rows["anthropic"]["label"] == "Claude"
assert rows["anthropic"]["requests"] == 2
assert rows["openai"]["label"] == "OpenAI"
assert rows["openai"]["requests"] == 3
def test_agent_classifier_uses_model_before_generic_provider() -> None:
agent, label, source = _classify_agent_from_log(
{
"provider": "openai",
"model": "gpt-5.2-codex",
"tags": {},
}
)
assert (agent, label, source) == ("codex", "Codex", "model")
def test_agent_usage_upgrades_source_when_stronger_evidence_arrives() -> None:
summary = _build_agent_usage_summary(
[
{
"provider": "anthropic",
"model": "claude-sonnet-4-6",
"tags": {},
"input_tokens_original": 10,
"input_tokens_optimized": 8,
"tokens_saved": 2,
},
{
"provider": "anthropic",
"model": "claude-sonnet-4-6",
"tags": {"client": "claude-code"},
"input_tokens_original": 20,
"input_tokens_optimized": 12,
"tokens_saved": 8,
},
],
requests_by_provider={},
requests_by_model={},
global_before_tokens=30,
global_after_tokens=20,
global_tokens_saved=10,
global_output_tokens=0,
)
row = summary["agents"][0]
assert row["agent"] == "claude-code"
assert row["source"] == "client"
assert row["requests"] == 2
def test_agent_key_normalizes_wrapped_underscore_clients() -> None:
assert _normalize_agent_key("wrap_claude_cli") == "claude-code"
def test_agent_key_normalizes_claude_code_cli_alias() -> None:
assert _normalize_agent_key("claude-code-cli") == "claude-code"
def test_agent_label_title_cases_unknown_agent_key() -> None:
assert _agent_label("custom-agent") == "Custom Agent"
def test_agent_classifier_uses_stack_tag_before_model() -> None:
agent, label, source = _classify_agent_from_log(
{
"provider": "openai",
"model": "gpt-5.2-codex",
"tags": {"headroom-stack": "openclaw"},
}
)
assert (agent, label, source) == ("openclaw", "OpenClaw", "stack")
def test_agent_classifier_falls_back_to_unknown() -> None:
agent, label, source = _classify_agent_from_log(
{
"provider": "",
"model": "",
"tags": [],
}
)
assert (agent, label, source) == ("unknown", "Unidentified", "unknown")
def test_agent_usage_recovers_before_tokens_from_after_and_saved() -> None:
summary = _build_agent_usage_summary(
[
{
"provider": "openai",
"model": "custom-model",
"tags": {"client": "custom-agent"},
"input_tokens_original": 0,
"input_tokens_optimized": 70,
"output_tokens": 5,
"tokens_saved": 30,
}
],
requests_by_provider={},
requests_by_model={},
global_before_tokens=100,
global_after_tokens=70,
global_tokens_saved=0,
global_output_tokens=5,
)
row = summary["agents"][0]
assert row["agent"] == "custom-agent"
assert row["label"] == "Custom Agent"
assert row["before_tokens"] == 100
assert row["savings_percent"] == 30.0
assert row["after_percent"] == 70.0
assert row["share_of_saved_percent"] == 0.0
assert summary["totals"]["savings_percent"] == 0.0
def test_agent_usage_clamps_negative_token_values() -> None:
summary = _build_agent_usage_summary(
[
{
"provider": None,
"model": None,
"tags": {},
"input_tokens_original": -100,
"input_tokens_optimized": -50,
"output_tokens": -5,
"tokens_saved": -25,
}
],
requests_by_provider={},
requests_by_model={},
global_before_tokens=0,
global_after_tokens=0,
global_tokens_saved=0,
global_output_tokens=0,
)
row = summary["agents"][0]
assert row["agent"] == "unknown"
assert row["requests"] == 1
assert row["before_tokens"] == 0
assert row["after_tokens"] == 0
assert row["tokens_saved"] == 0
assert row["output_tokens"] == 0
assert row["has_exact_tokens"] is False

View file

@ -1,3 +1,4 @@
import asyncio
import base64
import json
import sys
@ -10,6 +11,7 @@ from fastapi import Request
from headroom.proxy.handlers.openai import (
OpenAIHandlerMixin,
_openai_responses_unit_cache_key,
_resolve_codex_routing_headers,
)
@ -100,6 +102,38 @@ def test_resolve_codex_routing_ignores_invalid_jwt_payloads():
assert headers["authorization"] == f"Bearer {token}"
def test_openai_responses_unit_cache_key_includes_target_ratio() -> None:
unit = SimpleNamespace(
text="large tool output",
provider="openai",
endpoint="responses",
role="tool",
item_type="function_call_output",
cache_zone="live",
mutable=True,
min_bytes=100,
context=None,
question=None,
bias=None,
metadata={},
)
default_key = _openai_responses_unit_cache_key(unit, model="gpt-5.4")
aggressive_key = _openai_responses_unit_cache_key(
unit,
model="gpt-5.4",
target_ratio=0.10,
)
balanced_key = _openai_responses_unit_cache_key(
unit,
model="gpt-5.4",
target_ratio=0.50,
)
assert aggressive_key != default_key
assert aggressive_key != balanced_key
class _DummyMetrics:
async def record_request(self, **kwargs): # noqa: ANN003
return None
@ -253,6 +287,37 @@ def test_handle_openai_responses_routes_chatgpt_auth_to_backend_api(monkeypatch)
assert response.status_code == 200
def test_handle_openai_responses_chatgpt_codex_timeout_fails_open(monkeypatch):
token = _jwt(
{
"https://api.openai.com/auth": {
"chatgpt_account_id": "acct-from-jwt",
}
}
)
request = _build_request(
{"model": "gpt-5.4", "input": "large context"},
{"Authorization": f"Bearer {token}"},
)
handler = _DummyOpenAIHandler()
handler.config.optimize = True
async def timeout_compression(*args, **kwargs): # noqa: ANN002, ANN003
raise asyncio.TimeoutError()
handler._compress_openai_responses_payload_in_executor = timeout_compression
monkeypatch.setattr("headroom.tokenizers.get_tokenizer", lambda model: _DummyTokenizer())
response = anyio.run(handler.handle_openai_responses, request)
assert response.status_code == 200
assert handler.captured_request is not None
method, url, headers, body = handler.captured_request
assert method == "POST"
assert url == "https://chatgpt.com/backend-api/codex/responses"
assert body["input"] == "large context"
def test_handle_openai_responses_routes_api_key_auth_direct_to_openai(monkeypatch):
request = _build_request(
{"model": "gpt-4o-mini", "input": "hello"},

View file

@ -0,0 +1,162 @@
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from headroom.proxy import server
from headroom.proxy.models import ProxyConfig
from headroom.proxy.server import create_app
class FakeRequestLogger:
def __init__(self) -> None:
self._logs: list[dict[str, object]] = []
@property
def logs(self) -> list[dict[str, object]]:
return self._logs
@logs.setter
def logs(self, value: list[dict[str, object]]) -> None:
self._logs = value
def get_recent(self, limit: int) -> list[dict[str, object]]:
return self._logs[-limit:]
class FakeLogEntry(dict[str, object]):
def __getattr__(self, name: str) -> object:
return self.get(name)
def test_stats_refreshes_recent_requests_when_cached() -> None:
app = create_app(
ProxyConfig(
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
log_requests=False,
ccr_inject_tool=False,
ccr_handle_responses=False,
ccr_context_tracking=False,
http2=False,
)
)
logger = FakeRequestLogger()
app.state.proxy.logger = logger
first_log = FakeLogEntry(
{
"timestamp": "2026-06-11T10:00:00Z",
"provider": "openai",
"model": "gpt-4.1",
"input_tokens_original": 100,
"input_tokens_optimized": 60,
"tokens_saved": 40,
"savings_percent": 40.0,
}
)
second_log = FakeLogEntry(
{
"timestamp": "2026-06-11T10:01:00Z",
"provider": "anthropic",
"model": "claude-sonnet",
"input_tokens_original": 200,
"input_tokens_optimized": 120,
"tokens_saved": 80,
"savings_percent": 40.0,
}
)
with TestClient(app) as client:
logger.logs = [first_log]
first_response = client.get("/stats?cached=1")
assert first_response.status_code == 200
assert first_response.json()["recent_requests"][-1]["model"] == "gpt-4.1"
logger.logs = [first_log, second_log]
second_response = client.get("/stats?cached=1")
assert second_response.status_code == 200
second_payload = second_response.json()
assert second_payload["recent_requests"][-1]["model"] == "claude-sonnet"
assert second_payload["request_logs"][-1]["model"] == "claude-sonnet"
def test_agent_usage_totals_use_proxy_only_savings(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("HEADROOM_REQUIRE_RUST_CORE", "false")
monkeypatch.setattr(
server,
"_get_context_tool_stats",
lambda: {
"tool": "rtk",
"label": "RTK",
"tokens_saved": 500,
"session": {},
"lifetime": {},
},
)
app = create_app(
ProxyConfig(
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
log_requests=False,
ccr_inject_tool=False,
ccr_handle_responses=False,
ccr_context_tracking=False,
http2=False,
)
)
logger = FakeRequestLogger()
app.state.proxy.logger = logger
logger.logs = [
FakeLogEntry(
{
"timestamp": "2026-06-11T10:00:00Z",
"provider": "openai",
"model": "gpt-5.2-codex",
"tags": {"client": "codex"},
"input_tokens_original": 1000,
"input_tokens_optimized": 900,
"output_tokens": 50,
"tokens_saved": 100,
"savings_percent": 10.0,
}
)
]
with TestClient(app) as client:
proxy = client.app.state.proxy
proxy.metrics.tokens_input_total = 900
proxy.metrics.tokens_saved_total = 100
proxy.metrics.tokens_output_total = 50
response = client.get("/stats")
assert response.status_code == 200
payload = response.json()
assert payload["tokens"]["saved"] == 600
assert payload["agent_usage"]["totals"]["before_tokens"] == 1000
assert payload["agent_usage"]["totals"]["tokens_saved"] == 100
assert payload["agent_usage"]["totals"]["savings_percent"] == 10.0
assert payload["agent_usage"]["agents"][0]["share_of_saved_percent"] == 100.0
def test_stats_preserves_default_smart_crusher_compaction_state() -> None:
config = ProxyConfig(
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
)
client = TestClient(create_app(config))
response = client.get("/stats")
assert response.status_code == 200
assert response.json()["config"]["smart_crusher_with_compaction"] is None

View file

@ -4,6 +4,8 @@ from __future__ import annotations
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
@ -103,26 +105,37 @@ def test_no_openssl_sys_in_wheel_build_tree() -> None:
import subprocess
for crate in ("headroom-py", "headroom-proxy", "headroom-core"):
result = subprocess.run(
[
"cargo",
"tree",
"--target",
"x86_64-unknown-linux-gnu",
"-p",
crate,
"-i",
"openssl-sys",
],
cwd=str(ROOT),
capture_output=True,
text=True,
check=False,
)
try:
result = subprocess.run(
[
"cargo",
"tree",
"--target",
"x86_64-unknown-linux-gnu",
"-p",
crate,
"-i",
"openssl-sys",
],
cwd=str(ROOT),
capture_output=True,
text=True,
check=False,
)
except FileNotFoundError:
pytest.skip("cargo is unavailable in this environment")
# `cargo tree -i <pkg>` exits 101 with "did not match any
# packages" when the package is NOT in the tree — the GREEN
# case. Exit 0 with a tree of consumers means it IS pulled.
not_in_tree = result.returncode != 0 and "did not match any packages" in result.stderr
if (
result.returncode != 0
and "package ID specification `openssl-sys` did not match"
not in (result.stderr + result.stdout)
):
pytest.skip(
"cargo dependency tree for the Linux wheel target is unavailable in this environment"
)
assert not_in_tree, (
f"openssl-sys is back in {crate}'s build tree:\n"
f"stdout:\n{result.stdout}\n"