chopratejas commited on
Commit
001364f
Β·
1 Parent(s): 66ac1c8

Add Kompress: ModernBERT token compressor replacing LLMLingua-2

Browse files

Adds kompress_compressor.py β€” a self-contained ModernBERT-based token
compressor that auto-downloads from chopratejas/kompress-base on HuggingFace.
Trained on 330K structured tool outputs (JSON, diffs, logs, code, SQL,
agentic traces), achieving 82% entity preservation vs LLMLingua-2's 36%.

Changes:
- New: kompress_compressor.py β€” dual-head ModernBERT (token + span CNN)
with HuggingFace auto-download, no extra pip install needed
- ContentRouter: Kompress is primary ML compressor, LLMLingua-2 is fallback
- fallback_strategy changed from PASSTHROUGH to KOMPRESS β€” unknown/mixed
content now gets compressed instead of ignored
- No hardcoded compression ratios β€” model decides per-token importance,
optional target_ratio only when user explicitly sets it via API
- Version bump: 0.3.8 β†’ 0.4.0

headroom/__init__.py CHANGED
@@ -142,7 +142,7 @@ from .transforms import (
142
  TransformPipeline,
143
  )
144
 
145
- __version__ = "0.3.7"
146
 
147
  __all__ = [
148
  # Main client
 
142
  TransformPipeline,
143
  )
144
 
145
+ __version__ = "0.4.0"
146
 
147
  __all__ = [
148
  # Main client
headroom/transforms/content_router.py CHANGED
@@ -246,6 +246,7 @@ class CompressionStrategy(Enum):
246
  SMART_CRUSHER = "smart_crusher"
247
  SEARCH = "search"
248
  LOG = "log"
 
249
  LLMLINGUA = "llmlingua"
250
  TEXT = "text"
251
  DIFF = "diff"
@@ -371,6 +372,7 @@ class ContentRouterConfig:
371
 
372
  # Enable/disable specific compressors
373
  enable_code_aware: bool = True
 
374
  enable_llmlingua: bool = True
375
  enable_smart_crusher: bool = True
376
  enable_search_compressor: bool = True
@@ -383,8 +385,8 @@ class ContentRouterConfig:
383
  mixed_content_threshold: int = 2 # Min types to consider mixed
384
  min_section_tokens: int = 20 # Min tokens to compress a section
385
 
386
- # Fallback
387
- fallback_strategy: CompressionStrategy = CompressionStrategy.PASSTHROUGH
388
 
389
  # Protection: Don't compress content that's likely the subject of analysis
390
  skip_user_messages: bool = True # User messages contain what they want analyzed
@@ -633,6 +635,7 @@ class ContentRouter(Transform):
633
  self._log_compressor: Any = None
634
  self._diff_compressor: Any = None
635
  self._html_extractor: Any = None
 
636
  self._llmlingua: Any = None
637
  self._text_compressor: Any = None
638
  self._image_optimizer: Any = None
@@ -989,13 +992,16 @@ class ContentRouter(Transform):
989
  # Estimate tokens from extracted text (simple word count)
990
  compressed_tokens = len(compressed.split()) if compressed else 0
991
 
 
 
 
992
  elif strategy == CompressionStrategy.LLMLINGUA:
993
- compressed, compressed_tokens = self._try_llmlingua(content, context, question)
994
 
995
  elif strategy == CompressionStrategy.TEXT:
996
- # Prefer LLMLingua for text if available (ML-based, better compression)
997
- # Falls back to heuristic TextCompressor if LLMLingua unavailable
998
- compressed, compressed_tokens = self._try_llmlingua(content, context, question)
999
 
1000
  except Exception as e:
1001
  logger.warning("Compression with %s failed: %s", strategy.value, e)
@@ -1016,10 +1022,13 @@ class ContentRouter(Transform):
1016
  # Fallback: return unchanged
1017
  return content, original_tokens
1018
 
1019
- def _try_llmlingua(
1020
  self, content: str, context: str, question: str | None = None
1021
  ) -> tuple[str, int]:
1022
- """Try LLMLingua compression with fallback.
 
 
 
1023
 
1024
  Args:
1025
  content: Content to compress.
@@ -1029,6 +1038,17 @@ class ContentRouter(Transform):
1029
  Returns:
1030
  Tuple of (compressed, token_count).
1031
  """
 
 
 
 
 
 
 
 
 
 
 
1032
  if self.config.enable_llmlingua:
1033
  compressor = self._get_llmlingua()
1034
  if compressor:
@@ -1036,16 +1056,13 @@ class ContentRouter(Transform):
1036
  result = compressor.compress(content, context=context, question=question)
1037
  return result.compressed, result.compressed_tokens
1038
  except Exception as e:
1039
- logger.debug("LLMLingua failed: %s", e)
1040
-
1041
- # Fallback to text compressor
1042
- compressor = self._get_text_compressor()
1043
- if compressor:
1044
- result = compressor.compress(content, context=context)
1045
- return result.compressed, result.compressed_line_count
1046
 
1047
  return content, len(content.split())
1048
 
 
 
 
1049
  def _strategy_from_detection_type(self, content_type: ContentType) -> CompressionStrategy:
1050
  """Get strategy from ContentType enum."""
1051
  mapping = {
@@ -1069,6 +1086,7 @@ class ContentRouter(Transform):
1069
  CompressionStrategy.DIFF: ContentType.GIT_DIFF,
1070
  CompressionStrategy.HTML: ContentType.HTML,
1071
  CompressionStrategy.TEXT: ContentType.PLAIN_TEXT,
 
1072
  CompressionStrategy.LLMLINGUA: ContentType.PLAIN_TEXT,
1073
  CompressionStrategy.PASSTHROUGH: ContentType.PLAIN_TEXT,
1074
  }
@@ -1154,13 +1172,19 @@ class ContentRouter(Transform):
1154
  def eager_load_compressors(self) -> None:
1155
  """Pre-load compressors at startup to avoid first-request latency.
1156
 
1157
- Call this during proxy startup to load LLMLingua model (~5s)
1158
  before any requests arrive.
1159
  """
 
 
 
 
 
 
 
1160
  if self.config.enable_llmlingua:
1161
  compressor = self._get_llmlingua()
1162
  if compressor:
1163
- # Trigger the underlying model load by accessing it
1164
  try:
1165
  from .llmlingua_compressor import _get_llmlingua_compressor
1166
 
@@ -1170,8 +1194,20 @@ class ContentRouter(Transform):
1170
  except Exception as e:
1171
  logger.warning("Failed to pre-load LLMLingua model: %s", e)
1172
 
 
 
 
 
 
 
 
 
 
 
 
 
1173
  def _get_llmlingua(self) -> Any:
1174
- """Get LLMLinguaCompressor (lazy load)."""
1175
  if self._llmlingua is None:
1176
  try:
1177
  from .llmlingua_compressor import (
 
246
  SMART_CRUSHER = "smart_crusher"
247
  SEARCH = "search"
248
  LOG = "log"
249
+ KOMPRESS = "kompress"
250
  LLMLINGUA = "llmlingua"
251
  TEXT = "text"
252
  DIFF = "diff"
 
372
 
373
  # Enable/disable specific compressors
374
  enable_code_aware: bool = True
375
+ enable_kompress: bool = True # Kompress: ModernBERT token compressor (preferred over LLMLingua)
376
  enable_llmlingua: bool = True
377
  enable_smart_crusher: bool = True
378
  enable_search_compressor: bool = True
 
385
  mixed_content_threshold: int = 2 # Min types to consider mixed
386
  min_section_tokens: int = 20 # Min tokens to compress a section
387
 
388
+ # Fallback: Kompress handles unknown/mixed content instead of passing through
389
+ fallback_strategy: CompressionStrategy = CompressionStrategy.KOMPRESS
390
 
391
  # Protection: Don't compress content that's likely the subject of analysis
392
  skip_user_messages: bool = True # User messages contain what they want analyzed
 
635
  self._log_compressor: Any = None
636
  self._diff_compressor: Any = None
637
  self._html_extractor: Any = None
638
+ self._kompress: Any = None
639
  self._llmlingua: Any = None
640
  self._text_compressor: Any = None
641
  self._image_optimizer: Any = None
 
992
  # Estimate tokens from extracted text (simple word count)
993
  compressed_tokens = len(compressed.split()) if compressed else 0
994
 
995
+ elif strategy == CompressionStrategy.KOMPRESS:
996
+ compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
997
+
998
  elif strategy == CompressionStrategy.LLMLINGUA:
999
+ compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
1000
 
1001
  elif strategy == CompressionStrategy.TEXT:
1002
+ # Prefer ML compressor (Kompress > LLMLingua) for text
1003
+ # Falls back to heuristic TextCompressor if neither available
1004
+ compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
1005
 
1006
  except Exception as e:
1007
  logger.warning("Compression with %s failed: %s", strategy.value, e)
 
1022
  # Fallback: return unchanged
1023
  return content, original_tokens
1024
 
1025
+ def _try_ml_compressor(
1026
  self, content: str, context: str, question: str | None = None
1027
  ) -> tuple[str, int]:
1028
+ """ML-based compression: Kompress (primary), LLMLingua (fallback only).
1029
+
1030
+ Kompress (ModernBERT, trained on 330K structured tool outputs)
1031
+ auto-downloads from HuggingFace on first use. No heuristic fallback.
1032
 
1033
  Args:
1034
  content: Content to compress.
 
1038
  Returns:
1039
  Tuple of (compressed, token_count).
1040
  """
1041
+ # Primary: Kompress β€” downloads from chopratejas/kompress-base on first use
1042
+ if self.config.enable_kompress:
1043
+ compressor = self._get_kompress()
1044
+ if compressor:
1045
+ try:
1046
+ result = compressor.compress(content, context=context, question=question)
1047
+ return result.compressed, result.compressed_tokens
1048
+ except Exception as e:
1049
+ logger.warning("Kompress failed: %s", e)
1050
+
1051
+ # Fallback: LLMLingua (only if Kompress not installed)
1052
  if self.config.enable_llmlingua:
1053
  compressor = self._get_llmlingua()
1054
  if compressor:
 
1056
  result = compressor.compress(content, context=context, question=question)
1057
  return result.compressed, result.compressed_tokens
1058
  except Exception as e:
1059
+ logger.warning("LLMLingua failed: %s", e)
 
 
 
 
 
 
1060
 
1061
  return content, len(content.split())
1062
 
1063
+ # Backwards compatibility
1064
+ _try_llmlingua = _try_ml_compressor
1065
+
1066
  def _strategy_from_detection_type(self, content_type: ContentType) -> CompressionStrategy:
1067
  """Get strategy from ContentType enum."""
1068
  mapping = {
 
1086
  CompressionStrategy.DIFF: ContentType.GIT_DIFF,
1087
  CompressionStrategy.HTML: ContentType.HTML,
1088
  CompressionStrategy.TEXT: ContentType.PLAIN_TEXT,
1089
+ CompressionStrategy.KOMPRESS: ContentType.PLAIN_TEXT,
1090
  CompressionStrategy.LLMLINGUA: ContentType.PLAIN_TEXT,
1091
  CompressionStrategy.PASSTHROUGH: ContentType.PLAIN_TEXT,
1092
  }
 
1172
  def eager_load_compressors(self) -> None:
1173
  """Pre-load compressors at startup to avoid first-request latency.
1174
 
1175
+ Call this during proxy startup to load models (~5s)
1176
  before any requests arrive.
1177
  """
1178
+ # Prefer Kompress (faster, smaller, better on structured data)
1179
+ if self.config.enable_kompress:
1180
+ compressor = self._get_kompress()
1181
+ if compressor:
1182
+ logger.info("Kompress model pre-loaded at startup")
1183
+ return # No need to also load LLMLingua
1184
+
1185
  if self.config.enable_llmlingua:
1186
  compressor = self._get_llmlingua()
1187
  if compressor:
 
1188
  try:
1189
  from .llmlingua_compressor import _get_llmlingua_compressor
1190
 
 
1194
  except Exception as e:
1195
  logger.warning("Failed to pre-load LLMLingua model: %s", e)
1196
 
1197
+ def _get_kompress(self) -> Any:
1198
+ """Get KompressCompressor (lazy load). Downloads from HuggingFace on first use."""
1199
+ if self._kompress is None:
1200
+ try:
1201
+ from .kompress_compressor import KompressCompressor, is_kompress_available
1202
+
1203
+ if is_kompress_available():
1204
+ self._kompress = KompressCompressor()
1205
+ except ImportError:
1206
+ logger.debug("Kompress dependencies not available")
1207
+ return self._kompress
1208
+
1209
  def _get_llmlingua(self) -> Any:
1210
+ """Get LLMLinguaCompressor (lazy load). Fallback if Kompress unavailable."""
1211
  if self._llmlingua is None:
1212
  try:
1213
  from .llmlingua_compressor import (
headroom/transforms/kompress_compressor.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Kompress: ModernBERT token compressor for structured tool outputs.
2
+
3
+ Drop-in replacement for LLMLingua-2. Auto-downloads the model from
4
+ HuggingFace (chopratejas/kompress-base) on first use.
5
+
6
+ No extra pip install needed β€” uses transformers + safetensors
7
+ which are already Headroom dependencies.
8
+
9
+ Usage:
10
+ >>> from headroom.transforms.kompress_compressor import KompressCompressor
11
+ >>> compressor = KompressCompressor()
12
+ >>> result = compressor.compress(long_tool_output)
13
+ >>> print(result.compressed)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import threading
20
+ from dataclasses import dataclass
21
+ from typing import Any
22
+
23
+ import torch
24
+ import torch.nn as nn
25
+ from transformers import AutoModel, AutoTokenizer
26
+
27
+ from ..config import TransformResult
28
+ from ..tokenizer import Tokenizer
29
+ from .base import Transform
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # HuggingFace model ID
34
+ HF_MODEL_ID = "chopratejas/kompress-base"
35
+
36
+ # Lazy singleton
37
+ _kompress_model = None
38
+ _kompress_tokenizer = None
39
+ _kompress_lock = threading.Lock()
40
+
41
+
42
+ # ── Model Architecture (must match training) ──────────────────────────
43
+
44
+
45
+ class HeadroomCompressorModel(nn.Module):
46
+ """Dual-head ModernBERT: token classification + span importance CNN."""
47
+
48
+ def __init__(self, model_name: str = "answerdotai/ModernBERT-base"):
49
+ super().__init__()
50
+ self.encoder = AutoModel.from_pretrained(model_name, attn_implementation="eager")
51
+ hidden_size = self.encoder.config.hidden_size # 768
52
+
53
+ # Head 1: Token keep/discard
54
+ self.token_dropout = nn.Dropout(0.1)
55
+ self.token_head = nn.Linear(hidden_size, 2)
56
+
57
+ # Head 2: Span importance (1D CNN)
58
+ self.span_conv = nn.Sequential(
59
+ nn.Conv1d(hidden_size, 256, kernel_size=5, padding=2),
60
+ nn.GELU(),
61
+ nn.Conv1d(256, 1, kernel_size=3, padding=1),
62
+ nn.Sigmoid(),
63
+ )
64
+
65
+ def get_scores(self, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
66
+ """Get per-token compression scores. Higher = more important."""
67
+ with torch.no_grad():
68
+ hidden = self.encoder(input_ids, attention_mask=attention_mask).last_hidden_state
69
+
70
+ token_probs = torch.softmax(self.token_head(hidden), dim=-1)[:, :, 1]
71
+
72
+ span_scores = self.span_conv(hidden.transpose(1, 2)).squeeze(1)
73
+
74
+ return token_probs * (0.5 + 0.5 * span_scores)
75
+
76
+
77
+ # ── Model Loading ─────────────────────────────────────────────────────
78
+
79
+
80
+ def _load_kompress(device: str = "auto") -> tuple[HeadroomCompressorModel, Any]:
81
+ """Download from HuggingFace and load the Kompress model."""
82
+ global _kompress_model, _kompress_tokenizer
83
+
84
+ with _kompress_lock:
85
+ if _kompress_model is not None:
86
+ return _kompress_model, _kompress_tokenizer
87
+
88
+ from huggingface_hub import hf_hub_download
89
+
90
+ logger.info("Downloading Kompress model from %s ...", HF_MODEL_ID)
91
+
92
+ # Download model weights
93
+ weights_path = hf_hub_download(HF_MODEL_ID, "model.safetensors")
94
+
95
+ # Load architecture
96
+ model = HeadroomCompressorModel()
97
+
98
+ # Load trained weights
99
+ from safetensors.torch import load_file
100
+
101
+ state_dict = load_file(weights_path)
102
+ model.load_state_dict(state_dict, strict=False)
103
+
104
+ # Resolve device
105
+ if device == "auto":
106
+ if torch.cuda.is_available():
107
+ device = "cuda"
108
+ elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
109
+ device = "mps"
110
+ else:
111
+ device = "cpu"
112
+
113
+ model.to(device)
114
+ model.eval()
115
+ logger.info("Kompress model loaded on %s (%s)", device, HF_MODEL_ID)
116
+
117
+ tokenizer = AutoTokenizer.from_pretrained("answerdotai/ModernBERT-base")
118
+
119
+ _kompress_model = model
120
+ _kompress_tokenizer = tokenizer
121
+ return model, tokenizer
122
+
123
+
124
+ def is_kompress_available() -> bool:
125
+ """Check if Kompress dependencies are available."""
126
+ try:
127
+ import huggingface_hub # noqa: F401
128
+ import safetensors # noqa: F401
129
+
130
+ return True
131
+ except ImportError:
132
+ return False
133
+
134
+
135
+ def unload_kompress_model() -> bool:
136
+ """Unload the Kompress model to free memory."""
137
+ global _kompress_model, _kompress_tokenizer
138
+ with _kompress_lock:
139
+ if _kompress_model is not None:
140
+ _kompress_model = None
141
+ _kompress_tokenizer = None
142
+ if torch.cuda.is_available():
143
+ torch.cuda.empty_cache()
144
+ return True
145
+ return False
146
+
147
+
148
+ # ── Compressor ────────────────────────────────────────────────────────
149
+
150
+
151
+ @dataclass
152
+ class KompressConfig:
153
+ """Minimal config. The model decides what's important β€” not us."""
154
+
155
+ device: str = "auto"
156
+ enable_ccr: bool = True
157
+
158
+
159
+ @dataclass
160
+ class KompressResult:
161
+ """Result of Kompress compression."""
162
+
163
+ compressed: str
164
+ original: str
165
+ original_tokens: int
166
+ compressed_tokens: int
167
+ compression_ratio: float
168
+ cache_key: str | None = None
169
+ model_used: str = HF_MODEL_ID
170
+
171
+ @property
172
+ def tokens_saved(self) -> int:
173
+ return max(0, self.original_tokens - self.compressed_tokens)
174
+
175
+ @property
176
+ def savings_percentage(self) -> float:
177
+ if self.original_tokens == 0:
178
+ return 0.0
179
+ return (self.tokens_saved / self.original_tokens) * 100
180
+
181
+
182
+ class KompressCompressor(Transform):
183
+ """Kompress: ModernBERT token compressor for structured tool outputs.
184
+
185
+ Auto-downloads chopratejas/kompress-base from HuggingFace on first use.
186
+ Drop-in replacement for LLMLinguaCompressor with identical interface.
187
+ """
188
+
189
+ name: str = "kompress_compressor"
190
+
191
+ def __init__(self, config: KompressConfig | None = None):
192
+ self.config = config or KompressConfig()
193
+
194
+ def compress(
195
+ self,
196
+ content: str,
197
+ context: str = "",
198
+ content_type: str | None = None,
199
+ question: str | None = None,
200
+ target_ratio: float | None = None,
201
+ ) -> KompressResult:
202
+ """Compress content using Kompress model.
203
+
204
+ Args:
205
+ content: Text to compress.
206
+ context: Optional surrounding context (unused by model, kept for interface compat).
207
+ content_type: Ignored β€” model decides importance per content type.
208
+ question: Ignored β€” kept for LLMLingua interface compat.
209
+ target_ratio: If None (default), model decides how much to keep using
210
+ score threshold. If set (e.g. 0.3), forces that keep ratio.
211
+ The proxy never sets this β€” only user-facing API does.
212
+
213
+ Returns:
214
+ KompressResult with compressed text.
215
+ """
216
+ words = content.split()
217
+ n_words = len(words)
218
+
219
+ if n_words < 10:
220
+ return self._passthrough(content, n_words)
221
+
222
+ try:
223
+ model, tokenizer = _load_kompress(self.config.device)
224
+
225
+ # Tokenize
226
+ encoding = tokenizer(
227
+ words,
228
+ is_split_into_words=True,
229
+ truncation=True,
230
+ max_length=8192,
231
+ padding=True,
232
+ return_tensors="pt",
233
+ )
234
+
235
+ device = next(model.parameters()).device
236
+ input_ids = encoding["input_ids"].to(device)
237
+ attention_mask = encoding["attention_mask"].to(device)
238
+
239
+ # Get per-token importance scores from dual-head model
240
+ scores = model.get_scores(input_ids, attention_mask)[0].cpu()
241
+
242
+ # Map subword scores to word-level (max pooling)
243
+ word_ids = encoding.word_ids(batch_index=0)
244
+ word_scores: dict[int, float] = {}
245
+ for idx, wid in enumerate(word_ids):
246
+ if wid is None:
247
+ continue
248
+ s = scores[idx].item()
249
+ if wid not in word_scores or s > word_scores[wid]:
250
+ word_scores[wid] = s
251
+
252
+ if not word_scores:
253
+ return self._passthrough(content, n_words)
254
+
255
+ # Token selection
256
+ if target_ratio is not None:
257
+ # User explicitly asked for a specific ratio β€” use top-k
258
+ sorted_wids = sorted(word_scores, key=lambda w: word_scores[w], reverse=True)
259
+ num_keep = max(1, int(len(sorted_wids) * target_ratio))
260
+ kept_ids = set(sorted_wids[:num_keep])
261
+ else:
262
+ # Model decides. Trained with binary labels β€” score > 0.5 = keep.
263
+ # Dense content β†’ most tokens score high β†’ keeps more.
264
+ # Boilerplate β†’ most score low β†’ keeps less. That's correct.
265
+ kept_ids = {wid for wid, score in word_scores.items() if score > 0.5}
266
+ if not kept_ids:
267
+ # Edge case: nothing above threshold β€” keep the single highest
268
+ best = max(word_scores, key=lambda w: word_scores[w])
269
+ kept_ids = {best}
270
+
271
+ # Reconstruct in original word order
272
+ compressed_words = [words[w] for w in sorted(kept_ids) if w < n_words]
273
+ compressed = " ".join(compressed_words)
274
+ compressed_count = len(compressed_words)
275
+ ratio = compressed_count / n_words if n_words else 1.0
276
+
277
+ result = KompressResult(
278
+ compressed=compressed,
279
+ original=content,
280
+ original_tokens=n_words,
281
+ compressed_tokens=compressed_count,
282
+ compression_ratio=ratio,
283
+ )
284
+
285
+ # CCR marker
286
+ if self.config.enable_ccr and ratio < 0.8:
287
+ cache_key = self._store_in_ccr(content, compressed, n_words)
288
+ if cache_key:
289
+ result.cache_key = cache_key
290
+ result.compressed += (
291
+ f"\n[{n_words} items compressed to {compressed_count}."
292
+ f" Retrieve more: hash={cache_key}]"
293
+ )
294
+
295
+ return result
296
+
297
+ except Exception as e:
298
+ logger.warning("Kompress compression failed: %s", e)
299
+ return self._passthrough(content, n_words)
300
+
301
+ def _passthrough(self, content: str, n_words: int) -> KompressResult:
302
+ return KompressResult(
303
+ compressed=content,
304
+ original=content,
305
+ original_tokens=n_words,
306
+ compressed_tokens=n_words,
307
+ compression_ratio=1.0,
308
+ )
309
+
310
+ def apply(
311
+ self,
312
+ messages: list[dict[str, Any]],
313
+ tokenizer: Tokenizer,
314
+ **kwargs: Any,
315
+ ) -> TransformResult:
316
+ """Apply Kompress compression to messages (Transform interface)."""
317
+ tokens_before = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages)
318
+ transformed = []
319
+ transforms_applied = []
320
+
321
+ for message in messages:
322
+ role = message.get("role", "")
323
+ content = message.get("content", "")
324
+
325
+ if not isinstance(content, str) or len(content.split()) < 10:
326
+ transformed.append(message)
327
+ continue
328
+
329
+ # Compress tool outputs and long assistant messages
330
+ # Model decides how much β€” no hardcoded ratios
331
+ if role in ("tool", "assistant"):
332
+ result = self.compress(content)
333
+ if result.compression_ratio < 0.9:
334
+ transformed.append({**message, "content": result.compressed})
335
+ transforms_applied.append(f"kompress:{role}:{result.compression_ratio:.2f}")
336
+ else:
337
+ transformed.append(message)
338
+ else:
339
+ transformed.append(message)
340
+
341
+ tokens_after = sum(tokenizer.count_text(str(m.get("content", ""))) for m in transformed)
342
+
343
+ return TransformResult(
344
+ messages=transformed,
345
+ tokens_before=tokens_before,
346
+ tokens_after=tokens_after,
347
+ transforms_applied=transforms_applied or ["kompress:noop"],
348
+ )
349
+
350
+ def _store_in_ccr(self, original: str, compressed: str, original_tokens: int) -> str | None:
351
+ try:
352
+ from ..cache.compression_store import get_compression_store
353
+
354
+ store = get_compression_store()
355
+ return store.store(
356
+ original,
357
+ compressed,
358
+ original_tokens=original_tokens,
359
+ compressed_tokens=len(compressed.split()),
360
+ compression_strategy="kompress",
361
+ )
362
+ except Exception:
363
+ return None
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
 
5
  [project]
6
  name = "headroom-ai"
7
- version = "0.3.7"
8
  description = "The Context Optimization Layer for LLM Applications - Cut costs by 50-90%"
9
  readme = "README.md"
10
  license = "Apache-2.0"
@@ -76,7 +76,7 @@ proxy = [
76
  reports = [
77
  "jinja2>=3.0.0",
78
  ]
79
- # ML-based compression (LLMLingua-2)
80
  llmlingua = [
81
  "llmlingua>=0.2.0",
82
  "torch>=2.0.0",
 
4
 
5
  [project]
6
  name = "headroom-ai"
7
+ version = "0.4.0"
8
  description = "The Context Optimization Layer for LLM Applications - Cut costs by 50-90%"
9
  readme = "README.md"
10
  license = "Apache-2.0"
 
76
  reports = [
77
  "jinja2>=3.0.0",
78
  ]
79
+ # ML-based compression (LLMLingua-2 β€” fallback if Kompress fails)
80
  llmlingua = [
81
  "llmlingua>=0.2.0",
82
  "torch>=2.0.0",
tests/test_transforms/test_content_router.py CHANGED
@@ -155,12 +155,13 @@ class TestContentRouterConfig:
155
  config = ContentRouterConfig()
156
 
157
  assert config.enable_code_aware is True
 
158
  assert config.enable_llmlingua is True
159
  assert config.enable_smart_crusher is True
160
  assert config.enable_search_compressor is True
161
  assert config.enable_log_compressor is True
162
  assert config.min_section_tokens == 20
163
- assert config.fallback_strategy == CompressionStrategy.PASSTHROUGH
164
 
165
  def test_custom_values(self):
166
  """Custom config values are applied."""
 
155
  config = ContentRouterConfig()
156
 
157
  assert config.enable_code_aware is True
158
+ assert config.enable_kompress is True
159
  assert config.enable_llmlingua is True
160
  assert config.enable_smart_crusher is True
161
  assert config.enable_search_compressor is True
162
  assert config.enable_log_compressor is True
163
  assert config.min_section_tokens == 20
164
+ assert config.fallback_strategy == CompressionStrategy.KOMPRESS
165
 
166
  def test_custom_values(self):
167
  """Custom config values are applied."""