🤖 I have created a release *beep* *boop* --- ## [0.36.5](https://github.com/headroomlabs-ai/headroom/compare/v0.36.4...v0.36.5) (2026-08-22) ### Bug Fixes * **codex:** detect ChatGPT auth from id_token claims so wrap/init emit requires_openai_auth ([#3212](https://github.com/headroomlabs-ai/headroom/issues/3212)) ([2f81fa5](2f81fa5931)) * **doctor:** report project-scoped Claude routing instead of a false negative ([#3213](https://github.com/headroomlabs-ai/headroom/issues/3213)) ([8f3e33a](8f3e33a00e)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
471 KiB
Changelog
All notable changes to Headroom will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Features
- proxy: opt-in cost-aware model routing (#1706). Set
HEADROOM_MODEL_ROUTER_ENABLED=1andHEADROOM_MODEL_ROUTES(a JSON array of ordered rules) to rewrite the upstream model based on estimated input size and tool presence, complementary to content compression, e.g. send small, tool-free requests to a cheaper model. First matching rule wins, and each decision is logged with a reason so routing stays observable. Malformed rules fail open (the rule is skipped, never silently widened). Disabled by default so behavior is unchanged, skipped underx-headroom-bypass/passthrough, and currently applied on the Anthropic/v1/messagespath. - install:
headroom install applynow accepts--code-aware/--no-code-aware,--intercept-tool-results,--protect-tool-results, and--bedrock-profile, mirroring the equivalent flags already onheadroom proxy. Previously the only way to run a persistent deployment with these settings was to hand-editmanifest.jsonafter the fact, which silently reverts on the nextinstall apply. - install:
headroom install apply --env KEY=VALUE(repeatable) passes environment variables into supervised runners (macOS launchd, Linux systemd/cron, Windows services/tasks). These runners previously started with a bare environment and did not inherit the interactive shell's exports — e.g. a customHEADROOM_WORKSPACE_DIRnever reached the supervised process, soheadroom install agent runlooked for its manifest in the wrong location and failed outright even thoughinstall applyitself succeeded.--envvalues are merged intoDeploymentManifest.base_envlast, so they can override auto-derived defaults, and are threaded into the generatedrun-headroom.sh/ensure-headroom.sh(and Windows equivalents) asexport/$env:lines before theexec.
Fixed
- memory: preserve semantically similar memories after
memory_save. Cosine similarity now produces a consolidation hint only; it no longer schedules a background deletion, because related memories can describe distinct facts. Supersession remains available through the explicitmemory_updatepath with a caller-supplied memory ID. - mcp: reap orphaned
headroom mcp serveprocesses when the launching client dies. An MCP stdio server relies on stdin EOF to shut down, but an abrupt clientSIGKILLleaves the SDK's blocking stdin-reader thread wedged, soserver.run()never returns; the process is reparented to init/launchd (ppid == 1) and lingers, pinning one Python interpreter + tree-sitter grammars per dead session (observed 3+ simultaneously).run_stdio()now runs a parent-death watchdog alongsideserver.run()that fires when the captured parent pid changes andos._exit(0)s from inside the stdio context manager, bypassing the same wedged teardown (#2185, #1761). - cache/prefix-freeze: resolve
PrefixCacheTrackers per conversation lineage within a session id, so concurrent conversations sharing a fallback id no longer thrash one tracker's frozen-prefix state (#2085). Without anx-headroom-session-idheader the fallback id hashesmodel + system prompt— identical across a Claude Code session and every one of its parallel subagents (and any set of sessions reusing one system prompt). On the shared tracker their interleaved histories cross-contaminate the freeze state: the forwarded prefix is byte-unstable on nearly every turn and the provider prompt cache is re-written instead of read — reported as ~4.4x cache-creation inflation and a 2.5–3x net cost increase under Claude Code.SessionTrackerStore.resolve_trackernow reuses the tracker whose previous request messages are a prefix of the incoming history (client histories are append-only, so a conversation's next request always extends its previous one), starts a fresh lineage when the history diverges or was rewritten (client-side compaction — that provider cache line is gone anyway), and caps lineages per session id (PrefixFreezeConfig.max_lineages_per_session, default 32; over-cap conversations share one overflow tracker instead of evicting established lineages, so a fan-out storm past the cap degrades only its own tail — and0disables lineage splitting). Matching compares the original client bytes under the same canonical cross-turn equivalence as the cache-stable delta path (_canonicalize_for_prefix_compare), so a moved cache breakpoint, string<->block content sugar, or per-turn transport annotations do not read as a rewrite. Separately, the fallback id now hashes only the LEADING run ofrole:"system"messages: agentic clients interleave<system-reminder>turns into the history as actual system-role messages (hook output, skills lists, truncation notices), and hashing those rotated the session id mid-conversation — orphaning the prefix tracker and every other session-sticky subsystem (beta headers, CCR/memory registries, the compression cache) each time a reminder landed. Both handler paths now derive the session id and the lineage from the same original client bytes, so a turn-dependent hook rewrite cannot rotate one without the other. The session id itself never changes: session-sticky state keyed on it (beta-header stickiness, CCR and memory-tool registries, the compression cache) is untouched, and a single-conversation session keeps its exact previous behavior (the first lineage lives under the bare id). - proxy/bedrock: wire
PrefixCacheTrackerupdates into both Bedrock backend paths (handle_anthropic_messages's non-streaming branch inanthropic.py, and_stream_response_bedrockinstreaming.py).update_from_response()was previously only called from the direct-Anthropic-API branch; both Bedrock branches returned before ever reaching it, so the tracker's state stayed permanently empty for the life of a session on any--backend bedrockdeployment:extract_cache_stable_delta()always saw no previous turn, and--mode cachefell back to full unmodified passthrough on every turn instead of freezing the already-cached prefix and compressing only the new suffix. - install:
install_supervisor's macOS branch did an unconditionallaunchctl bootoutfollowed by a barebootstrapwith no retry, unlikestart_supervisor, which already rides out the ~15s EIO (error 5) window launchd exhibits for several seconds after a bootout. This leftinstall apply's own reinstall path (and anything that re-applies a deployment, e.g. a futureheadroom doctor --fix) exposed to a race that previously required manual recovery (bootout + remove the plist + reapply). Extracted the retry loop already used bystart_supervisorinto a shared_bootstrap_with_retry()helper, now used by both call sites. - proxy/savings:
SavingsTracker.record_request()only appended a history point whentokens_saved > 0(headroom's own lossy compression). In--mode cache,tokens_savedis near-always 0 by design, since the frozen prefix is byte-replayed rather than compressed to keep the provider's prompt cache warm. That silently dropped every history point on a cache-mode deployment even whencache_read_tokens/cache_savings_usdwere large, makingheadroom-monthly-style tooling read as a total savings collapse. The guard now fires ontokens_savedORcache_read_tokens, and the appended entry carriescache_read_tokens/cache_savings_usdso downstream consumers can show them;_normalize_history_entrydefaults both fields to 0/0.0 for legacy entries that predate this change. - litellm: vendor-specific top-level fields on
/v1/chat/completions, including vLLM'schat_template_kwargsfor per-request Qwen3 thinking-mode toggles, now reach OpenAI-compatible backends through LiteLLMextra_bodyinstead of being dropped by the standard-parameter allowlist (#2128). - cache aligner: hash the actual frozen Claude Code prefix instead of only system-message text, so
stable_prefix_hash/prefix_changednow surface prompt-cache churn when a cached tool-result block changes without any system-prompt edit (#2085). - proxy image compression: run native image compression in a spawned subprocess so an OpenCV or KleidiCV crash now fails open to the original image payload instead of taking down the whole proxy process (#2107).
- proxy/windows: support Windows selector-event-loop startup on uvicorn versions older than 0.36. Newer uvicorn accepts
loop="asyncio:SelectorEventLoop"as a custom loop-factory import path, but older versions treat it as an unknown built-in loop name and raiseKeyError. Windows now setsWindowsSelectorEventLoopPolicyfor those older versions instead of passing an unsupportedloopvalue (#1650, #1621). - backends/litellm: preserve
cache_controlontool_resultblocks when converting Anthropic messages for the Bedrock Converse path, and complete streaming cache-stats surfacing instream_message. Complements #1390, which preservescache_controlon the system prompt and plain text blocks but explicitly leavestool_resultout of scope — in agent loops the moving cache breakpoint lands on the tailtool_resultfar more often than on the system prompt, so that gap left most of the caching benefit on the table. Separately,stream_messagenever requestedstream_options.include_usage, so LiteLLM/Bedrock never returned a usage chunk over SSE andcache_read_input_tokens/cache_creation_input_tokensalways reported 0 downstream even when the prompt cache was genuinely engaged; the terminalmessage_deltanow carries the real cache values captured from the trailing usage chunk once the stream completes. - shared_context:
SharedContext.putno longer evicts an unrelated entry when it merely updates a key that is already cached at capacity — same defect class fixed forSemanticCachein #2094. - compress: stop mutating the caller's
CompressConfig.compress(config=my_cfg, protect_recent=0, target_ratio=0.2)used to write those kwargs ontomy_cfg, so a shared per-agent config was silently rewritten by every request that overrode a single option. - paths: reject
.,.., and NUL as plugin names soplugin_config_dir/plugin_workspace_dircannot resolve outside theplugins/sandbox. Previouslyplugin_config_dir("..")returned the entire config root andplugin_workspace_dir("..")returned the workspace root (savings ledger, memory DB, license cache, logs). - backends/litellm: drop tool names over 64 chars before calling Bedrock Converse (
send_messageandstream_message), instead of letting the whole request 401. The Bedrock Converse API hard-rejects any tool name past that length, and Claude Code includes every globally-added claude.ai MCP connector tool in every request, even ones the user hasn't enabled locally, so a single oversized connector name broke every call through this backend. Only thebedrockprovider filters; other providers forward tool names unfiltered. - memory: annotate
_EMBEDDER_CACHEasdict[tuple[str, str, str], Embedder]to match the 3-element key (backend, model, ollama_base_url). The stale 2-tuple annotation mademypy headroomfail onmain, which broke thelintCI job on every open PR. - install: include
orjsonin the[proxy]extra souv tool install "headroom-ai[all]"satisfies LiteLLM OpenRouter/provider backends that import it at runtime (#2056). - The dashboard's per-request metadata (the
recent_requests/request_logstail and theconfigblock with upstream URLs) is gated to loopback callers via_request_is_loopback. When Headroom runs in a bridge-network container (Docker/podman, or Apple Containerization / mocker), a browser on the host reaches the proxy through the container gateway, sorequest.client.hostis the gateway IP rather than127.0.0.1— the sensitive block was stripped and the "Recent Requests" table rendered empty even though the operator is local. A peer inside an operator-configured trusted-gateway CIDR (HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS, already used to sanitizeX-Forwarded-*) is now treated as loopback-equivalent, while the loopbackHost-header gate is retained as the DNS-rebinding defence. Opt-in and empty by default, so there is no behavior change unless the gateway CIDR is allow-listed. - Non-finite values (
NaN,Infinity) inproxy_savings.jsonor in upstream cost/token metadata no longer crash the proxy or corrupt the savings dashboard.SavingsTracker's numeric coercion caught onlyTypeErrorandValueError, soint(float('inf'))raised an uncaughtOverflowErrorwhile loading persisted state (SavingsTracker.__init__failed and the proxy would not start), andfloat('nan')/float('inf')passed straight through, then serialized toNaN/Infinityliterals that the dashboard'sJSON.parserejects.json.loadsaccepts those literals, so one bad write poisoned every later start. Both coercion helpers now also catchOverflowErrorand reject non-finite floats, failing open to safe defaults. headroom learnnow honorsCLAUDE_CONFIG_DIR. It resolved the Claude config directory as~/.claudeand wrote global memory to~/.claude/CLAUDE.md, so users who relocate their Claude config via that env var hadlearnscan the wrong directory and detect no projects. The scanner and memory writer now read/write the configured directory (#1630).--backend bedrocknow fails fast with an actionable error when temporary AWS credentials (AWS_SESSION_TOKEN) are used but botocore is not installed (e.g. the slim default Docker image). litellm's session-token auth path imports botocore, so the missing dependency previously surfaced only at request time as a misleadingauthentication_error: No module named 'botocore'. The proxy now tells the user to install thebedrockextra up front (#1551).- Content detection no longer crashes the proxy on text containing an
orphaned
+++target line with no preceding---source line (common inset -xxtrace output and partial diffs). The bundledunidiff0.4.0 parser panics on that input instead of returning an error; the Rust diff detector now contains the panic and treats the fragment as plain text, so the request is compressed and forwarded normally instead of returning HTTP 500 (#1547). - Proactive expansion blocks injected into user turns are now wrapped in
<headroom_proactive_expansion>XML tags, giving downstream consumers (LLMs, loggers, attribution parsers) a machine-readable provenance boundary and preventing misattribution in multi-agent threads. - cli: the startup banner no longer advertises
HEADROOM_COMPRESSION_STABLE_AFTER_TURNandHEADROOM_STALE_READ_COMPRESS_AFTER_TURNSas tuning knobs. Both were read only to render thePerformance Tuningbanner section and were never wired into the compression path, so setting them changed the banner but had no effect on behavior. The banner now surfaces only the embedding sidecar, which is a real, consumed setting. - memory/embedder: cap CPU thread oversubscription in the local
torch/sentence-transformers embedder. Concurrent encodes previously each
fanned out to ~
os.cpu_count()BLAS/OpenMP threads, so under load the memory path starved the asyncio event loop and spiked/livezlatency to several seconds. CPU encodes now run on a dedicated, size-limited executor whose workers each pin their thread pool, bounding total embedding threads toHEADROOM_EMBED_CONCURRENCY×HEADROOM_EMBED_NUM_THREADS(defaultsmin(4, cpu)× 1). The ONNX embedder already capped its threads; this brings the torch path to parity (#198).
Changed
- telemetry: anonymous usage telemetry is now opt-in (off by default) instead of opt-out. Nothing is collected or sent unless you set
HEADROOM_TELEMETRY=onor pass--telemetrytoheadroom proxy/headroom install apply.is_telemetry_enabled()is fail-closed — only explicit on-values (on/true/1/yes/enable/enabled) enable it; unset, empty, or unrecognized values stay disabled. The existing--no-telemetryflag andHEADROOM_TELEMETRY=offremain accepted for back-compat, and install manifests now write theHEADROOM_TELEMETRYvalue explicitly so generated deployments are unambiguous. - ccr:
headroom_statsnow labels its formatted proxy output as a rolling/window-scoped session and adds a lifetime savings section from/stats persistent_savings.lifetimewhen present, while keeping existing summary structure and fallback JSON output behavior. - docs/ccr: qualify the current CCR auto-resolution claim by provider. The docs now state that transparent
headroom_retrievehandling is wired on the Anthropic and OpenAI proxy paths, while native Gemini still lacks that server-side response-handler path and Gemini's OpenAI-compatible endpoint can fail round-2 continuations withMALFORMED_FUNCTION_CALL(#2041). - docs/claude: document that
ENABLE_TOOL_SEARCH=trueis correct for the standalone Claude CLI through Headroom but currently breaks tool-result rendering in Anthropic's VSCode extension webview, and point persistent-install users at the manifest override to settool_envs.claude.ENABLE_TOOL_SEARCHto"false"for that target (#2028).
Added
- integrations: CrewAI tool compression —
wrap_tools_with_headroom()wraps CrewAIBaseToolinstances with automatic output compression viacompress_tool_result(), with per-tool metrics tracking (#1379). - integrations: AutoGen tool compression —
wrap_tools_with_headroom()wraps AutoGenFunctionToolinstances (sync and async) with automatic output compression, including per-tool metrics tracking (#1379).
Features
-
grok-build: add first-class Grok Build support —
headroom wrap grok-build/headroom unwrap grok-build, reversible~/.grok/config.tomlinjection (in-placebase_urlrewrite when[model.grok-build]already exists),GrokRegistrarMCP install, and install/telemetry wiring (#1629). -
wrap: add
headroom wrap omp/headroom unwrap ompfor Oh My Pi — points omp's built-inanthropicprovider at the local proxy via a marker-fencedproviders.anthropic.baseUrloverride in~/.omp/agent/models.yml, snapshotting the pre-wrap file byte-for-byte and restoring it on unwrap. omp resolves its Anthropic chat endpoint from models.yml (ANTHROPIC_BASE_URLonly feeds its web-search helper), and a same-ID override keeps omp's bundled model catalog and stored credentials (#1149) -
compress: expose
frozen_message_countin library-modecompress()via a newCompressConfigfield (default0, unchanged behavior).read_lifecycle.apply()already skips stale-Read replacements inside a frozen message prefix, but only the proxy handlers could pass it —ContentRouterreads it from transform kwargs and the public API never forwarded it. Library-mode callers that manage their own conversation loop can now stop transforms from rewriting messages already anchored in the provider's prompt cache, which would otherwise convert 0.1x cached prefix reads into full-price cache writes (#2178). -
proxy: report a new-content-relative input savings rate in
/stats:tokens.new_input_tokens(provider-billed non-cache-read input: uncached + cache-write tokens, from response usage) andtokens.new_input_savings_percent(savings as a fraction of new input plus the tokens compression removed before they could be billed). The existing whole-request ratios recount the full transcript on every turn, so a 200-turn session counts its history 200x into the denominator and long-running cached sessions (especially 1M-context models, which never compact) dilute toward ~0% regardless of how well compression performs on content newly entering context. Purely additive; existing fields unchanged. Reports 0 when no cache usage data exists (e.g. providers without cache metrics) rather than dividing savings by themselves. -
transforms: first-class C# support in
CodeAwareCompressorvia the tree-sittercsharpgrammar already shipped in the pinnedtree-sitter-language-pack— no new dependencies (#1664). Parity with Java/C++/Rust: signatures preserved verbatim, method/constructor/destructor/operator/local-function bodies compressed; block-scoped and file-scoped namespaces, records, structs, interfaces, and enums handled; C#-distinctive auto-detection. Preprocessor conditionals (#if…#endif) are preserved verbatim as opaque regions (blocks wrapping onlyusingdirectives stay with the imports),#regionmarkers no longer swallow the following line during class-member extraction, and top-of-file license banners /#region Licenseheaders stay on top instead of being relocated below the code. Real-repo runs: 16.1% tokens saved on Newtonsoft.Json (945 files), 37.8% on Polly (797 files), output syntax-valid for 1742/1742 files. -
proxy: add provider-only HTTP proxy routing via
--http-proxyandHEADROOM_HTTP_PROXY. Upstream LLM provider calls can now use an HTTP proxy without setting process-wideHTTP_PROXY/HTTPS_PROXYvariables that are inherited by tool executions; proxied provider clients use HTTP/1.1 so HTTPS provider APIs can tunnel through CONNECT. -
proxy: add output shaping for OpenAI Responses traffic on
/v1/responsesHTTP requests and Codex WebSocketresponse.createframes, with stable output-savings holdout keys and counted WS token strata for the experiment. -
stats: per-bucket output-shaping savings in
/stats-history. Eachseriesbucket (hourly/daily/weekly/monthly) now carriesoutput_tokens_saved_deltaandoutput_savings_usd_deltaalongside the existing compression deltas, sourced from a per-request synthetic-control estimate (SavingsRecorder.estimate_request_savings) threaded throughrecord_requestinto the rollup. Lets dashboards chart output-shaping savings over time as a distinct series — previously it existed only as a single global aggregate. Additive and backward-compatible: pre-feature checkpoints default the new fields to 0 (#1816). -
observability: the
headroom.compression.pipelinespan now also carries the OpenTelemetry GenAI semantic-convention attributegen_ai.request.modelalongside the existingheadroom.*attributes, so Headroom's traces group and filter by the standardgen_ai.*schema in any OTel-native backend (Grafana, Datadog, etc.). Purely additive; no existing attribute changed.gen_ai.operation.name,gen_ai.provider.name, andgen_ai.usage.*are deliberately deferred (they need per-caller operation threading, reliable upstream-provider resolution, and response-path usage respectively). -
wrap:
headroom wrap claude --1mpreserves the 1M context window. Behind a customANTHROPIC_BASE_URL(the proxy) Claude Code drops thecontext-1mbeta header and caps the window at 200k for entitled subscription users; the opt-in flag setsANTHROPIC_MODEL=<opus>[1m]on the launched process so the 1M window activates through Headroom. A model already selected viaANTHROPIC_MODELis preserved (only the[1m]suffix is appended) (#1158). -
learn: weight loops in
headroom learn. A new loop detector (headroom/learn/loops.py) recognizes repeated tool-call patterns — including RTK re-fetch loops, where RTK's output truncation makes the agent re-run larger-limit variants of a successful command — collapses output-limit variants to one signature, measures the wasted tokens, surfaces loops as a highest-priority digest section, and weights loop guardrails above one-off rules by their measured waste. Previously loops had no special weight and a no-failure re-fetch loop was skipped entirely. Adds an RTK-loop eval (benchmarks/rtk_loop_learn_eval.py) that reproduces a loop, runs it through Learn, and asserts the generated guardrail ranks first and prevents re-triggering. -
learn: write per-project learnings to the personal, gitignored
CLAUDE.local.mdby default instead of the team-sharedCLAUDE.md, matching Claude Code's memory convention so machine-specific paths and tool-discovery byproducts no longer pollute the shared file. Adds a--targetflag to override the destination (e.g.--target CLAUDE.mdto opt back into the shared file, or any custom path), and auto-migrates a stale learned-patterns block out of an existingCLAUDE.mdintoCLAUDE.local.mdwith a warning (#1072). -
proxy/transforms: take large cold-start contexts off the synchronous kompress path — the root cause behind the
compression_first_stage30s-timeout + leaked-thread → executor-saturation cascade (#1171). A token size-gate inside the ML boundary routes oversized text away from ModernBERT (HEADROOM_KOMPRESS_MAX_TOKENS); a cooperative chunk-deadline bounds any kompress run that does proceed (HEADROOM_COMPRESSION_DEADLINE_MS); an opt-in off-path mode forwards uncompressed immediately and compresses in a single per-process background drain so the request never blocks on ML (HEADROOM_BACKGROUND_COMPRESSION); and a new nativeTextCrusher— a fast deterministic extractive prose compressor inheadroom._corethat reuses the shared BM25 relevance scorer — is the fast alternative to ModernBERT for large plain text (HEADROOM_TEXT_CRUSHER). All default off and fail-open. On a SQuAD answer-retention eval (requires the SQuAD dev set) TextCrusher keeps ~94% of buried answers at 30% size vs ~36% for truncate/random, and runs in one O(n) pass -- sub-second where ModernBERT takes minutes (self-contained speed benchmark inbenchmarks/text_crusher_quality_eval.py). -
proxy/transforms:
TextCrushernow compresses CJK (Chinese/Japanese/Korean) text (#1171). CJK has no spaces or ASCII sentence terminators, so the prior ASCII splitter/tokenizer collapsed a whole CJK paragraph into one segment/one token and passed it through near-uncompressed. CJK-bearing input now takes an ICU (icu_segmenter, UAX#29 + dictionary) sentence/word segmentation path with a local BM25 relevance over the ICU tokens; pure-ASCII text is byte-identical to before, and the shared BM25 scorer is untouched. On real CMRC2018 Chinese QA, answer-retention under compression rises from 34% to ~91%; end-to-end aggregate savings on real CJK content rise from 16% to 40%. -
proxy: measure and surface rolling and current token throughput metrics (active/wall-clock input, compression, effective forward, and streamed generation) in
headroom perfCLI and the dashboard (#959). -
vibe: add Mistral Vibe CLI support with
headroom wrap vibe. -
proxy: per-project savings breakdown on the dashboard for all wrapped agents — Claude Code, Codex, aider, Copilot, and Cursor (#802).
headroom wrap claude/codextag requests with anX-Headroom-Projectheader (launch-directory name);wrap aider/copilot/cursor— whose clients cannot send custom headers — use a/p/<name>base-URL prefix the proxy strips. Savings are aggregated per project (persisted, schema v3 with transparent v2 migration), exposed assavings.per_projectin/statsandprojectsin/stats-history, and shown in a Per-Project Savings dashboard table. -
memory: opt-in Apple-GPU (MPS) embedding offload via
HEADROOM_EMBEDDER_RUNTIME=pytorch_mps. When set (and Apple MPS is available), the memory embedder runs on the torch sentence-transformers backend on the Apple GPU instead of the default ONNX CPU embedder, freeing the CPU under load. If MPS or the dependencies are unavailable, Headroom logs a warning and uses the existing default embedder selection path (ONNX when available, then the pre-existing local fallback). MPS encode calls are serialized internally (torch-MPS is not thread-safe). Adds the new[pytorch-mps]extra (pip install 'headroom-ai[pytorch-mps]'). Default behavior is unchanged. -
proxy: cross-region Bedrock inference-profile detection — geo-prefixed model IDs (
eu./us./apac./global.) are now resolved to their canonical vendor, so Anthropic cross-region profiles (e.g.eu.anthropic.claude-haiku-4-5-20251001-v1:0) receive live-zone compression instead of being silently skipped (#999). -
proxy: Converse-body compression on the native Bedrock route — the live-zone dispatcher now recognizes Bedrock Converse content blocks (typeless
{"text": …}, not only Anthropic{"type":"text", …}), so Converse user-message text compresses;run_anthropic_compressionno longer bails to passthrough when the body lacks an InvokeModelanthropic_versionenvelope, and envelope re-emit stays gated on successful parse (#999). -
docker: bundle
headroom-proxybinary in publishedruntimeandruntime-slimimages — closes #976 (#999). -
transforms: add opt-in audit-safe mode to
SmartCrusher—SmartCrusherConfig(audit_safe=True, protected_patterns=[...], fail_closed_on_protected_loss=True). Rows matching a protected pattern are scanned before JSON-array compression and guaranteed to survive the compressed output verbatim afterward (never dropped, never replaced by an opaque<<ccr:...>>marker only). Applies on both thecrush_array_jsonconvenience API and the_smart_crush_contentpathapply()uses for real tool-output compression. If a protected row still can't be preserved after the splice-back pass, the crusher fails closed by returning the original uncompressed content (or ships a best-effort result with a warning whenfail_closed_on_protected_loss=False). Default isaudit_safe=False— no behavior change for existing callers (#1705).
Bug Fixes
- ccr: normalise a retrieved hash to lowercase so an uppercase echo still hits the store.
parse_tool_callvalidated the hash case-insensitively (hash_key.lower()) but returned it verbatim, while the compression store keys every entry by a lowercase hash (sha256 hexdigest, andexplicit_hash.lower()on write) andretrieve/get_entry_statuslook the key up as-is. A model that echoed the marker hash in uppercase therefore passed validation but missed the store, failing an otherwise-validheadroom_retrieveand losing the original content.parse_tool_callnow returns the canonical lowercase form. - proxy: run a cold-start fast pass before background-compression deferral so byte-identical freeze doesn't lock sessions to the uncompressed transcript. Since #1850, a session's provider-cached prefix is frozen in whatever form its cold start forwarded; deferring the WHOLE pipeline (
HEADROOM_BACKGROUND_COMPRESSION=1, frozen=0, ≥50k tokens) therefore cached the raw transcript and forfeited the session's compression savings for its lifetime — including sub-second lossless wins likeread_lifecyclestale-read drops, observed in the field as sessions permanently stuck at 0 savings. The deferral branch now runs the pipeline synchronously with the newskip_kompress=Truekwarg (everything except the Kompress ML stage — the only stage that can blow the request budget per #1171) under a bounded fast-pass budget (HEADROOM_COLD_START_FAST_PASS_TIMEOUT_SECONDS, default 10s), forwards the pruned form, and defers only Kompress to the background job (taggeddeferred:kompress_background). Fail-open: on fast-pass timeout/error the request forwards uncompressed exactly as before. Units routed to Kompress underskip_kompresstake the same fallback as when the model isn't ready. - proxy/batch: preserve sibling tool configs on Google batch requests. When optimizing a Gemini batch item, the handler rebuilt the request's
toolsas a single[{"functionDeclarations": ...}]entry, discarding any other entries in the array (googleSearch,codeExecution, etc.). A batch request that combined function calling with Google Search or code execution therefore reached Google with those siblings stripped, silently disabling them. The handler now replaces only thefunctionDeclarationsentry (appending one when the original had none) and keeps every sibling entry intact. - savings: record the pre-compression original as the ledger
before, not the forwarded count. InPrometheusMetrics.record_requestthe durable savings ledger was written withtokens_before=input_tokens, butinput_tokensthere is the optimized (post-compression) count that was forwarded (emit_request_outcomepassesoutcome.optimized_tokens).headroom savingsderives the reported reduction percent as saved / before, so understatingbeforebytokens_savedinflated it — a real 40% reduction (1000 → 600) was reported as ~67% (400 / 600). The event now recordstokens_before=input_tokens + tokens_saved(the reconstructed original) andtokens_after=input_tokens(the forwarded count); thesavedand cost figures are unchanged. - proxy/gemini: forward a non-JSON upstream error body with its real status instead of a synthetic 502. In
handle_gemini_generate_contentthe token-extractionexcept (KeyError, TypeError, AttributeError)guardingresponse.json()omittedjson.JSONDecodeError/ValueError, so a non-JSON body (an HTML/empty error page from an overloaded Google/Vertex/Copilot frontend, common on 5xx/429) escaped to the outerexcept Exceptionand was returned as a generic 502 — discarding the real upstreamstatus_codeand body and defeating the client's retry/backoff. The except now catches the JSON/ValueError family, matching the all-non-text sibling branch, so the true status and body are forwarded verbatim. - init/codex: don't overwrite the user's
hooks.json._ensure_codex_hookswrote a fresh payload containing only Headroom's two hooks, wholesale-replacing~/.codex/hooks.json— so any user-managed Codex hooks (and other top-level keys) were silently destroyed onheadroom init codex. It now read-merges: existing entries are preserved, Headroom's are deduped on theheadroom-init-codexmarker and appended, matching_ensure_claude_hooks/_ensure_copilot_hooks. - cli/init: fail with an actionable error when a target's settings file contains invalid JSON.
_json_file(used to read-merge-write Claude'ssettings.json, Codex'shooks.json, etc.) calledjson.loadsunguarded, so a hand-edit typo (e.g. a trailing comma) crashedheadroom initwith a rawJSONDecodeErrortraceback. It now raises aClickExceptionnaming the file and the parse error and telling the user to fix it or move it aside, without touching the file (returning{}would have made the follow-up write overwrite the user's settings). - ccr: detect
read_lifecyclestale/superseded markers in the retrieve-tool injector so they stay redeemable. Those markers ([Read content stale: … Retrieve original: hash=<hash>]) store the original bytes in the CCR store under a valid hash, but none ofCCRToolInjector's patterns matched them — every pattern required the word "compressed" or the<<ccr:form. So on a frozen-prefix turn (bothread_lifecycleand prefix freezing are on by default) the injector reported no compressed content, theheadroom_retrievetool was not injected, and the model was handed a marker advertisingRetrieve original: hash=Xwith no tool to redeem it — silent data loss for stale reads, where retrieval is the only way to recover the original-at-read-time content (the exact case the #1006 guard exists to prevent). Added a pattern matching the load-bearingRetrieve original: hash=phrase, aligning the injector with the siblingread_maturationmarker that was already (incidentally) detected. - install: don't crash
headroom install statuswhen the health payload'sconfigis a non-dict. The command didpayload.get('config', {}).get('backend', manifest.backend), butdict.get(..., {})only defaults on a missing key — a present-but-non-dictconfig(null, a string, a list, e.g. when a different or older service is answering on the port) reached the chained.get('backend', ...)and raisedAttributeError, crashing the command with a raw traceback. The value is now guarded withisinstance(config, dict)before the lookup, mirroringwrap.py's_proxy_health_config, so it falls back to the manifest's backend. - telemetry: only advance the usage-report baseline after a confirmed 200.
UsageReporter._report_usagesends usage as a delta against the last snapshot, but it called_snapshot_metrics()(and advanced_last_report_time) unconditionally after the POST — including when the send returned non-200 or raised. So a report that failed to reach the cloud (which the module is explicitly designed to tolerate) still rebased the baseline, permanently dropping that window's requests/tokens from usage-based billing/quota; the next report started from the advanced baseline and never re-included them. The baseline now advances only on a 200, so a failed send leaves the window intact for the next report to retry. - savings: stop the durable savings ledger from billing free (0-priced) models at the
$3/Mfallback.estimate_cost_usdguarded the litellm estimate withif priced > 0, so a genuinely free model — where_estimate_compression_savings_usdcorrectly returns0.0— was treated as "unpriced" and fell through to the blended fallback rate, writing phantom cost-avoided into the JSONL ledger and surfacing it inheadroom savings. The ledger now trusts the estimate verbatim for known models (it already falls back internally for models litellm can't price and returns0.0for free ones), fixing the same defect at this call site that was already fixed inside the helper. - init/codex: stop
headroom init codexfrom deleting per-profile provider settings._ensure_codex_providerremoved the root-levelmodel_provider/openai_base_url(which init owns) with a multiline regex that matched those keys in every table, so a user's[profiles.*]overrides (e.g.[profiles.work] model_provider = "azure") were silently stripped and those profiles fell through to the injected"headroom"default — config corruption. The strip is now scoped to the document root (everything before the first table header), so per-profile overrides are preserved while init still replaces a root-level assignment. - proxy: strip output-only content blocks from request messages before forwarding. Anthropic's server-side refusal-fallback feature (
server-side-fallback-2026-06-01) emits a{"type":"fallback","from":{...},"to":{...}}block inside the assistant response to signal that a refused request was re-served by the fallback model. That block is valid on the response path but rejected on the request path, so when a client replays the assistant turn the next request 400s (invalid_request_error: messages.N.content.0: Input tag 'fallback' ...) and the conversation gets permanently stuck through the proxy.read_request_json_with_bytes(Anthropic/OpenAI/Bedrock) and_read_request_json(Gemini) now drop such blocks — re-encoding the raw bytes so byte-faithful passthrough cannot leak the pre-strip body, backfilling a benign text block if a turn is emptied, and leaving requests without such blocks byte-identical (no cache churn). - wrap/doctor: make the Claude Remote Control gate warning accurate and stop it firing for users who never had the feature (#1779). Claude Code 2.1.196 added a client-side check that deterministically disables first-party Remote Control (
/remote-control//rc) wheneverANTHROPIC_BASE_URLpoints at a non-api.anthropic.comhost — which Headroom always does. The old notice hedged ("may hide the Remote Control menu"); it now states the disable as fact, names the/rccommand, and detects the installed Claude Code version so the wording is exact (2.1.196when known,2.1.196+when not). The gate is upstream and RC's control-plane talks toclaude.ai(not the API host), so Headroom cannot restore it — the warning tells you to run Claude without Headroom for RC sessions. The warning is suppressed for auth modes that never had Remote Control (API-key/PAYG viaANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN, and Bedrock/Vertex/Foundry cloud IAM) and on Claude Code builds older than 2.1.196 where RC is unaffected by a custom base URL. Both theheadroom wrap claudelaunch banner andheadroom doctorco-report the sibling base-URL gates Headroom does restore — on-demand tool loading (#746, automatic) and the 1M context window (#1158, via--1m) — and the wrap-side co-report is session-accurate: it says "already restored via --1m" when the flag is in effect and reports tool deferral OFF (not falsely "kept on") when the user chose--tool-search false/ENABLE_TOOL_SEARCH=false; theENABLE_TOOL_SEARCH=...banner line got the same accuracy fix.is_custom_anthropic_base_urlnow recognizes scheme-less values (myproxy.local:8080,127.0.0.1:8787) as custom hosts and degrades gracefully on malformed URLs instead of crashingdoctor.doctorresolves the Claude Code version lazily, so runs with no custom base URL never pay theclaude --versionsubprocess. No request bytes are touched (cache-safe); this is UX/notice-only. - install: default the docker image to
ghcr.io/headroomlabs-ai/headroom:latestinstead of the deadghcr.io/chopratejas/headroom:latest. After the repo moved to theheadroomlabs-aiorg, GHCR did not redirect the old package, soheadroom install/headroom initand the install scripts pulled a frozen0.27.0image while current releases publish to the new path (#1867). - transforms/content-router: stop a profile-derived
read_protection_windowkwarg from weakening an explicit--protect-tool-resultsguarantee.ContentRouter.apply()computesread_protection_windowfromprotect_recent_reads_fraction, where0.0(the sentinel--protect-tool-resultssets) means "protect all excluded-tool output regardless of conversation depth" per #1374's documented contract — but the method then unconditionally overwrote that window with aread_protection_windowkwarg whenever one was present.proxy_pipeline_kwargs()supplies that kwarg on every request from the activeAgentSavingsProfile.protect_recent(the defaultcodingprofile setsprotect_recent=2), so in practice only the last 2 messages ever kept read-protection and older excluded-tool output silently fell through to lossy compression. The runtime kwarg may now only narrow the window whenprotect_recent_reads_fraction > 0; it can no longer shrink the "protect everything" guarantee set by--protect-tool-results. - memory: drop a superseded memory from the search indexes so it stops resurfacing.
supersedeset the old memory'svalid_untilin the store and indexed the new version, but never touched the old entry in the vector/text index. Those indexes keep a cached metadata copy (captured at index time withvalid_until=None), and default search filters superseded rows off that cached copy — so the superseded, outdated version kept coming back from semantic/text search alongside the new one, injecting contradictory facts into recall.supersedenow removes the old id from the vector and text indexes (mirroringdelete); the store still keeps the row forget_history. - proxy: don't let a stray
HEADROOM_QDRANT_PORTcrash proxy startup.ProxyConfig.memory_qdrant_portusedqdrant_env.qdrant_env_portas its fielddefault_factory, and that function raisesValueErroron a non-integer or out-of-range value. Because adefault_factoryruns on everyProxyConfig()construction, an inherited or typo'dHEADROOM_QDRANT_PORTcrashed the proxy before it served a request — even though memory (and the qdrant backend) are off by default and unrelated to core proxying. The field now resolves the port through a fail-soft wrapper that falls back to the default (6333) with a warning; the strictqdrant_env_port()is unchanged for explicit qdrant setup. - memory: size the HNSW
index_batchresize off the assigned-id high-water mark, not the live entry count. hnswlib never reclaims a slot onmark_deleted(used by remove/evict), so its usable capacity is bounded by the number of ids ever assigned (_next_hnsw_id).index_batchcomputedrequired_capacity = len(self._memory_to_hnsw) + len(new_memories)— the live count — which drops below_next_hnsw_idafter deletion/eviction churn, so the resize was skipped andadd_itemsraisedRuntimeError: number of elements exceeds the specified limit, crashing the save path on the HNSW backend. It now resizes off_next_hnsw_id, matching the single-itemindex()guard. - memory: apply the
turn_idscope filter even whenagent_idis absent. InSQLiteMemoryStore._build_query_conditionstheturn_idcondition was nested inside theagent_idblock, so a query filtered byuser_id+session_id+turn_id(noagent_id) dropped theturn_idpredicate entirely and returned every memory in the session instead of the single turn — an over-broad result that leaks sibling-turn memories into recall (and makescount()wrong for that scope).agent_idandturn_idare now applied independently. - transforms: stop the lossless
difffold from silently dropping lines out of non-diff content.ContentRouter._lossless_firsttries everycompact_losslessfold on all content, but thediffkind (diff_strip_index) is the only one with no exact-inverse check — it removes any line shaped likeindex <hex>..<hex>. Applied to arbitrary text/log/search payloads that happen to contain such a line, that line was deleted with no CCR marker, so it was unrecoverable — a violation of the lossless no-loss contract the method's own docstring promises. Thedifffold now runs only when the strategy isDIFFor the content is diff-shaped (_looks_like_diff); genuine diffs still have theirindexbookkeeping folded. - proxy: reject a 0
rate_limit_requests_per_minutewhen rate limiting is enabled, instead of 500-ing every request. The token-bucket wait computation divides by the per-minute rate (consume_from_bucket), so arate_limit_requests_per_minuteof 0 raisedZeroDivisionErroron every request that hit the limiter. The CLI already guards this withIntRange(min=1), but theHEADROOM_PROXY_CONFIG_JSON/ programmatic config paths bypassed it.ProxyConfig.__post_init__now validatesrate_limit_requests_per_minute >= 1whenrate_limit_enabled(mirroring the existingretry_max_attemptscheck), so a bad value fails fast at construction with a clear message; it stays inert when limiting is disabled. - memory: include the Ollama server URL in the embedder cache key so a second backend can't get an embedder bound to the wrong server.
_create_embeddercached by(backend, model)only, but the Ollama embedder is constructed withbase_url=config.ollama_base_url. Two configs in the same process that shared a backend and model but pointed at different Ollama servers (e.g. a per-project storage router) collided on one cache slot, so the second silently reused the first's embedder and embedded against the wrong host. The cache key now also includesollama_base_url. - tokenizers: use
o200k_basefor the gpt-4.1 / gpt-4.5 / o4 families inget_encoding_for_model.gpt-4.1*andgpt-4.5*matched the broadgpt-4prefix and were encoded withcl100k_base, ando4*matched no prefix and fell through to thecl100k_basedefault — all three useo200k_base, so their token counts were computed with the wrong vocabulary. Added explicitgpt-4.1/gpt-4.5prefixes ahead ofgpt-4and ano4prefix;gpt-4andgpt-3.5snapshots still resolve tocl100k_base. - cache/ccr: stop counting a successful eviction as a retrieval in the compression feedback learner, which inverted the learning signal. When an entry is evicted without ever being retrieved,
CompressionStoreemits a syntheticretrieval_type="eviction_success"event to mark that the compression was sufficient (the LLM never needed the original).CompressionFeedback.record_retrievalhad no branch for it, so — because the type is not"full"— it was counted as a search retrieval, inflating the tool'sretrieval_rate/search_rate.get_compression_hintsreads a high retrieval rate as "compressing too aggressively" and backs off, so a compression that actually worked pushed the learner toward less compression (a standalone repro scores one successful eviction as a 100% retrieval rate). The event is now recognized and left out of the retrieval counters; the compression is still counted byrecord_compressionat store time, so a never-retrieved entry correctly yields a low retrieval rate. Genuine retrievals are unaffected. - proxy/anthropic: don't launder a non-2xx upstream into HTTP 200 when enterprise security scans the response. On the non-streaming
/v1/messagespath the response-scan branch rebuilt the reply ashttpx.Response(status_code=200)and returned it without checking the upstream status, so a rate-limit (429), overloaded (529), or other 4xx error whose JSON body was scanned reached the client as an HTTP 200 — the client's retry/backoff never fired and an error looked like success. The branch is now gated on a 200 upstream, matching the sibling CCR/cache/buffered-stream blocks in the same handler; non-2xx responses fall through and keep their real status. - learn: classify timeout and connection tool failures correctly instead of as generic runtime errors. In
classify_errorthe genericRUNTIME_ERRORpattern (Traceback|Exception:|Error:) was checked before the dedicatedTIMEOUTandCONNECTION_ERRORpatterns. Because every Python exception repr isXxxError: ..., aTimeoutError: ...orConnectionError: ...matched the catch-all first and was miscategorized asRUNTIME_ERROR, leaving those two categories unreachable for the common colon-repr form (they only fired for tokenless phrasings likedeadline exceeded). TheTIMEOUTandCONNECTION_ERRORpatterns are now checked before the generic catch-all; tokenless generic errors still classify asRUNTIME_ERROR. - tokenizers: resolve HuggingFace tokenizer names by the most-specific prefix.
get_tokenizer_namescannedMODEL_TO_TOKENIZERin dict-insertion order and returned the first key the model merely starts with, so a short family key shadowed a more-specific one —qwen2-7b-instructmatchedqwenbeforeqwen2/qwen2-7band resolved to the Qwen1 tokenizer (a different vocabulary, hence wrong token counts);qwen2.5-*anddeepseek-v2.xwere mis-resolved the same way. It now picks the longest matching prefix, mirroring the order-dependent-prefix guard the sibling tiktokenget_encoding_for_modelalready documents. - pricing: map retired
claude-3-sonnet-20240229to a Sonnet-tier price instead of Haiku. When LiteLLM's cost DB lacks the retired model, resolution falls through toMODEL_ALIASES, which pointed Claude 3 Sonnet (a $3/$15-per-1M model) atclaude-3-haiku-20240307($0.25/$1.25) — a different tier that underpriced every cost/savings figure for that model ~12x on both input and output. It now aliases toclaude-sonnet-4-20250514, the same-price target the other retired-Sonnet aliases already use. - cache/semantic: don't evict an unrelated entry when re-storing a key that is already cached.
SemanticCache.putran its at-capacity eviction loop before computing the entry's key, so overwriting a key that was already present (a duplicate or retried store) still evicted the LRU-oldest distinct entry even though an in-place update grows nothing. That silently dropped a live entry and turned a later lookup for it into a false cache miss. The key is now computed first and the eviction loop only runs when the key is genuinely new (mirroringCompressionCache.store_compressed, which deletes-then-inserts). - learn: keep
verbosity._ordered_eventsin lockstep with_parse_sessionon empty assistant turns._parse_sessioncreates a_Responseonly when an assistant message has content (words > 0 or out_tok > 0), but_ordered_eventsconsumed a response slot for every assistant line. An empty assistant turn (e.g. a puretool_useturn with no usage) therefore shifted theresponseslist out of alignment, so a later human reply was paired with a future-timestamped response, the read gap went negative, and a spuriousfast_skipwas recorded — inflatingfast_skip_rateand skewing the recommended verbosity level._ordered_eventsnow applies the samewords > 0 or out_tok > 0guard before consuming a response, matching the filter the user side already mirrors. - wrap/claude: don't raise
UnboundLocalErrorin the cleanupfinallywhen the proxy fails to start.claude()referenced_wrap_settings_pathin itsfinallyblock but only assigned it inside thetry, after_ensure_proxy(which raises on port exhaustion or a failed proxy start). An early failure therefore made thefinallyraiseUnboundLocalError— replacing the real error with a raw traceback and, because thefinallyaborted beforecleanup(), skipping proxy cleanup and wrap-marker clearing._wrap_settings_pathis now bound before thetryalongside the other cleanup holders (proxy_holder,_saved_base_url, …), so thefinallyis always safe. - wrap/codex: export the detected custom upstream base URL so Codex actually routes to it.
_inject_codex_provider_configdetects an OpenAI-compatible gateway declared in~/.codex/config.tomland writes anX-Headroom-Base-Urlheader mapped toHEADROOM_CODEX_UPSTREAM_BASE_URL, and it returns that URL for the caller to export. But_prepare_codex_wrap_statediscarded the return and_run_codex_wrapnever set the env var, so Codex emitted no header, the proxy fell back toapi.openai.com, and the user's gateway key was sent to OpenAI (which rejects it). This restores the wiring a later refactor dropped:_prepare_codex_wrap_statenow returns the URL and_run_codex_wrapexports it into the launch env when set (a user-provided value still wins) (regression of #1614). - cache: normalize embeddings before the semantic dynamic-content similarity check.
SemanticDetectorscored sentences withnp.dotagainst exemplar embeddings and compared the result tosemantic_threshold(a 0-1 cosine value), butsentence_transformers.encode(..., convert_to_numpy=True)does not normalize, so the dot product was an unbounded inner product (vector norms ~5-15) rather than a cosine similarity. Nearly every sentence cleared the 0.7 threshold, so the semantic tier flagged almost all text as dynamic and stripped static content, busting the cache it is meant to protect (a standalone repro scores an unrelated sentence, true cosine ~0.1, at a raw dot of ~9). Bothencodecalls now passnormalize_embeddings=True, matching the siblings inprediction/feature_extractor.pyandmemory/adapters/embedders.py, so the dot product is a true cosine in [-1, 1]. - subscription: cap
HeadroomContribution.efficiency_pctat a real removal ratio so it can't exceed 100%. The numerator usedtotal_saved()(which includestokens_saved_cache_reads) while the denominatorraw_without_headroom()excludes cache reads, so a contribution with large prefix-cache reads and small forwarded input reported impossible values (e.g.tokens_submitted=100,tokens_saved_cache_reads=1000→1000.0%on the dashboard). Cache reads are a provider-side discount on tokens that were still forwarded, not tokens Headroom removed, so the ratio now uses the siblingcompression_saved()(compression + CLI filtering) as the numerator — bounded by its own denominator.total_saved()is unchanged for its other callers. - proxy/anthropic: cache the response under the same messages it was looked up by. The non-streaming
/v1/messagespath snapshots the scalar cache-key fields (system, tools, etc.) once before upstream to avoid post-mutation key drift (#327), butmessages— the primary key component — was passed live at bothcache.getandcache.set. Between the two,messagesis reassigned by the enterprise security scan, thepre_compresshook, and image compression, so when any of those fired the response was stored under a different key than it was read by: the response cache never hit and accumulated unreachable entries until eviction. The lookup messages are now snapshotted alongside the other key fields and reused verbatim atcache.set. - tokenizers: stop
TiktokenCounter.count_messagesfrom exploding on non-text content blocks. Its multi-part branch handled onlytextand OpenAIimage_url; every other shape (Anthropicimage/tool_result/tool_use, Strands blocks) fell through tocount_text(str(part)), which json-stringified the base64 payload and tokenized it as text — a 1MB image counted as ~330K phantom tokens (~218x overcount in a standalone repro), corrupting every downstream budgeting/compression decision for multimodal OpenAI-model requests. Unknown block shapes now delegate to the base_count_content_parts, which prices images/documents by a bounded estimate (the overcount that helper already exists to prevent). - install: don't let a host env export override the manifest in persistent-docker deployments.
build_runtime_commandemitted the manifest's pinned--env NAME=VALUEpairs and then, for every host var matching a passthrough prefix, a bare--env NAME. Docker resolves duplicate--envlast-wins, so a stale host export (e.g.HEADROOM_BACKEND=anyllm) that shared a passthrough prefix with a pinned manifest value (HEADROOM_BACKEND=anthropic) was appended after it and silently won, diverging the container from its deployment config. The bare passthrough is now skipped for any name the manifest already pins. - memory: honor explicit
store=falseon OpenAI/v1/responsesrequests by skipping Headroom memory-tool injection that depends on stored-response continuations. Memory context injection stays available, and requests no longer get rewritten tostore=truebehind the client's back (#1944). - proxy/batch: stop corrupting Google
batchGenerateContentrequests whose contents interleave text turns with text-less entries (functionCall/functionResponse/images). The batch handler restored preserved (non-text) entries with the raw-index loop that #836 replaced everywhere else — indexing the shorteroptimized_contents(text-less entries produce no message) by the originalcontents[]index, which overwrites the wrong entry and drops any preserved entry whose original index is past the optimized length. A request like[user text, model functionCall, user functionResponse, model text]was forwarded to Google as two entries: the model's answer overwritten by the functionCall and the functionResponse dropped. The batch handler now uses the shared_rebuild_gemini_contentsinterleaving helper, so all entries survive in order. - proxy/gemini: preserve Gemini code-execution parts (
executableCode/codeExecutionResult) across the compression round-trip._has_non_text_partsonly recognizedinlineData/fileData/functionCall/functionResponse, so a content entry carrying code-execution parts was not marked as preserved. A mixedtext+executableCodeentry lost its code payload (only the text survived), and a text-less code-execution entry was treated as a phantom that dropped the entire turn and shifted a neighboring message into the wrong role slot. Both keys are now recognized so those entries are preserved verbatim. - cache/ccr: don't evict a live entry when a duplicate hash is re-stored at capacity.
CompressionStore.storeran_evict_if_needed()before checking whether the key already existed, so re-storing an already-present hash while the store was full evicted the oldest distinct entry to "make room" and then merely overwrote the existing key in place — no room was ever needed. The store dropped belowmax_entriesand a live, never-retrieved entry was destroyed, so its<<ccr:...>>marker (still in the conversation) resolved to a 404. The CCR mirror bridge re-stores the sameexplicit_hashevery turn a marker is re-encountered, so this fired routinely. Eviction now runs only for a genuinely new key. - tokenizers: recurse into a native
tool_resultwhose content is a list of blocks instead of JSON-serializing it._count_content_partscounted atool_resultwith list content via_count_serialized(json.dumps + sample), so a base64 image nested in a tool result (computer-use / MCP screenshot tools) was priced as text — a ~50-200x overcount (a ~200KB screenshot read as ~70K tokens instead of ~1600). It now recurses into the nested blocks, matching the sibling StrandstoolResultbranch, so the image is priced structurally. The overcount made a single screenshot appear to blow past the model's context window and triggered unnecessary/over-aggressive compression. - tokenizers: price dense scripts (CJK/Kana/Hangul) in the fixed-ratio estimator path.
EstimatingTokenCounter.count_textapplied the CJK correction only on the auto-detect path; its fixed-ratio early return divided by the Latin ratio with no adjustment. The registry builds every provider-calibrated counter with a fixed ratio (Anthropic 3.5, Google 4.0, Cohere 4.0, Moonshot 3.1), and the Anthropic/Gemini proxy handlers count viaget_tokenizer(model).count_messages, so a CJK-heavy context read as ~40-55% of its true token size — it could fall under the size/backpressure gates and skip compression, and everyx-headroom-tokens-beforemetric for CJK traffic was materially wrong. The fixed-ratio path now applies the same dense-script split. - ccr: don't crash
parse_tool_callon a CCR tool call whose arguments aren't an object. For the OpenAI/openai_responsesshape the arguments arejson.loads-decoded and onlyJSONDecodeErrorwas caught, so a model that emittedarguments='[]'/'"abc"'/'123'(decoding to a list/str/number) — or a non-dict Anthropicinput— reachedinput_data.get("hash")and raisedAttributeError; a nullargumentsraised an uncaughtTypeErrorfromjson.loads(None). Both are now handled: the decode also catchesTypeError, and a non-dictinput_datareturnsNone(not a valid CCR call) instead of crashing CCR response processing. - proxy/memory: capture the user's prompt from Anthropic text blocks when building the memory-retrieval query.
extract_memory_query_sourcesonly recordedlatest_userwhen a user message'scontentwas a plain string; for the standard Anthropic/v1/messagesshape (content=[{"type":"text","text":...}], used by Claude Code) it routed into the tool-result extractor and never read thetextblocks — so the actual question was discarded. On a first turn the embedding query was then empty and memory injection was silently skipped entirely; with history it keyed on stale assistant/tool context instead of the user's ask. The user turn'stextblocks are now captured. - memory/sqlite: stop
SQLiteMemoryStore.queryfrom emittingOFFSETwithout aLIMIT. The query builder appendedLIMITonly whenfilter.limit is not NoneandOFFSETindependently whenfilter.offset > 0, but SQLite acceptsOFFSETonly as part of aLIMITclause — so aMemoryFilter(offset=N)with no limit produced... OFFSET ?and crashed withsqlite3.OperationalError: near "OFFSET": syntax error. An offset-without-limit now emits SQLite's unboundedLIMIT -1so pagination works. - mcp/codex: don't corrupt an unparseable or non-table
config.tomlon register.CodexRegistrar.register_serveronly guarded against clobbering a user-managed entry whenget_serverreturned one, butget_serverreturnsNoneboth for an unparseable TOML file and for anmcp_servers/mcp_servers.<name>that is present but not a table. In those casesregister_serverfell through to_write_block, which blindly appended a[mcp_servers.<name>]table — appending into an unparseable file, or creating a duplicate[mcp_servers.headroom]key alongside a non-table entry (e.g.headroom = "..."), whichtomllib/codex then reject, destroying a previously-valid config. It now refuses (FAILED) and leaves the file untouched, mirroring the claude (#1660) and opencode (#1661) guards. - proxy/vertex: route Vertex
publisher=google(Gemini) requests to the region matching the request path.vertex_generate_content,vertex_stream_generate_content, andvertex_count_tokensdiscarded the path'slocationand forwarded to the single fixed host from_api_target(proxy, "vertex")(defaultus-central1), instead of the region-aware_vertex_target_for_locationthe sibling AnthropicrawPredictroute already uses. So a request to.../locations/europe-west1/publishers/google/...was sent to aus-central1host, which Vertex rejects on the region/host mismatch. The three google routes now derive the host from the request'slocation(operator-pinned upstreams are still honored). - proxy/anthropic: give each Anthropic conversation its own session id.
SessionTrackerStore.compute_session_idderived its fallback id frommodel+ system text harvested only fromrole:"system"entries insidemessages— but Anthropic carries the system prompt as a top-levelbody["system"]field, so genuine Anthropic requests (which never carryx-headroom-session-id) collapsed tomd5(model:[])and every conversation on the same model shared onePrefixCacheTracker. That let session-sticky state cross-contaminate: conversation A's stickyheadroom_retrieve/memory tools andanthropic-betaheaders were injected into conversation B, and frozen-prefix/compression-cache state mixed across conversations. The Anthropic handler now folds the top-levelsysteminto the session-id inputs (prepending a syntheticrole:"system"message used only to derive the id), giving distinct conversations distinct ids. - install/codex: persistent provider routing is now lifecycle-coupled.
headroom install applywaits until the runtime is ready before writing managed Codex routing, and stop/remove/recovery paths revert that routing before tearing the proxy down, so Codex Desktop is not left pointed at a dead127.0.0.1:8787provider after a failed or stopped deployment (#2038). - cache/semantic: key entries by the full-context hash, not the trailing query text.
SemanticCache.putstored each response undersha256(query)[:16]wherequeryis only the last user message, and the exact-match branch ofgetreturned the slot without checking the stored entry'smessages_hash. Two requests that share a trailing message ("continue", "yes", "run the tests") but differ in earlier context therefore collided on one slot — the second overwrote the first, and the first's hash then resolved to the second's cached response (wrong data served). Entries are now keyed bymessages_hashwhen present, andgetverifiesentry.messages_hashbefore returning. - wrap/opencode:
headroom unwrap opencodenow removes the Headroom rtk instruction block from the project and globalAGENTS.md.wrap opencodeinjects the marker-fenced "prefix shell commands withrtk" guidance into both./AGENTS.mdand<opencode-home>/AGENTS.md, but unwrap only restored the config and MCP state — so a plainopencodelaunch kept following the rtk guidance and failed once the managed rtk binary was off PATH. Unwrap now strips the marker-fenced block from both files, mirroringunwrap codex(#1421) andunwrap copilot. - proxy/savings: stop billing the $3/M fallback rate for genuinely free models.
_estimate_compression_savings_usdand_estimate_input_cost_usdreadinput_cost_per_tokenfrom litellm and usedif not input_cost_per_token: raise, which treats a legitimate0.0(a free / local / vendored-at-0 model that litellm does carry) as "price unavailable" and falls back toDEFAULT_FALLBACK_INPUT_COST_PER_TOKEN— fabricating dollar savings/cost for a model that costs nothing. Both now use an explicitis Nonecheck so a present0.0flows through as$0while a missing key still falls back. - proxy/cost: value prefix-cache savings with the most-used model's price, not the first-recorded one.
build_prefix_cache_statsscannedcost_tracker._tokens_sent_by_modeland broke on the first provider-matching model with a price — despite the "most-used model" comment — so a Claude Code session (Sonnet for the main loop, Haiku for titles/subagents) priced all of a provider's cache-read savings at whichever model happened to be recorded first. If Haiku ($0.80/M) came before Sonnet ($3/M), the dashboard understated cache savings ~3.75x (and vice-versa). It now picks the provider-matching, priced model with the highest token volume. - proxy/openai: stop overriding an explicit client
stream_options.include_usageon the streaming chat path. To count tokens from the trailing usage chunk, the handler setinclude_usage: Trueunconditionally — including flipping an explicit clientfalsetotrue. The upstream then appended a usage-only chunk (choices: []) the client never requested, and the commonchunk.choices[0].deltaloop raisedIndexError. The option is now only filled in when the client left the choice open (nostream_options, or a dict withoutinclude_usage); an explicittrue/falseis respected. - proxy/openai: stop PRE_SEND from reintroducing
tools: []after the direct #728 fix. The OpenAI request handler now mirrors the existingtools or _original_tools is not Nonebody-write guard during PRE_SEND write-back, so providers that reject empty tool arrays no longer see a tools field when the client omitted it, while explicit clienttools: []remains preserved (#1983). - proxy/openai: keep the exact Responses function name
terminalresident during OpenAI tool-search deferral so cache-mode optimization stops forwardingterminal.terminaland triggering the reserved-namespace 400 on Codex Responses (#1946). - proxy/transforms: startup warmup no longer calls Kompress native preload before the proxy binds its port. Enabled Kompress is deferred to first use, unavailable Kompress stays reported unavailable, and cached-model startup avoids the native ONNX load path that crashed older glibc hosts (#1908).
- subscription/copilot: show a fully-consumed Copilot quota as 100% used instead of unknown.
parse_copilot_quotareadremaining = raw.get("remaining") or raw.get("quota_remaining"), so a category reportingremaining: 0(quota fully spent) had that legitimate0treated as falsy and — with noquota_remainingalias in the real payload — collapsed toNone.CopilotQuotaCategory.used/used_percentthen returnedNone, so the dashboard rendered the exhausted category asused: -/ 0% (green gauge) rather than300/300/ 100%. Now uses an explicitis Nonecheck. - proxy/gemini: thread the savings-profile kwargs into the native Gemini/Vertex compression paths.
handle_gemini_generate_content,handle_google_cloudcode_stream, andhandle_gemini_count_tokenscalledopenai_pipeline.apply()withoutproxy_pipeline_kwargs(self.config), soHEADROOM_SAVINGS_PROFILEand the ProxyConfig knobs (target_ratio/min_tokens_to_compress/protect_recent/max_items_after_crush/...) were silently dropped on the Gemini path — those requests compressed with router defaults instead of the configured profile, diverging from the Claude/Codex/Cursor paths. This is the same fix #1534 made for the OpenAI chat path; it now covers Gemini too. - wrap:
headroom wrap claudeno longer installs RTK or lean-ctx by default. Claude context-tool setup is now explicit via--context-tool,--no-context-toolremains accepted, and other wrap commands keep their current defaults (#1915). - proxy/openai: thread the savings-profile kwargs into the live
/v1/chat/completionscompression path. The chat handler calledopenai_pipeline.apply()withoutproxy_pipeline_kwargs(config), soHEADROOM_SAVINGS_PROFILE=agent-90(and the individualcompress_user_messages/target_ratio/min_tokens_to_compress/... knobs) were silently dropped — OpenAI-compatible clients like OpenCode kept protecting user messages and missed the configured profile. Both the token-mode and non-token chat branches now pass the profile kwargs, matchinghandlers/anthropic.pyand the dedicated OpenAI compress endpoint (#1534). - proxy: forward Codex Desktop
/v1/responsesposts byte-faithfully so they stop returning upstream400 {"detail":"Bad Request"}.handle_openai_responsesdecoded the inbound body to inspect it but always re-serialized a canonical body on the way out, and it never stripped the inboundcontent-encodingheader — so acontent-encoding: zstdCodex Desktop request was forwarded as already-decoded JSON still advertisingzstd, and the upstream ChatGPT Codex endpoint rejected it. The handler now keeps the original decoded bytes and forwards them verbatim whenever nothing (compression or memory injection) mutated the request, and drops the stalecontent-encodingheader, mirroring the byte-faithful passthrough the chat and Anthropic paths already use (#1542). - wrap/codex:
headroom unwrap codexnow removes the Headroom rtk instruction block from the Codex globalAGENTS.md.wrap codexinjects it there, but unwrap only restoredconfig.tomland MCP state, so a plaincodexlaunch kept following the "prefix shell commands withrtk" guidance and failed once the managed rtk binary was off PATH. Unwrap now strips the marker-fenced block (preserving the rest of the file), mirroringunwrap copilot(#1421). - proxy/auth: classify real Anthropic OAuth tokens correctly.
classify_auth_modematched OAuth on thesk-ant-oat-prefix, but real access tokens aresk-ant-oat01-...(a version number, no dash afteroat), so every real subscription/OAuth token fell through to thesk-branch and was taggedPAYG— enabling aggressive lossy compression, autocache_control, andprompt_cache_keyinjection on subscription-bound requests the classifier is meant to route to the passthrough-prefer path. The prefix is now the dash-lesssk-ant-oat(still matches the legacy dashed shape). The existing parity tests only passed because they used a syntheticsk-ant-oat-01-fixture; a regression test now covers the realsk-ant-oat01-format. - install: stop leaking a file descriptor on every
headroom install start.start_detached_agent()opened the agent log file and handed it tosubprocess.Popenbut never closed the parent's copy, so each call leaked one fd (and pinned the log file open against rotation). The parent now closes its copy in atry/finallyonce the child has inherited it — the close also runs ifPopenraises (#1554). - memory/sync: stop the Codex AGENTS.md sync adapter from erasing previously-synced memories on every export.
sync_exporthands each adapter only the delta (memories the agent lacks), butCodexAdapter.write_memoriesrebuilt its whole managed section from just that delta — so each sync overwrote the section with only the new items, thrashing the file between disjoint subsets and never accumulating. It now merges the delta into the facts already present (deduped), matching the additive contract the ClaudeCode adapter already follows. - memory/sync: stop the Claude Code sync adapter from clobbering distinct memories that share a first line.
write_memoriesderived each file name from the first line of the content only (headroom_{slug}.md), so two different DB memories whose first lines slugify identically wrote to the same file and the second silently overwrote the first — and because the loser never landed on disk, the next sync re-exported it, ping-ponging the pair forever. When the slug is already taken by a different memory (distinctheadroom_id) the file name is now disambiguated with a content-hash suffix; an update to the same memory still rewrites its slug file in place, so existing file names are unchanged. - transforms/code: stop raising
ValueErroron common language hints and fence tags.CodeAwareCompressor.compress()built the language withCodeLanguage(language.lower()), which only accepts the exact enum values (python/javascript/typescript/…). A markdown```js/```ts/```pyfence tag (or any caller passing an alias) raisedValueError— crashing direct callers, and inside the content router the error was swallowed so those blocks silently skipped code-aware compression. A newcoerce_languagehelper maps the common aliases to their canonical language and returnsUNKNOWN(never raises) for unrecognized tags, falling back to content-based detection. - cli/proxy: honor
HEADROOM_MIN_TOKENS=0/HEADROOM_MAX_ITEMS=0. The Clickproxycommand built these with_get_env_int_optional(name) or 500/or 50, so an explicit0— a legitimate value (min_tokens_to_crush=0means "crush every item") — was treated as falsy and silently replaced with the default. Theheadroom proxyargparse path already preserved0via_get_env_int, so the two entry points disagreed. The Click path now uses the same None-checking helper. - proxy: strip the inbound
Content-Encoding/Transfer-Encodingrequest headers on the Anthropic/v1/messagesand OpenAI/v1/chat/completionspaths before forwarding upstream.read_request_json_with_bytesalready decompresses the inbound body (zstd/gzip/deflate/br), so the bytes forwarded upstream are plain JSON — but these two handlers left the originalcontent-encodingheader in place, so a client (or an edge proxy like a Cloudflare Worker) that sent a compressed body got its request rejected with upstream HTTP 400 because the provider tried to decompress already-decoded JSON. The/v1/responseshandler already carried this fix (#1542); it is now applied to the messages and chat paths too. - models: fix the model registry's prefix fallback silently returning the wrong context window.
ModelRegistry.getaccepted any registered name as astr.startswithprefix and returned the first match, sogpt-4-32k-0613resolved togpt-4(8192) instead ofgpt-4-32k(32768), and unregistered ids likegpt-4.1/gpt-4.5inheritedgpt-4's 8192-token window — making the proxy think a nearly-empty context was almost full and compress far too aggressively. The fallback now requires the registered name to end at a version boundary in the query (sogpt-4.1no longer matchesgpt-4) and picks the longest qualifying name (sogpt-4-32k-0613→gpt-4-32k). - mcp/claude: stop the file-based MCP registrar from destroying an existing but unparseable Claude config. When the
claudeCLI is unavailable,_register_via_fileread~/.claude/.claude.jsonvia a helper that returns{}onJSONDecodeError, then rewrote the whole file with only{"mcpServers": {...}}— wiping unrelated Claude state (projects,oauthAccount, session history) if the file was momentarily corrupt or hand-edited. The write path now refuses to overwrite a present-but-invalid config and returns aFAILEDresult with an actionable message; an absent or empty file still registers fresh, and a valid file still merges with all other keys preserved. - install: stop
resolve_targetsfrom rejecting valid--providers all/autoinstalls under provider scope. The provider-scope "unsupported targets" validation ran before the mode dispatch, soheadroom install apply --scope provider --providers all --target cursorraisedClickExceptioneven thoughall/autoignore the requested target list entirely (user scope silently ignores the same input). The check now runs only on the manual path that actually consults the requested list. - mcp/opencode: stop the OpenCode MCP registrar from destroying an existing but unparseable
opencode.json._write_entryread the config via a helper that returns{}onJSONDecodeError, then rewrote the whole file with only{"mcp": {...}}— wiping the user'stheme/model/providerand any other MCP servers (OpenCode configs are commonly JSONC / hand-edited). The write path now refuses to overwrite a present-but-invalid config and returns aFAILEDresult; absent/empty files still register fresh and valid files still merge with all other keys preserved. (Same class of fix as the Claude registrar.) - proxy: include the system prompt, tools, and the response-shaping request fields in the SemanticCache key.
_compute_keyhashed only{model, messages}, so two non-streaming requests with identical messages but a different top-levelsystemprompt, tool set, sampling config, or output-shaping field collided on one key and the second caller was served the first's cached response — generated under different request semantics, in the default config (cache_enableddefaults on). The key now folds the request fields that shape generation —temperature/top_p/top_k/max_tokens/stop, plus OpenAItool_choice/response_format/parallel_tool_calls/seed/presence_penalty/frequency_penalty/logit_bias/n/logprobs/top_logprobs/reasoning_effort/verbosity/modalitiesand Anthropicthinking/tool_choice/output_config— canonicalizingsystem/toolsso a movedcache_controlbreakpoint does not fragment it, and the handlers snapshot the fields once at the cache read and reuse them at write so a body mutated by the pipeline cannot diverge the key. Non-streaming path only. - learn (verbosity):
--verbosity --apply --allnow aggregates the savings baseline across every project instead of overwriting it per project (last-project-wins), which previously left the output shaper with a tiny, unrepresentative baseline. The applied verbosity level comes from the project with the most samples (#1288). - proxy/anthropic: restore token-mode compression on continued Claude Code turns with a frozen prefix and deferred CCR tool injection. Token mode now runs request-side compression even when the client did not pre-register
headroom_retrieve, relying on the existing marker-triggered injection override to keep emitted CCR markers redeemable (#1487). - proxy: the dedicated OpenAI handlers (
/v1/chat/completions,/v1/responses) now honor thex-headroom-base-urlrequest header, matching the generic passthrough route. Previously only the catch-all passthrough honored it, so OpenAI-compatible gateways (LiteLLM, CPA, self-hosted vLLM, Azure OpenAI) routed correctly for passthrough traffic but the dedicated chat/responses handlers ignored the header and fell back to the defaultOPENAI_API_URL, sending requests (and the user's provider key) to the wrong upstream. - subscription: stop zeroing the 5-hour headroom contribution counters on every poll. The rollover check compared
five_hour.resets_atwith a bare!=, but the usage API reports that timestamp with second-level jitter (observed flapping between01:59:59Zand02:00:00Zon consecutive polls within the same window), so a spurious "5h window rolled over" reset fired every poll interval (~5 min) and the dashboard's per-window savings stuck near 0%. Only a forward jump larger than_ROLLOVER_MIN_ADVANCE(1 minute) now counts as a real rollover. - wrap: keep the shared proxy alive when the agent that launched it closes ungracefully on Windows.
_start_proxyspawned the proxy without detaching it, so it stayed in the launcher's console and Job object; closing that terminal window (ortaskkill/a crash) tree-killed the proxy, bypassing the marker-based reference counting in_make_cleanupand breaking every otherheadroom wrapinstance routed through the same port. The proxy is now created withCREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB(with a graceful fallback when the launcher's Job forbids breakaway); POSIX behavior is unchanged.CREATE_NO_WINDOW(rather thanDETACHED_PROCESS) gives the proxy its own hidden console:DETACHED_PROCESSleaves a console-subsystem exe (python.exe) consoleless, so Windows surfaces a visible console window whose close button kills the proxy. - transforms/content_router: stop replacing
role="tool"output with a lossy-unrecoverable summary on the live compression path (refs #1307).ContentRouter.apply()routed OpenAI-stylerole="tool"string messages —Bash/grep/ls/catoutput — through the ML/word-drop summarizers; when the result carried no CCR retrieve marker (CCR off, ratio >= 0.8, or the size-gate fallback) the original was unrecoverable and the agent acted on a fabricated summary. Tool-role string content is now kept verbatim unless the compressed form is CCR-recoverable. Assistant/user text is unaffected, and structurally-lossless passes (SmartCrusher/Log/Search) still apply. The Anthropictool_resultblock path is tracked separately. - rtk: stop
rtkhook registration from spuriously timing out duringheadroom wrap. Output is captured to a temp file instead of pipes, andstdinis closed, so a background process forked byrtk initcan no longer hold the pipe open and blocksubprocess.runpast its 10s timeout after the hooks were already registered. - ccr: stop re-compressing
headroom_retrieveoutput, which created an infinite retrieval loop, and stop emitting retrieval markers when theheadroom_retrievetool is not injected, which silently dropped data (#1077, #1006). - dashboard: include RTK stats in the Historical tab;
/stats-historynow attaches live RTK/CLI-filtering stats the same way the Session tab does, so they survive a proxy restart (#1177). - opencode: write Headroom MCP config as a local stdio server instead of a remote
/mcpURL, keep provider-only installs from adding MCP config, and allowinstall apply --target opencode(#1380). - proxy: stop discarding a finished compression on very large requests. After the transform pipeline completed, a telemetry-only waste-signal re-parse of the original messages ran on the critical path; on huge Claude Code transcripts (~400k tokens) that parse could exceed the Anthropic compression timeout, so the proxy failed open and forwarded the uncompressed request despite "Pipeline complete" logging real savings (
tokens_saved: 0,transforms_applied: [], ~31s latency). Waste-signal detection is now skipped aboveMAX_WASTE_SIGNAL_DETECTION_TOKENS(100k) so the compression result stays on the critical path (#296). - codex: retag existing Codex threads when
headroom initinjects theheadroomprovider, so Codex Desktop history stays visible. Codex filters its sidebar/search by the activemodel_provider; the init path setmodel_provider = "headroom"without retagging, so existing nativeopenaithreads disappeared from the menu (data was never deleted, only hidden)._ensure_codex_providernow reconciles thread tags openai→headroom, matching what the install andwrappaths already do;headroom unwrap codexhandles the revert direction (#961). - install: stop duplicating the container ENTRYPOINT in the
persistent-dockerruntime command. The published image already runsheadroom proxyas its ENTRYPOINT, butbuild_runtime_commandre-addedheadroom proxyafter the image name, so the container ranheadroom proxy headroom proxy --host 0.0.0.0 …and Click aborted with "Got unexpected extra arguments (headroom proxy)" — the deployment never became ready and rollback left nothing running. The runtime command now appends only the proxy flags (#833). - proxy: retry upstream
529 overloaded_errorlike a 429 on both the streaming and non-streaming forwarders, honoringRetry-After. The streaming path previously surfaced a 529 straight to the client with no retry (interactive sessions saw "Overloaded" immediately), and_retry_requestretried it only via the generic 5xx path — raising on exhaustion instead of returning the 529 verbatim, and ignoringRetry-After. A sharedRETRYABLE_OVERLOAD_STATUSES = {429, 529}keeps the two forwarders in agreement (extends #1221). - gemini: run compression off the asyncio event loop. The Gemini handlers (
generateContent, Cloud Code stream,countTokens) ran the CPU-bound compression pipeline (Magika detection plus ML compression) synchronously on the loop, stalling every concurrent request for the duration of each Gemini request's compression. They now offload it via the shared compression executor, matching the existing OpenAI and Anthropic paths. - proxy: run image compression off the asyncio event loop. The Anthropic and OpenAI handlers ran the CPU-bound image compressor (ONNX technique routing plus Pillow resize and OCR) synchronously on the loop, stalling every concurrent request for the duration of each image request's compression. They now offload it via the shared compression executor with a timeout and fail open on error, matching the existing text-compression path.
- proxy: queue mid-turn user messages on non-Bedrock streaming path instead of silently dropping them — closes #902.
- proxy: add
--protect-tool-results/HEADROOM_PROTECT_TOOL_RESULTSto prevent lossy compression of exact-output tool results (e.g.Bash cat/grepresults) — closes #1307. - cli: add
--rpm/--tpmandHEADROOM_RPM/HEADROOM_TPMto the Click proxy command for rate-limit parity with the legacy CLI -- closes #1350 (Problem 1). - proxy: register
ToolResultInterceptorTransformin explicit transforms list whenHEADROOM_INTERCEPT_ENABLEDis set — closes #829. - opencode: write Headroom MCP config as a local stdio server instead of a remote
/mcpURL, keep provider-only installs from adding MCP config, and allowinstall apply --target opencode(#1380). - code: keep Python
from __future__imports before executable code during AST compression and validate compressed Python withcompile(..., "exec")so compile-time syntax rules are enforced (#1233). - proxy/transforms: stop the SMART_CRUSHER → Log fallback from collapsing truncated/invalid JSON tool outputs to a single CCR-retrieval marker (99.9% data loss when CCR retrieval isn't configured). The native magika detector classifies content by shape, not parseability, so a mid-stream-truncated JSON payload is tagged
json_arrayand routed to SmartCrusher, which returns it unchanged; Kompress passes it through; the LogCompressor then treated the broken JSON as a multi-thousand-line "log" and reduced it to a retrieval marker. A JSON-validity guard now skips the Log fallback for content that failsjson.loads, so invalid JSON passes through verbatim. Valid JSON arrays still reach the Log fallback (LogCompressor is a no-op on them). The guard also catchesRecursionErrorfrom deeply nested JSON (e.g.[[[[...]]]]with 10k+ levels) so the router falls through to a safe strategy instead of crashing (#1306). - proxy/transforms: fix MIXED false-positive on source code.
is_mixed_contentuses regex heuristics that misclassify Python code with dict/list literals ({,[at line start →has_json_blocks) and docstrings/comments (has_prose) as mixed content, routing it through_compress_mixedwhich splits it into sections and dispatches each to KOMPRESS — wasting 1–1.4s of latency with 0% compression. When the native magika detector confidently saysSOURCE_CODE(confidence ≥ 0.8),_determine_strategynow trusts it over the regex heuristics. Additionally, whenprefer_code_aware_for_code=False(the default), source code now usesPASSTHROUGHinstead of falling back toKOMPRESS, which can destroy code semantics (98% compression, 11% fact recall on large blobs). This honours the config's stated intent ("let code pass through unmangled") and reduces latency on code blobs by 23–33×. - proxy/transforms: catch
RecursionErrorin_try_detect_jsonso deeply nested JSON arrays (10k+ nesting levels) no longer crash the content detector. The router falls through to a safe strategy (TEXTorPASSTHROUGH) instead of raising an unhandled exception. - proxy: report real input tokens on the streaming
message_startevent for LiteLLM/Bedrock-backed requests. LiteLLM streaming never surfaces prompt tokens mid-stream, somessage_start.usage.input_tokenswas always0; Anthropic clients (e.g. Claude Code) read input-token metrics from that event, underreporting token usage by ~99% in OTel/CloudWatch dashboards. The Bedrock streamer now backfillsinput_tokenswith the count Headroom actually sent upstream when the backend leaves it unset, preserving any non-zero value the backend genuinely reports (#1132). - proxy: give buffered Anthropic request paths their own longer read timeout, so long
/v1/messagesturns and Anthropic batch or passthrough reads no longer trip the generic proxy cap while unrelated request timeouts stay unchanged. - proxy: retry upstream 429 rate limits honoring
Retry-Afterinstead of passing them straight to the client. Both the non-streaming (_retry_request) and streaming (_stream_response) forwarders returned an upstream 429 verbatim, so a parallel agent fan-out that exceeded the per-minute limit aborted every run; 429s are now retried with backoff (honoring the upstreamRetry-After, capped atretry_max_delay_ms), surfacing only the exhausted 429 to the client (#1221). - proxy: force Responses API
store=truewhen Headroom injects memory tools soprevious_response_idcontinuations work after memory tool calls from clients that requestedstore=false(#1103). - proxy: build SSL contexts for custom CA bundles so enterprise/private PKI roots work with Python/OpenSSL strict verification.
- dashboard: the Proxy $ Saved tile no longer shows a bare
$0.00when cost pricing is unavailable. Pricing depends on litellm, which pyproject gates off on Python 3.14+, so/statsnow exposes a top-levellitellm_availableflag and the tile points you to reinstall on Python 3.13 when it is false (#1296). - proxy: the output-savings recorder now reloads the learned baseline before estimating and before each flush, so a baseline written by
headroom learn --verbosity --applywhile the proxy is running takes effect without a restart and the periodic flush no longer overwrites it. Fixes Output Tokens Saved staying at "—" after enabling the shaper (#1296). - tokenizers: bound token-counting of oversized tool-content blobs instead of running
count_textover the whole serialized string.count_messagesruns on the proxy request path; serializing is cheap, butcount_textover a multi-megabytetool_result/tool_usestring took seconds and could freeze/healthand in-flight requests. For payloads over ~50KB serialized,count_textnow runs on an even-spread sample of the string and scales by length; it stays model-accurate, bounded for any blob shape, and biased to under-count. Smaller payloads stay exact. - codex: stop persisting a project-specific
--dbpath in the globalheadroom_memoryMCP config, soheadroom wrap codex --memoryfalls back to the active cwd's.headroom/memory.dbat runtime while keeping the current project's local bootstrap work scoped correctly (#1147). - ccr: stop emitting Anthropic request-side retrieval markers on frozen-prefix turns when
headroom_retrieveinjection is deferred, so cache-preserving requests forward original content instead of irrecoverable marker-only payloads (#1006). - proxy: route Codex OAuth image generation and edit requests through the ChatGPT Codex image backend, while preserving OpenAI API-key image passthrough (#1215).
- wrap (codex): keep RTK guidance in the global Codex
AGENTS.mdinstead of modifying the shared projectAGENTS.md(#1235). - subscription: run the transcript token-window scan off the event loop (
asyncio.to_thread). The subscription tracker's poll loop scanned every~/.claude/projects/**/*.jsonltranscript andjson.loads'd each line inline on the proxy's single asyncio event loop; on large or long-running sessions this took seconds and froze/healthand every in-flight proxied request — a periodic "wedge" recurring on the poll interval. The scan now runs in a worker thread so the loop stays responsive. - gemini: resolve future Gemini model capabilities through the shared model registry so token counting and context lookup no longer reject new Gemini families.
- proxy: enable SSO credential resolution in the native Bedrock route via the
aws-configssofeature flag, making the credential chain match whatdocs/bedrock.mdalready documented (#999). - proxy: route native Bedrock
/model/{id}/converserequests to the upstream Converse endpoint instead of the hard-coded/invokeaction — the non-streaming handler now resolves the action from the inbound path, matching the streaming handler (#999). - proxy: preserve byte-faithful
/v1/messagesforwarding when Anthropic tool arrays are already canonical, and only canonicalize-and-mutate tool lists when sorting changes ordering (#1042). - ccr: make retrieval store TTL configurable with
HEADROOM_CCR_TTL_SECONDS, expose the effective TTL in/v1/retrieve/stats, and distinguish expired retrievals from missing hashes. - proxy: make
force_kompressskip ContentRouter auto-detection during compression and pass savings-profile kwargs through Anthropic batch requests. - proxy: add native Bedrock
/model/{id}/converse-streamroute and forward it through the existing streaming EventStream/SSE pipeline. - proxy/kompress: make pre-upstream backpressure and kompress execution saturation fail-open, so Anthropic requests no longer return 503 during temporary saturation while healthy capacity still compresses and explicit passthrough markers preserve operator visibility (#1025).
- wrap (codex): fix
headroom wrap codexproducing aconfig.tomlwith duplicate top-levelmodel_provider/openai_base_urlkeys (TOML-spec error) when the user had already configured their own provider. The injector now rewrites pre-existing top-levelmodel_providerandopenai_base_urllines in place — the previous value is kept in a# was: …trailing comment — instead of unconditionally prepending a duplicate, socodexcan start against the proxy. The pre-wrap snapshot mechanism continues to byte-for-byte restore the original file onheadroom unwrap codex. - install (macOS): fix
headroom install restart/install startfor launchdpersistent-servicedeployments.stopbootouts the job butstartonly ranlaunchctl kickstart, which cannot recover the un-bootstrapped statestop/restartleave behind (launchctl error 113), so the proxy was left stopped.startnow trieskickstart(fast path for an already-bootstrapped job) and, on failure,bootstraps the plist fresh — retrying for ~15s to ride out the transientbootstrapEIO (error 5) window while launchd releases the label after abootout.stoptolerates only the already-absent case (bootoutESRCH / error 3) and still raises on any otherbootoutfailure (#1289). - wrap: isolate wrapped proxy subprocess stdout/stderr into
proxy-stdio.log, soproxy.logremains the canonical rotating runtime log and Windows rollover failures fromRotatingFileHandlerare no longer blocked by wrapper stdio handles (#1184). - langchain: fix
HeadroomChatModel.ainvoke()crashing withAttributeError: 'AsyncStream' object has no attribute 'model_dump'when the wrapped model hasstreaming=True._agenerate()now uses a per-call non-streaming copy of the wrapped model instead of mutating shared state across anawait(#1285). - proxy: a transient rtk/lean-ctx stat-read failure (timeout, non-zero exit, bad JSON) no longer corrupts the dashboard's CLI-filtering session metrics. Failed reads now return "no data" instead of a synthetic zero payload, and the session baseline is only ever pinned from successful installed-tool reads — previously one hiccup re-pinned the baseline to zero and the next successful read inflated session savings by the tool's entire lifetime, at every proxy boot and
POST /stats/reset. - proxy: Concurrent large requests no longer 502 on a transient HTTP/2 stream reset. A single upstream
StreamResetpoisons the shared h2 connection and raisesRemoteProtocolError/LocalProtocolErroron every in-flight request; those transport errors weren't in the proxy's retry paths, so they collapsed straight to a 502 with no reconnect. The Anthropic non-streaming and streaming retry paths now treat anyhttpx.TransportError(including h2 protocol errors) as retryable before the first client byte, so the bad connection is dropped and the request re-sent on a fresh one (#1639). - install:
headroom wrap claudeno longer leaves a deadANTHROPIC_BASE_URLin a project's.claude/settings.local.jsonafter an unclean exit (SIGKILL, OOM, reboot, or terminal/tmux close viaSIGHUP, which was not caught)._write_claude_wrap_base_url/_restore_claude_wrap_base_urlonly removed or restored the entry from the wrap process's ownfinallyblock, so a crash skipped it and every later bareclaudeinvocation in that project inherited the stale proxy URL and hung indefinitely retrying a dead port. A wrap session now stamps a sidecar marker (pid, port, prior value); the nextwrap,unwrap, orheadroom doctorrun detects a marker whose pid is dead or reused and restores the recorded prior value automatically.claude()also now catchesSIGHUPalongside the existingSIGTERMhandler (#1768). - proxy: Non-finite values (
NaN,Infinity) inproxy_savings.jsonor in upstream cost/token metadata no longer crash the proxy or corrupt the savings dashboard.SavingsTracker's numeric coercion caught onlyTypeErrorandValueError, soint(float('inf'))raised an uncaughtOverflowErrorwhile loading persisted state (SavingsTracker.__init__failed and the proxy would not start), andfloat('nan')/float('inf')passed straight through, then serialized toNaN/Infinityliterals that the dashboard'sJSON.parserejects.json.loadsaccepts those literals, so one bad write poisoned every later start. Both coercion helpers now also catchOverflowErrorand reject non-finite floats, failing open to safe defaults. - learn:
headroom learnnow honorsCLAUDE_CONFIG_DIR. It resolved the Claude config directory as~/.claudeand wrote global memory to~/.claude/CLAUDE.md, so users who relocate their Claude config via that env var hadlearnscan the wrong directory and detect no projects. The scanner and memory writer now read/write the configured directory (#1630). - cli:
--backend bedrocknow fails fast with an actionable error when temporary AWS credentials (AWS_SESSION_TOKEN) are used but botocore is not installed (e.g. the slim default Docker image). litellm's session-token auth path imports botocore, so the missing dependency previously surfaced only at request time as a misleadingauthentication_error: No module named 'botocore'. The proxy now tells the user to install thebedrockextra up front (#1551). - compression: Content detection no longer crashes the proxy on text containing an orphaned
+++target line with no preceding---source line (common inset -xxtrace output and partial diffs). The bundledunidiff0.4.0 parser panics on that input instead of returning an error; the Rust diff detector now contains the panic and treats the fragment as plain text, so the request is compressed and forwarded normally instead of returning HTTP 500 (#1547). - proxy: persist lifetime cache-read savings (tokens + USD) in
proxy_savings.json(schema v4, additive) so cache-mode savings survive proxy restarts and upgrades. Previously prefix-cache read savings lived only in process memory and every restart reset the dashboard's cache figure to zero; the "Cache Reads (lifetime)" tile now reads the persisted value and the Prefix Cache Impact card renders after a restart with zero traffic, marking session-scoped tiles "no activity since restart". - compression: Proactive expansion blocks injected into user turns are now wrapped in
<headroom_proactive_expansion>XML tags, giving downstream consumers (LLMs, loggers, attribution parsers) a machine-readable provenance boundary and preventing misattribution in multi-agent threads. - cli: the startup banner no longer advertises
HEADROOM_COMPRESSION_STABLE_AFTER_TURNandHEADROOM_STALE_READ_COMPRESS_AFTER_TURNSas tuning knobs. Both were read only to render thePerformance Tuningbanner section and were never wired into the compression path, so setting them changed the banner but had no effect on behavior. The banner now surfaces only the embedding sidecar, which is a real, consumed setting. - memory/embedder: cap CPU thread oversubscription in the local torch/sentence-transformers embedder. Concurrent encodes previously each fanned out to ~
os.cpu_count()BLAS/OpenMP threads, so under load the memory path starved the asyncio event loop and spiked/livezlatency to several seconds. CPU encodes now run on a dedicated, size-limited executor whose workers each pin their thread pool, bounding total embedding threads toHEADROOM_EMBED_CONCURRENCY×HEADROOM_EMBED_NUM_THREADS(defaultsmin(4, cpu)× 1). The ONNX embedder already capped its threads; this brings the torch path to parity (#198). - proxy: Buffered passthrough routes (e.g.
GET /v1/models) no longer return an opaque HTTP 502 when an OpenAI-compatible upstream closes a pooled keep-alive connection mid-response (httpx.RemoteProtocolError/ "incomplete chunked read"). Headroom now retries the request once on a fresh connection — mirroring a directcurl— and only returns a clearupstream_protocol_error502 if the upstream is genuinely sending an incomplete response (#1112). - ccr: buffered Anthropic CCR re-streaming now preserves adaptive-thinking response shape, including empty
thinkingblocks,signature_delta,redacted_thinking.data, verbatimstop_reasonvalues such asrefusal, andstop_details. - cursor:
headroom wrap cursorno longer injects thertkcustom-instructions block into.cursorruleswhen rtk's own native Cursor hook registers successfully. rtk supports a real hook for Cursor viartk init --agent cursor(the same mechanism headroom already uses for Claude Code), which rewrites shell commands transparently — the injected.cursorrulestext duplicated that guidance for no benefit.wrap cursornow tries the native hook first and only falls back to injecting.cursorrulesif hook registration fails (#756). - proxy: The Headroom dashboard no longer tunnels
GET /favicon.icoto the wrapped upstream provider. No route matched that path, so it fell through to the proxy's catch-all passthrough route and was forwarded to the configured Anthropic/OpenAI/etc. backend — burning a real upstream request (and possibly failing auth) for a browser's automatic favicon fetch on/dashboard. A dedicated/favicon.icoroute now answers with204 No Contentdirectly, registered ahead of the passthrough catch-all (#1787). - learn: fix three Windows-specific failures in
headroom learn --verbosityand CLI-backed analysis (#1624).verbosity.pyread transcripts and profiles with the platform-default text codec instead of UTF-8, so non-ASCII content raised a silently-caughtUnicodeDecodeError, producingSessions: 0, human turns: 0for every project._greedy_path_decodelisted a directory's children withis_dir()inline in the same expression asiterdir(), so a singlePermissionErroron an inaccessible sibling (e.g. theAppData\Local\Temporary Internet Filesjunction present on most Windows profiles) aborted the whole listing and silently mis-decoded any project path that walked through it, causing--project <path>to report "No matching project" or resolve the wrong directory._call_cli_llmlaunched CLI backends viaPopen/run, which useCreateProcesson Windows and don't apply the shell'sPATHEXTextension search, so an npm-installed.cmdshim (e.g.claude,codex) raisedFileNotFoundErroreven though it was onPATH; ashutil.which-based retry now resolves the shim. - proxy: The Anthropic Messages route (
POST /v1/messages) now honors thex-headroom-base-urlper-request upstream override. It previously ignored the header and always forwarded toapi.anthropic.com, so clients that speak the Anthropic Messages wire format while authenticating against a non-Anthropic gateway (e.g. OpenCode Zen) were rejected upstream with401 invalid x-api-key. The route now forwards to<x-headroom-base-url>/v1/messages, consistent with the OpenAI-compatible and passthrough routes (#1760). - proxy: the savings store now fsyncs its parent directory after the atomic rename, so the most recent
proxy_savings.jsonwrite survives a power-loss or crash._save_lockedfsynced the temp file's contents but never the directory entry the rename created, leaving the rename itself non-durable on POSIX. Best-effort — a no-op on Windows and virtual filesystems where directory fsync is unsupported.
- code: fix two
CodeAwareCompressorAST-reassembly bugs: an exported JS/TS function or class (export function foo() {) produced a duplicatedexport exportkeyword and invalid syntax, because line-based node slicing (used to preserve indentation) pulled in the precedingexportsibling's text on top of theexport_statementhandler's own prefix reconstruction. Separately, in every supported language, a doc comment immediately above a top-level function, class, or type was detached from its declaration during extraction and re-emitted in a cluster at the end of the compressed output instead of staying attached to what it documents. -
- proxy: Buffered upstream responses containing a
server_tool_use(or any other unrecognized Anthropic content block) no longer turn a fully-generated response into an HTTP 502.StreamingMixin._response_to_sseraisedValueErroron unknown block types after the entire upstream generation had already been buffered, so a slow-but-successful response failed and the client retried the whole multi-minute request. Unknown blocks are now emitted verbatim incontent_block_start(following the existing redacted_thinkingpattern), soserver_tool_use,server_tool_result,mcp_tool_use`, and future block types round-trip (#1806).
- proxy: Buffered upstream responses containing a
0.36.5 (2026-08-22)
Bug Fixes
- codex: detect ChatGPT auth from id_token claims so wrap/init emit requires_openai_auth (#3212) (2f81fa5)
- doctor: report project-scoped Claude routing instead of a false negative (#3213) (8f3e33a)
0.36.4 (2026-08-22)
Bug Fixes
- dashboard: pin MIME types for the vendored static assets (#3193) (b485768)
- proxy/responses: keep the Codex additional_tools carrier on the wire (#3194) (1617f83)
- security: validate caller-supplied upstreams on every resolution path (#3195) (3e3c409)
- skip cross-turn dedup pointers on OpenAI chat streaming (#3191) (9c30b62)
- wrap: make the Serena pre-index stall budget configurable (#3183) (202c189)
0.36.3 (2026-08-21)
Bug Fixes
0.36.2 (2026-08-21)
Bug Fixes
- copilot: bind the minted token to the integration ID we forward (#3164) (397803a)
- kompress: accept ccr_original on the remote compressor (#3162) (45cb1b9)
- proxy: count output tokens from the stream's text, not its wire size (#3163) (4006964)
Dependencies
- bump ai from 6.0.138 to 7.0.59 in /sdk/typescript (#2281) (0891062)
- bump ai from 6.0.149 to 7.0.59 in /docs (#2277) (f7e5d37)
- bump md-5 from 0.10.6 to 0.11.0 (#3146) (c6dd823)
- bump ruff from 0.16.2 to 0.16.3 in the pip-minor-patch group (#3143) (c8db13d)
- bump the cargo-minor-patch group with 8 updates (#3145) (9c14e3a)
- bump tiktoken-rs from 0.11.0 to 0.12.0 (#3147) (a307c11)
- bump tokenizers from 0.22.2 to 0.23.1 (#3149) (6e2e10f)
- bump typescript from 5.9.3 to 7.0.2 in /plugins/openclaw (#2279) (85774fc)
- bump typescript from 5.9.3 to 7.0.2 in /plugins/opencode (#2280) (a382137)
- update mcp requirement from <2.0.0,>=1.28.1 to >=1.28.1,<3.0.0 (#3144) (6928d19)
0.36.1 (2026-08-20)
Bug Fixes
- docker: give :latest exactly one writer (#3154) (bf651c3)
- metrics: attribute tool-schema savings per model, not just compression (#3155) (81fe9d5)
- security: address u9up assessment findings (WEB-01–07) (#2207) (1f96dab)
0.36.0 (2026-08-20)
Features
- add deterministic runtime rollout controls (#1490) (3077ac8)
- proxy: let extensions report cost savings and their own latency (#3051) (f9807fd)
- proxy: unify savings attribution across stats, perf, metrics, and dashboard (1b0b0b8), closes #2976
- wrap/claude: make the --1m fallback model configurable via HEADROOM_1M_MODEL (#2983) (2a84725)
Bug Fixes
- anthropic: honor the [1m] 1M-context tier, and price it correctly (#3073) (6d2254d)
- ccr: make --no-ccr disable server-side response handling too (#3101) (131b119), closes #3082
- ccr: make StreamingCCRHandler work on OpenAI streams (#3069) (7ef736f)
- ccr: only buffer a stream when a marker is actually redeemable (#3092) (c502087)
- ccr: re-inject headroom_retrieve when history references it on the sessionless path (942af56)
- ccr: relay a successful upstream turn when post-processing fails (#3094) (0ec73fa)
- ccr: send Accept: application/json on a buffered stream:false turn (#3102) (139c7cb), closes #3078
- ccr: verify a scanned marker's hash before advertising it (#2908) (41dab2d)
- ci: prevent native detector from hanging test shards (#2996) (a708c05)
- ci: scope the release credential and stop persisting it to disk (#3062) (ac8646a)
- ci: unjam release and Docker publishing (#2958) (e269afb)
- claude: reject conflicting auth before proxy startup (#2993) (2d88e31)
- cli/install: resolve the deployment profile instead of dead-ending on default (#2832) (8252619)
- cli: stop the macOS malloc re-exec replacing an embedder's process (#3064) (96c25f5)
- copilot: route VS Code inline completions to Copilot, not OpenAI (#3077) (204e751)
- copilot: send VS Code inline completions to the host that serves them (#3112) (b77d612)
- deps: bump datasets past PYSEC-2026-3716 (#3136) (df6ff6b)
- deps: clear the two Rust advisories and make cargo audit blocking (#3121) (93c474e)
- deps: raise the GitPython floor to 3.1.58 to clear 9 open advisories (#3120) (8156d4d)
- docker: publish compose ports on loopback only (#3061) (481e0b8)
- docker: ship Bedrock auth and current registry (#2982) (eafdf11)
- doctor: surface that Claude Desktop agent sessions bypass the proxy (#2987) (be5b26d)
- install: consolidate Windows fallback and cleanup safety (#2980) (ddd2a25)
- install: honor HEADROOM_PORT in install apply and deploy (#3085) (58f28dc)
- install: stop the PowerShell installer leaking temp dirs into the real user PATH (#2985) (ddd9f76)
- learn: include stdout in CLI failure messages, not just stderr (#3080) (c5563d3)
- mcp: restore SDK v1 compatibility cap (#2978) (6077e5a)
- memory: sanitize entity_refs to prevent dict-shaped entries crashing search (#2951) (2d1e96b)
- onnx: enforce Rust API-24 runtime compatibility (#2979) (a3fe5cb)
- openclaw-plugin: circuit breaker + per-request timeout for proxy resilience (#639) (6576ef6)
- opencode: send x-headroom-project header on all proxied requests (#2868) (eeb038b)
- policy: price net-cost mutations with the 1h cache-write tier (#2780) (ef7e07e)
- providers: don't crash on a non-object HEADROOM_MODEL_LIMITS / models.json (#3089) (3ed8f76)
- proxy/anthropic: don't buffer a CCR stream when passthrough discards the stream flip (#2953) (f1c34d3)
- proxy/anthropic: don't replay recorded prefix over live history (#3026) (#3052) (c16be9b)
- proxy/anthropic: repair headroom_retrieve history references the tools array cannot support (#2876) (7de3573)
- proxy/anthropic: stop answering a non-streaming turn with an event stream (#3142) (0e26fb8)
- proxy/cache: strip cache_control from messages in the semantic cache key (#3086) (2cae0f8)
- proxy/gemini: guard CCR continuation usage against present-null counts (#3035) (a01897c)
- proxy/openai: propagate provider usage on the Responses WS->HTTP fallback (#2988) (536c949)
- proxy: adapt 200 SSE upstream replies on buffered /v1/responses instead of 502 (#2622) (d76fce0)
- proxy: align signed-thinking wire accounting (#3015) (b3f4436)
- proxy: complete stateless Responses and buffered CCR lifecycle (#2997) (8a1d38b)
- proxy: guard feedback endpoints and add CSRF checks to loopback writes (#3060) (a6ab359)
- proxy: keep prefixed core tools resident (#3046) (2f4d001)
- proxy: preserve Codex WebSocket model attribution (#3029) (a06a51e)
- proxy: relocate stray system-role messages to the top-level system param (#765) (#1357) (9fde127)
- proxy: restore the buffered-CCR heartbeat behind a grace window (#3091) (a29d201)
- proxy: scope the signed-thinking lock to blocks that actually changed (#3124) (17522fb)
- proxy: stop a lone surrogate turning a thinking body into a 500 (#3134) (284ff31)
- proxy: stop cached responses replaying the producing turn's wire framing (#3024) (9d37059)
- proxy: stop operator secrets following a client-chosen upstream (#3122) (05f5ef4)
- proxy: tune macOS libmalloc and trim allocator pages so long-lived RSS stays bounded (#2879) (6d87825)
- reporting: show net vs gross savings, real skip thresholds, and the effective profile (#3123) (250ede2)
- tool_search_tool_regex deferred and falsely resolved on direct-Anthropic path (#2971) (8ea87e7)
- vscode: persist compatible Claude modes and route Copilot CAPI (#2986) (1aa701a)
- wrap: set xAI upstream for grok-build proxy (#2772) (c831081)
- wrap: stop the Serena pre-index stalling the launch path for 300s (#2945) (6147883)
- wrap: verify proxy deps before mutating Codex config (#1628) (b7f342c)
Performance Improvements
Dependencies
- bump axum from 0.7.9 to 0.8.9 (#2966) (5731be7)
- bump criterion from 0.5.1 to 0.8.2 (#2965) (b30f339)
- bump ruff from 0.15.22 to 0.16.2 in the pip-minor-patch group across 1 directory (#2962) (ff17961)
- bump sha2 from 0.10.9 to 0.11.0 (#2288) (322425c)
- bump the cargo-minor-patch group across 1 directory with 4 updates (#2964) (888a9f4)
- bump tokio-tungstenite from 0.24.0 to 0.30.0 (#2967) (bbe9013)
- update mcp requirement from <2.0.0,>=1.28.1 to >=1.28.1,<3.0.0 (#2963) (d6fb536)
0.35.0 (2026-08-12)
Features
- beacon: allowlist the routing summary key (#2818) (7940c05)
- beacon: hourly R2 compaction, per-strategy savings, and a stack that reports (#2853) (e0870ef)
- cli,pricing: add CLI extension seam and prompt-cache TTL pricing (#2802) (6ec3e34)
Bug Fixes
- anthropic: strip first-party tool search on custom upstreams (#2539) (7f6950b)
- backends/anyllm: convert Anthropic tools and tool_choice to OpenAI shape (0d6866b)
- backends/anyllm: stream tool_use blocks and map finish_reason on the streaming path (e4904e2)
- backends/litellm: None-guard core token counts in OpenAI usage block (#2324) (12f9f58)
- beacon: report all-layers savings, not context-compression only (#2796) (e9a24f3)
- beacon: split session failures by status code (#2815) (2954e37)
- cache: bound compression cache bookkeeping (0ae948c)
- cache: enforce Anthropic's 1h-before-5m cache_control ordering before forwarding (#2941) (3752458)
- cache: mirror client cache_control positions instead of single-marker consolidation (def3d76)
- cache: stabilize Anthropic block-growing lineages (#2917) (1a04c95)
- ccr: avoid injecting tool on chat streaming (d0c1f5b)
- ccr: preserve exact SQLite TTL boundary (#2669) (d0a86d4)
- ccr: report embedded hashes from compress endpoint (#717) (685ebe4)
- ccr: resolve <<ccr:...>> markers inline when no retrieve-tool path exists (#2512) (ce8ce83)
- ccr: tolerate null/malformed OpenAI data in response handling (#2467) (e583e08)
- ci: publish latest from the root Docker manifest (#2252) (5568d73)
- claude: stop forcing tool search on Foundry (#2477) (7981396)
- cli/update: let install ownership win over bare /.dockerenv so venv installs self-update (#2830) (7092b53)
- codex: route alpha search through the Codex backend (#2538) (a540eb2)
- content-router: protect custom-tag blocks before mixed-content section split (d7bc1e2)
- deps: bump h2 to 4.4.1 for CVE-2026-71554 (#2839) (564e0a8)
- deps: enforce audited transitive dependency floors (#2791) (64e2039)
- doctor: flag
ollama launch claudeproxy bypass instead of misdirecting (#2566) (7f24d69) - emit SSE ping before message_start on Bedrock streaming path (issue #902) (#1080) (4dab254)
- gemini: resolve native CCR retrieval calls (#2253) (2483f57)
- health: label kompress as degraded/optional when not yet loaded (#2865) (8949371)
- image: decouple routing types from trained_router so importing the compressor doesn't import torch (#2513) (#2537) (d7cf981)
- install/windows: register persistent-task from S4U hidden XML (#2453) (#2459) (1edaeb8)
- install: don't crash the PowerShell installer when $PROFILE is unset (#2469) (fc5c4e2)
- install: trust Docker bridge for dashboard metadata (e044139)
- install: use --userns=keep-id under Podman so bind-mount writes don't fail (#2846) (3488f8d)
- learn/gemini: stop double-counting session tokens (#2230) (29d8a5e)
- learn/grok: detect a Windows absolute project path (#2283) (e240df2)
- learn: stop classifying a successful exit code 0 as an error (#2289) (a24fe7d)
- litellm: add async_post_call_success_hook to HeadroomCallback (#1322) (3107994)
- litellm: don't forward a caller key the target cannot accept (#2883) (2f2950a)
- memory: bound the TrafficLearner pending-pattern accumulator (memory leak) (#2579) (1f5feff)
- memory: close DirectMem0 resources (6596182)
- memory: close MCP backend on shutdown (4bd8ecd)
- memory: don't crash inline memory extraction on a non-object <memory> block (#2470) (e00c6ff)
- memory: keep vector metadata in sync (#2295) (c471800)
- memory: make explicit-project and user store keys collision-resistant (#2231) (f840d5f)
- memory: skip <system-reminder> blocks when building the retrieval query (#2195) (#2541) (4e5a67a)
- memory: sync FTS5 and vector indexes on CLI delete/edit/prune/purge (fd4628d)
- oauth2: make repository lint checks pass (c85abf7)
- observability: aggregate tool savings in OTEL (#2936) (941c25d)
- onnx: stop ONNX thread pools from spinning idle cores (#2495) (#2540) (5c561bd)
- openai: skip Responses tool-search deferral for clients that cannot execute it (#2696) (54ea28d)
- opencode: ship the transport hook-shim so wheel installs route Node child traffic (702dbc5)
- providers/anthropic: don't crash token estimation on null tool_calls (#2472) (08466f3)
- providers/openai: bound tiktoken vocab loads with the guarded loader (#2554) (0805e8e)
- proxy/anthropic: inject headroom_retrieve whenever a CCR marker is present, not only for new markers (#2848) (3808f60)
- proxy/anthropic: None-guard usage token counts on the direct buffered path (#2434) (2b5ee7c)
- proxy/anthropic: run tool-search history repair after turn hooks (c6f9948)
- proxy/batch: don't crash an OpenAI batch on a valid-JSON non-object line (#2316) (1f2c681)
- proxy/bedrock: report uncached input tokens from backend usage, not the live-zone count (#2318) (c19e412)
- proxy/gemini: keep streaming-parity baseline so eligible_pct can't exceed 100 (#2824) (b97c7c6)
- proxy/metrics: cap client-supplied model label cardinality (#2480) (e24a7e6)
- proxy/metrics: escape label values in the Prometheus export (#2463) (6a53861)
- proxy/openai: don't crash the Responses memory tool loops on null arguments (#2273) (a30db2c)
- proxy/openai: feed Codex WS traffic into the traffic learner (#2334) (f669149)
- proxy/openai: run response hooks on Responses, and bill their re-drives (#2872) (675d13f)
- proxy: allow settings routes for trusted gateway/dashboard clients (#2491) (a5b0a8f)
- proxy: cache litellm model resolution to stop repeated Provider List spam (99f07e7)
- proxy: cancel periodic TOIN task on shutdown (739fdef)
- proxy: close the upstream stream when a streaming body is never consumed (0951663)
- proxy: compress cache-mode cold starts and tag prefix-mismatch passthrough (#2365) (aaeba0a)
- proxy: emit request log timestamps in UTC (620028f)
- proxy: enable tool search by default and repair poisoned transcripts (#2807) (0237cbf)
- proxy: gate mid-turn message coalescing to Claude Code clients (#1643) (a4bd2e6)
- proxy: give each Codex /v1/responses WS turn a unique request_id (#2164) (d02df10)
- proxy: graceful shutdown and reliable Ctrl+C exit (#621) (17cdb18)
- proxy: guard telemetry and TOIN endpoints (cde1513)
- proxy: include tool_search_deferral savings in the savings ledger (12149f7)
- proxy: pass through cross-region prefixed Bedrock model IDs directly (#2330) (64cb46e)
- proxy: port session-sticky beta headers to the Rust proxy (#2381) (f6398a6)
- proxy: preserve merged session and quarantine contracts (#2943) (039cd24)
- proxy: preserve signed Anthropic thinking blocks on outbound re-serialize (#2254) (dc163bc)
- proxy: stop discarding compressed Codex WS later-frame payloads (#2823) (4ec416d)
- proxy: time-cap the compression timeout-debt quarantine (#2360) (#2412) (c5a08d2)
- proxy: unwrap Hermes tool_call bridge in tool name map (#2717) (a97b824)
- publish headroom-opencode in release workflow (#2372) (7859154)
- settings: accept documented HEADROOM_* env names as settings keys (#2833) (de9e052)
- subscription: dedup transcript usage by message id (#2340 token inflation) (#2408) (74275b7)
- toin: bound private query and pattern retention (8cd1380)
- tokenizer: coerce non-string tool_call fields before counting (#2801) (b6f9877)
- tokenizer: price CJK in the Rust fixed-ratio estimator (Python parity) (#2260) (6840153)
- transforms/adaptive-sizer: honor max_k on small-input fast path (#2319) (8a90523)
- transforms/smart_crusher: don't crash on a tool call with a null function (#2232) (3bb02f8)
- Vertex model pricing shows $0.00 for versioned model names and vertex:anthropic provider (#2517) (eb5b5e4)
- wrap/claude: keep --1m effective when an explicit --model is passed through (c093bf1)
- wrap/opencode: verify the opencode binary before mutating config (ae38486)
- wrap/serena: install Serena from the serena-agent PyPI wheel, not the git source (d7b25ae)
- wrap: honor Copilot OAuth wire-api override and model default (#2387) (1db6d88)
- wrap: serialize shared proxy startup (#2946) (e540d64)
- wrap: stop the launch cwd from shadowing the installed package in the proxy subprocess (#2843) (c49be26)
Performance Improvements
- cut hot-path latency 27% (token-count memo, startup preloads, JSON scan memo) (#2838) (53af90d)
- proxy: bound upstream calls and hot-path costs (#2852) (f624d3a)
- subscription: skip transcripts older than the window in compute_window_tokens (#2861) (91d6bf3)
Dependencies
- bump brace-expansion from 5.0.7 to 5.0.9 in /docs (#2751) (56ee57b)
- bump bytesize from 1.3.3 to 2.4.2 (#2286) (6448545)
- bump hf-hub from 0.4.3 to 0.5.0 (#2285) (4925bf6)
- bump next from 16.2.10 to 16.3.0 in /docs (#2750) (0fd0b99)
- bump postcss from 8.5.19 to 8.5.25 in /plugins/openclaw (#2749) (cd60ee9)
- bump postcss from 8.5.19 to 8.5.25 in /plugins/opencode (#2748) (ff4e016)
- bump postcss from 8.5.19 to 8.5.25 in /sdk/typescript (#2747) (267c2bd)
- bump postcss from 8.5.19 to 8.5.26 in /docs (#2881) (e6e5826)
- bump ruff from 0.15.17 to 0.15.22 in the pip-minor-patch group (#2501) (ecf130d)
- bump rusqlite from 0.32.1 to 0.40.1 (#2287) (522faa1)
- bump the cargo-minor-patch group across 1 directory with 22 updates (#2916) (148d860)
0.34.0 (2026-08-05)
Features
- claude: support Claude Code in VS Code (#2752) (13a310a)
- code: add PHP support to CodeAwareCompressor (#2423) (6d5516d)
- compress: accept config.frozen_message_count on /v1/compress (#2718) (2797099)
- compress: reach the lossless provider seam on the general path and default /v1/compress to marker-free output (#2691) (f2c48e2)
- copilot: proxy VS Code models transparently (#2687) (007446c)
Bug Fixes
- ccr: stop persisting retrieval markers as original content (#2694) (#2703) (3e348f3)
- ci: restrict Codecov shard uploads (#2745) (3f2ca99)
- compression: honor qualified CCR names across integrations (#2698) (dcb674b)
- compress: resolve the /v1/compress tokenizer per model, and document the real contract (#2743) (6422a80)
- cost: send litellm the total prompt so --budget stops seeing $0 (#2757) (a033ac4)
- deps: bump aiohttp and cryptography to clear the CVEs blocking 0.34.0 (#2753) (0221e7f)
- kompress: let orgs run Kompress on their own inference stack (#2736) (3d23d76)
- kompress: load merged.pt for the v2 checkpoint instead of the unmerged PEFT safetensors (#2716) (46da91b)
- kompress: reject artifacts that fail at run, and prefetch model files at startup (#2740) (224578e)
- learn: filter ambient user-role scaffolding (#2275) (3eb0122)
- learn: run project discovery off the event loop (#2731) (a70e5ff)
- normalize /p/<project> prefix on WebSocket upgrades so the Responses WS route is not rejected with 403 (#2379) (789a4f3)
- providers: give every model exactly one tokenizer (#2761) (cd92ed5)
- providers: stop a shorter model family shadowing a longer one (#2762) (0cb72f4)
- providers: stop pricing modern content blocks at zero (#2760) (06add9e)
- proxy/cost: mark estimated-basis budget records and add an enforcement policy (#2713) (#2725) (01df245)
- proxy/debug: reconcile Kompress warmup state in /debug/warmup (#2711) (3a27c4d)
- proxy/openai: run tool-description compaction on chat-completions (#2741) (f9db5b5)
- proxy: route Codex Live voice through a dedicated /v1/live transport (#2709) (232fb49)
- proxy: skip OpenAI tool_search deferral for Codex client (#2729) (56b3e4c)
- proxy: stop toggling headroom_retrieve in the Anthropic tools array (#2672) (08fce29)
- remove rtk and lean-ctx CLI context tools (#2677) (e0ce4b1)
- router: stop counting an image's base64 payload as suffix tokens (#2778) (f03cc6d)
- savings: surface request growth the tok_saved clamp swallows (#2708) (184146b)
- stats: report one "Tokens Saved" headline across every harness (#2737) (8262a4a)
- telemetry: anonymous compression stats — no prompts, no data (#2728) (9cfb008)
- telemetry: stop mixing tokenizer scales in RequestOutcome, and fix the overhead framing (#2756) (04e1517)
- tokenizers: count HuggingFace chat templates, and resolve gpt-5 / gateway-wrapped names (#2758) (0ed306b)
- tokenizers: resolve gpt-5 and mixed-case model names to the right encoding (#2776) (fc4680b)
- transforms: stop ContentRouter recompressing headroom_retrieve results (#2654) (677e097)
- wrap/serena: stop creating serena_config.yml, unbricking Serena on fresh installs (#2676) (759209c)
Code Refactoring
- pricing: make LiteLLM the source of truth, not the hardcoded table (#2779) (0e1d6bf)
- remove the dead headroom/prediction module (#2692) (b7a79ac)
0.33.0 (2026-07-29)
Features
- lossless: factor shared directory prefix in the grep search fold (#2547) (7dc9a97)
- metrics: record per-extension token savings (#2371) (02eb90f)
- opencode: ship the transport plugin in pip installs (#2601) (f54f04f)
- opencode: support Copilot subscription backend for headroom models (#2441) (#2445) (9089e7f)
- proxy/hooks: run fold-only (stream-safe) turn hooks on streaming OpenAI chat (#2549) (a6d4921)
- proxy/savings: aggregate tool-schema savings into Metrics + all reporting sinks (#2546) (9f1ffef)
- proxy: label GitHub Copilot traffic as "copilot" in the outcome… (#2377) (d7a8cdb)
- proxy: make /v1/compress usable as a gateway/Kong sidecar (#2458) (1329ed7)
- proxy: model-aware cold-prefix hook — reasoning compaction (Kimi/GLM) + cold recompaction (CC) (#2555) (cb8f4b6)
- proxy: route selected external compressors through the content router (#2388) (e3c7964)
- proxy: select built-in compressors via --compressor + registry inventory (#2373) (56c7d4a)
- rust: add structured prose offload plumbing (#334) (#2378) (9e07785)
- rust: port CodeCompressor AST compressor to Rust (parity-only) (#1154) (e530de5)
- rust: port Kompress ML prose compressor to Rust (parity-only) (#1153) (83e27e5)
- telemetry: record provider cache read/write/uncached tokens per request (#2450) (bec4cce)
- transforms: add compressed signal + dispatch code_aware/html/diff via registry (#2400) (7ebda67)
- transforms: add pluggable compressor registry + headroom.compressor entry point (#2370) (a02073e)
- transforms: dispatch kompress/text via the compressor registry + forward question (#2411) (446ec26)
- transforms: dispatch smart_crusher via the compressor registry (defer kompress/text ML boundary) (#2404) (7c7bf43)
- transforms: make built-in compressors real Compressor implementations (adapters) (#2391) (981616c)
- wrap: boost Serena — symbol-first guidance, wrap-time pre-index, repo-language scoping (#2425) (fd0e1a8)
- wrap: default code-memory to Serena (dashboard browser off) behind unified --code-memory (#2413) (6e4425a)
- wrap: reduce-at-source — SAFE quiet-CLI env defaults for the launched agent (#2548) (c990cfb)
Bug Fixes
- backends/litellm: guard None completion_tokens in usage mapping (#2322) (44a174f)
- backends: don't crash the OpenAI->Anthropic converter on empty choices (#2484) (43a7b57)
- cache: preserve cache_control ttl when re-anchoring a breakpoint (#2651) (e0d2cd0)
- cache: preserve client cache_control ttl when consolidating breakpoints (#2382) (8906d3a)
- ccr: guard empty/malformed OpenAI choices in _extract_assistant_message (#2389) (89319fb)
- ccr: sliding idle-window TTL with max-lifetime ceiling in the Rust core backends (#2604) (#2631) (e825588)
- ci: align Ruff tooling versions (#2406) (2bb14d1)
- cli: warn when Headroom proxy URL leaks into the shell after unwrap claude (#2238) (#2571) (904bc67)
- codex: detect keyring-backed ChatGPT auth (#2478) (46293f4)
- compression: report source-line span in CCR compression marker (#2597) (18e1c3c)
- copilot: derive GHE credential host from API URL (#800) (#2511) (4a8157f)
- copilot: normalize subscription API routing (#2441) (#2455) (2eca5ee)
- copilot: preserve /v1 for the Anthropic /v1/messages endpoint (#2409) (#2414) (c400f90)
- deps: bump mcp to 1.28.1 to clear 3 high-severity CVEs (#2348) (a90be94)
- grok: preserve business-seat auth while routing only inference (#2514) (e4076bb)
- image: reuse image models instead of rebuilding them per request (#2513) (#2536) (2a63ec7)
- install: carry upstream-routing env overrides into supervised deployments (#2429) (170b04a)
- install: default to cache mode, matching
headroom proxy(#1893 follow-up) (#2563) (b121223) - install: migrate deployments off the retired chopratejas image repo (#2427) (17ff13c)
- install: use CREATE_NO_WINDOW instead of DETACHED_PROCESS on Windows (#2527) (045f3df)
- kompress: raise the default execution-slot wait (#2456) (5bd2266)
- learn: detect the active OpenCode database (#2587) (f74d874)
- learn: keep traceback tail in tool-error digest preview (#2596) (85e8699)
- learn: treat unreadable candidate paths as absent in project decode (#2446) (a09ba6c)
- mcp: pin mcp dependency to <2.0.0 to prevent server startup crash (#2642) (b3f016b)
- proxy/cost: count Gemini thinking tokens in output usage (#2639) (22b707f)
- proxy/cost: record each request's savings exactly once (drop 3 double-counts) (#2545) (0845b26)
- proxy/cost: warn once per model when pricing lookup fails (#2504) (#2535) (fa47637)
- proxy/gemini: None-guard token counts from usageMetadata (#2347) (f64aac9)
- proxy/gemini: tolerate malformed parts on the compression path (#2486) (07cf547)
- proxy/metrics: move the savings-ledger append off the event loop (#2439) (4aac068)
- proxy/openai: cache under looked-up messages (#2420) (7052d52)
- proxy/openai: don't record Codex WS savings without input accounting (#2493) (2195ba7)
- proxy/openai: feed chat/completions traffic into the traffic learner (#2333) (6cdfd3f)
- proxy/openai: None-guard usage token counts on the chat path (#2431) (313c290)
- proxy/openai: replay incremental events in buffered Responses SSE (#2410) (#2415) (0cbc0e8)
- proxy/output-shaping: tolerate a non-string system block text in steering (#2435) (3e97671)
- proxy/perf: count turn-hook message folds in token accounting (#2520) (c371d5a)
- proxy/perf: tokenizer-consistent token accounting + surface tool-schema savings (#2542) (1cc53c9)
- proxy/streaming: tolerate malformed content in _response_to_sse (#2481) (77b26c0)
- proxy: keep buffered CCR streams alive (#2479) (a2e42fb)
- proxy: keep core tools and the client's ToolSearch resident for PascalCase clients (#2647) (1d29738)
- proxy: offload OpenAI and Gemini tokenizer counting off the event loop (#2498) (806d2e4)
- proxy: promote Kompress health after runtime load (#2402) (54526bc)
- proxy: reassemble server_tool_use.input from streamed partial_json (#2449) (8c8fae0)
- proxy: report deferred Kompress status and promote health from cache (#2564) (d50cfab)
- proxy: skip max_tokens rename for backend-routed openai chat (#2401) (d6a1af4)
- release: publish Windows wheel + sdist (disable PyPI attestations, #112) (#2405) (f9cbdd6)
- release: sync generated version metadata on the release branch (#2659) (5383c6b)
- rust: port CJK-aware relevance-query matching to CodeCompressor (#2634) (e86c639)
- security: exclude compromised ast-grep-cli 0.44.1 (supply-chain trojan) (#2342) (494fb5a)
- tokenizers: price Claude against a real BPE (tiktoken o200k) not a char estimate (#2543) (285176b)
- transforms/cross-turn-dedup: don't renumber-fold zero-padded line prefixes (#2369) (f4070c4)
- transforms/kompress-remote: keep compress fail-open on malformed 200 (#2320) (b759990)
- wrap: emit bare dotted keys for Codex --config overrides (#2383) (f57e959)
- wrap: make RTK opt-in (off by default) across wrap subcommands (#2344) (44136ed)
- wrap: skip Serena project setup outside real project roots (#2574) (0994ea0)
- wrap: stop same-port persistent routing during claude unwrap (#2340) (#2350) (cf5fa64)
Performance Improvements
Dependencies
- bump the cargo-minor-patch group with 10 updates (#2284) (3266ed7)
- bump the npm-minor-patch group across 3 directories with 7 updates (#2276) (961866b)
Code Refactoring
- transforms: dispatch simple built-in strategies via the compressor registry (#2399) (fc9c63f)
- wrap: retire tokensave; Serena is the code-memory MCP (#2499) (5d23a0a)
0.32.0 (2026-07-17)
Features
- 3-layer context compression pipeline (L1+L2+L3) (#1405) (3dd9660)
- add CrewAI and AutoGen tool compression integrations (#1384) (e8bff1c)
- cli: add
headroom inspectto view original vs compressed content (#1595) (942e916) - cli: add
wrap openclaudefor OpenClaude CLI (#1416) (5415008) - codex: keep wrap routing session-scoped (#1507) (ad9d086)
- compress: expose frozen_message_count in library-mode compress() (#2178) (021a762)
- core: gate ONNX transforms behind a default-on
mlfeature (static/lexical builds) (#2165) (cdba2ec) - dashboard: add settings dashboard for proxy configuration (#2101) (96bc4cd)
- dashboard: persist lifetime proxy metrics (#2198) (0537cbf)
- deploy: Add turnkey deploy command (#1404) (560ffae)
- evals: register multilingual multi-wiki-qa (zh/ja/ko) dataset (#1530) (f891506)
- evals: weekly HotpotQA answer-recall report on the prose path (#1188) (46d4378)
- grok-build: add Grok Build wrap command and MCP integration (#1629) (420dc90)
- install: add apply flag parity, --env passthrough, and EIO retry (#2152) (896454e)
- kompress: optional remote compression endpoint (HEADROOM_KOMPRESS_ENDPOINT) (#2171) (b6eb7a7)
- mcp: add streamable HTTP MCP transport (#1773) (4ea96a4)
- mcp: publish canonical server.json (#1510) (e9e9cd5)
- memory: add explicit supersession repair (#2217) (ce52b30)
- metrics: export compression-failed and kompress size-gate counters (#1569) (d728338)
- observability: add gen_ai.request.model to the compression span (#1667) (7f7af66)
- proxy: add opt-in cost-aware model router (#1706) (#2205) (57e8dcb)
- proxy: apply output shaper to OpenAI-compatible endpoints (#1725) (e65b9b3)
- proxy: expose retry delay configuration (#2077) (099c664)
- proxy: extend output shaping to the OpenAI Responses path (Codex HTTP + WS) (#1943) (71cbb6a)
- proxy: opt-in compression for catch-all passthrough routes (#1699) (4cbd5da)
- proxy: persist per-model savings breakdown in proxy_savings.json (#2055) (12a38d3)
- simulators: add provider simulator service (#2014) (2c9eb7c)
- stats: per-bucket output-shaping savings in /stats-history (#1819) (12a9710)
- text-crusher: CJK-aware segmentation + relevance via ICU (#1504) (4035c04)
- text-crusher: fold full-width ASCII to half-width in CJK token keys (#2259) (844d9ca)
- wrap: add
headroom wrap kimifor Kimi CLI (#1426) (eac4965) - wrap: add omp target (Oh My Pi) with models.yml override and unwrap (#1811) (fcf455a)
- wrap: add ZCode desktop app support (#1845) (2a954b6)
- wrap: allow project RTK instruction opt-out (#2078) (f53f720)
Bug Fixes
- adaptive-sizer: char bigrams for spaceless CJK items (#1748) (8879c50)
- add DeepSeek V4/V3.2/R1 tokenizer mappings and context limits (#912) (0c70875)
- add Vercel deploy config and workflow for docs site (#1739) (b0fa84e)
- auth: support GitHub Enterprise Copilot OAuth domain (#2192) (5dbe331)
- backend/bedrock: preserve system-prompt cache_control breakpoint (list form) (#2225) (ea0115c)
- backends/litellm: drop oversized tool names before Bedrock Converse (#2129) (3c1a5cd)
- backends/litellm: preserve tool_result cache_control, complete streaming cache stats (#2144) (0ad7dc7)
- bedrock: resolve global.* inference profiles + pin per-user app-profile ARNs (#1795) (33c7f6c)
- build: support Intel macOS (x86_64-apple-darwin) via ort-load-dynamic (fixes #941) (#1797) (1590913)
- cache-aligner: hash the frozen conversation prefix so Claude Code cache invalidation is detected (#2085) (#2161) (cc072f0)
- cache/ccr: don't count a successful eviction as a retrieval (#2106) (eecb81e)
- cache/ccr: don't evict a live entry on a duplicate store at capacity (#2082) (1138946)
- cache/semantic: don't evict an unrelated entry on an update at capacity (#2094) (cf6367a)
- cache/semantic: key entries by context hash, not query text (#2022) (d8783ab)
- cache: extract tool_result content from list-of-blocks format (#2092) (3bcef2b)
- cache: normalize embeddings before the semantic similarity check (#2122) (f8eaaeb)
- cache: partial cached-prefix replay + idle-aware net-cost; don't… (#1933) (b0440f9)
- cache: stable session identity and per-conversation prefix trackers under agentic clients (#2193) (7bfb1d7)
- cache: stop DynamicContentDetector false positives corrupting cached prompts (#2110) (#2119) (908a9a1)
- ccr: detect read_lifecycle stale/superseded markers in the injector (#2148) (ec97443)
- ccr: don't crash parse_tool_call on non-object tool arguments (#2071) (984a2c7)
- ccr: don't crash tool-call detection on a null function/functionCall (#2269) (1612f06)
- ccr: lowercase a retrieved hash so an uppercase echo still hits the store (#2236) (842d7e1)
- ccr: skip compact summaries for proactive expansion (#2242) (3f241e4)
- ccr: store pre-protection original, not tag placeholder, in CCR (#1208) (a61f534)
- ci/deps: clear audit and release smoke failures (#2190) (fce93bf)
- claude: treat non-zero claude --version exit as version-unknown … (#2233) (f71fef1)
- cli/init: fail clearly on a target settings file with invalid JSON (#2227) (daca1dd)
- code-compressor: recover valid Python rewrites after local syntax rejection (#2202) (dbbef4b)
- code: parse-probe tree-sitter availability in code_handler (#1231) (#1300) (1de35e7)
- code: pin tree-sitter-language-pack <1.0.0 in [code] extra (#1219) (412db40)
- code: quarantine Perl parser from code-aware compression (#2204) (8522fcb)
- codex: preserve wrapped sessions and recover state (#2160) (dec60de)
- codex: rerun memory lookup on every response.create WS frame (#2113) (38479fc)
- codex: rewrite config.toml properly so Codex will route through … (#2102) (5d9bbbe)
- codex: skip sockets in session home overlay (#2104) (c4ddcb9)
- content_router: pin FREEZE_BLOCK_DECISION verdict to stop cache-write churn (#1620) (a069979)
- content-router: protect_tool_results must not be weakened by profile-derived read_protection_window (#2105) (3d0e59e)
- copilot-auth: stop discarding the caller's valid Copilot auth token (#1879) (f52ca19)
- copilot: refresh wrapped subscription tokens (#2156) (#2182) (4364eb8)
- core: avoid unidiff panic on bash xtrace (#1506) (3757a7c)
- dashboard: serve per-request metadata to trusted-gateway peers (#1766) (560319c)
- dedup: shorter fold pointer + MIN_LINES=3, dedup Anthropic list-… (#1932) (10e4829)
- deps: bump pillow to 12.3.0 and click to 8.4.2 (#2097) (8870b69)
- deps: clear Dependabot lockfile alerts (#2175) (ea3d5a8)
- deps: enforce transformers security floor (#2201) (cbfa267)
- deps: raise transformers security floor (09be107)
- diff-compressor: CJK-aware relevance scoring for hunk selection (#2220) (528517c)
- emit unknown Anthropic content blocks verbatim in buffered-to-SSE conversion (#1825) (d05802b)
- harden fd lifecycle and SystemError handling in runtime and proxy kill (#1556) (f42ce4a)
- harden persistent install startup (#1851) (1d2b76e)
- health: exclude kompress from aggregate readiness + adversarial PBT (#2066) (f1663ea)
- init/codex: don't delete per-profile provider settings (#2146) (8da4384)
- install: add orjson to [proxy] extra for LiteLLM provider backends (#2074) (4f3d5ab)
- install: default docker image to headroomlabs-ai GHCR registry (#1867) (#2039) (c3db8e4)
- install: don't let host env override the manifest in persistent-docker (#2090) (b097ef3)
- install: guard non-dict health config in 'install status' (#2150) (8f867e4)
- install: write deployment manifest atomically and tolerate corrupt manifests (#1303) (42bdf23)
- kompress: fail-open wall-clock guard on single-cache-miss compression (#2114) (e900086)
- kompress: surface model-not-ready state via logs and health endpoint (#2034) (12aa2cb)
- learn/claude: don't abort the whole scan on a null message line (#2299) (eed80dd)
- learn: don't desync verbosity pairing on empty assistant turns (#2123) (def2f9a)
- learn: don't shadow TIMEOUT/CONNECTION with the generic RUNTIME_ERROR (#2099) (c7b5a24)
- learn: handle Windows UTF-8, drive-letter paths, and CLI shim fallback (#1895) (e3b45e4)
- learn: ingest OpenAI Responses HTTP traffic (#2167) (ce14130)
- learn: parse fenced JSON even with a prose preamble (#1988) (d2170b1)
- litellm: forward chat_template_kwargs and other vendor top-level fields to OpenAI-compatible backends via extra_body (#2128) (#2163) (fb683e1)
- litellm: surface Bedrock cache token usage in non-streaming responses (#1848) (d604e86)
- mcp/claude: don't clobber an unparseable Claude config on register (#1660) (bc24e25)
- mcp/codex: don't clobber an unparseable/non-table config.toml (#2062) (415e03c)
- mcp/opencode: don't clobber an unparseable opencode.json on register (#1661) (d079614)
- mcp: correct default Claude Code config path in ClaudeRegistrar (#1859) (c85731d)
- mcp: mcp status checks ~/.claude.json, not only ~/.claude/mcp.json (#990) (9e376af)
- mcp: reap orphaned mcp serve on client death (#2226) (7a5d8a7)
- mcp: regenerate stale server.json (0.27.0 -> 0.32.0) (#2218) (79d8056)
- memory/sqlite: don't emit OFFSET without LIMIT in query (#2063) (a5bdc54)
- memory/sync: don't clobber memories sharing a first line (#1976) (5e14b8c)
- memory/sync: make Codex AGENTS.md adapter additive (stop wiping memories) (#1674) (7fd0c42)
- memory: annotate _EMBEDDER_CACHE key as 3-tuple (unbreak main lint) (#2153) (22af75a)
- memory: apply turn_id scope filter even without agent_id (#2130) (9e38905)
- memory: audit passive context injection (#2212) (2de07db)
- memory: filter inactive graph-expanded results (#2210) (aa4515c)
- memory: honor explicit store=false on Responses requests (#2017) (31abb69)
- memory: key the embedder cache on ollama_base_url (#2109) (1725cd1)
- memory: preserve semantically similar memories (#2303) (5279c33)
- memory: remove a superseded memory from the search indexes (#2143) (fa330f3)
- memory: require explicit updates for supersession (#2188) (6d897e8)
- memory: serialize MCP backend initialization (#2309) (0924755)
- memory: size HNSW index_batch resize off the id high-water mark (#2139) (b0afee8)
- memory: track MCP retrieval access (#2065) (d0ecc9a)
- models: version-boundary longest-prefix match in ModelRegistry.get (#1658) (b699bed)
- opencode: Use opencode.jsonc when present (#1590) (4e2bbfe)
- opencode: use type=local + environment field for MCP config (#1380) (#1388) (a51bbfb)
- packaging: guard torch extras on intel macos (#2011) (fd0d29c)
- patch nltk vulnerability (CVE-2026-54293) (#1929) (28ca61f)
- paths: reject '.', '..', and NUL as plugin names (#2132) (af7385a)
- pricing: alias retired claude-3-sonnet to Sonnet-tier price, not Haiku (#2095) (6137967)
- proxy/anthropic: cache response under the looked-up messages (#327) (#2124) (dbb4e4c)
- proxy/anthropic: preserve non-2xx upstream status through security scan (#2100) (aa78816)
- proxy/anthropic: scope session id by top-level system prompt (#2070) (ec6e60e)
- proxy/batch: preserve sibling tool configs on Google batch requests (#2177) (a5d7e12)
- proxy/bedrock: wire PrefixCacheTracker updates into Bedrock backend paths (#2196) (a352fa0)
- proxy/cost: price cache savings by most-used model, not first-seen (#2023) (b4f807f)
- proxy/gemini: forward a non-JSON upstream body with its real status (#2174) (f723925)
- proxy/gemini: preserve non-text content across the compression round-trip (#2079) (4056117)
- proxy/gemini: thread savings-profile kwargs into apply() (#1994) (38306a3)
- proxy/memory: capture user text blocks for the retrieval query (#2064) (f542b70)
- proxy/memory: don't crash memory tool-call detection on a null function (#2272) (8b7e797)
- proxy/openai: respect explicit stream_options.include_usage (#2026) (19201e8)
- proxy/savings: append history point on cache-only savings too (#2194) (d125805)
- proxy/savings: don't bill fallback rate for free (0-priced) models (#2024) (6ecbdd6)
- proxy/streaming: preserve non-standard content-block fields on SSE reconstruction (#2271) (6decbd1)
- proxy/vertex: route google-publisher requests to the request region (#2069) (1843346)
- proxy: accept Codex websocket before upstream retries (#2203) (551f473)
- proxy: aggregate tool-output size floor so Codex sessions compress (#2050) (#2116) (dbe2558)
- proxy: batch small Codex Responses tool outputs (#2239) (09c66ac)
- proxy: cache_savings_usd silently zeroes when litellm is unavailable (#2005) (75d7861)
- proxy: cold-start fast pass — defer only Kompress, not the whole pipeline (#2073) (fd9ddaa)
- proxy: compress Hermes scoped coding-agent passthrough (#1815) (09d1ef4)
- proxy: compress OpenCode tool schemas and embedded JSON (#1535) (05932d7)
- proxy: count exhausted upstream 5xx as failed across all providers (#1571) (e365ad7)
- proxy: dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189) (e5b3a63)
- proxy: don't 502 Anthropic streaming on a legal mixed CCR + client-tool turn (#2089) (#2117) (4951cf8)
- proxy: fail soft on a bad HEADROOM_QDRANT_PORT during config construction (#2141) (69aea2f)
- proxy: handle ClientDisconnect in passthrough body reads (#2033) (9db8a6b)
- proxy: handle ClientDisconnect in passthrough body reads + log sanitization (#2067) (605e269)
- proxy: handle content-part outputs in Codex Responses compression (#2052) (c9a7755)
- proxy: hoist ccr_workspace_key default so /v1/messages survives CCR-inject off (#1096) (1deb947)
- proxy: honor x-headroom-base-url on /v1/messages route (#1763) (bb2acf7)
- proxy: isolate image compression in a subprocess so a native SIGSEGV can't take down the proxy (#2107) (#2162) (09e7212)
- proxy: keep anthropic ccr compression active across deferred injection (#2291) (#2297) (26b43f6)
- proxy: keep Kompress warmup off the startup path (#2001) (10ed14e)
- proxy: keep PRE_SEND from reintroducing empty tool arrays (#2015) (d1db00a)
- proxy: keep recent stats request rows (#1922) (bd8de9f)
- proxy: key drift detector on conversations, not credentials; canonicalize drift hashes (#2301) (6744833)
- proxy: one bad extension no longer aborts proxy startup (#2215) (cb6c828)
- proxy: only queue mid-turn messages for opt-in clients with explicit session header (#1951) (c365c7f)
- proxy: preserve chatgpt responses streaming (#2012) (a617455)
- proxy: preserve content-part array structure in excluded-tool lossless fold write-back (#2261) (8951a26)
- proxy: preserve sub-path in X-Headroom-Base-Url custom upstream (#2037) (#2127) (2976d49)
- proxy: preserve terminal tool on Codex Responses (#2000) (41af39d)
- proxy: preserve upstream 5xx status on retry exhaustion (#1570) (7836aea)
- proxy: protect WebSearch/WebFetch tool results from lossy compression (#2115) (d2fbd55)
- proxy: quarantine compression while timed-out workers run (#2292) (517bf99)
- proxy: record cache metrics for non-streaming backend paths (#1271) (8580404)
- proxy: record Prometheus metrics for POST /v1/compress (#2247) (81d40a6)
- proxy: reject rate_limit_requests_per_minute=0 when limiting is enabled (#2142) (8a71947)
- proxy: repair main lint (ruff-format drift + mypy host_header) (#2268) (718c8dc)
- proxy: satisfy rustfmt import ordering (#2158) (f008336)
- proxy: skip Responses memory tools for ChatGPT auth (#1579) (1c50eca)
- proxy: strip [1m] model suffix before upstream forwarding (#2027) (52a024d)
- proxy: Strip Codex responses-lite marker from response.create frame body (#1820) (5cece7b)
- proxy: strip duplicated upstream server headers (#1828) (d2a86b5)
- proxy: strip inbound Content-Encoding on messages/chat forward (#1970) (4cb33cd)
- proxy: support Codex WS compatible gateways (#1281) (ac7ee4e)
- proxy: support Windows selector loop on uvicorn < 0.36 (#1655) (e0eb094)
- release: sync all package versions to v0.31.0 (#1882) (662b7bc)
- replace computer_call_output with apply_patch_call_output in output_shaper (#2250) (63f74aa)
- router: stop protecting passing build/test output as error traces (#1740) (7ab83c5)
- savings: cap ledger retention at 30 days (#1985) (b3a559b)
- savings: coding profile compresses the recent delta (protect_recent 2->0, min_tokens 25->10) (#2145) (eca3db6)
- savings: don't bill free models at the $3/M fallback in the ledger (#2147) (fb17156)
- savings: don't fabricate output savings for a free (zero-priced) model (#2298) (ec12e18)
- savings: record pre-compression original as ledger before, not forwarded count (#2176) (195ed90)
- scripts: rename .releaseetadata to .releasemetadata (#1246) (772adc9)
- search-compressor: CJK-aware relevance + harden Rust/Python parity (#1749) (985621d)
- stats: tag streamed output token source (#2214) (1c9585d)
- strip output-only fallback blocks from request messages (#1870) (1448718)
- subscription/copilot: preserve remaining=0 for exhausted quota (#1997) (cbb7750)
- subscription: keep efficiency_pct from exceeding 100% (#2121) (5fb449e)
- subscription: read newest transcript tail (#2310) (793d20f)
- telemetry: only advance usage-report baseline after a 200 (#2149) (0cddac6)
- tests: repair three main-branch test failures (#2306) (1d79e70)
- tokenizers: don't tokenize image blocks as text in TiktokenCounter (#2093) (ae10d6c)
- tokenizers: price CJK in the fixed-ratio estimator path (#2080) (cd3d5aa)
- tokenizers: recurse into list-content tool_result blocks (#2081) (dfb1d37)
- tokenizers: resolve HF tokenizer names by most-specific prefix (#2096) (e0232df)
- tokenizers: use o200k_base for gpt-4.1/gpt-4.5/o4 families (#2108) (6979b52)
- transforms/code: coerce language aliases instead of raising (#1975) (27ddde1)
- transforms: guard Log fallback against invalid JSON + fix MIXED false-positive on source code (#1347) (02c7764)
- transforms: guard the lossless diff fold to diff-shaped content only (#2140) (5e0f1a2)
- update: let Windows self-update replace headroom.exe (#2016) (2678bb1)
- update: prevent _core.pyd corruption on Windows when proxy is running (#1581) (0750bbf)
- version: mark source-checkout builds as -dev (#2072) (1cc9979)
- windows: unwedge compression on degraded ONNX runtimes (every request timing out at 30s, 0% savings) (#822) (36202f4)
- wrap/claude: bind _wrap_settings_path before the try (#2126) (faed4dc)
- wrap/codex: export the detected custom upstream base URL (#2125) (d236b27)
- wrap/opencode: unwrap removes the rtk block from AGENTS.md (#2025) (20968a4)
- wrap: drop -p short flag from wrap claude so claude's own -p/--print passes through (#2048) (14011b4)
- wrap: keep Claude context-tool setup explicit (#1999) (f536aa0)
- wrap: preserve custom Codex provider base_url during proxy injection (#1894) (372d6c8)
- wrap: read/write instruction files as UTF-8 on Windows (#1245) (6413cc7)
- wrap: self-heal a stale ANTHROPIC_BASE_URL left by a dead proxy (#2223) (8537e2c)
- wrap: surface Claude Remote Control base-URL gate accurately (#1… (#1883) (daeff69)
- wrap: use canonical headroom-openclaw npm package for wrap openclaw (#1969) (#2120) (c5545d6)
Performance Improvements
Dependencies
- bump @types/node from 22.19.15 to 26.1.1 in /plugins/openclaw (#1685) (350daeb)
- bump @types/node from 22.20.0 to 26.1.1 in /plugins/opencode (#1688) (8715195)
- bump @types/node from 25.5.2 to 26.1.1 in /docs (#1683) (75fff43)
- bump fumadocs-typescript from 4.0.14 to 5.3.0 in /docs (#1684) (e8b66a2)
- bump prometheus from 0.13.4 to 0.14.0 (#1518) (5229c98)
- bump thiserror from 1.0.69 to 2.0.18 (#1519) (e448d7b)
- bump toml from 0.8.23 to 1.1.2+spec-1.1.0 (#1517) (6c705b4)
- update tree-sitter requirement from <0.26,>=0.25.2 to >=0.25.2,<0.27 (#1681) (ce3c959)
Code Refactoring
- cache: isolate compression strategy outcomes (#1938) (b5aa8a3)
- cache: isolate semantic key policy (#1953) (740fb9b)
- ccr: isolate tool call classification (#1937) (fd5b9e7)
- memory: isolate injection decision policy (#1952) (c20f3b1)
- memory: isolate query construction policy (#1950) (235c986)
- output: isolate savings policy (#1947) (c29b4ba)
- output: isolate verbosity steering (#1940) (0ce09fb)
- pricing: isolate litellm model resolution (#1936) (4210d6e)
- providers: split proxy route adapters (#1934) (e6243f6)
- proxy: extract beta header merge policy (#1993) (f359f21)
- proxy: extract beta header policy (#1992) (603f5bc)
- proxy: extract ccr golden replay policy (#2006) (7c9a032)
- proxy: extract ccr marker policy (#2004) (ec3c3cd)
- proxy: extract ccr session tracker (#2003) (e92c253)
- proxy: extract internal header policy (#1990) (868b88b)
- proxy: extract memory golden replay policy (#2007) (8c68f48)
- proxy: extract tool definition serialization (#1998) (ad6ab48)
- proxy: extract tool injection config (#2010) (0f846e5)
- proxy: extract tool injection logging (#2009) (9c7b9d5)
- proxy: extract tool injection policy (#1995) (d6259b2)
- proxy: extract tool injection tracker (#2002) (d1c484b)
- proxy: extract tool name policy (#2008) (1000175)
- proxy: isolate auth classification policy (#1945) (5a7265d)
- proxy: isolate body forwarding policy (#1935) (1f3696a)
- proxy: isolate forwarded header policy (#1942) (cb38f79)
- proxy: isolate image compression policy (#1958) (2b09ece)
- proxy: isolate memory rank policy (#1960) (b1e871d)
- proxy: isolate output effort policy (#1961) (094a53c)
- proxy: isolate output turn policy (#1962) (c904a70)
- proxy: isolate output verbosity policy (#1963) (0415dc8)
- proxy: isolate project attribution policy (#1957) (1c1e360)
- proxy: isolate proxy mode policy (#1965) (82af5cd)
- proxy: isolate rate limit policy (#1954) (ea19515)
- proxy: isolate semantic cache key policy (#1964) (2f53a18)
- transforms: isolate mixed content parsing (#1939) (9bacf48)
0.32.0 (2026-07-13)
Features
- add --disable-kompress-fallback to restore legacy PASSTHROUGH fallback (#1185) (f309244)
- add first-class OpenCode support (wrap, learn, mcp install) (#559) (91cd210)
- add HEADROOM_KEEPALIVE_EXPIRY to keep upstream connections warm (#1124) (85786b3)
- Add support for Mistral Vibe CLI (#935) (0932b8b)
- agent-savings: land coding + general workload personas on main (#1732) (d8db7da)
- anthropic: add Claude 5 family pricing & align current rates (#1767) (e84ca98)
- azure-foundry: derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE (#1138) (e5031b0)
- cache: attribute prompt-cache misses to TTL lapse vs prefix change (#1313) (#1343) (4658721)
- cache: provider-agnostic cache-mode delta + cc-agnostic prefix comparison (#1868) (7c2f0ea)
- ccr: wire retrieve-tool interception into OpenAI Responses handler (#1898) (62cd307)
- cli: add headroom doctor setup diagnostics (#926) (e45cf4e)
- cli: add headroom update command and release banner (#1088) (26be2c3)
- code: add Perl support to code-aware compressor (#1125) (f39858c)
- codex: keep wrap routing session-scoped (#1507) (ad9d086)
- compression extraction — Rust knob exposure, CCR hardening, traffic audits (#818) (b7be381)
- compression: add audit-safe mode with protected pattern matching (#1899) (bb112dd)
- content-router: accept any real compression (remove min-savings floor) (#1771) (6c31db9)
- content-router: lossless-excluded compaction (grep/log/json) + enable in coding/general personas (#1762) (f067040)
- content-router: lossless-first dispatch, cross-turn dedup, and A7 lossy-after-fold (#1818) (60af15f)
- headroom wrap opencode / unwrap opencode CLI (#1105) (b4571cc)
- learn: weight loops in Headroom Learn + RTK-loop eval (#1160) (14e8dc4)
- learn: write per-project learnings to CLAUDE.local.md by default (#1115) (ced75e4)
- measure and surface token throughput (tokens/sec) through the proxy (#983) (0d89c67)
- observability: add gen_ai.request.model to the compression span (#1667) (7f7af66)
- output-token reduction — verbosity shaper, per-user learning, counterfactual savings (#965) (a99dc61)
- policy: decay P_alive from idle time near cache TTL (#856 P3b) (#1028) (fe4f9ee)
- providers: add Cortex Code (Snowflake CoCo) as a supported agent (#1190) (d9d0bf4)
- proxy: add --lossless no-CCR mode with format-native compaction (#1721) (c75ebde)
- proxy: add provider-only HTTP proxy (#1807) (ebe0a3b)
- proxy: add request timeout config (#738) (c0745d4)
- proxy: add turn-hook extension point for buffered model turns (#1891) (ec950f7)
- proxy: cc-switch reconciler — keep Headroom in the request path alongside cc-switch (#1030) (e8fc8a0)
- proxy: expose retry delay configuration (#2077) (099c664)
- proxy: extend output shaping to the OpenAI Responses path (Codex HTTP + WS) (#1943) (71cbb6a)
- proxy: hot-reload live env knobs so a reused proxy picks them up without a restart (#1090) (6904d47)
- proxy: make COMPRESSION_TIMEOUT_SECONDS configurable via env (#946) (#991) (addebdb)
- proxy: persist per-model savings breakdown in proxy_savings.json (#2055) (12a38d3)
- proxy: pilot hardening — inbound auth, security headers, audit log, air-gap switch (#1537) (546ab55)
- proxy: support glob patterns in exclude_tools (#870) (#1259) (a2159c0)
- read-maturation: activity-based hold-back Read maturation (Mechanism B) (#1068) (723b80c)
- savings: durable savings ledger + headroom savings command (#1127) (978ffa0)
- ship the coding profile as Headroom's out-of-box default posture (#1893) (68676da)
- simulators: add provider simulator service (#2014) (2c9eb7c)
- stats: surface Codex WS compression counters in /stats summary (#1680) (2fe19c3)
- transforms: adaptive Otsu KEEP/DROP threshold (+ land relevance split on main) (#1726) (eea667a)
- transforms: tabular + spreadsheet (.xlsx/.xls) compression (#1128) (d789a7c)
- vertex: turnkey Claude Code + Vertex compression (+ fixes from the Vertex review) (#1113) (0e05915)
- wrap: add --1m to preserve the 1M context window on wrap claude (#1158) (#1351) (b50d9c1)
- wrap: allow project RTK instruction opt-out (#2078) (f53f720)
- wrap: make tokensave the primary coding-task compressor, Serena the backup (#1230) (dca9853)
Bug Fixes
- adaptive-sizer: char bigrams for spaceless CJK items (#1748) (8879c50)
- agent-evals: Phase 0 — coding-agent accuracy A/B framework (#1037) (84f9871)
- agno: tolerate streaming tool-call SDK objects in parser (#1312) (#1336) (5986c22)
- bedrock: add boto3 1.41 + CRT for aws login credentials (#1486) (4db3bc9)
- bedrock: fail fast when session-token auth lacks botocore (#1553) (54cfa36)
- bedrock: resolve global.* inference profiles + pin per-user app-profile ARNs (#1795) (33c7f6c)
- bedrock: route ARNs via converse, named AWS profiles, and au. re… (#1456) (7d87aa2)
- build: enable Intel macOS pip installs via ort-load-dynamic (#1538) (32ce99e)
- bump codebase-memory-mcp to v0.8.1 (#1284) (530318b)
- cache/ccr: don't count a successful eviction as a retrieval (#2106) (eecb81e)
- cache/ccr: don't evict a live entry on a duplicate store at capacity (#2082) (1138946)
- cache/semantic: don't evict an unrelated entry on an update at capacity (#2094) (cf6367a)
- cache/semantic: key entries by context hash, not query text (#2022) (d8783ab)
- cache: avoid fallback session collisions (#1827) (0f606b6)
- cache: partial cached-prefix replay + idle-aware net-cost; don't… (#1933) (b0440f9)
- cache: stop DynamicContentDetector false positives corrupting cached prompts (#2110) (#2119) (908a9a1)
- ccr: accept 12-char SmartCrusher hashes in tool injection (#1095) (#1141) (9f7f3ad)
- ccr: don't crash parse_tool_call on non-object tool arguments (#2071) (984a2c7)
- ccr: honor workspace dir for sqlite store (#1564) (96e1dfe)
- ccr: make expired retrieve misses terminal (#1781) (9cbdba4)
- ccr: make headroom_retrieve a hash-only full-content lookup (#1532) (c2fc4d3)
- ccr: preserve Anthropic re-stream shape (#1854) (f663894)
- ccr: preserve thinking blocks in buffered stream re-synthesis (#1897) (ede085c)
- ccr: propagate --no-ccr-marker flag to all compressors (#1022) (#1197) (0c9b42a)
- ccr: return stored content when headroom_retrieve query matches nothing (#1213) (#1236) (08fb845)
- ccr: skip Anthropic marker emission when tool injection is deferred (#1273) (2cae13d)
- ci: extend gitleaks allowlist to cover test fixtures + verified examples (#1539) (d2565a6)
- ci: guarantee model present in test shards to end cache-miss flakiness (#1399) (2e29c72)
- ci: normalize Windows CRLF line endings in PR governance script (#1012) (5194388)
- claude: surface Remote Control proxy incompatibility (#1610) (4bf7f92)
- cli/proxy: preserve explicit HEADROOM_MIN_TOKENS=0 / MAX_ITEMS=0 (#1886) (3a33af1)
- cli: add explicit UTF-8 encoding to file I/O in wrap commands (#1126) (#1164) (a0cb798)
- cli: fall back gracefully when embedding-server sidecar is absent (#1206) (38f1404)
- cli: harden all CLI surfaces + fix docs accuracy (#1491) (bd76235)
- cli: stop advertising unwired compression tuning env vars in banner (#1634) (d5bf98d)
- cli: wire --http2/--no-http2 (HEADROOM_HTTP2) into proxy command (#1373) (e06b616)
- cli: wire --rpm/--tpm and HEADROOM_RPM/HEADROOM_TPM to the Click proxy command (#1375) (8aab8f2)
- code-compressor: CJK-aware relevance-query symbol matching (#1747) (b38315c)
- code: parse-probe tree-sitter availability in code_handler (#1231) (#1300) (1de35e7)
- code: slice tree-sitter byte offsets as UTF-8 (#1332) (8238402)
- code: validate Python compressed syntax (#1302) (cbd361d)
- code: verify a real parse in tree-sitter availability check (#1231) (#1299) (5e0bb69)
- codex: avoid duplicate headroom provider config (#1431) (ddd4adf)
- codex: discover updated Codex state stores (#1889) (9d42eba)
- codex: OpenCode Zen telemetry attribution (#1648) (f18c6bd)
- codex: rerun memory lookup on every response.create WS frame (#2113) (38479fc)
- codex: retag thread providers so history menu stays whole across the proxy boundary (#1034) (74ae781)
- codex: retag threads on init so Codex Desktop history stays visible (#961) (#1349) (e6bbc40)
- codex: skip sockets in session home overlay (#2104) (c4ddcb9)
- codex: stop pinning Codex memory MCP to one project db (#1269) (ad7993b)
- compression: reject lossy unmarked tool output in unit router path (#1479) (de24cd5)
- content-detector: detect and compress space-separated JSON objects (#1742) (5194bdc)
- content-router: honor target_ratio in compression cache + add proxy --target-ratio flag (#1108) (8894ee0)
- content-router: protect_tool_results must not be weakened by profile-derived read_protection_window (#2105) (3d0e59e)
- content-router: token-measure lossless folds at the acceptance gate (#1772) (c5493ea)
- copilot-auth: stop discarding the caller's valid Copilot auth token (#1879) (f52ca19)
- copilot: normalize subscription routing host (#1836) (afd9cbd)
- copilot: route mixed-model requests per model (#1785) (5af5e22)
- cortex-code: migrate to current Cortex REST API endpoints + add e2e benchmarks (#1474) (f00ace6)
- dashboard: align token savings headline denominator (#1653) (646e705)
- dashboard: deduplicate repeated savings metrics (#1804) (88f935a)
- dashboard: derive per-project setup URL from live origin (#1511) (e035aef)
- dashboard: distinguish unavailable RTK from zero stats in Docker (#1900) (87f6e93)
- dashboard: distinguish unavailable RTK from zero stats in Docker (#1901) (361adcd)
- dashboard: include RTK stats in the historical tab (#1324) (35939c3)
- dashboard: light-mode backgrounds + aligned savings tables (#1064) (5eae32b)
- dashboard: price proxy savings without litellm (#1728) (188e382)
- dashboard: serve per-request metadata to trusted-gateway peers (#1766) (560319c)
- dedup: shorter fold pointer + MIN_LINES=3, dedup Anthropic list-… (#1932) (10e4829)
- deps: bump pillow to 12.3.0 and click to 8.4.2 (#2097) (8870b69)
- deps: make litellm optional on Python 3.14 (#956) (#993) (b2f04e4)
- deps: remediate dependency CVEs and publish SBOM (#1509) (5771a80)
- detect and clear stale ANTHROPIC_BASE_URL from crashed wrap sessions (#1768) (#1837) (84509a4)
- detection: contain unidiff panic on orphaned +++ target line (#1548) (e386c09)
- docker: persist headroom workspace in compose (#1839) (5e29c06)
- docker: persist session history across container revisions (#1118) (5912d65)
- docker: report source build version (#1862) (3807488)
- e2e: align Codex wrap e2e with global-only RTK guidance (#1240) (#1254) (bc12ace)
- emit unknown Anthropic content blocks verbatim in buffered-to-SSE conversion (#1825) (d05802b)
- evals: CJK-aware F1 tokenization + token estimation (#1527) (99a8540)
- evals: default unparseable judge scores below pass threshold (#1892) (42ebbc6)
- gemini: offload compression to the executor (#1382) (615848e)
- gemini: resolve Google model capabilities through ModelRegistry (#1276) (17ecad9)
- harden persistent install startup (#1851) (1d2b76e)
- health: exclude kompress from aggregate readiness + adversarial PBT (#2066) (f1663ea)
- init: set ENABLE_TOOL_SEARCH=true so Claude Code keeps deferring tools (#746) (#995) (500ec2b)
- install: add orjson to [proxy] extra for LiteLLM provider backends (#2074) (4f3d5ab)
- install: close parent log fd in start_detached_agent (#1576) (816cb85)
- install: default docker image to headroomlabs-ai GHCR registry (#1867) (#2039) (c3db8e4)
- install: don't let host env override the manifest in persistent-docker (#2090) (b097ef3)
- install: guard install_agent_ensure against duplicate runtime spawns (#1301) (8da0b4e)
- install: pass sc.exe create as raw command line so binPath= quoting survives (#1654) (#1702) (d6e0710)
- install: persist --no-http2 override through install apply (#1676) (6fb5f3b)
- install: repair macOS launchd restart/start lifecycle (#1290) (da1a397)
- install: stop duplicating ENTRYPOINT in persistent-docker runtime command (#833) (#1348) (feedead)
- install: use Windows-safe PID liveness probe in runtime_status (#1544) (#1560) (6b227b9)
- install: write deployment manifest atomically and tolerate corrupt manifests (#1303) (42bdf23)
- io: use UTF-8 with locale fallback and preserve line endings on config/text I/O (#1498) (1baa04e)
- kompress: hard override keeps must-keep tokens regardless of model score (#1400) (42612c8)
- kompress: never block the request path on the cold-cache model download (#1161) (3fc2a78)
- kompress: surface model-not-ready state via logs and health endpoint (#2034) (12aa2cb)
- langchain: disable streaming on wrapped model during ainvoke() (#1287) (3590046)
- learn: aggregate verbosity baselines across projects instead of overwriting (#1288) (27a5468)
- learn: don't shadow TIMEOUT/CONNECTION with the generic RUNTIME_ERROR (#2099) (c7b5a24)
- learn: handle Windows UTF-8, drive-letter paths, and CLI shim fallback (#1895) (e3b45e4)
- learn: parse fenced JSON even with a prose preamble (#1988) (d2170b1)
- litellm: surface Bedrock cache token usage in non-streaming responses (#1848) (d604e86)
- mcp/codex: don't clobber an unparseable/non-table config.toml (#2062) (415e03c)
- mcp/opencode: don't clobber an unparseable opencode.json on register (#1661) (d079614)
- mcp: correct default Claude Code config path in ClaudeRegistrar (#1859) (c85731d)
- mcp: isolate ClaudeRegistrar CLI config env (#1888) (1c947b1)
- mcp: register managed installs with a resolvable headroom command (#1386) (22def93)
- mcp: report correct savings_percent in headroom_compress (#1106) (f216e43)
- mcp: show lifetime totals and label rolling session scope in headroom_stats (#1428) (1c0e152)
- mcp: surface dead proxy state (#1786) (931eed8)
- memory/sqlite: don't emit OFFSET without LIMIT in query (#2063) (a5bdc54)
- memory/sync: don't clobber memories sharing a first line (#1976) (5e14b8c)
- memory/sync: make Codex AGENTS.md adapter additive (stop wiping memories) (#1674) (7fd0c42)
- memory: annotate _EMBEDDER_CACHE key as 3-tuple (unbreak main lint) (#2153) (22af75a)
- memory: cap local embedder CPU thread oversubscription (#198) (#1559) (b84afbf)
- memory: honor explicit store=false on Responses requests (#2017) (31abb69)
- memory: key the embedder cache on ollama_base_url (#2109) (1725cd1)
- memory: resolve Trae cwd metadata from user reminders (#1737) (#1887) (3e85eb1)
- memory: singleflight LocalBackend init to stop cold-start races (#1691) (bec47a1)
- memory: track MCP retrieval access (#2065) (d0ecc9a)
- memory: use ONNX embedder for
wrap --memorysync (#1092) (#1262) (4f9feda) - models: version-boundary longest-prefix match in ModelRegistry.get (#1658) (b699bed)
- openclaw: detect uv-installed headroom binary in ~/.local/bin (#1459) (adaeb88)
- openclaw: wrap plugin export as {register} object for OpenClaw 2026.x compatibility (#1218) (2e6c442)
- opencode: preserve custom OpenAI gateway paths (#1596) (c19347c)
- opencode: route native providers + load transport plugin, fix Serena context (#1573) (ad0034f)
- opencode: use local MCP config (#1383) (4bd3ddf)
- opencode: write local MCP config (#1381) (6c83790)
- packaging: guard torch extras on intel macos (#2011) (fd0d29c)
- packaging: move hnswlib to optional [vector] extra so [all] needs no C++ toolchain (#1499) (80fa086)
- patch nltk vulnerability (CVE-2026-54293) (#1929) (28ca61f)
- patch rtk hook script to use absolute path after register_claude_hooks (#571) (b618d2d)
- perf: surface RTK/CLI context-tool savings in perf and the session card (#1433) (9362747)
- preserve anthropic passthrough tool order (#1427) (a932247)
- pricing: alias retired claude-3-sonnet to Sonnet-tier price, not Haiku (#2095) (6137967)
- providers: update DeepSeek V3 context limit from 128K to 1M (#1038) (#1137) (bcabc5c)
- proxy/anthropic: preserve non-2xx upstream status through security scan (#2100) (aa78816)
- proxy/anthropic: scope session id by top-level system prompt (#2070) (ec6e60e)
- proxy/auth: match real Anthropic OAuth token prefix (sk-ant-oat) (#1672) (8cddf9b)
- proxy/cost: price cache savings by most-used model, not first-seen (#2023) (b4f807f)
- proxy/gemini: preserve non-text content across the compression round-trip (#2079) (4056117)
- proxy/gemini: thread savings-profile kwargs into apply() (#1994) (38306a3)
- proxy/memory: capture user text blocks for the retrieval query (#2064) (f542b70)
- proxy/openai: respect explicit stream_options.include_usage (#2026) (19201e8)
- proxy/openai: thread savings-profile kwargs into chat completions (#1606) (7ff842d)
- proxy/openai: translate max_tokens -> max_completion_tokens on chat path (#1774) (285808b)
- proxy/savings: don't bill fallback rate for free (0-priced) models (#2024) (6ecbdd6)
- proxy/vertex: route google-publisher requests to the request region (#2069) (1843346)
- proxy: add --protect-tool-results to prevent lossy compression of exact-output Bash results (#1374) (51d4bcf)
- proxy: add an Anthropic buffered read-timeout override (#1331) (3be2526)
- proxy: add versionless Vertex AI routes for Claude Code compatibility (#1321) (bb3e040)
- proxy: aggregate tool-output size floor so Codex sessions compress (#2050) (#2116) (dbe2558)
- proxy: allow disabling periodic TOIN stats logging (#1265) (b5f63d8)
- proxy: bind before eager preload so a hung compressor load can't block startup (#1500) (d5ac07f)
- proxy: bound Codex WS compression fallback latency (#1802) (d24a3f8)
- proxy: bound HF tokenizer load and offload token counting off event loop (#1738) (46d5d68)
- proxy: build SSL contexts for custom CA bundles (#1134) (561ba17)
- proxy: cache_savings_usd silently zeroes when litellm is unavailable (#2005) (75d7861)
- proxy: cancel retry backoff on shutdown (#1834) (da2d8dc)
- proxy: compress Anthropic user text blocks when enabled (#1875) (e36439a)
- proxy: compress Hermes scoped coding-agent passthrough (#1815) (09d1ef4)
- proxy: count exhausted upstream 5xx as failed across all providers (#1571) (e365ad7)
- proxy: expose persistent savings metrics (#1647) (5fe4e7b)
- proxy: fail open when kompress saturation would exhaust pre-upstream budget (#1430) (15ac650)
- proxy: forward request-id headers on the streaming path (#1100) (#1258) (3d59df7)
- proxy: freeze must forward cached (compressed) prefix byte-identical — stop token-mode cache busting (#1850) (248ae0f)
- proxy: fsync savings dir after atomic rename (#1764) (7de2c1e)
- proxy: gate CCR retrieve/compress endpoints to loopback (#1338) (acafb2d)
- proxy: handle ClientDisconnect in passthrough body reads (#2033) (9db8a6b)
- proxy: handle ClientDisconnect in passthrough body reads + log sanitization (#2067) (605e269)
- proxy: handle content-part outputs in Codex Responses compression (#2052) (c9a7755)
- proxy: handle streaming CCR retrieval (#1451) (d337e3b)
- proxy: hoist ccr_workspace_key default so /v1/messages survives CCR-inject off (#1096) (1deb947)
- proxy: honor force_kompress routing profile (#996) (b4682d6)
- proxy: honor HEADROOM_EXCLUDE_TOOLS for Codex /v1/responses tool outputs (#940) (#1053) (f03e77b)
- proxy: honor x-headroom-base-url on /v1/messages route (#1763) (bb2acf7)
- proxy: include system/tools/sampling in cache key (#1473) (312129a)
- proxy: keep cache_control bounded + stable so the freeze overlay stops busting (#1852) (4820134)
- proxy: keep Kompress warmup off the startup path (#2001) (10ed14e)
- proxy: keep large compression results on the critical path (#296) (#1352) (90734b6)
- proxy: keep OpenAI tool observations mutable in cache mode (#1884) (55efb1c)
- proxy: keep PRE_SEND from reintroducing empty tool arrays (#2015) (d1db00a)
- proxy: keep recent stats request rows (#1922) (bd8de9f)
- proxy: offload /v1/compress to the compression executor to stop blocking the loop (#1501) (27e010e)
- proxy: only queue mid-turn messages for opt-in clients with explicit session header (#1951) (c365c7f)
- proxy: persist lifetime cache-read savings across restarts (#1665) (908997e)
- proxy: preserve byte-faithful Anthropic tool forwarding (#1222) (1f18d59)
- proxy: preserve chatgpt responses streaming (#2012) (a617455)
- proxy: preserve Responses memory continuations with store=false (#1103) (cdfeeac)
- proxy: preserve Responses passthrough bytes (#1598) (2a34a82)
- proxy: preserve streaming passthrough beta headers (#1783) (0f553a8)
- proxy: preserve terminal tool on Codex Responses (#2000) (41af39d)
- proxy: preserve upstream 5xx status on retry exhaustion (#1570) (7836aea)
- proxy: queue mid-turn user messages on non-Bedrock streaming path (#1377) (b09f027)
- proxy: record cache metrics for non-streaming backend paths (#1271) (8580404)
- proxy: register interceptor in explicit transforms list when HEADROOM_INTERCEPT_ENABLED (#1376) (55c700c)
- proxy: release _active_streams session lock on setup-phase errors (#1864) (2ccd831)
- proxy: report real input tokens on streaming message_start (#1132) (#1305) (70cc96a)
- proxy: retry HTTP/2 stream resets instead of 502ing (#1645) (2ce19c2)
- proxy: retry passthrough on transient upstream connection close (#1513) (5d14080)
- proxy: retry upstream 429 with Retry-After on both forwarders (#1329) (90bee89)
- proxy: retry upstream 529 overloaded like 429 on both forwarders (#1495) (547b15d)
- proxy: route Codex OAuth image requests (#1215) (381d771)
- proxy: route Foundry Anthropic messages (#1878) (739f654)
- proxy: scope CORS to loopback + gate operator/content endpoints (#1226) (bd55a42)
- proxy: serve /favicon.ico locally instead of tunneling upstream (#1787) (#1847) (3076e32)
- proxy: stamp X-Client: codex on Responses endpoint for unidentified callers (#1036) (b0cd032)
- proxy: stop re-compressing headroom_retrieve output and emitting unredeemable markers (#1323) (43494ff)
- proxy: stop rtk stat failures from corrupting session baseline (#1693) (681b9a8)
- proxy: strip 1m model suffix before upstream forwarding (#1840) (e22d745)
- proxy: strip Codex lite header from OpenAI WebSockets (#1543) (5d3803a)
- proxy: strip Codex lite header on the HTTP /responses path (#1663) (9fbd47b)
- proxy: strip duplicated upstream server headers (#1828) (d2a86b5)
- proxy: strip inbound Content-Encoding on messages/chat forward (#1970) (4cb33cd)
- proxy: subtract cache write premiums from net savings (#1800) (53a465b)
- proxy: treat NODE_EXTRA_CA_CERTS as additive, not replacement (#998) (#1031) (c987283)
- proxy: wire --compression-max-workers / HEADROOM_COMPRESSION_MAX_WORKERS (#1632) (814ffa3)
- read-lifecycle: persist STALE Read originals in the CCR store (#1488) (9157173)
- recover persistent proxy feature checks and reject non-Copilot exchange URL (#1465) (16c638b)
- release: sync all package versions to v0.31.0 (#1882) (662b7bc)
- relevance: gate ONNX embedding backend behind AVX2 to avoid SIGILL (#1723) (#1765) (728b330)
- remove agents.md (#1540) (a7d3360)
- respect COPILOT_PROVIDER_TYPE env var when provider_type is auto (#549) (24cf256)
- restore token-mode compression on frozen prefixes (#1489) (8e0dadf)
- route v1internal code assist requests to cloudcode-pa.googleapis… (#821) (e20f16b)
- router: degrade to pure-Python detection on native panic (#1123) (#1260) (a00fb67)
- router: honor MCP aliases in excluded tools (#1822) (#1863) (140d6e4)
- rtk: link managed rtk onto PATH instead of mutating the hook (#1698) (140cb05)
- rtk: stop hook registration timing out on a forked daemon (#1314) (9758817)
- savings: cap ledger retention at 30 days (#1985) (b3a559b)
- savings: count cache-read tokens in input cost estimate (#1429) (72ade37)
- scripts: rename .releaseetadata to .releasemetadata (#1246) (772adc9)
- search-compressor: CJK-aware relevance + harden Rust/Python parity (#1749) (985621d)
- skip Magika backend on x86 CPUs without AVX2 (#1162) (64783d8)
- smart-crusher: honor enable_ccr_marker on the opaque-blob path (#1130) (27d6f8e)
- streaming: preserve server_tool_use sse blocks (#1826) (4ac5493)
- strip output-only fallback blocks from request messages (#1870) (1448718)
- subscription/copilot: preserve remaining=0 for exhausted quota (#1997) (cbb7750)
- subscription: only reset 5h contribution on real rollover, not API jitter (#1255) (8d6c175)
- subscription: run transcript token scan off the event loop (#1263) (f03021f)
- surface output reduction without a restart, and explain $0.00 savings on Python 3.14 (#1296) (c30ec4c)
- telemetry: switch anonymous telemetry to opt-in (off by default) (#1223) (b998697)
- tests: reset whole headroom logger subtree so caplog stays deterministic (#1117) (fda4670)
- tls: add HEADROOM_TLS_STRICT=0 toggle for corporate SSL inspection (#1308) (#1341) (52068dd)
- toin: publish skip compression recommendations (#1782) (be51008)
- tokenizers: bound tiktoken vocab load so a stalled download cannot hang requests (#956) (#994) (7e86baf)
- tokenizers: don't tokenize image blocks as text in TiktokenCounter (#2093) (ae10d6c)
- tokenizers: price CJK in the fixed-ratio estimator path (#2080) (cd3d5aa)
- tokenizers: price CJK/Kana/Hangul at ~1 token per char in EstimatingTokenCounter (#1093) (a35fe86)
- tokenizers: recurse into list-content tool_result blocks (#2081) (dfb1d37)
- tokenizers: resolve HF tokenizer names by most-specific prefix (#2096) (e0232df)
- tokenizers: use o200k_base for gpt-4.1/gpt-4.5/o4 families (#2108) (6979b52)
- transforms/code: coerce language aliases instead of raising (#1975) (27ddde1)
- transforms/content-router: route grep/log output away from HTML extractor (#1719) (0d18ef2)
- transforms: bound native content detection with a Windows watchdog (#575) (#1563) (95abca3)
- transforms: gate tool string output from lossy compression (#1307) (#1387) (c6c921a)
- transforms: normalize diff compressor context (#1801) (838c523)
- transforms: pass through ragged tables instead of misaligning columns (#1713) (c7665ca)
- unwrap: remove ANTHROPIC_BASE_URL + ENABLE_TOOL_SEARCH and init hooks on unwrap (#992) (5b84691)
- update: prevent _core.pyd corruption on Windows when proxy is running (#1581) (0750bbf)
- use rtk native Cursor hook instead of injecting .cursorrules (#756) (#1846) (1573f1f)
- version: mark source-checkout builds as -dev (#2072) (1cc9979)
- Vertex AI support for Claude Code with ANTHROPIC_VERTEX_BASE_URL (#1393) (cff7247)
- websocket: harden responses websocket origin handling (#1481) (c632023)
- windows: pin UTF-8 encoding on text-mode subprocess calls (#1311) (d633e81)
- wrap/opencode: unwrap removes the rtk block from AGENTS.md (#2025) (20968a4)
- wrap: add Copilot unwrap command (#1251) (b4fde0c)
- wrap: detach the shared proxy on Windows so it survives an ungraceful agent close (#1464) (6cba441)
- wrap: isolate proxy stdio from proxy.log on Windows (#1191) (959ab0d)
- wrap: keep agent savings opt-in (#1294) (b829ceb)
- wrap: keep Claude context-tool setup explicit (#1999) (f536aa0)
- wrap: keep Codex RTK guidance global (#1240) (7c26a54)
- wrap: percent-encode non-ASCII cwd names in X-Headroom-Project header (#1071) (9f712cc)
- wrap: preserve custom Codex provider base_url during proxy injection (#1894) (372d6c8)
- wrap: preserve custom Vertex base URL (#1477) (75427bb)
- wrap: remove rtk instructions from Codex AGENTS.md on unwrap (#1604) (c9d717c)
- wrap: replace stale-proxy detection with Vite-style port fallback (#1406) (b4205c6)
- wrap: show the dashboard URL when the proxy is already running (#1313) (b0146c4)
- wrap: surface Claude Remote Control base-URL gate accurately (#1… (#1883) (daeff69)
- wrap: use canonical headroom-openclaw npm package for wrap openclaw (#1969) (#2120) (c5545d6)
- wrap: write env.ANTHROPIC_BASE_URL to settings.json so daemon-spawned conversations inherit proxy (#951) (#1078) (a554c3a)
Performance Improvements
- compression: take large cold-start contexts off the synchronous kompress path (#1171) (#1298) (6c68ff4)
- proxy: cap compression workers to CPU count (#1803) (0a3851b)
- savings: batch tracker persistence off the request hot path (#1817) (451b9f0)
Dependencies
- bump @types/node from 22.19.15 to 26.1.1 in /plugins/openclaw (#1685) (350daeb)
- bump @types/node from 22.20.0 to 26.1.1 in /plugins/opencode (#1688) (8715195)
- bump @types/node from 25.5.2 to 26.1.1 in /docs (#1683) (75fff43)
- bump fumadocs-typescript from 4.0.14 to 5.3.0 in /docs (#1684) (e8b66a2)
- bump prometheus from 0.13.4 to 0.14.0 (#1518) (5229c98)
- bump the cargo-minor-patch group across 1 directory with 7 updates (#1909) (45601d9)
- bump the npm-minor-patch group across 4 directories with 18 updates (#1907) (8872bbc)
- bump thiserror from 1.0.69 to 2.0.18 (#1519) (e448d7b)
- bump toml from 0.8.23 to 1.1.2+spec-1.1.0 (#1517) (6c705b4)
- update tree-sitter requirement from <0.26,>=0.25.2 to >=0.25.2,<0.27 (#1681) (ce3c959)
Code Refactoring
- cache: isolate compression strategy outcomes (#1938) (b5aa8a3)
- cache: isolate semantic key policy (#1953) (740fb9b)
- ccr: isolate tool call classification (#1937) (fd5b9e7)
- memory: isolate injection decision policy (#1952) (c20f3b1)
- memory: isolate query construction policy (#1950) (235c986)
- output: isolate savings policy (#1947) (c29b4ba)
- output: isolate verbosity steering (#1940) (0ce09fb)
- pricing: isolate litellm model resolution (#1936) (4210d6e)
- providers: split proxy route adapters (#1934) (e6243f6)
- proxy: extract beta header merge policy (#1993) (f359f21)
- proxy: extract beta header policy (#1992) (603f5bc)
- proxy: extract ccr golden replay policy (#2006) (7c9a032)
- proxy: extract ccr marker policy (#2004) (ec3c3cd)
- proxy: extract ccr session tracker (#2003) (e92c253)
- proxy: extract internal header policy (#1990) (868b88b)
- proxy: extract memory golden replay policy (#2007) (8c68f48)
- proxy: extract tool injection config (#2010) (0f846e5)
- proxy: extract tool injection logging (#2009) (9c7b9d5)
- proxy: extract tool injection policy (#1995) (d6259b2)
- proxy: extract tool injection tracker (#2002) (d1c484b)
- proxy: extract tool name policy (#2008) (1000175)
- proxy: isolate auth classification policy (#1945) (5a7265d)
- proxy: isolate body forwarding policy (#1935) (1f3696a)
- proxy: isolate forwarded header policy (#1942) (cb38f79)
- proxy: isolate image compression policy (#1958) (2b09ece)
- proxy: isolate memory rank policy (#1960) (b1e871d)
- proxy: isolate output effort policy (#1961) (094a53c)
- proxy: isolate output turn policy (#1962) (c904a70)
- proxy: isolate output verbosity policy (#1963) (0415dc8)
- proxy: isolate project attribution policy (#1957) (1c1e360)
- proxy: isolate proxy mode policy (#1965) (82af5cd)
- proxy: isolate rate limit policy (#1954) (ea19515)
- proxy: isolate semantic cache key policy (#1964) (2f53a18)
- transforms: isolate mixed content parsing (#1939) (9bacf48)
0.31.0 (2026-07-09)
Features
- cache: provider-agnostic cache-mode delta + cc-agnostic prefix comparison (#1868) (7c2f0ea)
- ccr: wire retrieve-tool interception into OpenAI Responses handler (#1898) (62cd307)
- compression: add audit-safe mode with protected pattern matching (#1899) (bb112dd)
- content-router: accept any real compression (remove min-savings floor) (#1771) (6c31db9)
- content-router: lossless-first dispatch, cross-turn dedup, and A7 lossy-after-fold (#1818) (60af15f)
- proxy: add provider-only HTTP proxy (#1807) (ebe0a3b)
- proxy: add turn-hook extension point for buffered model turns (#1891) (ec950f7)
Bug Fixes
- build: enable Intel macOS pip installs via ort-load-dynamic (#1538) (32ce99e)
- cache: avoid fallback session collisions (#1827) (0f606b6)
- ccr: make expired retrieve misses terminal (#1781) (9cbdba4)
- ccr: preserve Anthropic re-stream shape (#1854) (f663894)
- ccr: preserve thinking blocks in buffered stream re-synthesis (#1897) (ede085c)
- cli/proxy: preserve explicit HEADROOM_MIN_TOKENS=0 / MAX_ITEMS=0 (#1886) (3a33af1)
- code-compressor: CJK-aware relevance-query symbol matching (#1747) (b38315c)
- codex: discover updated Codex state stores (#1889) (9d42eba)
- codex: OpenCode Zen telemetry attribution (#1648) (f18c6bd)
- content-detector: detect and compress space-separated JSON objects (#1742) (5194bdc)
- content-router: token-measure lossless folds at the acceptance gate (#1772) (c5493ea)
- copilot: normalize subscription routing host (#1836) (afd9cbd)
- copilot: route mixed-model requests per model (#1785) (5af5e22)
- dashboard: deduplicate repeated savings metrics (#1804) (88f935a)
- dashboard: distinguish unavailable RTK from zero stats in Docker (#1900) (87f6e93)
- dashboard: distinguish unavailable RTK from zero stats in Docker (#1901) (361adcd)
- dashboard: price proxy savings without litellm (#1728) (188e382)
- detect and clear stale ANTHROPIC_BASE_URL from crashed wrap sessions (#1768) (#1837) (84509a4)
- docker: persist headroom workspace in compose (#1839) (5e29c06)
- docker: report source build version (#1862) (3807488)
- evals: default unparseable judge scores below pass threshold (#1892) (42ebbc6)
- install: pass sc.exe create as raw command line so binPath= quoting survives (#1654) (#1702) (d6e0710)
- install: persist --no-http2 override through install apply (#1676) (6fb5f3b)
- mcp: isolate ClaudeRegistrar CLI config env (#1888) (1c947b1)
- mcp: surface dead proxy state (#1786) (931eed8)
- memory: resolve Trae cwd metadata from user reminders (#1737) (#1887) (3e85eb1)
- opencode: use local MCP config (#1383) (4bd3ddf)
- proxy/openai: thread savings-profile kwargs into chat completions (#1606) (7ff842d)
- proxy/openai: translate max_tokens -> max_completion_tokens on chat path (#1774) (285808b)
- proxy: bound Codex WS compression fallback latency (#1802) (d24a3f8)
- proxy: bound HF tokenizer load and offload token counting off event loop (#1738) (46d5d68)
- proxy: cancel retry backoff on shutdown (#1834) (da2d8dc)
- proxy: compress Anthropic user text blocks when enabled (#1875) (e36439a)
- proxy: freeze must forward cached (compressed) prefix byte-identical — stop token-mode cache busting (#1850) (248ae0f)
- proxy: fsync savings dir after atomic rename (#1764) (7de2c1e)
- proxy: keep cache_control bounded + stable so the freeze overlay stops busting (#1852) (4820134)
- proxy: persist lifetime cache-read savings across restarts (#1665) (908997e)
- proxy: preserve streaming passthrough beta headers (#1783) (0f553a8)
- proxy: release _active_streams session lock on setup-phase errors (#1864) (2ccd831)
- proxy: retry HTTP/2 stream resets instead of 502ing (#1645) (2ce19c2)
- proxy: retry passthrough on transient upstream connection close (#1513) (5d14080)
- proxy: route Foundry Anthropic messages (#1878) (739f654)
- proxy: serve /favicon.ico locally instead of tunneling upstream (#1787) (#1847) (3076e32)
- proxy: stop rtk stat failures from corrupting session baseline (#1693) (681b9a8)
- proxy: strip 1m model suffix before upstream forwarding (#1840) (e22d745)
- proxy: subtract cache write premiums from net savings (#1800) (53a465b)
- router: honor MCP aliases in excluded tools (#1822) (#1863) (140d6e4)
- rtk: link managed rtk onto PATH instead of mutating the hook (#1698) (140cb05)
- streaming: preserve server_tool_use sse blocks (#1826) (4ac5493)
- toin: publish skip compression recommendations (#1782) (be51008)
- transforms: normalize diff compressor context (#1801) (838c523)
- transforms: pass through ragged tables instead of misaligning columns (#1713) (c7665ca)
- use rtk native Cursor hook instead of injecting .cursorrules (#756) (#1846) (1573f1f)
- wrap: replace stale-proxy detection with Vite-style port fallback (#1406) (b4205c6)
Performance Improvements
- proxy: cap compression workers to CPU count (#1803) (0a3851b)
- savings: batch tracker persistence off the request hot path (#1817) (451b9f0)
Dependencies
- bump the cargo-minor-patch group across 1 directory with 7 updates (#1909) (45601d9)
- bump the npm-minor-patch group across 4 directories with 18 updates (#1907) (8872bbc)
0.29.0 (2026-07-03)
Features
- proxy: add --lossless no-CCR mode with format-native compaction (#1721) (c75ebde)
- stats: surface Codex WS compression counters in /stats summary (#1680) (2fe19c3)
- transforms: adaptive Otsu KEEP/DROP threshold (+ land relevance split on main) (#1726) (eea667a)
Bug Fixes
- bedrock: fail fast when session-token auth lacks botocore (#1553) (54cfa36)
- bedrock: route ARNs via converse, named AWS profiles, and au. re… (#1456) (7d87aa2)
- ccr: honor workspace dir for sqlite store (#1564) (96e1dfe)
- claude: surface Remote Control proxy incompatibility (#1610) (4bf7f92)
- cli: stop advertising unwired compression tuning env vars in banner (#1634) (d5bf98d)
- codex: avoid duplicate headroom provider config (#1431) (ddd4adf)
- compression: reject lossy unmarked tool output in unit router path (#1479) (de24cd5)
- cortex-code: migrate to current Cortex REST API endpoints + add e2e benchmarks (#1474) (f00ace6)
- dashboard: align token savings headline denominator (#1653) (646e705)
- dashboard: derive per-project setup URL from live origin (#1511) (e035aef)
- detection: contain unidiff panic on orphaned +++ target line (#1548) (e386c09)
- evals: CJK-aware F1 tokenization + token estimation (#1527) (99a8540)
- install: close parent log fd in start_detached_agent (#1576) (816cb85)
- install: use Windows-safe PID liveness probe in runtime_status (#1544) (#1560) (6b227b9)
- learn: aggregate verbosity baselines across projects instead of overwriting (#1288) (27a5468)
- mcp: show lifetime totals and label rolling session scope in headroom_stats (#1428) (1c0e152)
- memory: cap local embedder CPU thread oversubscription (#198) (#1559) (b84afbf)
- memory: singleflight LocalBackend init to stop cold-start races (#1691) (bec47a1)
- openclaw: detect uv-installed headroom binary in ~/.local/bin (#1459) (adaeb88)
- opencode: preserve custom OpenAI gateway paths (#1596) (c19347c)
- opencode: route native providers + load transport plugin, fix Serena context (#1573) (ad0034f)
- preserve anthropic passthrough tool order (#1427) (a932247)
- proxy/auth: match real Anthropic OAuth token prefix (sk-ant-oat) (#1672) (8cddf9b)
- proxy: expose persistent savings metrics (#1647) (5fe4e7b)
- proxy: fail open when kompress saturation would exhaust pre-upstream budget (#1430) (15ac650)
- proxy: handle streaming CCR retrieval (#1451) (d337e3b)
- proxy: include system/tools/sampling in cache key (#1473) (312129a)
- proxy: preserve Responses passthrough bytes (#1598) (2a34a82)
- proxy: strip Codex lite header on the HTTP /responses path (#1663) (9fbd47b)
- proxy: wire --compression-max-workers / HEADROOM_COMPRESSION_MAX_WORKERS (#1632) (814ffa3)
- savings: count cache-read tokens in input cost estimate (#1429) (72ade37)
- skip Magika backend on x86 CPUs without AVX2 (#1162) (64783d8)
- transforms/content-router: route grep/log output away from HTML extractor (#1719) (0d18ef2)
- transforms: bound native content detection with a Windows watchdog (#575) (#1563) (95abca3)
- Vertex AI support for Claude Code with ANTHROPIC_VERTEX_BASE_URL (#1393) (cff7247)
- wrap: detach the shared proxy on Windows so it survives an ungraceful agent close (#1464) (6cba441)
- wrap: preserve custom Vertex base URL (#1477) (75427bb)
- wrap: remove rtk instructions from Codex AGENTS.md on unwrap (#1604) (c9d717c)
0.28.0 (2026-06-29)
Features
- add --disable-kompress-fallback to restore legacy PASSTHROUGH fallback (#1185) (f309244)
- add first-class OpenCode support (wrap, learn, mcp install) (#559) (91cd210)
- add HEADROOM_KEEPALIVE_EXPIRY to keep upstream connections warm (#1124) (85786b3)
- azure-foundry: derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE (#1138) (e5031b0)
- cache: attribute prompt-cache misses to TTL lapse vs prefix change (#1313) (#1343) (4658721)
- code: add Perl support to code-aware compressor (#1125) (f39858c)
- headroom wrap opencode / unwrap opencode CLI (#1105) (b4571cc)
- learn: weight loops in Headroom Learn + RTK-loop eval (#1160) (14e8dc4)
- learn: write per-project learnings to CLAUDE.local.md by default (#1115) (ced75e4)
- proxy: add request timeout config (#738) (c0745d4)
- proxy: pilot hardening — inbound auth, security headers, audit log, air-gap switch (#1537) (546ab55)
- proxy: support glob patterns in exclude_tools (#870) (#1259) (a2159c0)
- read-maturation: activity-based hold-back Read maturation (Mechanism B) (#1068) (723b80c)
- savings: durable savings ledger + headroom savings command (#1127) (978ffa0)
- wrap: add --1m to preserve the 1M context window on wrap claude (#1158) (#1351) (b50d9c1)
- wrap: make tokensave the primary coding-task compressor, Serena the backup (#1230) (dca9853)
Bug Fixes
- agent-evals: Phase 0 — coding-agent accuracy A/B framework (#1037) (84f9871)
- agno: tolerate streaming tool-call SDK objects in parser (#1312) (#1336) (5986c22)
- bedrock: add boto3 1.41 + CRT for aws login credentials (#1486) (4db3bc9)
- bump codebase-memory-mcp to v0.8.1 (#1284) (530318b)
- ccr: make headroom_retrieve a hash-only full-content lookup (#1532) (c2fc4d3)
- ccr: propagate --no-ccr-marker flag to all compressors (#1022) (#1197) (0c9b42a)
- ccr: skip Anthropic marker emission when tool injection is deferred (#1273) (2cae13d)
- ci: extend gitleaks allowlist to cover test fixtures + verified examples (#1539) (d2565a6)
- ci: guarantee model present in test shards to end cache-miss flakiness (#1399) (2e29c72)
- ci: normalize Windows CRLF line endings in PR governance script (#1012) (5194388)
- cli: add explicit UTF-8 encoding to file I/O in wrap commands (#1126) (#1164) (a0cb798)
- cli: fall back gracefully when embedding-server sidecar is absent (#1206) (38f1404)
- cli: harden all CLI surfaces + fix docs accuracy (#1491) (bd76235)
- cli: wire --http2/--no-http2 (HEADROOM_HTTP2) into proxy command (#1373) (e06b616)
- cli: wire --rpm/--tpm and HEADROOM_RPM/HEADROOM_TPM to the Click proxy command (#1375) (8aab8f2)
- code: slice tree-sitter byte offsets as UTF-8 (#1332) (8238402)
- code: validate Python compressed syntax (#1302) (cbd361d)
- code: verify a real parse in tree-sitter availability check (#1231) (#1299) (5e0bb69)
- codex: retag threads on init so Codex Desktop history stays visible (#961) (#1349) (e6bbc40)
- codex: stop pinning Codex memory MCP to one project db (#1269) (ad7993b)
- dashboard: include RTK stats in the historical tab (#1324) (35939c3)
- deps: remediate dependency CVEs and publish SBOM (#1509) (5771a80)
- docker: persist session history across container revisions (#1118) (5912d65)
- gemini: offload compression to the executor (#1382) (615848e)
- gemini: resolve Google model capabilities through ModelRegistry (#1276) (17ecad9)
- install: guard install_agent_ensure against duplicate runtime spawns (#1301) (8da0b4e)
- install: repair macOS launchd restart/start lifecycle (#1290) (da1a397)
- install: stop duplicating ENTRYPOINT in persistent-docker runtime command (#833) (#1348) (feedead)
- io: use UTF-8 with locale fallback and preserve line endings on config/text I/O (#1498) (1baa04e)
- kompress: hard override keeps must-keep tokens regardless of model score (#1400) (42612c8)
- langchain: disable streaming on wrapped model during ainvoke() (#1287) (3590046)
- mcp: register managed installs with a resolvable headroom command (#1386) (22def93)
- mcp: report correct savings_percent in headroom_compress (#1106) (f216e43)
- opencode: write local MCP config (#1381) (6c83790)
- packaging: move hnswlib to optional [vector] extra so [all] needs no C++ toolchain (#1499) (80fa086)
- patch rtk hook script to use absolute path after register_claude_hooks (#571) (b618d2d)
- perf: surface RTK/CLI context-tool savings in perf and the session card (#1433) (9362747)
- proxy: add --protect-tool-results to prevent lossy compression of exact-output Bash results (#1374) (51d4bcf)
- proxy: add an Anthropic buffered read-timeout override (#1331) (3be2526)
- proxy: add versionless Vertex AI routes for Claude Code compatibility (#1321) (bb3e040)
- proxy: bind before eager preload so a hung compressor load can't block startup (#1500) (d5ac07f)
- proxy: build SSL contexts for custom CA bundles (#1134) (561ba17)
- proxy: forward request-id headers on the streaming path (#1100) (#1258) (3d59df7)
- proxy: gate CCR retrieve/compress endpoints to loopback (#1338) (acafb2d)
- proxy: honor force_kompress routing profile (#996) (b4682d6)
- proxy: keep large compression results on the critical path (#296) (#1352) (90734b6)
- proxy: offload /v1/compress to the compression executor to stop blocking the loop (#1501) (27e010e)
- proxy: preserve Responses memory continuations with store=false (#1103) (cdfeeac)
- proxy: queue mid-turn user messages on non-Bedrock streaming path (#1377) (b09f027)
- proxy: register interceptor in explicit transforms list when HEADROOM_INTERCEPT_ENABLED (#1376) (55c700c)
- proxy: report real input tokens on streaming message_start (#1132) (#1305) (70cc96a)
- proxy: retry upstream 429 with Retry-After on both forwarders (#1329) (90bee89)
- proxy: retry upstream 529 overloaded like 429 on both forwarders (#1495) (547b15d)
- proxy: stop re-compressing headroom_retrieve output and emitting unredeemable markers (#1323) (43494ff)
- proxy: strip Codex lite header from OpenAI WebSockets (#1543) (5d3803a)
- read-lifecycle: persist STALE Read originals in the CCR store (#1488) (9157173)
- recover persistent proxy feature checks and reject non-Copilot exchange URL (#1465) (16c638b)
- remove agents.md (#1540) (a7d3360)
- respect COPILOT_PROVIDER_TYPE env var when provider_type is auto (#549) (24cf256)
- restore token-mode compression on frozen prefixes (#1489) (8e0dadf)
- router: degrade to pure-Python detection on native panic (#1123) (#1260) (a00fb67)
- rtk: stop hook registration timing out on a forked daemon (#1314) (9758817)
- smart-crusher: honor enable_ccr_marker on the opaque-blob path (#1130) (27d6f8e)
- subscription: only reset 5h contribution on real rollover, not API jitter (#1255) (8d6c175)
- subscription: run transcript token scan off the event loop (#1263) (f03021f)
- surface output reduction without a restart, and explain $0.00 savings on Python 3.14 (#1296) (c30ec4c)
- tests: reset whole headroom logger subtree so caplog stays deterministic (#1117) (fda4670)
- tls: add HEADROOM_TLS_STRICT=0 toggle for corporate SSL inspection (#1308) (#1341) (52068dd)
- tokenizers: price CJK/Kana/Hangul at ~1 token per char in EstimatingTokenCounter (#1093) (a35fe86)
- transforms: gate tool string output from lossy compression (#1307) (#1387) (c6c921a)
- websocket: harden responses websocket origin handling (#1481) (c632023)
- windows: pin UTF-8 encoding on text-mode subprocess calls (#1311) (d633e81)
- wrap: add Copilot unwrap command (#1251) (b4fde0c)
- wrap: isolate proxy stdio from proxy.log on Windows (#1191) (959ab0d)
- wrap: keep agent savings opt-in (#1294) (b829ceb)
- wrap: show the dashboard URL when the proxy is already running (#1313) (b0146c4)
Performance Improvements
- compression: take large cold-start contexts off the synchronous kompress path (#1171) (#1298) (6c68ff4)
0.27.0 (2026-06-22)
Features
- cli: add headroom doctor setup diagnostics (#926) (e45cf4e)
- cli: add headroom update command and release banner (#1088) (26be2c3)
- compression extraction — Rust knob exposure, CCR hardening, traffic audits (#818) (b7be381)
- measure and surface token throughput (tokens/sec) through the proxy (#983) (0d89c67)
- output-token reduction — verbosity shaper, per-user learning, counterfactual savings (#965) (a99dc61)
- policy: decay P_alive from idle time near cache TTL (#856 P3b) (#1028) (fe4f9ee)
- providers: add Cortex Code (Snowflake CoCo) as a supported agent (#1190) (d9d0bf4)
- proxy: cc-switch reconciler — keep Headroom in the request path alongside cc-switch (#1030) (e8fc8a0)
- proxy: hot-reload live env knobs so a reused proxy picks them up without a restart (#1090) (6904d47)
- proxy: make COMPRESSION_TIMEOUT_SECONDS configurable via env (#946) (#991) (addebdb)
- transforms: tabular + spreadsheet (.xlsx/.xls) compression (#1128) (d789a7c)
- vertex: turnkey Claude Code + Vertex compression (+ fixes from the Vertex review) (#1113) (0e05915)
Bug Fixes
- ccr: accept 12-char SmartCrusher hashes in tool injection (#1095) (#1141) (9f7f3ad)
- ccr: return stored content when headroom_retrieve query matches nothing (#1213) (#1236) (08fb845)
- content-router: honor target_ratio in compression cache + add proxy --target-ratio flag (#1108) (8894ee0)
- dashboard: light-mode backgrounds + aligned savings tables (#1064) (5eae32b)
- deps: make litellm optional on Python 3.14 (#956) (#993) (b2f04e4)
- e2e: align Codex wrap e2e with global-only RTK guidance (#1240) (#1254) (bc12ace)
- init: set ENABLE_TOOL_SEARCH=true so Claude Code keeps deferring tools (#746) (#995) (500ec2b)
- kompress: never block the request path on the cold-cache model download (#1161) (3fc2a78)
- memory: use ONNX embedder for
wrap --memorysync (#1092) (#1262) (4f9feda) - openclaw: wrap plugin export as {register} object for OpenClaw 2026.x compatibility (#1218) (2e6c442)
- providers: update DeepSeek V3 context limit from 128K to 1M (#1038) (#1137) (bcabc5c)
- proxy: allow disabling periodic TOIN stats logging (#1265) (b5f63d8)
- proxy: honor HEADROOM_EXCLUDE_TOOLS for Codex /v1/responses tool outputs (#940) (#1053) (f03e77b)
- proxy: preserve byte-faithful Anthropic tool forwarding (#1222) (1f18d59)
- proxy: route Codex OAuth image requests (#1215) (381d771)
- proxy: scope CORS to loopback + gate operator/content endpoints (#1226) (bd55a42)
- proxy: stamp X-Client: codex on Responses endpoint for unidentified callers (#1036) (b0cd032)
- proxy: treat NODE_EXTRA_CA_CERTS as additive, not replacement (#998) (#1031) (c987283)
- telemetry: switch anonymous telemetry to opt-in (off by default) (#1223) (b998697)
- tokenizers: bound tiktoken vocab load so a stalled download cannot hang requests (#956) (#994) (7e86baf)
- unwrap: remove ANTHROPIC_BASE_URL + ENABLE_TOOL_SEARCH and init hooks on unwrap (#992) (5b84691)
- wrap: keep Codex RTK guidance global (#1240) (7c26a54)
- wrap: percent-encode non-ASCII cwd names in X-Headroom-Project header (#1071) (9f712cc)
- wrap: write env.ANTHROPIC_BASE_URL to settings.json so daemon-spawned conversations inherit proxy (#951) (#1078) (a554c3a)
0.26.0 (2026-06-16)
Features
- add Copilot BYOK provider wrapper utilities and CLI support (#1041) (e67ee2a)
- add dashboard agent usage stats (#814) (6d3f39f)
- Add support for Mistral Vibe CLI (#935) (0932b8b)
- attribute reread waste to over-compression via marker check (#901) (f928576)
- bedrock: cross-region + Converse compression; bundle proxy binary in images (#999) (0dc2e1c)
- dashboard: surface compression-vs-cache net impact in Prefix Cache panel (#913) (2a4d300)
- evals: adversarial-input robustness grid for compressors (#918) (5939004)
- parser: detect re-issued identical tool calls as reread waste (#909) (7d4ae86)
- policy: batch deep edits through one cache-bust (#856 P3a) (#1015) (c2e52fe)
- policy: consume net-cost mutation gate in ContentRouter (#856 P2) (#905) (553ade4)
- proxy: compress AWS Bedrock InvokeModel requests via configurable upstream (#720) (7edb27a)
Bug Fixes
- anthropic: strip styled Claude model ids (#651) (0c5c89d)
- anyllm: forward openai api_base/api_key to the any-llm backend (#942) (#954) (a7ee8a6)
- cache: guard None exemplar embeddings in dynamic detector (#950) (1ec9320)
- cache: name the missing piece in semantic detector guard (#1018) (3b0bcee)
- ci: check out repo in PR Governance label job (#1021) (4558bc2)
- ci: make PR governance advisory (#1047) (74dff94)
- codex: compute waste signals on the OpenAI Responses path (#898) (b9e2761)
- codex: poll /wham/usage for subscription limits (handshake no longer sends x-codex-* headers) (#924) (8c00f71)
- codex: PR health label check state (#986) (99c874d)
- codex: retag thread providers so history menu stays whole across the proxy boundary (#1034) (74ae781)
- codex: write canonical hooks feature flag and migrate deprecated codex_hooks (#743) (dff6a19)
- compression: convert tree-sitter byte offsets to char offsets (#892) (b1f700f)
- compression: correct JSON array item counting and entropy gate (#887) (d6f0f0f)
- compression: keep container bodies compressible in code handler (#890) (16ed73b)
- compression: measure short-value threshold on payload, not token (#889) (65b0e8c)
- compression: use thread-local tree-sitter parsers in code handler (#893) (6cdb846)
- gemini: surface functionResponse payloads to waste-signal detection (#897) (9b0c840)
- learn: decode directory names with spaces in Windows project paths (#997) (#1027) (2d3701b)
- learn: scan subagent and workflow transcripts (#1045) (0ddd4ed)
- openclaw: declare headroom_retrieve tool contract (#947) (7c8c909)
- policy: correct warm-cache penalty in net_mutation_gain to (S + dT) (#903) (0632eba)
- proxy: add native Bedrock converse-stream route (#917) (b08ec15)
- proxy: keep codex image-generation WS turns alive through the relay (#1000) (7dbbb40)
- proxy: make budget enforcement actually work (#885) (a14ab45)
- proxy: read RTK gain stats globally by default (#957) (b70fccb)
- route v1internal code assist requests to cloudcode-pa.googleapis… (#821) (e20f16b)
- serena: stop the Serena dashboard popup and make --no-serena actually disable Serena (#1003) (919379a)
- support Copilot Business subscription auth (#641) (0b4a4bd)
- wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy entrypoint (#943) (9b7b436)
- wrap: avoid duplicate top-level keys when injecting codex provider (#884) (dd22cfd)
Code Refactoring
0.25.0 (2026-06-12)
Features
- add differential network capture harness (#761) (11ab5f8)
- add light mode for dashboard (#834) (c425893)
- add OAuth2 client-credentials upstream-auth proxy extension (#778) (#784) (eb2e50f)
- add Vertex AI proxy routing (#793) (3c77e52)
- cli: comprehensive help text, validation, and exception handling improvements (#640) (028efab)
- compression safety rails — error-output protection, pipeline circuit breaker, library inflation guard (#851) (c0cadcc)
- dashboard: per-model savings breakdown and expected-vs-actual cost on historical charts (#807) (34dafe6)
- detect re-served tool results as over-compression waste signal (#854) (5f1d88a)
- evals: add zero-cost tool schema compaction integrity eval (#817) (53a08c6)
- gated Markdown-KV compaction formatter (serialization-aware output) (#859) (06b2625)
- kompress: warn on unrecognized HEADROOM_KOMPRESS_BACKEND + document backend selection (#204) (6367d0b)
- memory: add opt-in Apple-GPU (MPS) embedding runtime (#766) (c71592d)
- net-cost cache mutation formula on CompressionPolicy (#856 P1) (#857) (d5f5802)
- plugins: Hermes agent headroom_retrieve plugin (#824) (058bced)
- probe-based retention scoring of recorded compression events (#862) (c2106cb)
- proxy: add CLI opt-outs for CCR injection (compression-only mode) (#823) (693d9d2)
- proxy: attribute savings history rollups per provider (#791) (0b8b8d9)
- proxy: log compressed messages alongside original request (#261) (2269e40)
- proxy: per-project savings breakdown on the dashboard (claude, codex, aider, copilot, cursor) (#803) (914a60a)
- support Python 3.14+ via pyo3 abi3 stable ABI (#516) (19eac8e)
- switch Kompress default to kompress-v2-base with weight-only int8 ONNX (#799) (74392b2)
- transforms: attribute read_lifecycle + smart_crush tags (#249) (8f37426)
Bug Fixes
- anthropic: CCR exception must re-raise, not silently swallow (#838) (8db5efc)
- ccr: key Rust search/diff/log markers with explicit_hash (#852) (bfcb07d)
- ccr: make retrieval TTL configurable (#715) (2533f77)
- ccr: skip CCR when model calls headroom_retrieve alongside user tools (#839) (30078f8)
- ccr: use shared compression store (#875) (249af6c)
- ci: correct comments, timeouts, and pip reliability in native e2e workflows (#878) (b716c8c)
- ci: pin cosign-installer to v3 (v4 does not exist) (#774) (199d693)
- codex: respect CODEX_HOME for wrap config (#731) (96abf38)
- content_router: guard against empty compression output causing Anthropic 400 (#771) (2f9ff07)
- copilot: use responses API for subscription reasoning models (#647) (84ac332)
- correct preserved-entry index mapping in Gemini content round-trip (#836) (0ffe2b6)
- dashboard: stable 'Proxy $ Saved' hero tile under --workers > 1 (#481) (fd73b88)
- don't inject empty tools:[] when client omitted the tools field (#772) (574bbae)
- harden Copilot API auth token handling (#557) (6b0c09f)
- health: readyz verifies upstream connectivity, not just process liveness (#744) (5dfb446)
- init: guard persistent task startup (#616) (9252d85)
- init: normalize Windows hook paths to forward slashes (#788) (6ea6e31)
- init: suppress hook recovery output (#760) (b439599)
- learn: claude-cli streams output with idle timeout (#373) (9bff575)
- make headroom wrap readiness probe timeout configurable for slow ML imports (#581) (163677b)
- parser: detect waste signals in Anthropic tool_result content blocks (#815) (929698a)
- proxy: F4 — trust X-Forwarded-* only behind allow-listed gateway (d10bd5f)
- proxy: lazy-import server to avoid fastapi crash (#442) (93c6937)
- proxy: make CCR multi-worker warning conditional on backend (#770) (d76a729)
- proxy: make Kompress eager preload cache-only so a cold cache can't block startup (#783) (841663d)
- proxy: restore Codex usage headers on WS and streaming SSE transports (#577) (#794) (0ce68de)
- schema compaction must not drop property names that match DROP_KEYS (#785) (ae2122f)
- security: block DNS-rebinding on /debug/* and /stats/reset via Host-header allowlist (#605) (b4b5025)
- ssl: upstream httpx client inherits SSL_CERT_FILE, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS (#745) (e50fbb3)
- suppress LiteLLM provider banner before import (#874) (f9384ef)
- transforms: use thread-local tree-sitter parsers to prevent pyo3 Unsendable panic (#604) (2ad300a)
- wrap: track shared proxy clients with markers (#877) (05bd56b)
Code Refactoring
- extract litellm model resolution to shared utility (ec7d006)
0.24.0 (2026-06-08)
Features
- perf: add --format {text,json,csv} to
headroom perf(#648) (9fe4886) - proxy: show resolved upstream API targets in startup banner (#586) (8dbe7ad), closes #583
- relevance: weight BM25 score_batch by corpus IDF (#646) (88177bd)
- support CLAUDE_CODE_USE_FOUNDRY and custom upstream gateways (#726) (d90cdce)
Bug Fixes
- ci: restore green lint gate on main (fe50f9d)
- codex: auto-enable fail-open on compression timeout in headroom wrap codex (#531) (5f5f261)
- copilot: restore generic endpoint for non-subscription OAuth (#610) (#612) (18925b8)
- deps: move gunicorn to [proxy-prod] extra, add Windows guard (#537) (fa558c5)
- proxy: fail-open on corrupt golden bytes instead of RuntimeError (#603) (2170a1b)
- proxy: route Claude Code model metadata to Anthropic (#627) (30c1ac8)
- security: patch loopback guard, retry None raise, async subprocess, and cache race (06d7cb9)
- security: patch loopback guard, retry None raise, blocking subprocess, and cache stats race (78f3a4d)
- startup: move HF/httpx log suppression before sentence_transformers init (#622) (176d4c7)
- startup: suppress proxy startup log noise (#619) (4555901)
- wrap: report unbindable proxy ports (#602) (6dfcaa8)
Unreleased
Added
- wrap: add
headroom wrap zcode/headroom unwrap zcodefor the ZCode desktop app (zcode.z.ai). Follows the Pattern-B (proxy-only watcher) approach: starts the proxy, injects RTK guidance intoAGENTS.mdat the project root, and prints the ZCode settings the user should configure (OpenAI and Anthropic base URLs). Auto-detects the enabled provider from~/.zcode/v2/config.jsonand configures the proxy upstream accordingly. Unwrap removes the injected RTK instructions and stops the proxy. - kompress: warn when
HEADROOM_KOMPRESS_BACKENDis set to an unrecognized value instead of silently falling back toauto, and document the backend selection env var (auto/onnx/onnx_cpu/onnx_coreml/pytorch/pytorch_mpsplus shorthand aliases) inwiki/configuration.md(issue #202, PR #204). - proxy: per-provider attribution in the savings history rollups. Each
/stats-historybucket (hourly/daily/weekly/monthly) now carries aby_providermap breaking downtokens_saved,compression_savings_usd_delta,total_input_tokens_delta, andtotal_input_cost_usd_deltaper provider, so consumers can show how savings and spend are distributed across providers within a time period. Providers only appear in a bucket where they moved a counter; legacy history checkpoints with no provider collapse into"unknown". Affected files:headroom/proxy/savings_tracker.py,headroom/proxy/prometheus_metrics.py. - cli: startup banner now includes a
Performance Tuningsection that surfaces activeHEADROOM_COMPRESSION_STABLE_AFTER_TURN,HEADROOM_STALE_READ_COMPRESS_AFTER_TURNS, and embedding-server socket values when set; shows a hint to set them when all defaults are in use.
Changed
- deps: loosen over-pinned constraints and add upper bounds
litellm==1.82.3->>=1.86.2,<2.0(exact pin blocked security patches; floor stays above the CVE-2026-42271 fix)transformers>=4.30.0->>=4.30.0,<6.0(add upper bound; library already crossed a major version silently)sentence-transformers>=2.2.0->>=2.2.0,<6.0(same; applied inmemory,evals, anddevextras)neo4j>=5.20.0->>=5.20.0,<7.0(client had already crossed the 5.x/6.x boundary)mem0ai>=0.1.100->>=1.0.0,<2.0(floor was pre-1.0; locked package is already 1.0.11)langchain-core>=0.2.0->>=1.3.3,<4.0(floor stays above current high-severity advisory fixes)langchain-openai>=0.1.0->>=1.1.14,<2.0(floor stays above current advisory fixes)qdrant-client>=1.9.0->>=1.9.0,<2.0uvicorn>=0.23.0->>=0.23.0,<1.0(applied inproxyanddevextras)- Same
transformersandlitellmbounds applied consistently acrossml,voice, anddevextras
- docker: bump
neo4jimage indocker-compose.ymlfrom5.15.0to5.26(latest 5.x LTS) - docker: bump
UV_VERSIONinDockerfilefrom0.11.16to0.11.18
Bug Fixes
- wrap: check feature configuration before reusing persistent deployments. A persistent proxy started for one use case (e.g.
--backend anthropic) would be silently reused for another (e.g.--subscription --provider-type openai) causing 401 auth failures because_ensure_proxy()only checked health + version, skipping the feature configuration check (memory, openai_api_url, learn, code_graph). - codex: respect
CODEX_HOMEwhenheadroom wrap codexwrites provider, MCP, memory, backup, and globalAGENTS.mdconfig, and warn whenunwrap codexmay be looking at the default Codex home becauseCODEX_HOMEis unset. - proxy: multi-worker CCR warning is now conditional on backend — when
HEADROOM_CCR_BACKENDis unset (defaultInMemoryBackend, per-process), the startup warning includes CCR retrieval failures and suggestsHEADROOM_CCR_BACKEND=sqlite; when a cross-worker backend is already configured, the warning covers only the remaining per-worker stores (compression cache, prefix tracker, TOIN, CostTracker). UpdatedRUST_DEV.mdto accurately document PythonCompressionStoreas per-process by default. - deps: move
gunicornto[proxy-prod]extra withsys_platform != 'win32'guard; removed from[proxy]to avoid forcing a Unix-only package on dev, CI, and Windows users (#537) - startup: suppress proxy startup log noise -- litellm banner, trafilatura parse errors, HuggingFace Hub unauthenticated warnings, tiktoken fallback warning, and httpx INFO lines from sentence_transformers HEAD checks. Affected files:
headroom/providers/litellm.py,headroom/transforms/html_extractor.py,headroom/memory/adapters/embedders.py,headroom/providers/anthropic.py,headroom/providers/registry.py,headroom/image/onnx_router.py,headroom/transforms/kompress_compressor.py.
0.23.0 (2026-06-04)
Features
- copilot: GitHub Copilot subscription mode through Headroom (f4dff9b)
Bug Fixes
- ccr: scope proactive expansion by workspace (cross-project leak) (197601b)
- ccr: scope proactive expansion by workspace (cross-project leak) (1bc163f)
- codex: keep init model_provider at config root (#260) (304dcc7)
- codex: keep init model_provider at config root (#260) (849b46d)
- copilot: deterministic subscription token handoff to the proxy (72da461)
- copilot: support subscription auth through Headroom (ff4a0c6)
- correct tiktoken encoding for unknown gpt-4 model snapshots (#552) (0e551de)
- decode/encode owned config, state and template assets as UTF-8 (2f1538a)
- decode/encode owned config, state and template assets as UTF-8 (fixes #533) (92075b9)
- docker: upgrade base images to Python 3.13 / debian13 (e6bf7a0)
- docker: upgrade base images to Python 3.13 / debian13, drop digest pinning (08a2197)
- docs: bump next.js to 16.2.6 for GHSA-h64f-5h5j-jqjh (CVE-2026-44577) (a6a09e6)
- docs: mkdocs configuration to build with correct folder (#543) (5557944)
- docs: update brace-expansion to 5.0.6 to remediate GHSA-jxxr-4gwj-5jf2 (CVE-2026-45149) (6eb6fb5)
- docs: update bun.lock to next 16.2.6 for GHSA-h64f-5h5j-jqjh (CVE-2026-44577) (91e0937)
- ignore brackets inside JSON strings when splitting mixed content (#553) (bdcfc32)
- learn: decode Unix home dirs whose username contains '.', '-' or '_' (211daae)
- learn: decode Unix home dirs whose username contains '.', '-' or '_' (491a8b3)
- learn: finish gemini-flash-latest default model sweep (982d01b)
- learn: finish gemini-flash-latest default model sweep (#532) (d797366)
- memory: READ-ONLY framing + fail-closed unresolved-project fallback (a178249)
- memory: READ-ONLY framing + fail-closed unresolved-project fallback (482f80e)
- update dashboard doc link (#544) (378d77e)
- Update Next.js to 16.2.4 in docs/bun.lock to address GHSA-gx5p-jg67-6x7h (CVE-2026-44580) (0b9f11a)
- Update Next.js to 16.2.6 in docs/package.json and package-lock.json to address GHSA-h64f-5h5j-jqjh (CVE-2026-44577) (db5d15f)
- Upgrade litellm to 1.86.2 to remediate CVE-2026-42271 (07581b9)
Code Refactoring
- cli: factor shared wrap-subcommand scaffolding (8eeb926)
- cli: factor shared wrap-subcommand scaffolding (c74ad11)
0.22.4 (2026-05-26)
Bug Fixes
- cli: G1 remediation — non-string clobber, per-model systemMessage, openhands gate (ea1976e)
- cli: wrap CLI breadth — cline, continue, goose, openhands (8625f80)
- cli: wrap subcommands for cline, continue, goose, openhands (c375fa1)
- observability: G3 remediation — bound cardinality + wire dead metrics (2a717a9)
- observability: RTK metrics + Rust observability (Phase H blocker) (b36ad9f)
- observability: wire Phase G PR-G3 RTK + proxy metrics (H-blocker) (5f264a5)
- release: tag format vX.Y.Z (drop release-please component prefix) (4a39ef5)
- release: tag format vX.Y.Z (drop release-please component prefix) (0f3e3af)
- subscription: address G2 review findings — phantom delta, multi-worker race, silent fallbacks (f68090c)
- subscription: wire tokens_saved_rtk data plane (c7d1247)
- subscription: wire tokens_saved_rtk from RTK stats endpoint (44c605f)
- tests: drive RTK subprocess failure with real exec, not monkeypatched run (9b6d637)
- tests: mock logger.warning directly instead of relying on caplog (c38dac3)
- tests: patch headroom.rtk.get_rtk_path, not the helpers alias (317dffe)
- tests: tomllib fallback to tomli on python 3.10 (74843d1)
Unreleased
Security
/debug/memoryloopback guard. The endpoint was missing theDepends(_require_loopback)guard that all other/debug/*endpoints carry. External callers can no longer reach it.retry_max_attemptszero guard. Whenretry_enabled=Trueandretry_max_attempts=0the retry loop exited without settinglast_error, causingraise last_errorto raiseTypeError: exceptions must derive from BaseException. ARuntimeErrorwith an actionable message is now raised instead, andProxyConfig.__post_init__rejectsretry_max_attempts < 1at construction time.- Blocking subprocess on async event loop.
_read_rtk_lifetime_statsand_read_lean_ctx_lifetime_statscalledsubprocess.rundirectly on the asyncio thread. Theinitialize_context_tool_session_baselinefunction is nowasyncand offloads the subprocess viaasyncio.to_thread; the stats endpoint usesawait asyncio.to_thread(_get_context_tool_stats). - Hardcoded Neo4j credential in
docker-compose.yml.NEO4J_AUTHnow defaults to${NEO4J_AUTH:-neo4j/devpassword}and is documented in.env.example(excluded from.gitignorevia!.env.example). SemanticCache.get_memory_stats()concurrent iteration. The method iteratesself._cache.values()without holding the async lock. A snapshot is now taken vialist(self._cache.values())before iterating to avoidRuntimeError: dictionary changed size during iterationunder async load.- Default Neo4j password in
ProxyConfig.memory_neo4j_passworddefault changed from"password"to"". The proxy startup path now emits alogger.warningwhenmemory_backend == "qdrant-neo4j"and the password is empty, prompting operators to set a real credential.
Fixed
-
PyPI install clarity and release gating. Documented
pipx --python python3.13for environments where unsupported Python wheel tags cause older-version resolution, made PyPI publish failures block GitHub Releases unlessPYPI_SKIP=true, and added an sdistLICENSEinvariant. -
headroom learnwith claude-cli no longer fails silently on slow networks or large digests. The CLI backend timeout was a hard 120s wall-clock cap with no liveness signal: a successful long analysis and a hung connection looked identical, and exit 0 with "no recommendations" was the only user-visible signal. Two changes: (1) Streaming + idle timeout for claude-cli: the command now uses--output-format stream-json --verboseand a watchdog thread reads events as they arrive. The process is killed only afterHEADROOM_LEARN_CLI_IDLE_TIMEOUT_SECS(default 60s) of zero output, or afterHEADROOM_LEARN_CLI_TIMEOUT_SECS(default 300s, was 120s) total. Long-but-active analyses run to completion; genuine hangs are caught fast. The finaltype:"result"event carries the assistant response. Drains stdout/stderr via reader threads so the watchdog works on Windows too. (2) Env-var overrides for all CLI backends:HEADROOM_LEARN_CLI_TIMEOUT_SECSis honored by gemini-cli and codex-cli as the wall-clock timeout; idle override applies only to the streaming claude-cli path. -
Learned: error recoverysection in MEMORY.md no longer bloats with stale, one-shot, or contradictory entries. The matchers paired up unrelated tool calls (e.g.state.rsandlib.rsin the same dir becomingFile state.rs does not exist. The correct path is lib.rs.), the dedup key was the literal rendered bullet text so near-duplicates each created their own row, the shutdown flush dropped the evidence gate to 1 so every singleton landed at session end, and there was no TTL or re-validation. Fixed at every layer: (1) Emission: Read recoveries require the failed/successful basenames to be identical or close in edit distance; Bash recoveries require a shared binary (allowingpython↔python3andruff↔.venv/bin/ruffvariants) plus low-edit-distance OR a shared substantive non-flag token. Unrelated pairs are rejected at the source. (2) Dedup: error-recovery rows are hashed on recovery intent — Read on(basename(error_path), basename(success_path)), Bash on the primary command stripped of volatile suffixes (| tail -N,2>&1, etc.). Near-duplicates collapse into one row. (3) Evidence gating: defaultmin_evidenceraised from 2 to 5; shutdown-relaxation removed; new--min-evidenceflag andHEADROOM_MIN_EVIDENCEenvvar so embedded clients can tighten the threshold further. (4) Render-time refinement: drop rows not re-observed in 21 days, re-validate Read success paths against the filesystem, collapse same-error_path-with-multiple-targets into one "use Glob/Grep first" bullet, rank byevidence_count * 0.5 ** (days/5), cap the section at 15. A→B / B→A contradiction pairs are also dropped at flush time. Patterns now stampfirst_seen_at/last_seen_aton every save;_bump_persisted_evidenceupdates them viajson_set. OtherLearned: …categories (environment, preference, architecture) are untouched. -
headroom unwrap codexnow actually undoesheadroom wrap codex— previously there was nounwrap codexsubcommand at all, so the injectedmodel_provider = "headroom"/[model_providers.headroom]block stayed in~/.codex/config.tomlforever and Codex continued routing through the (potentially stopped) proxy, surfacing asMissing environment variable: OPENAI_API_KEY.wrap codexnow snapshots the pre-wrapconfig.tomltoconfig.toml.headroom-backupbefore its first injection, andunwrap codexrestores that snapshot byte-for-byte (or, if the backup is missing, strips only the Headroom-managed block and leaves surrounding user content intact). Safe no-op when run without a prior wrap. Reported by @raenaryl in Discord. -
Image compressors now release shared router models after use and proxy shutdown — the proxy/image compression path no longer keeps global
technique-routerandSigLIPmodel instances pinned in memory after one-off image optimization work. Theget_compressor()helper now returns a fresh, caller-owned compressor instead of a process-lifetime singleton. -
headroom learnno longer clobbers prior recommendations on re-run — the marker block inCLAUDE.md/MEMORY.mdis now merged with the prior block instead of wholesale-replaced. Sections re-surfaced by the new run win; sections not re-surfaced are carried forward so learnings accumulate across runs instead of disappearing. To fully rebuild the block, delete it manually and re-run. (#231) -
headroom learnno longer emits dangling cross-references when a section is re-surfaced — the analyzer now includes the project's current<!-- headroom:learn -->block (fromCLAUDE.mdandMEMORY.md) in the LLM digest as a "Prior Learned Patterns" section, and the system prompt instructs the LLM that re-emitting a section replaces the prior one wholesale. Prevents bullets like "Xis also large — same rule asY,Z" from appearing afterYandZgot dropped during per-section replacement. The writer's section-level carry-forward from #231 remains in place as a safety net for sections the LLM omits entirely. New helperextract_marker_blockadded toheadroom.learn.writer.
Added
turn_idlinking agent-loop API calls to a single user prompt — a newcompute_turn_id(model, system, messages)helper inheadroom/proxy/helpers.pyhashes the message prefix up to and including the last user-text message, yielding an id that is stable across every agent-loop iteration of one prompt but rolls over when the user sends a new prompt (or runs/compact,/clear).RequestLoggained aturn_id: str | Nonefield, which is stamped at every log site (anthropic handler bedrock + direct branches, and the streaming handler) and surfaced asturn_idin/transformations/feed. Lets downstream consumers (e.g. the Headroom Desktop Activity tab) aggregate savings per user prompt rather than per API call.- Live flush of traffic-learned patterns to CLAUDE.md / MEMORY.md — the
TrafficLearnernow writes to agent-native context files continuously during proxy operation, not just at shutdown. A new dirty-flag debounced_flush_worker(10s window,FLUSH_DEBOUNCE_SECONDS) callsflush_to_file()whenever_accumulate()marks the learner dirty, so patterns surface inCLAUDE.md/MEMORY.mdnear real-time. Flushes read both persisted rows (via_load_persisted_patterns_from_sqlite) and the in-memory accumulator, bucket patterns by project via the learn plugin registry (plugin.discover_projects()+ longest-path anchoring in_project_for_pattern), and route byPatternCategoryto the correct file (_patterns_to_recommendations+_CATEGORY_TO_TARGET). Live flushes requireevidence_count >= 2; the shutdown flush accepts single-evidence rows.
Fixed
- Traffic-learner evidence count stuck at 1; duplicate DB rows across
restarts.
_accumulatequeued patterns with the defaultExtractedPattern.evidence_count = 1regardless of how many times the pattern was actually seen, so every persisted row landed at1and never crossed the live-flush gate (evidence_count >= 2). Worse, once a pattern was in_saved_hashesit was early-returned on every re-sighting, and_saved_hashesreset on process restart — so a second sighting in a later session inserted a duplicate row rather than bumping the existing one. Now:_accumulatewrites the real accumulated count at save time,start()hydrates_saved_hashes+ a new_persisted_idsmap from the DB, and re-sightings bump the persisted row'smetadata.evidence_countvia an atomicjson_setUPDATE(_bump_persisted_evidence)._load_persisted_patterns_from_sqlitenow filters viajson_extract(metadata, '$.source')instead of a LIKE on the raw JSON string, so rows survive metadata rewrites.
Added
HEADROOM_QDRANT_*environment variables for memory Qdrant configuration (#31) —Memory(backend="qdrant-neo4j"),Mem0Config,MemoryConfig, andProxyConfignow resolve their Qdrant connection fromHEADROOM_QDRANT_URL,HEADROOM_QDRANT_HOST,HEADROOM_QDRANT_PORT,HEADROOM_QDRANT_API_KEY,HEADROOM_QDRANT_HTTPS,HEADROOM_QDRANT_PREFER_GRPC, andHEADROOM_QDRANT_GRPC_PORT. Explicit constructor arguments still win; unset env keeps the existinglocalhost:6333defaults. Adds matching--memory-qdrant-{url,host,port,api-key}CLI flags. Enables hosted Qdrant (Qdrant Cloud) and shared/remote Qdrant stacks without code changes. New helper:headroom/memory/qdrant_env.py.- Telemetry stack & install-mode identity fields — anonymous beacon now
reports
headroom_stack(how Headroom is invoked:proxy,wrap_claude,adapter_ts_openai, ...) andinstall_mode(wrapped/persistent/on_demand), plusrequests_by_stackfor proxies that serve multiple integrations. Proxy exposes aby_stackbucket alongsideby_provider/by_modelon/stats, a matchingheadroom_requests_by_stackPrometheus counter, and anX-Headroom-Stackheader honored by the FastAPI middleware.headroom wrap <tool>setsHEADROOM_STACK=wrap_<agent>; the TS SDK and all four adapters (openai,anthropic,gemini,vercel-ai) tag their compress calls. Schema migration:sql/upgrade_telemetry_stack_context.sql. - Canonical filesystem contract (issue #175) — new
HEADROOM_CONFIG_DIR(default~/.headroom/config, read-mostly) andHEADROOM_WORKSPACE_DIR(default~/.headroom, read-write state) env vars recognized by the Python proxy/CLI and the npm SDK. Additive; all existing per-resource env vars (HEADROOM_SAVINGS_PATH,HEADROOM_TOIN_PATH,HEADROOM_SUBSCRIPTION_STATE_PATH,HEADROOM_MODEL_LIMITS) continue to work with identical semantics. Docker install scripts anddocker-compose.native.ymlforward the new vars into containers so savings, logs, and telemetry resolve to the bind-mounted.headroompath. Seewiki/filesystem-contract.md.
Changed
/stats-historynow returns compact checkpoint history by default — the JSON response keeps recent checkpoints dense while evenly sampling older checkpoints so long-running installs do not return ever-growing payloads. Addhistory_mode=fullto fetch the full retained checkpoint list, orhistory_mode=noneto skip it entirely while still receiving the derived hourly/daily/weekly/monthly rollups. Responses now include ahistory_summaryblock describing stored versus returned points.
Fixed
- Streaming Anthropic requests are now visible to
/stats.recent_requestsand/transformations/feed—_finalize_stream_responsedid not callself.logger.log(...), so the entire streaming Anthropic code path (the one Claude Code uses) silently bypassed the request logger. Only the non-streaming Anthropic path and the Bedrock streaming path were logged. As a consequence,--log-messageshad no observable effect on the live transformations feed for typical traffic. The streaming finalizer now emits the sameRequestLogshape the other paths do, includingrequest_messageswhenlog_full_messagesis enabled.
[0.5.22] - 2026-04-11
Added
- Cross-agent memory — Claude saves a fact, Codex reads it back. All agents sharing one proxy share one memory store. Project-scoped DB at
.headroom/memory.db, auto user_id from$USER. - Agent provenance tracking — every memory records which agent saved it (
source_agent,source_provider,created_via), with edit history on updates. - LLM-mediated dedup — on
memory_save, enriched response hints similar existing memories to the LLM. Background async dedup auto-removes >92% cosine duplicates. Zero extra LLM calls. - Memory for OpenAI and Gemini handlers — context injection + tool handling wired into all three provider handlers (Anthropic, OpenAI, Gemini).
- Plugin architecture for
headroom learn— each agent (Claude, Codex, Gemini) is a self-contained plugin. External plugins register viaheadroom.learn_pluginentry points.--agentflag for CLI. - GeminiScanner for
headroom learn— reads~/.gemini/tmp/*/chats/session-*.jsonand.jsonl. - Code graph integration —
headroom wrap claude --code-graphauto-indexes the project via codebase-memory-mcp for call-chain traversal, impact analysis, and architectural queries. Opt-in, ~200 token overhead with Claude Code's MCP Tool Search. - OpenAI embedder auto-detection — memory backend uses OpenAI embeddings when
sentence-transformersis unavailable (no torch/2GB dependency needed). - Live traffic learning flush —
headroom wrap <agent> --learnflushes learned patterns to the correct agent-native file (MEMORY.md / AGENTS.md / GEMINI.md) at proxy shutdown.
Changed
- CodeCompressor disabled by default — AST-based code compression produced invalid syntax on 40% of real files. Code now passes through uncompressed. Use
--code-graphfor code intelligence instead, or re-enable with--code-aware. - Shared tool name map — consolidated tool normalization across all learn plugins into
_shared.py. - Dynamic CLI agent detection —
headroom learndiscovers agents via plugin registry, no hardcoded choices.
Fixed
- CodeCompressor statement-based truncation — body truncation now walks AST statements (not lines), never cuts mid-expression. Fixes syntax errors on multi-line dict literals and function calls.
- Docstring FIRST_LINE mode — uses source lines directly instead of reconstructing from byte offsets. Properly handles all quote styles.
- Memory shutdown queue drain — patterns in the save queue were lost on proxy shutdown. Now drained before exit.
Unreleased
Added
- Codex-proxy resilience hardening — reduces event-loop starvation under cold-start reconnect storms
- Stage-timing instrumentation — per-stage durations for both Codex WS accept and Anthropic
/v1/messagespre-upstream phases emitted as a singleSTAGE_TIMINGSstructured log line per request plus Prometheus histograms - Per-pipeline shared warmup — Anthropic + OpenAI pipelines eagerly load compressors/parsers once at startup; status merged into
WarmupRegistryfor/debug/warmupand/readyz - WS session registry — first-class tracking of active Codex WS sessions with deterministic relay-task cancellation and termination-cause classification (
client_disconnect,upstream_error,client_timeout, etc.) - Bounded pre-upstream Anthropic concurrency —
--anthropic-pre-upstream-concurrency/HEADROOM_ANTHROPIC_PRE_UPSTREAM_CONCURRENCYcaps simultaneous/v1/messagespre-upstream work (body read, deep copy, first compression stage, memory-context lookup, upstream connect) so replay storms cannot starve/livez,/readyz, and new Codex WS opens. Default: automax(2, min(8, cpu_count));0or negative disables (unbounded) - Loopback-only debug endpoints —
/debug/tasks,/debug/ws-sessions,/debug/warmupreturn404(not403) to non-loopback callers so external scanners cannot enumerate them - Reconnect-storm repro harness —
scripts/repro_codex_replay.pydrives concurrent WS + HTTP replay traffic against a local proxy and asserts/livezp99 under threshold;--jsonoutput routes JSON to stdout and the human summary to stderr
- Stage-timing instrumentation — per-stage durations for both Codex WS accept and Anthropic
- Proxy liveness and readiness health checks
- Adds
GET /livezfor process liveness andGET /readyzfor traffic readiness - Keeps
GET /healthbackward compatible while expanding it with readiness details and subsystem checks - Eagerly initializes configured memory backends during proxy startup so readiness reflects real serving capability
- Wires
/readyzinto the Docker imageHEALTHCHECKand the exampledocker-compose.yml
- Adds
- Durable proxy savings history
- Persists proxy compression savings history locally at
~/.headroom/proxy_savings.json - Supports
HEADROOM_SAVINGS_PATHto override the storage location - Adds
/stats-historywith lifetime totals plus hourly/daily/weekly/monthly rollups - Supports JSON and CSV export from
/stats-history - Extends
/statswith apersistent_savingsblock while keepingsavings_historybackward compatible - Adds a historical mode to
/dashboardbacked by/stats-history, including export actions
- Persists proxy compression savings history locally at
- Proxy telemetry SDK override via
HEADROOM_SDK- Downstream apps can override the anonymous telemetry
sdkfield without patching installed files - Blank values fall back to the default
proxylabel
- Downstream apps can override the anonymous telemetry
headroom learn— Offline failure learning for coding agents- Analyzes past conversation history (Claude Code, extensible to Cursor/Codex)
- Success correlation: for each failure, finds what succeeded after and extracts the specific correction
- 5 analyzers: Environment, Structure, Command Patterns, Retry Prevention, Cross-Session
- Writes specific learnings to CLAUDE.md (stable project facts) and MEMORY.md (session patterns)
- Generic architecture: tool-agnostic
ToolCallmodel, pluggable Scanner/Writer adapters - Dry-run by default,
--applyto write,--allfor all projects - Example output: "FirstClassEntity.java is not at axion-formats/ — actually at axion-scala-common/"
- Read Lifecycle Management — Event-driven compression of stale/superseded Read outputs
- Detects when a Read output becomes stale (file was edited after) or superseded (file was re-read)
- Replaces stale/superseded content with compact CCR markers, stores originals for retrieval
- 75% of Read output bytes are provably stale or redundant (from real-world analysis of 66K tool calls)
- Fresh Reads (latest read, no subsequent edit) are never touched — Edit safety preserved
- Opt-in via
ReadLifecycleConfig(enabled=True), disabled by default - Handles both OpenAI and Anthropic message formats
- any-llm backend - Route requests through 38+ LLM providers (OpenAI, Mistral, Groq, Ollama, etc.) via any-llm
- Enable with
--backend anyllm --anyllm-provider <provider> - Install with:
pip install 'headroom-ai[anyllm]'
- Enable with
- Production-ready proxy server with caching, rate limiting, and metrics
- CLI command
headroom proxyto start the proxy server - IntelligentContextManager (semantic-aware context management)
- Multi-factor importance scoring: recency, semantic similarity, TOIN importance, error indicators, forward references, token density
- No hardcoded patterns - all importance signals learned from TOIN or computed from metrics
- TOIN integration for retrieval_rate and field_semantics-based scoring
- Strategy selection: NONE, COMPRESS_FIRST, DROP_BY_SCORE based on budget overage
- Atomic tool unit handling (call + response dropped together)
- Configurable scoring weights via
ScoringWeightsdataclass IntelligentContextConfigfor full configuration control- Backwards compatible with
RollingWindowConfig
- LLMLingua-2 Integration (opt-in ML-based compression)
LLMLinguaCompressortransform using Microsoft's LLMLingua-2 model- Content-aware compression rates (code: 0.4, JSON: 0.35, text: 0.3)
- Memory management utilities:
unload_llmlingua_model(),is_llmlingua_model_loaded() - Proxy integration via
--llmlinguaflag - Device selection:
--llmlingua-device(auto/cuda/cpu/mps) - Custom compression rate:
--llmlingua-rate - Helpful startup hints when llmlingua is available but not enabled
Install with:(thepip install headroom-ai[llmlingua][llmlingua]extra was removed in 0.9.x)
- Code-Aware Compression (AST-based, syntax-preserving)
CodeAwareCompressortransform using tree-sitter for AST parsing- Supports Python, JavaScript, TypeScript, Go, Rust, Java, C, C++
- Preserves imports, function signatures, type annotations, error handlers
- Compresses function bodies while maintaining structural integrity
- Guarantees syntactically valid output (no broken code)
- Automatic language detection from code patterns
- Memory management:
is_tree_sitter_available(),unload_tree_sitter() - Uses
tree-sitter-language-packfor broad language support - Install with:
pip install headroom-ai[code]
- ContentRouter (intelligent compression orchestrator)
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing (file paths, tool names)
- Handles mixed content (e.g., markdown with code blocks)
- Strategies: CODE_AWARE, SMART_CRUSHER, SEARCH, LOG, TEXT, LLMLINGUA
- Configurable strategy preferences and fallbacks
- Routing decision log for transparency and debugging
- Custom Model Configuration
- Support for new models: Claude 4.5 (Opus), Claude 4 (Sonnet, Haiku), o3, o3-mini
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Custom model config via
HEADROOM_MODEL_LIMITSenvironment variable - Config file support:
~/.headroom/models.json - Graceful fallback for unknown models (no crashes)
- Updated pricing data for all current models
Fixed
- Event.wait task leak in subscription trackers —
asyncio.shieldpattern prevents cancellation of the outerwait_forfrom leaking the innerEvent.waittask - Python 3.10 compatibility for memory-context fail-open — catches
asyncio.TimeoutError(the 3.10-compatible alias) rather thanTimeoutErrorto preserve behaviour on older runtimes - uvicorn
proxy_headers=False— refusesForwarded/X-Forwarded-Forrewrites so the loopback guard on/debug/*cannot be spoofed by a misconfigured reverse proxy - First-frame timeout for Codex WS accepts — guards against a client that opens a handshake and never sends the first frame; relays cancel deterministically with
client_timeout - Semaphore leak on unexpected exception in Anthropic pre-upstream path — the finalizer now releases the pre-upstream semaphore on every exit path (early 4xx, cache hit, upstream error, streaming handoff)
active_relay_tasksgauge double-decrement —deregister_and_countreturns(handle, released_task_count)atomically so the handler decrements the Prometheus gauge by the exact number it registered, eliminating drift
Internal
- IPv6-mapped loopback recognition — the loopback guard parses
::ffff:127.0.0.1and other dual-stack literals throughipaddress.ip_address(...).is_loopback - Lock-free stage-timing accumulators —
record_stage_timingswrites to per-path counters that do not contend with/metricsexport orrecord_request - Narrow
contextlib.suppressin relay classification — onlyCancelledErroris suppressed where we reclassify it; other exceptions propagate so termination cause stays truthful jitter_delay_mshelper — shared exponential-backoff + 50-150% jitter formula inheadroom/proxy/helpers.py; used by three proxy retry sites and mirrored inline in the repro harness
0.2.0 - 2025-01-07
Added
- SmartCrusher: Statistical compression for tool outputs
- Keeps first/last K items, errors, anomalies, and relevance matches
- Variance-based change point detection
- Pattern detection (time series, logs, search results)
- Relevance Scoring Engine: ML-powered item relevance
BM25Scorer: Fast keyword matching (zero dependencies)EmbeddingScorer: Semantic similarity with sentence-transformersHybridScorer: Adaptive combination of both methods
- CacheAligner: Prefix stabilization for better cache hits
- Dynamic date extraction
- Whitespace normalization
- Stable prefix hashing
- RollingWindow: Context management within token limits
- Drops oldest tool units first
- Never orphans tool results
- Preserves recent turns
- Multi-Provider Support:
- Anthropic with official
count_tokensAPI - Google with official
countTokensAPI - Cohere with official
tokenizeAPI - Mistral with official tokenizer
- LiteLLM for unified interface
- Anthropic with official
- Integrations:
- LangChain callback handler (
HeadroomOptimizer) - MCP (Model Context Protocol) utilities
- LangChain callback handler (
- Proxy Server (
headroom.proxy):- Semantic caching with LRU eviction
- Token bucket rate limiting
- Retry with exponential backoff
- Cost tracking with budget enforcement
- Prometheus metrics endpoint
- Request logging (JSONL)
- Pricing Registry: Centralized model pricing with staleness tracking
- Benchmarks: Performance benchmarks for transforms and relevance scoring
Changed
- Improved token counting accuracy across all providers
- Enhanced tool output compression with relevance-aware selection
Fixed
- Mistral tokenizer API compatibility
- Google token counting for multi-turn conversations
0.1.0 - 2025-01-05
Added
- Initial release
HeadroomClient: OpenAI-compatible client wrapperToolCrusher: Basic tool output compression- Audit mode for observation without modification
- Optimize mode for applying transforms
- Simulate mode for previewing changes
- SQLite and JSONL storage backends
- HTML report generation
- Streaming support
Safety Guarantees
- Never removes human content
- Never breaks tool ordering
- Parse failures are no-ops
- Preserves recency (last N turns)
Migration Guide
From 0.1.x to 0.2.x
The 0.2.0 release is backward compatible. New features are opt-in:
# Old code still works
from headroom import HeadroomClient, OpenAIProvider
# New SmartCrusher (replaces ToolCrusher for better compression)
from headroom import SmartCrusher, SmartCrusherConfig
config = SmartCrusherConfig(
min_tokens_to_crush=200,
max_items_after_crush=50,
)
crusher = SmartCrusher(config)
# New relevance scoring
from headroom import create_scorer
scorer = create_scorer("hybrid") # or "bm25" for zero deps
Using the Proxy
New in 0.2.0 - run Headroom as a proxy server:
# Start the proxy
headroom proxy --port 8787
# Use with Claude Code
ANTHROPIC_BASE_URL=http://localhost:8787 claude