OpenClaw plugin:
- Fix assistant content always returned as array (fixes flatMap crash)
- Clean up debug logging, restore clean plugin entry point
- Add test for text-only assistant round-trip
- Set headroom-ai dependency to npm (not local path)
Telemetry:
- Switch from sb_publishable_ to JWT anon key (PostgREST needs JWT for RLS)
- Switch from upsert to plain INSERT (anon role upsert was failing with 42501)
- Verified: full payload with all JSONB columns writes successfully (201)
Cost tracker:
- Use output_buffer instead of hardcoded 500 for cost estimation in client.py
- CostTracker counterfactual: value removed tokens at avg effective $/token
(actual_spend / billed_tokens) instead of uncached list price — aligns
savings with real billing mix (cache reads, writes, uncached)
- Add debug log for /stats summary payload
- Add 3 tests for new cost savings calculation
TypeScript SDK:
- Add tokenBudget param to compress(), HeadroomClient, CompressOptions
- Proxy /v1/compress accepts optional token_budget to override model limit
The previous commit (854d9f3) incorrectly removed the Authorization
header. The real cause of broken telemetry since 0.5.6 was the payload
containing columns (overhead_avg_ms, pipeline_timing, etc.) that don't
exist in the Supabase table — PostgREST rejects unknown columns with
a 400 error that was silently swallowed.
Fix: restore the Authorization header. The actual table schema fix
(adding the missing columns) must be done in Supabase.
Supabase migrated from JWT anon keys to sb_publishable_ keys in 2025.
The new keys are not JWTs and cannot be sent as Bearer tokens in the
Authorization header. The apikey header alone is sufficient for
PostgREST operations. This was silently failing (swallowed exception)
since the key migration, causing zero telemetry for all versions > 0.5.5.
Windows (fixes#65):
- Reconfigure stdout/stderr to UTF-8 on Windows in wrap.py (cp1252 can't
encode box-drawing characters in CLI banner)
- Pass PYTHONIOENCODING=utf-8 to proxy subprocess env
- Add encoding="utf-8", errors="replace" to subprocess.run() calls in
rtk/installer.py
Bedrock (fixes#64):
- Fix Claude 4.6 model IDs in static fallback map:
anthropic.claude-opus-4-6-v1:0 → anthropic.claude-opus-4-6-v1
anthropic.claude-sonnet-4-6-v1:0 → anthropic.claude-sonnet-4-6
- ToolCallPart: read `input` (v6) with fallback to `args` (legacy) in vercelToOpenAI
- ToolCallPart: produce `input` instead of `args` in openAIToVercel
- ToolResultPart: produce `output: { type, value }` instead of `result` in openAIToVercel
- ToolResultPart: read `output` (v6) with fallback to `result` (legacy) in vercelToOpenAI
- Add MDX example tests: verify every code sample from PR docs against real APIs
- 116 tests passing (unit + integration + e2e with OpenAI + Anthropic)
- compress() now accepts messages in any of 4 formats and returns the same format
- Format detection is structural: Gemini (parts field), Vercel (tool-call hyphen),
Anthropic (tool_use underscore), OpenAI (tool_calls field)
- Added converters: anthropicToOpenAI, geminiToOpenAI, and their inverses
- Updated openAIToVercel to produce AI SDK v6 output format (output: {type, value})
- E2E tested with real Vercel AI SDK + OpenAI gpt-4o-mini + Anthropic claude-haiku-4-5
- headroomMiddleware() proven working: transparently compresses before LLM call
- 14 format detection unit tests, 5 format integration tests, 3 Vercel AI SDK e2e tests
RTK_BIN_PATH was hardcoded to 'rtk' (no extension). On Windows, the
downloaded binary is 'rtk.exe', causing FileNotFoundError during chmod
and verification after download.
Fix: use platform.system() to set RTK_BIN_PATH to 'rtk.exe' on Windows.
Also skip chmod on Windows (no Unix permissions).
Fixes#58
- New `compress()` function: HTTP client calling POST /v1/compress on the proxy
- HeadroomClient: reusable client with retry, fallback, auth support
- Vercel AI SDK adapter: headroomMiddleware() for wrapLanguageModel()
- OpenAI SDK adapter: withHeadroom() Proxy wrapper
- Anthropic SDK adapter: withHeadroom() Proxy wrapper
- Format converters: Vercel AI SDK ↔ OpenAI message format round-trip
- POST /v1/compress proxy endpoint: compression without LLM call
- 90 TypeScript tests (84 unit + 6 integration) + 9 Python tests
- Zero runtime dependencies, all framework peers optional
- Updated README, proxy docs, integration guide, and 6 other doc pages
- New docs/typescript-sdk.md with full SDK documentation
- Removed docs/superpowers/ from tracking (.gitignore)
Claude correctly rejects <system-reminder> in user messages as prompt
injection. Real workflow tags appear in tool outputs, not user messages.
Restructured test to use tool_call → tool result pattern.
Verified passing with real ANTHROPIC_API_KEY from .env.
LLMLingua was the original ML text compressor (BERT-based). Kompress
(ModernBERT, trained on 330K structured tool outputs) replaced it with
better compression quality and simpler architecture.
Removed across 35 files:
- Deleted headroom/transforms/llmlingua_compressor.py
- Deleted tests/test_transforms/test_llmlingua_compressor.py
- Deleted tests/test_proxy_llmlingua.py
- Removed all enable_llmlingua config, _get_llmlingua methods,
LLMLingua fallback paths, LLMLINGUA strategy enum values
- Removed CLI flags, model configs, compression handler references
- Simplified ContentRouter: Kompress is primary and only text compressor
LLM workflows use tags like <system-reminder>, <tool_call>, <thinking>
as structural markers. Kompress/LLMLingua treated these as droppable
HTML noise and silently removed them, breaking downstream tools.
Fix: tag_protector.py detects custom tags (anything NOT in KNOWN_HTML_TAGS),
replaces entire blocks with placeholders before compression, restores after.
Standard HTML tags are unaffected.
- KNOWN_HTML_TAGS: 120+ HTML5 Living Standard elements
- protect_tags / restore_tags utility functions
- Hooked into ContentRouter._try_ml_compressor
- Config: compress_tagged_content flag (default False)
- 28 new tests (unit + integration + real API gated by key)
07-context-compression.ipynb: Context Compression technique using Headroom.
Same RAG setup as notebooks 01-06 (Lilian Weng blog posts, Claude Sonnet,
OpenAI embeddings). Replaces GPT-4o-mini pruning/summarization with local
Headroom compress() — zero extra LLM calls, zero cost.
Ready to fork and PR to https://github.com/langchain-ai/how_to_fix_your_context
Previous chunks were too dense (400 chars, no filler). Real blog post
retrieval returns verbose explanatory text (~1000 chars per chunk).
Kompress now compresses 31-47% within each item.
Demo results: 4424 → 2756 tokens (38% savings), all 12 items kept,
6/6 key concepts preserved, zero extra LLM calls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root fix: compute_optimal_k() now scales k with content diversity using
the SimHash uniqueness ratio already computed in the function.
diversity ~1.0 → keep 100% of items (all unique, dropping any loses info)
diversity ~0.5 → keep ~65%
diversity ~0.0 → keep ~30% (same as before for repetitive data)
No hardcoded RAG detection. No field name heuristics. Pure statistics —
works for any JSON array regardless of source (Pinecone, Chroma, Weaviate,
LangChain, custom APIs).
When all items are kept (high diversity), SmartCrusher tries to compress
text WITHIN each item's long string fields using Kompress (if available).
Falls back gracefully when Kompress is not installed.
Before: 12 unique RAG chunks → kept 2, dropped 10 (0/6 key concepts)
After: 12 unique RAG chunks → kept 12, compressed within (6/6 concepts)
Also adds tests/test_adaptive_sizer.py (16 tests covering high/low/moderate
diversity, knee interactions, bias, caps).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same fix as compress.py but for the proxy server. All 9 pipeline.apply()
calls now pass context=extract_user_query(messages) so SmartCrusher's
relevance scorer receives the actual user question.
Affected paths:
- Anthropic handler (streaming + non-streaming, token_headroom + default)
- OpenAI handler (streaming + non-streaming, token_headroom + default)
- Anthropic batch, OpenAI batch, JSONL batch
- Responses API handler
- Gemini countTokens handler
Also moved extract_user_query to utils.py (shared by compress.py + server.py).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ROOT CAUSE: compress() did not extract the user's question from messages.
The pipeline received empty context, so SmartCrusher selected items by
statistics only (position, anomaly, boundary) — keeping irrelevant chunks
and dropping relevant ones.
FIX: _extract_user_query() in compress.py finds the most recent user
message and passes it as `context` kwarg through the pipeline. SmartCrusher's
RelevanceScorer now receives the actual query and scores items by relevance.
Before: 12 RAG chunks → kept hallucination/video (0/6 key terms)
After: 12 RAG chunks → kept reward hacking content (3/4 key terms)
Also adds:
- examples/context_compression_demo.py — real compression demo for OSS PR
- examples/test_ccr.py — content preservation verification
- OSS_PR_STRATEGY.md — PR target list for LangChain ecosystem
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove CrewAI and OpenAI Agents SDK claims (not implemented)
- Upgrade LangChain from "Experimental" to "Stable" (fully implemented)
- Fix latency FAQ: "1-5ms" → accurate "15-200ms" with cost-benefit context
- Create docs/strands.md (README linked to it but file didn't exist)
- Align docs/index.md with compress() function API (was showing stale class API)
- Add Strands, MCP, Integration Guide to mkdocs nav
- Note stale v0.3.7 benchmarks in LATENCY_BENCHMARKS.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update test_hash_uses_sha256_truncated → test_hash_uses_md5_truncated
to match the SHA256→MD5 change in compression_store.py
- Use errors="surrogatepass" in compute_hash to handle lone surrogates
in unicode content (fixes pre-existing UnicodeEncodeError)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Performance:
- Replace json roundtrip with copy.deepcopy in deep_copy_messages (~20-80ms)
- Eliminate redundant token counting in pipeline (8→2 calls, ~30-100ms)
- Parallel message compression in ContentRouter via ThreadPoolExecutor (~100-200ms)
- Add granular timing metrics: deep_copy, token_count, parallel_compress
- Switch hot-path hashing from SHA256 to MD5 (2-3x faster, non-crypto)
Telemetry:
- Enrich beacon payload with overhead, TTFB, pipeline timing, request
patterns, compression cache stats, CCR usage, and waste signals
- Each extraction section guarded independently so one bad key never
blocks the rest; Supabase POST failure never affects proxy
Bug fix:
- Add Bash/bash to DEFAULT_EXCLUDE_TOOLS so tree/ls output is not
compressed by the text compressor
Add session_id (UUID4) and instance_id (hashed hostname) to beacon
payload. Switch from INSERT to Supabase upsert so each proxy session
produces exactly 1 row, overwritten every 5 minutes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The StreamingResponse for SSE streaming was not forwarding any upstream
response headers to the client. This caused anthropic-ratelimit-* headers
to be silently dropped, while they were correctly forwarded in
non-streaming responses.
Fix: open the upstream connection before the async generator so headers
are available at StreamingResponse construction time. Uses
contextlib.aclosing() to preserve the same cleanup semantics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SmartCrusher: eliminate 5-7x redundant json.dumps by threading cached item_strings
through _crush_array → _create_plan → _plan_* methods, TOIN token counting, and CCR
storage. Move ISO datetime regex to module level. Cache field name hashes in TOIN
semantic detection. Add item_strings param to error detection.
ContentRouter: compile prose detection regex at module level. Extend
eager_load_compressors() to pre-load Magika detector, tree-sitter parsers (8 common
languages), CodeAwareCompressor, and SmartCrusher at startup.
Magika: add as proxy dependency (was never declared in pyproject.toml). Update
detector.py for Magika 1.x API (result.output.label, result.score). Fix batch
detection to use identify_bytes loop (identify_bytes_batch removed in 1.x).
Proxy: simplify startup to use eager_load_compressors() return status dict for
unified component logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New MCP tool that reads files with smart caching via CCR:
- First read: returns full content, stores original in CCR (1h TTL)
- Re-read (unchanged): returns ~20 token cache marker + CCR hash
- Re-read (changed): detects new content hash, returns fresh content
- CCR expired: silently falls back to fresh read (no broken markers)
- fresh=true: bypass cache for subagents or post-compaction
Behind feature flag: HEADROOM_MCP_READ=on (off by default).
Works with existing headroom_compress and headroom_retrieve —
CCR is the shared store, tools don't conflict.
Based on analysis of real Claude Code sessions: 74% of Read calls
are re-reads of files already in context (server.py read 39x in
one session). Cache markers save ~50K tokens per re-read.
- Add anonymous telemetry beacon: proxy sends aggregate-only stats
(tokens saved, compression ratios, cache hit rates) to Supabase
every 5 minutes. No prompts, no content, no PII. Opt out with
--no-telemetry or HEADROOM_TELEMETRY=off.
- Fix Starlette 0.41+ TypeError: migrate from deprecated on_event
decorators to lifespan context manager. Fixes crash with
"Router.__init__() got an unexpected keyword argument 'on_startup'".
- Bump version to 0.5.4.
Three issues fixed:
1. _fetch_bedrock_inference_profiles crashed the proxy on startup when
boto3 was missing or the AWS API call failed (wrong credentials,
permissions, network). Now catches exceptions and falls back to a
static model map.
2. map_model_id produced invalid Bedrock model IDs for unmapped models.
Bare names like 'claude-sonnet-4-20250514' became
'bedrock/claude-sonnet-4-20250514' which is not a valid Bedrock
identifier. Now constructs region-prefixed IDs like
'bedrock/eu.anthropic.claude-sonnet-4-20250514-v1:0'.
3. No static fallback existed (_BEDROCK_MODEL_MAP was empty). Added
_build_bedrock_fallback_map() that generates region-aware model IDs
for all GA Claude models (us./eu./apac. prefixes).
Closes#28
Tests: 27 new tests covering region prefix mapping, static fallback map,
graceful degradation, and model ID mapping for EU/AP/US regions.