Root cause: CompressionCache.compute_frozen_count() stopped at the first
tool_result not in its cache, capping frozen_message_count at 2. Tool
results excluded by content_router (Read/Glob) or skipped (ratio too
high) never entered the cache, so every subsequent message was eligible
for recompression — causing 192 cache busts per session.
Four fixes:
1. Add _stable_hashes set to CompressionCache so excluded/skipped
tool_results don't block the frozen count walk
2. Fix _estimate_message_tokens to count tool_result content and
tool_use input fields (were counted as 0 tokens in Anthropic format)
3. Fix streaming handler to include assistant response and
original_messages in prefix tracker updates (parity with non-streaming)
4. TTL-aware batch recompression: defer first-time compressions within
the 5-min cache TTL window, batching them at the boundary to trade
many small busts for one
The 0.5.18 refactor added api_key forwarding from request headers to
LiteLLM kwargs in all 4 handler methods. This breaks Bedrock (AWS SigV4)
and Vertex AI (Google ADC) which authenticate via env vars, not API keys.
Forwarding a dummy key like sk-ant-dummy overrides AWS credentials.
Fix: skip api_key forwarding for bedrock, vertex_ai, vertex_ai_beta,
and sagemaker providers. Applied to all 4 occurrences.
Strands SDK sends content blocks without a 'type' field:
{"text": "..."} instead of {"type": "text", "text": "..."}
{"toolUse": {...}} instead of {"type": "tool_use", ...}
{"toolResult": {...}} instead of {"type": "tool_result", ...}
The tokenizer's _count_content_parts() only matched on type field,
causing Strands blocks to fall through to json.dumps estimation.
Now explicitly handles Strands text, toolUse, and toolResult formats
with proper recursive counting for nested toolResult content.
8 new tests covering Strands text blocks, tool blocks, and mixed formats.
Sync the Anthropic cache stability test double with the prefix tracker contract used by the handler.
Format the benchmark scripts that were failing ruff format --check in CI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root causes:
- ChatGPT session auth tokens sent to api.openai.com instead of chatgpt.com
- Fallback beta header was responses-api=v1 instead of responses_websockets=2026-02-06
Fixes:
- Detect ChatGPT-Account-ID header and route WS/HTTP to chatgpt.com/backend-api/codex/responses
- Update beta header fallback to match what Codex actually sends
- Add HTTP POST streaming fallback when upstream WS fails (relay SSE over client WS)
- Unwrap response.create envelope in HTTP fallback for correct POST body
- Initialize body before JSON parse to prevent NameError in fallback path
- Fix async test failures: convert asyncio.get_event_loop() to asyncio.run()
savings_usd is now tokens_saved * model list input price (monotonic,
transparent). Removed non-monotonic moving-average repricing and
confusing cost_without_headroom counterfactual.
Dashboard hero shows "Compression Savings" with clear subtitle.
Savings Breakdown section shows compression, cache, and RTK separately
with distinct colors and no scope mixing.
All beacon/telemetry fields preserved. RTK token counts still reported.
Fixes#83
KompressCompressor now tries ONNX Runtime first (156MB INT8 model),
falls back to PyTorch only if ONNX unavailable. No torch needed for
text compression — just onnxruntime (~50MB) + transformers (tokenizer).
Changes:
- Add onnxruntime + transformers to [proxy] extra in pyproject.toml
- Add _OnnxModel wrapper with get_scores/get_keep_mask interface
- _load_kompress() tries ONNX first, falls back to PyTorch
- is_kompress_available() returns True if EITHER backend available
- compress() handles both numpy (ONNX) and tensor (PyTorch) outputs
Dependency impact:
Before: pip install headroom-ai[proxy] → no text compression
After: pip install headroom-ai[proxy] → Kompress ONNX INT8 (156MB)
[ml] extra still available for full PyTorch (600MB, GPU support)
The /v1/responses handler was passing through without compression,
meaning Codex CLI users got zero savings. Now converts Responses API
items (function_call, function_call_output, reasoning, message) to
Chat Completions format, runs the existing pipeline, and converts back.
- New: headroom/proxy/responses_converter.py — pure conversion functions
- 21 unit tests + 3 integration tests (tested with real OpenAI API)
- Preserves reasoning items, images, unknown types verbatim
- Skips compression when previous_response_id is set
- 27% compression on real Codex-pattern payloads (500 records → 14K tokens saved)
Closes#73
Bedrock requires role=tool messages immediately after assistant tool_calls.
The previous fix inserted a user text message in between when the message
contained both text and tool_result blocks, breaking the pairing.
Drop text alongside tool_result (Claude Code never sends it in practice).
Added ordering regression tests for the Bedrock constraint.
_decode_project_path now detects single-letter first component as a
Windows drive letter: -C-MQ2-macros → C:\MQ2\macros instead of
/C/MQ2/macros (which becomes \\C\MQ2\macros on Windows).
- Add Windows drive detection before Unix path attempts
- Fix fallback path construction for Windows patterns
- Add Linux /home/ support in greedy decoder
- Add 2 tests for Windows drive letter patterns