headroom/tests/e2e_ws_responses_compression.py
chopratejas 89f7b6c2dd fix: complete /v1/responses compression telemetry, multi-frame WS, frozen-count cap
Builds on PR #406 (HTTP /v1/responses PyO3) and PR #410 (WS first-frame
PyO3) which landed the binding for `compress_openai_responses_live_zone`.
This change closes the remaining gaps so every (provider × endpoint ×
auth-mode × streaming) combination compresses AND surfaces in the
dashboard.

Telemetry: extend the PyO3 binding return tuple from `(bytes, modified)`
to `(bytes, modified, tokens_saved, transforms_applied)` by adding
`CompressionManifest::tokens_saved()` and `transforms_applied()`
accessors on the existing manifest. The Python proxy populates
request-log telemetry from the binding output instead of recounting
tokens. Updates the existing 2-tuple call sites in HTTP and WS
first-frame, plus the unpacks in tests.

WebSocket multi-frame compression: subscription Codex users keep a
long-lived WS open and send multiple `response.create` events per
session. PR #410 only compressed the first frame; subsequent frames
went raw. Added `_maybe_compress_response_create_frame` closure inside
`_client_to_upstream` that runs the same Rust dispatcher on every
client→upstream `response.create` text frame, passes other event
types (response.cancel, session.update, etc.) through unchanged, and
accumulates `tokens_saved` / `transforms_applied` /
`ws_frames_compressed` counters across the session.

Pre-existing dashboard gap: `streaming.py` and `anthropic.py` write
`RequestLog` entries; the non-streaming OpenAI HTTP and WS handlers
did not. Result: /transformations/feed was invisible for every Codex
turn and every Cline / OpenClaude / Aider turn. Added the same wiring
in `handle_openai_chat` (non-streaming), `handle_openai_responses`
(non-streaming HTTP), and `handle_openai_responses_ws` (session-end).
All three populate `auth_mode` + `endpoint` tags so the dashboard can
break compression activity down by client class (PAYG / OAuth /
Subscription) and surface (`chat_completions` / `responses_http` /
`responses_ws`). The WS metric record is now unconditional — was
previously gated on `tokens_saved > 0`, so first-frame no-changes
never registered.

compute_frozen_count over-freeze for prose-format clients:
`compute_frozen_count` walked until it found an unstable
`tool_result` / `role: "tool"` block. Cline / OpenClaude / Aider —
clients that embed tool calls as XML inside plain text — never
produce such a boundary, so the function returned `len(messages)` and
the pipeline froze 100% of messages including the brand-new user
turn. Live zone empty → `Transform content_router: 16414 → 16414
tokens (saved 0)`. Reported on Discord 2026-05-07 with Cline+DeepSeek.
Fix: cap at `max(0, len(messages) - 1)`. Updates 3 existing test
assertions whose expected values encoded the old over-freeze. Adds 6
new prose-format invariant tests.

CodeQL "clear-text logging of sensitive information" fix:
`tests/e2e_real_compression.py` previously stored API keys in local
variables in the same scope as diagnostic prints, which CodeQL flagged
via data-flow analysis. Refactored to read keys from `os.environ`
inside the request helper — the credentials never enter the runner's
main scope, so the taint flow never reaches the print.

End-to-end verification with real keys (.env):

  /v1/messages         (PAYG, non-stream)  tok 14109 → 969    saved 13140
  /v1/messages         (PAYG, stream)      tok 14109 → 969    saved 13140
  /v1/chat/completions (PAYG, non-stream)  tok 18460 → 1374   saved 17086
  /v1/chat/completions (PAYG, stream)      tok 18460 → 1374   saved 17086 (cache_hit=100%)
  /v1/responses HTTP   (PAYG, non-stream)  bytes 50138 → 597  saved 18391
  /v1/responses WS     (frame 1)           bytes 46429 → 488  saved 16791
  /v1/responses WS     (frame 2 multi)     bytes 46429 → 488  saved 16791
  /v1/responses WS     (response.cancel)   passthrough untouched

Tests: cargo workspace + pytest (4846 pass, 0 fail), make ci-precheck
passed, two E2E scripts (multi-turn HTTP, WS fake-upstream) all green.
2026-05-07 14:50:03 -07:00

279 lines
9.8 KiB
Python

"""End-to-end verification that /v1/responses WebSocket compression fires.
We can't reach OpenAI's WS endpoint without the `responses_websockets`
beta enabled on the test API key, so this test does the next-best
thing: it spins up a *fake upstream* WebSocket server, points the
proxy at it via OPENAI_API_URL, and connects a client to the proxy.
Verifies:
1. First-frame compression: the client sends a `response.create`
event with a 24 KB output_item; the fake upstream receives the
COMPRESSED frame (much smaller than what was sent).
2. Multi-frame compression: a second `response.create` on the same
WS session is also compressed (the new behavior — was previously
first-frame-only).
3. Other event types (e.g. `response.cancel`) pass through
unchanged.
4. Proxy log surfaces both compression events with token-saved
numbers.
Run via:
.venv/bin/python tests/e2e_ws_responses_compression.py
"""
from __future__ import annotations
import asyncio
import json
import os
import socket
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
import websockets
REPO_ROOT = Path(__file__).resolve().parent.parent
def free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
def wait_ready(port: int, timeout_s: float = 60.0) -> None:
deadline = time.time() + timeout_s
while time.time() < deadline:
try:
with urllib.request.urlopen(f"http://127.0.0.1:{port}/livez", timeout=2) as r:
if r.status == 200:
return
except Exception:
time.sleep(0.5)
raise TimeoutError("proxy not ready")
def long_build_log() -> str:
return "".join(
f"[2024-01-01 00:00:{i % 60:02d}] INFO compile.rs:42 building module foo_{i} "
f"(crate=workspace-{i // 10}, deps=[serde={i}, tokio={i}])\n"
for i in range(400)
)
def make_response_create_payload(turn_no: int) -> dict:
"""A wire-shape `response.create` envelope, with a long
function_call_output that the dispatcher will compress."""
return {
"type": "response.create",
"response": {
"model": "gpt-4o-mini",
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": f"Turn {turn_no} — please summarize the build output.",
}
],
},
{
"type": "function_call",
"call_id": f"call_e2e_ws_{turn_no}",
"name": "shell",
"arguments": '{"command": "cargo build --release"}',
},
{
"type": "function_call_output",
"call_id": f"call_e2e_ws_{turn_no}",
"output": long_build_log(),
},
],
"instructions": "You read shell output and reply tersely.",
"max_output_tokens": 30,
},
}
# ── Fake upstream WS server ─────────────────────────────────────────────
#
# Captures every text frame the proxy forwards so we can assert what
# arrived upstream actually got compressed.
class FakeUpstream:
def __init__(self) -> None:
self.received_frames: list[str] = []
self.server: websockets.server.WebSocketServer | None = None
self.port: int = 0
async def _handler(self, ws):
try:
async for msg in ws:
if isinstance(msg, str):
self.received_frames.append(msg)
# Echo a minimal completion event so the proxy doesn't
# think upstream is hung.
await ws.send(
json.dumps(
{
"type": "response.completed",
"response": {"id": "fake_resp", "output": []},
}
)
)
except websockets.exceptions.ConnectionClosed:
pass
async def start(self) -> int:
self.port = free_port()
self.server = await websockets.serve(self._handler, "127.0.0.1", self.port)
return self.port
async def stop(self) -> None:
if self.server:
self.server.close()
await self.server.wait_closed()
async def main_async() -> int:
fake = FakeUpstream()
upstream_port = await fake.start()
upstream_url = f"http://127.0.0.1:{upstream_port}"
proxy_port = free_port()
print(f"[ws-e2e] fake upstream at ws://127.0.0.1:{upstream_port}")
print(f"[ws-e2e] starting proxy on :{proxy_port}")
log_fp = open("/tmp/e2e_ws_proxy.log", "w")
proc = subprocess.Popen(
[
str(REPO_ROOT / ".venv/bin/headroom"),
"proxy",
"--port",
str(proxy_port),
"--no-telemetry",
# Point /v1/responses upstream at our fake server instead
# of api.openai.com.
"--openai-api-url",
upstream_url,
],
env={
**os.environ,
# Need *some* OpenAI key value so the proxy doesn't refuse;
# the fake upstream ignores it.
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", "sk-fake-for-test"),
"ANTHROPIC_API_KEY": os.environ.get("ANTHROPIC_API_KEY", "sk-ant-fake"),
"HEADROOM_REQUIRE_RUST_CORE": "true",
},
stdout=log_fp,
stderr=subprocess.STDOUT,
cwd=str(REPO_ROOT),
)
failures: list[str] = []
try:
wait_ready(proxy_port)
print("[ws-e2e] proxy ready")
# ── Connect WS client to the proxy ───────────────────────
proxy_ws_url = f"ws://127.0.0.1:{proxy_port}/v1/responses"
async with websockets.connect(
proxy_ws_url,
additional_headers={
"Authorization": f"Bearer {os.environ.get('OPENAI_API_KEY', 'sk-fake')}",
"OpenAI-Beta": "responses_websockets=2026-02-06",
},
) as ws:
# Frame 1: response.create with large content
payload_1 = make_response_create_payload(1)
payload_1_bytes = len(json.dumps(payload_1).encode("utf-8"))
print(f"[ws-e2e] sending frame 1 ({payload_1_bytes:,} bytes)")
await ws.send(json.dumps(payload_1))
# Wait for the fake upstream to receive (or timeout)
await asyncio.sleep(2.0)
# Frame 2: ANOTHER response.create on the same session
payload_2 = make_response_create_payload(2)
payload_2_bytes = len(json.dumps(payload_2).encode("utf-8"))
print(f"[ws-e2e] sending frame 2 ({payload_2_bytes:,} bytes)")
await ws.send(json.dumps(payload_2))
await asyncio.sleep(2.0)
# Frame 3: a non-response.create event — should pass through
cancel = {"type": "response.cancel"}
print("[ws-e2e] sending frame 3 (response.cancel — passthrough)")
await ws.send(json.dumps(cancel))
await asyncio.sleep(1.0)
# ── Inspect what arrived at the fake upstream ────────────
print(f"\n[ws-e2e] fake upstream received {len(fake.received_frames)} frames")
for i, frame in enumerate(fake.received_frames):
print(f" frame {i + 1}: {len(frame.encode()):,} bytes")
# First two frames should be MUCH smaller than what we sent.
# The third (response.cancel) should be a small fixed size.
if len(fake.received_frames) < 2:
failures.append(f"expected ≥2 frames at upstream, got {len(fake.received_frames)}")
else:
f1 = len(fake.received_frames[0].encode())
f2 = len(fake.received_frames[1].encode())
if f1 >= payload_1_bytes // 2:
failures.append(
f"frame 1 not compressed: arrived {f1:,} bytes (sent {payload_1_bytes:,})"
)
if f2 >= payload_2_bytes // 2:
failures.append(
f"frame 2 not compressed (multi-frame regression): "
f"arrived {f2:,} bytes (sent {payload_2_bytes:,})"
)
# ── Scrape proxy log for compression evidence ────────────
await asyncio.sleep(1.0)
canonical = Path.home() / ".headroom" / "logs" / "proxy.log"
log_lines = canonical.read_text(errors="replace").splitlines()[-1000:]
ws_compressed = [
line for line in log_lines if "WS /v1/responses" in line and "compressed" in line
]
print("\n[ws-e2e] WS compression log lines (last few):")
for line in ws_compressed[-6:]:
idx = line.find("] ")
print(" ", line[idx + 2 :] if idx > 0 else line)
if len(ws_compressed) < 2:
failures.append(
f"expected ≥2 WS compression log entries (first frame + multi-frame), "
f"saw {len(ws_compressed)}"
)
finally:
print("\n[ws-e2e] terminating proxy")
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
log_fp.close()
await fake.stop()
if failures:
print("\n=== WS E2E FAILURES ===")
for f in failures:
print(" -", f)
return 1
print("\n=== WS E2E ALL GREEN ===")
return 0
def main() -> int:
return asyncio.run(main_async())
if __name__ == "__main__":
sys.exit(main())