From bd8de9f3829e9f66e33d36a01464a99adcc42c1d Mon Sep 17 00:00:00 2001 From: Vinay Gupta <58447456+aivinay@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:09:51 -0500 Subject: [PATCH] fix(proxy): keep recent stats request rows (#1922) ## Description `/stats.recent_requests` was built from the request-log tail, but then filtered out any row whose compact token fields were incomplete. That made the dashboard/API summary diverge from the raw request counters: `requests.by_model` could show many recent requests for a model while `recent_requests` only showed the few rows with complete compression-token accounting. This fixes the stats payload so the compact `recent_requests` table mirrors the latest request-log rows without masking unknown token accounting as measured zero. Missing/non-finite compact numeric fields now remain `null`, and each row exposes `token_accounting_status` plus `has_exact_tokens` so API clients and the dashboard can distinguish complete, partial, and missing token accounting. Closes #1914 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring ## Changes Made - Removed the token-completeness filter from `_build_recent_request_payload()`. - Preserved unknown compact recent-request numeric fields as `null`. - Added `token_accounting_status` and `has_exact_tokens` to compact recent request rows. - Updated the dashboard recent-requests table to render unknown token/latency fields as `unknown`. - Hardened `build_session_summary()` against token-incomplete request-log rows and surfaced an `unknown_token_accounting` bucket. - Added TypeScript SDK fields for the compact recent-request stats contract. - Added a regression test covering missing, partial, and complete token-accounting rows. ## Testing - [x] Focused stats regression passes - [x] Adjacent summary/MCP tests pass - [x] TypeScript SDK typecheck/tests pass - [x] Ruff check passes - [x] Ruff format check passes - [x] Diff whitespace check passes - [ ] Full suite not run ### Test Output ```text $ uv run --no-project ... pytest tests/test_proxy_stats_recent_requests.py -q 4 passed, 1 warning $ uv run --no-project ... pytest tests/test_proxy_dashboard_stats_cache.py::test_session_summary_uses_generic_cli_filtering_keys tests/test_proxy_dashboard_stats_cache.py::test_session_summary_surfaces_codex_ws_counters tests/test_ccr_mcp_server.py -q 19 passed, 1 skipped $ npm run typecheck tsc --noEmit $ npm test 296 passed, 33 skipped $ uv run --no-project --with ruff ruff check headroom/proxy/server.py headroom/proxy/cost.py headroom/ccr/mcp_server.py tests/test_proxy_stats_recent_requests.py All checks passed! $ uv run --no-project --with ruff ruff format --check headroom/proxy/server.py headroom/proxy/cost.py headroom/ccr/mcp_server.py tests/test_proxy_stats_recent_requests.py 4 files already formatted $ git diff --check clean ``` ## Verification - Principal engineer review: no blockers after the final finite-number/accounting-status alignment. - Senior developer review: no blockers; implementation and focused coverage look solid for #1914. - Architect/design review: original API/UI contract blocker resolved; unknowns remain visible as `null`/`unknown` instead of measured zero. ## Notes The normal editable `uv run pytest ...` path is blocked locally by the known native build issue in `esaxx-rs` (`fatal error: 'cstdint' file not found`) while building the Rust extension on this machine. I validated the Python-only stats path with a temporary `headroom._core` import stub and `HEADROOM_REQUIRE_RUST_CORE=false`; no repository files were changed for that stub. ## Review Readiness - [x] I have performed a self-review - [ ] This PR is ready for human review --- headroom/ccr/mcp_server.py | 1 + headroom/dashboard/templates/dashboard.html | 32 ++++++--- headroom/proxy/cost.py | 42 +++++++++-- headroom/proxy/server.py | 77 ++++++++++++++------ sdk/typescript/src/types/models.ts | 9 +++ tests/test_proxy_stats_recent_requests.py | 79 +++++++++++++++++++++ 6 files changed, 204 insertions(+), 36 deletions(-) diff --git a/headroom/ccr/mcp_server.py b/headroom/ccr/mcp_server.py index b1e7ecc37..aa8a2b4d4 100644 --- a/headroom/ccr/mcp_server.py +++ b/headroom/ccr/mcp_server.py @@ -130,6 +130,7 @@ def _format_session_summary( "too_small": "Too small (< 500 tokens)", "passthrough": "Passthrough (token counting)", "no_compressible_content": "No compressible content (user/assistant only)", + "unknown_token_accounting": "Unknown token accounting", } for key, count in uncomp.items(): label = reason_labels.get(key, key) diff --git a/headroom/dashboard/templates/dashboard.html b/headroom/dashboard/templates/dashboard.html index 54d04c240..5febf2507 100644 --- a/headroom/dashboard/templates/dashboard.html +++ b/headroom/dashboard/templates/dashboard.html @@ -1307,12 +1307,12 @@
-
-
+
+
- +
-
+
@@ -1321,19 +1321,19 @@
Original Tokens
-
+
Compressed Tokens
-
+
Tokens Removed
-
+
Optimization Time
-
+
@@ -2054,12 +2054,28 @@ // --- Formatting --- + hasNumber(n) { + return typeof n === 'number' && Number.isFinite(n); + }, + formatNumber(n) { if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; return n.toString(); }, + formatOptionalNumber(n) { + return this.hasNumber(n) ? this.formatNumber(n) : 'unknown'; + }, + + formatOptionalPercent(n) { + return this.hasNumber(n) ? n.toFixed(0) + '%' : 'unknown'; + }, + + formatOptionalMs(n) { + return this.hasNumber(n) ? n.toFixed(0) + 'ms' : 'unknown'; + }, + formatCurrency(n) { if (n < 0) return '-' + this.formatCurrency(-n); if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; diff --git a/headroom/proxy/cost.py b/headroom/proxy/cost.py index 180be6550..f520d09cb 100644 --- a/headroom/proxy/cost.py +++ b/headroom/proxy/cost.py @@ -10,6 +10,7 @@ from __future__ import annotations import importlib.util import logging +import math from collections import deque from datetime import datetime, timedelta from typing import TYPE_CHECKING, Any @@ -472,23 +473,50 @@ def build_session_summary( "too_small": 0, "passthrough": 0, "no_compressible_content": 0, + "unknown_token_accounting": 0, } + def _entry_has_number(entry: Any, attr: str) -> bool: + value = getattr(entry, attr, None) + return ( + isinstance(value, (int, float)) + and not isinstance(value, bool) + and math.isfinite(float(value)) + ) + + def _entry_number(entry: Any, attr: str) -> int | float: + value = getattr(entry, attr, 0) + return value if _entry_has_number(entry, attr) else 0 + if proxy.logger: for entry in proxy.logger._logs: if entry.model and "count_tokens" in entry.model: uncompressed_reasons["passthrough"] += 1 continue - if entry.tokens_saved > 0: + tokens_saved = _entry_number(entry, "tokens_saved") + input_tokens_original = _entry_number(entry, "input_tokens_original") + input_tokens_optimized = _entry_number(entry, "input_tokens_optimized") + has_complete_token_accounting = all( + _entry_has_number(entry, attr) + for attr in ( + "input_tokens_original", + "input_tokens_optimized", + "tokens_saved", + "savings_percent", + ) + ) + if tokens_saved > 0 and has_complete_token_accounting: compressed_requests.append( { - "savings_pct": round(entry.savings_percent, 1), - "tokens_saved": entry.tokens_saved, - "original": entry.input_tokens_original, - "optimized": entry.input_tokens_optimized, + "savings_pct": round(_entry_number(entry, "savings_percent"), 1), + "tokens_saved": tokens_saved, + "original": input_tokens_original, + "optimized": input_tokens_optimized, } ) - elif entry.input_tokens_original > 0: + elif not has_complete_token_accounting: + uncompressed_reasons["unknown_token_accounting"] += 1 + elif input_tokens_original > 0: # Categorize why it wasn't compressed transforms = entry.transforms_applied or [] if not transforms: @@ -496,7 +524,7 @@ def build_session_summary( uncompressed_reasons["prefix_frozen"] += 1 elif all("excluded" in t or "protected" in t for t in transforms): uncompressed_reasons["no_compressible_content"] += 1 - elif entry.input_tokens_original < 500: + elif input_tokens_original < 500: uncompressed_reasons["too_small"] += 1 else: uncompressed_reasons["prefix_frozen"] += 1 diff --git a/headroom/proxy/server.py b/headroom/proxy/server.py index 275915947..578556e1c 100644 --- a/headroom/proxy/server.py +++ b/headroom/proxy/server.py @@ -30,6 +30,7 @@ import contextlib import hmac import json import logging +import math import os import sys import threading @@ -2964,29 +2965,63 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI: RECENT_REQUEST_LOG_WINDOW = 100 + def _is_recent_request_number(value: Any) -> bool: + return ( + isinstance(value, (int, float)) + and not isinstance(value, bool) + and math.isfinite(float(value)) + ) + + def _recent_request_optional_number(log: dict[str, Any], key: str) -> int | float | None: + value = log.get(key) + return value if _is_recent_request_number(value) else None + + def _recent_request_token_accounting_status(log: dict[str, Any]) -> str: + token_fields = ( + "input_tokens_original", + "input_tokens_optimized", + "tokens_saved", + "savings_percent", + ) + present = [_is_recent_request_number(log.get(field)) for field in token_fields] + if all(present): + return "complete" + if any(present): + return "partial" + return "missing" + 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"), - "tool_schema_saved_tokens": _tool_schema_saved_from_tags(log.get("tags")), - } - 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:] + dashboard_recent_requests = [] + for log in recent_request_logs: + token_accounting_status = _recent_request_token_accounting_status(log) + dashboard_recent_requests.append( + { + "request_id": log.get("request_id"), + "timestamp": log.get("timestamp"), + "provider": log.get("provider"), + "model": log.get("model"), + "input_tokens_original": _recent_request_optional_number( + log, "input_tokens_original" + ), + "input_tokens_optimized": _recent_request_optional_number( + log, "input_tokens_optimized" + ), + "output_tokens": _recent_request_optional_number(log, "output_tokens"), + "tokens_saved": _recent_request_optional_number(log, "tokens_saved"), + "savings_percent": _recent_request_optional_number(log, "savings_percent"), + "optimization_latency_ms": _recent_request_optional_number( + log, "optimization_latency_ms" + ), + "total_latency_ms": _recent_request_optional_number(log, "total_latency_ms"), + "has_exact_tokens": token_accounting_status == "complete", + "token_accounting_status": token_accounting_status, + "transforms_applied": log.get("transforms_applied", []), + "waste_signals": log.get("waste_signals"), + "tool_schema_saved_tokens": _tool_schema_saved_from_tags(log.get("tags")), + } + ) + dashboard_recent_requests = dashboard_recent_requests[-10:] return { "request_logs": recent_request_logs[-10:], "recent_requests": dashboard_recent_requests, diff --git a/sdk/typescript/src/types/models.ts b/sdk/typescript/src/types/models.ts index 58096733c..240856891 100644 --- a/sdk/typescript/src/types/models.ts +++ b/sdk/typescript/src/types/models.ts @@ -72,6 +72,15 @@ export interface RequestMetrics { model: string; stream: boolean; mode: string; + inputTokensOriginal?: number | null; + inputTokensOptimized?: number | null; + outputTokens?: number | null; + tokensSaved?: number | null; + savingsPercent?: number | null; + optimizationLatencyMs?: number | null; + totalLatencyMs?: number | null; + hasExactTokens?: boolean; + tokenAccountingStatus?: "complete" | "partial" | "missing"; tokensInputBefore: number; tokensInputAfter: number; tokensOutput?: number | null; diff --git a/tests/test_proxy_stats_recent_requests.py b/tests/test_proxy_stats_recent_requests.py index a902a3894..654632e8f 100644 --- a/tests/test_proxy_stats_recent_requests.py +++ b/tests/test_proxy_stats_recent_requests.py @@ -85,6 +85,85 @@ def test_stats_refreshes_recent_requests_when_cached() -> None: assert second_payload["request_logs"][-1]["model"] == "claude-sonnet" +def test_stats_recent_requests_includes_token_incomplete_requests() -> 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 + + logger.logs = [ + FakeLogEntry( + { + "request_id": "req-haiku-1", + "timestamp": "2026-07-09T10:00:00Z", + "provider": "anthropic", + "model": "claude-haiku", + "transforms_applied": [], + } + ), + FakeLogEntry( + { + "request_id": "req-haiku-2", + "timestamp": "2026-07-09T10:01:00Z", + "provider": "anthropic", + "model": "claude-haiku", + "input_tokens_original": None, + "input_tokens_optimized": None, + "output_tokens": None, + "tokens_saved": 0, + "savings_percent": 0.0, + "transforms_applied": [], + } + ), + FakeLogEntry( + { + "request_id": "req-sonnet-1", + "timestamp": "2026-07-09T10:02:00Z", + "provider": "anthropic", + "model": "claude-sonnet", + "input_tokens_original": 200, + "input_tokens_optimized": 120, + "output_tokens": 40, + "tokens_saved": 80, + "savings_percent": 40.0, + "transforms_applied": ["smart_crusher"], + } + ), + ] + + with TestClient(app, base_url="http://127.0.0.1", client=("127.0.0.1", 12345)) as client: + response = client.get("/stats") + + assert response.status_code == 200 + payload = response.json() + assert [req["model"] for req in payload["recent_requests"]] == [ + "claude-haiku", + "claude-haiku", + "claude-sonnet", + ] + assert payload["recent_requests"][0]["input_tokens_optimized"] is None + assert payload["recent_requests"][0]["token_accounting_status"] == "missing" + assert payload["recent_requests"][0]["has_exact_tokens"] is False + assert payload["recent_requests"][1]["output_tokens"] is None + assert payload["recent_requests"][1]["token_accounting_status"] == "partial" + assert payload["recent_requests"][1]["tokens_saved"] == 0 + assert payload["recent_requests"][2]["token_accounting_status"] == "complete" + assert payload["recent_requests"][2]["has_exact_tokens"] is True + assert payload["summary"]["uncompressed_requests"]["unknown_token_accounting"] == 2 + assert 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(