chopratejas commited on
Commit
356d8ba
·
1 Parent(s): ce7905c

Remove LLMLingua: Kompress is the sole text compressor

Browse files

LLMLingua was the original ML text compressor (BERT-based). Kompress
(ModernBERT, trained on 330K structured tool outputs) replaced it with
better compression quality and simpler architecture.

Removed across 35 files:
- Deleted headroom/transforms/llmlingua_compressor.py
- Deleted tests/test_transforms/test_llmlingua_compressor.py
- Deleted tests/test_proxy_llmlingua.py
- Removed all enable_llmlingua config, _get_llmlingua methods,
LLMLingua fallback paths, LLMLINGUA strategy enum values
- Removed CLI flags, model configs, compression handler references
- Simplified ContentRouter: Kompress is primary and only text compressor

.gitignore CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  # Byte-compiled / optimized / DLL files
2
  __pycache__/
3
  *.py[cod]
 
1
+ # Swift SDK (separate repo)
2
+ swift/
3
+
4
  # Byte-compiled / optimized / DLL files
5
  __pycache__/
6
  *.py[cod]
benchmarks/compression_benchmark.py CHANGED
@@ -44,13 +44,13 @@ try:
44
  except ImportError:
45
  HEADROOM_AVAILABLE = False
46
 
47
- # LLMLingua imports (SOTA baseline)
48
  try:
49
- from headroom.transforms.llmlingua_compressor import LLMLinguaCompressor, LLMLinguaConfig
50
 
51
- LLMLINGUA_AVAILABLE = True
52
  except ImportError:
53
- LLMLINGUA_AVAILABLE = False
54
 
55
 
56
  @dataclass
@@ -427,27 +427,21 @@ Provide a structured summary that retains all critical details."""
427
  return summary, cost, latency
428
 
429
 
430
- def llmlingua_compress(data: list[dict]) -> tuple[str, dict]:
431
  """
432
- Use LLMLingua-2 (Microsoft SOTA) for ML-based compression.
433
  Returns (compressed_text, metadata).
434
  """
435
- if not LLMLINGUA_AVAILABLE:
436
- raise RuntimeError(
437
- "LLMLingua not available. Install with: pip install headroom-ai[llmlingua]"
438
- )
439
 
440
- config = LLMLinguaConfig(
441
- target_compression_rate=0.3, # Keep ~30% of tokens
442
- min_tokens_for_compression=50,
443
- )
444
- compressor = LLMLinguaCompressor(config)
445
 
446
- # Convert data to string for LLMLingua (it works on text, not structured data)
447
  data_str = json.dumps(data, indent=2)
448
 
449
  start = time.time()
450
- result = compressor.compress(data_str, content_type="json")
451
  latency = (time.time() - start) * 1000
452
 
453
  metadata = {
@@ -590,7 +584,7 @@ class BenchmarkConfig:
590
  max_truncate_items: int = 20
591
  max_headroom_items: int = 20
592
  run_summarization: bool = True # Can disable to save cost
593
- run_llmlingua: bool = True # Run LLMLingua-2 (SOTA baseline)
594
 
595
 
596
  def run_scenario_benchmark(
@@ -701,12 +695,12 @@ def run_scenario_benchmark(
701
  except Exception as e:
702
  print(f" Summarization failed: {e}")
703
 
704
- # --- LLMLINGUA-2 (SOTA) ---
705
- if config.run_llmlingua:
706
- print("\n[3/4] Running LLMLingua-2 (Microsoft SOTA)...")
707
- if LLMLINGUA_AVAILABLE:
708
  try:
709
- ll_compressed, ll_metadata = llmlingua_compress(scenario.data)
710
  ll_tokens = count_tokens(ll_compressed)
711
 
712
  ll_answers = []
@@ -729,7 +723,7 @@ def run_scenario_benchmark(
729
 
730
  results.append(
731
  ApproachResult(
732
- approach="llmlingua-2",
733
  scenario=scenario.name,
734
  tokens_original=original_tokens,
735
  tokens_after=ll_tokens,
@@ -747,9 +741,9 @@ def run_scenario_benchmark(
747
  print(f" Accuracy: {ll_accuracy:.1%}")
748
  print(f" Compression latency: {ll_metadata['latency_ms']:.1f}ms")
749
  except Exception as e:
750
- print(f" LLMLingua-2 failed: {e}")
751
  else:
752
- print(" LLMLingua-2 not available. Install with: pip install headroom-ai[llmlingua]")
753
 
754
  # --- HEADROOM ---
755
  print("\n[4/4] Running Headroom...")
 
44
  except ImportError:
45
  HEADROOM_AVAILABLE = False
46
 
47
+ # Kompress imports (ML baseline)
48
  try:
49
+ from headroom.transforms.kompress_compressor import KompressCompressor, is_kompress_available
50
 
51
+ KOMPRESS_AVAILABLE = is_kompress_available()
52
  except ImportError:
53
+ KOMPRESS_AVAILABLE = False
54
 
55
 
56
  @dataclass
 
427
  return summary, cost, latency
428
 
429
 
430
+ def kompress_compress(data: list[dict]) -> tuple[str, dict]:
431
  """
432
+ Use Kompress (ModernBERT) for ML-based compression.
433
  Returns (compressed_text, metadata).
434
  """
435
+ if not KOMPRESS_AVAILABLE:
436
+ raise RuntimeError("Kompress not available. Install with: pip install headroom-ai[ml]")
 
 
437
 
438
+ compressor = KompressCompressor()
 
 
 
 
439
 
440
+ # Convert data to string for Kompress (it works on text, not structured data)
441
  data_str = json.dumps(data, indent=2)
442
 
443
  start = time.time()
444
+ result = compressor.compress(data_str)
445
  latency = (time.time() - start) * 1000
446
 
447
  metadata = {
 
584
  max_truncate_items: int = 20
585
  max_headroom_items: int = 20
586
  run_summarization: bool = True # Can disable to save cost
587
+ run_kompress: bool = True # Run Kompress (ML baseline)
588
 
589
 
590
  def run_scenario_benchmark(
 
695
  except Exception as e:
696
  print(f" Summarization failed: {e}")
697
 
698
+ # --- KOMPRESS (ML baseline) ---
699
+ if config.run_kompress:
700
+ print("\n[3/4] Running Kompress (ModernBERT ML baseline)...")
701
+ if KOMPRESS_AVAILABLE:
702
  try:
703
+ ll_compressed, ll_metadata = kompress_compress(scenario.data)
704
  ll_tokens = count_tokens(ll_compressed)
705
 
706
  ll_answers = []
 
723
 
724
  results.append(
725
  ApproachResult(
726
+ approach="kompress",
727
  scenario=scenario.name,
728
  tokens_original=original_tokens,
729
  tokens_after=ll_tokens,
 
741
  print(f" Accuracy: {ll_accuracy:.1%}")
742
  print(f" Compression latency: {ll_metadata['latency_ms']:.1f}ms")
743
  except Exception as e:
744
+ print(f" Kompress failed: {e}")
745
  else:
746
+ print(" Kompress not available. Install with: pip install headroom-ai[ml]")
747
 
748
  # --- HEADROOM ---
749
  print("\n[4/4] Running Headroom...")
headroom/ccr/tool_injection.py CHANGED
@@ -196,7 +196,6 @@ class CCRToolInjector:
196
  _detected_hashes: list[str] = field(default_factory=list)
197
  # Multiple marker patterns to match different compressors:
198
  # - SmartCrusher: [100 items compressed to 10. Retrieve more: hash=abc123]
199
- # - LLMLingua: [1000 items compressed to 300. Retrieve more: hash=abc123]
200
  # - Kompress: [100 lines compressed to 10. Retrieve more: hash=abc123]
201
  # - LogCompressor: [200 lines compressed to 20. Retrieve more: hash=abc123]
202
  # - SearchCompressor: [50 matches compressed to 5. Retrieve more: hash=abc123]
 
196
  _detected_hashes: list[str] = field(default_factory=list)
197
  # Multiple marker patterns to match different compressors:
198
  # - SmartCrusher: [100 items compressed to 10. Retrieve more: hash=abc123]
 
199
  # - Kompress: [100 lines compressed to 10. Retrieve more: hash=abc123]
200
  # - LogCompressor: [200 lines compressed to 20. Retrieve more: hash=abc123]
201
  # - SearchCompressor: [50 matches compressed to 5. Retrieve more: hash=abc123]
headroom/cli/proxy.py CHANGED
@@ -21,20 +21,6 @@ from .main import main
21
  @click.option("--no-rate-limit", is_flag=True, help="Disable rate limiting")
22
  @click.option("--log-file", default=None, help="Path to JSONL log file")
23
  @click.option("--budget", type=float, default=None, help="Daily budget limit in USD")
24
- # LLMLingua ML-based compression (ON by default if installed)
25
- @click.option("--no-llmlingua", is_flag=True, help="Disable LLMLingua-2 ML-based compression")
26
- @click.option(
27
- "--llmlingua-device",
28
- type=click.Choice(["auto", "cuda", "cpu", "mps"]),
29
- default="auto",
30
- help="Device for LLMLingua model (default: auto)",
31
- )
32
- @click.option(
33
- "--llmlingua-rate",
34
- type=float,
35
- default=0.3,
36
- help="LLMLingua compression rate 0.0-1.0 (default: 0.3 = keep 30%)",
37
- )
38
  # Code-aware compression (ON by default if installed)
39
  @click.option("--no-code-aware", is_flag=True, help="Disable AST-based code compression")
40
  # Read lifecycle (ON by default: compresses stale/superseded Read outputs)
@@ -149,9 +135,6 @@ def proxy(
149
  no_rate_limit: bool,
150
  log_file: str | None,
151
  budget: float | None,
152
- no_llmlingua: bool,
153
- llmlingua_device: str,
154
- llmlingua_rate: float,
155
  no_code_aware: bool,
156
  no_read_lifecycle: bool,
157
  no_intelligent_context: bool,
@@ -225,10 +208,6 @@ def proxy(
225
  rate_limit_enabled=not no_rate_limit,
226
  log_file=log_file,
227
  budget_limit_usd=budget,
228
- # LLMLingua: ON by default (use --no-llmlingua to disable)
229
- llmlingua_enabled=not no_llmlingua,
230
- llmlingua_device=llmlingua_device,
231
- llmlingua_target_rate=llmlingua_rate,
232
  # Code-aware: ON by default (use --no-code-aware to disable)
233
  code_aware_enabled=not no_code_aware,
234
  # Read lifecycle: ON by default (use --no-read-lifecycle to disable)
 
21
  @click.option("--no-rate-limit", is_flag=True, help="Disable rate limiting")
22
  @click.option("--log-file", default=None, help="Path to JSONL log file")
23
  @click.option("--budget", type=float, default=None, help="Daily budget limit in USD")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  # Code-aware compression (ON by default if installed)
25
  @click.option("--no-code-aware", is_flag=True, help="Disable AST-based code compression")
26
  # Read lifecycle (ON by default: compresses stale/superseded Read outputs)
 
135
  no_rate_limit: bool,
136
  log_file: str | None,
137
  budget: float | None,
 
 
 
138
  no_code_aware: bool,
139
  no_read_lifecycle: bool,
140
  no_intelligent_context: bool,
 
208
  rate_limit_enabled=not no_rate_limit,
209
  log_file=log_file,
210
  budget_limit_usd=budget,
 
 
 
 
211
  # Code-aware: ON by default (use --no-code-aware to disable)
212
  code_aware_enabled=not no_code_aware,
213
  # Read lifecycle: ON by default (use --no-read-lifecycle to disable)
headroom/compress.py CHANGED
@@ -203,7 +203,7 @@ def _get_pipeline() -> Any:
203
  # Default pipeline: CacheAligner → ContentRouter → IntelligentContext
204
  # CacheAligner: stabilizes prefix for provider KV cache hits
205
  # ContentRouter: routes to the right compressor per content type
206
- # (SmartCrusher for JSON, CodeCompressor for code, LLMLingua for text)
207
  # IntelligentContext: enforces token limits with score-based dropping
208
  _pipeline = TransformPipeline()
209
  logger.debug("Headroom compression pipeline initialized")
 
203
  # Default pipeline: CacheAligner → ContentRouter → IntelligentContext
204
  # CacheAligner: stabilizes prefix for provider KV cache hits
205
  # ContentRouter: routes to the right compressor per content type
206
+ # (SmartCrusher for JSON, CodeCompressor for code, Kompress for text)
207
  # IntelligentContext: enforces token limits with score-based dropping
208
  _pipeline = TransformPipeline()
209
  logger.debug("Headroom compression pipeline initialized")
headroom/compression/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
  This module provides intelligent, automatic compression that:
4
  1. Detects content type using ML (Magika)
5
  2. Preserves structure (keys, signatures, templates)
6
- 3. Compresses content with LLMLingua
7
  4. Enables retrieval via CCR
8
 
9
  Quick Start:
 
3
  This module provides intelligent, automatic compression that:
4
  1. Detects content type using ML (Magika)
5
  2. Preserves structure (keys, signatures, templates)
6
+ 3. Compresses content with Kompress
7
  4. Enables retrieval via CCR
8
 
9
  Quick Start:
headroom/compression/handlers/__init__.py CHANGED
@@ -4,7 +4,7 @@ Each handler knows how to extract structural information from a specific
4
  content type and create a StructureMask marking what should be preserved.
5
 
6
  Handlers don't compress - they only identify structure. The actual
7
- compression is done by LLMLingua on the non-structural parts.
8
  """
9
 
10
  from headroom.compression.handlers.base import (
 
4
  content type and create a StructureMask marking what should be preserved.
5
 
6
  Handlers don't compress - they only identify structure. The actual
7
+ compression is done by Kompress on the non-structural parts.
8
  """
9
 
10
  from headroom.compression.handlers.base import (
headroom/compression/handlers/base.py CHANGED
@@ -181,7 +181,7 @@ class BaseStructureHandler(ABC):
181
 
182
  Subclasses may override for more sophisticated tokenization.
183
  For mask purposes, character-level is often sufficient and
184
- aligns well with LLMLingua's token-level compression.
185
 
186
  Args:
187
  content: Content to tokenize.
 
181
 
182
  Subclasses may override for more sophisticated tokenization.
183
  For mask purposes, character-level is often sufficient and
184
+ aligns well with token-level compression.
185
 
186
  Args:
187
  content: Content to tokenize.
headroom/compression/masks.py CHANGED
@@ -1,11 +1,11 @@
1
  """Structure mask system for compression.
2
 
3
  A StructureMask identifies which parts of content are "structural" (should be
4
- preserved) vs "compressible" (can be compressed by LLMLingua).
5
 
6
  This separates the concerns of:
7
  1. Structure detection (handlers) - What tokens are navigational?
8
- 2. Content compression (LLMLingua) - What tokens can be removed?
9
 
10
  The mask is content-agnostic - it's just a boolean array aligned to tokens.
11
  """
@@ -21,7 +21,7 @@ class StructureMask:
21
  """A mask identifying structural vs compressible tokens.
22
 
23
  The mask is aligned to a token sequence. True means "preserve this token"
24
- (it's structural/navigational), False means "compressible" (LLMLingua can
25
  potentially remove it).
26
 
27
  Attributes:
@@ -207,7 +207,7 @@ def apply_mask_to_text(
207
  Args:
208
  text: Original text.
209
  mask: Structure mask aligned to tokens.
210
- compress_fn: Function to compress text (e.g., LLMLingua).
211
  tokenizer_decode: Optional function to decode tokens to text.
212
  If not provided, assumes tokens are strings and joins them.
213
 
@@ -245,7 +245,7 @@ class EntropyScore:
245
  be preserved because:
246
  1. They're information-dense (can't be reconstructed)
247
  2. They're often identifiers (semantically important)
248
- 3. LLMLingua may mangle them
249
 
250
  This is a self-signal - no external classifier needed.
251
  """
 
1
  """Structure mask system for compression.
2
 
3
  A StructureMask identifies which parts of content are "structural" (should be
4
+ preserved) vs "compressible" (can be compressed by Kompress).
5
 
6
  This separates the concerns of:
7
  1. Structure detection (handlers) - What tokens are navigational?
8
+ 2. Content compression (Kompress) - What tokens can be removed?
9
 
10
  The mask is content-agnostic - it's just a boolean array aligned to tokens.
11
  """
 
21
  """A mask identifying structural vs compressible tokens.
22
 
23
  The mask is aligned to a token sequence. True means "preserve this token"
24
+ (it's structural/navigational), False means "compressible" (Kompress can
25
  potentially remove it).
26
 
27
  Attributes:
 
207
  Args:
208
  text: Original text.
209
  mask: Structure mask aligned to tokens.
210
+ compress_fn: Function to compress text (e.g., Kompress).
211
  tokenizer_decode: Optional function to decode tokens to text.
212
  If not provided, assumes tokens are strings and joins them.
213
 
 
245
  be preserved because:
246
  1. They're information-dense (can't be reconstructed)
247
  2. They're often identifiers (semantically important)
248
+ 3. Token-level compressors may mangle them
249
 
250
  This is a self-signal - no external classifier needed.
251
  """
headroom/compression/universal.py CHANGED
@@ -3,7 +3,7 @@
3
  This is the main entry point for compression. It:
4
  1. Detects content type using Magika (ML)
5
  2. Extracts structure using appropriate handler
6
- 3. Compresses non-structural content with LLMLingua
7
  4. Optionally stores original in CCR for retrieval
8
 
9
  Usage:
@@ -51,7 +51,7 @@ class UniversalCompressorConfig:
51
 
52
  Attributes:
53
  use_magika: Use ML-based detection (requires magika package).
54
- use_llmlingua: Use LLMLingua for content compression.
55
  use_entropy_preservation: Preserve high-entropy tokens (UUIDs, etc.).
56
  entropy_threshold: Threshold for entropy-based preservation.
57
  min_content_length: Minimum content length to compress.
@@ -60,7 +60,7 @@ class UniversalCompressorConfig:
60
  """
61
 
62
  use_magika: bool = True
63
- use_llmlingua: bool = True
64
  use_entropy_preservation: bool = True
65
  entropy_threshold: float = 0.85
66
  min_content_length: int = 100
@@ -139,7 +139,7 @@ class UniversalCompressor:
139
  config: Compression configuration.
140
  handlers: Custom handlers for content types.
141
  compress_fn: Custom compression function. If None, uses
142
- LLMLingua when available, else simple truncation.
143
  """
144
  self.config = config or UniversalCompressorConfig()
145
 
@@ -165,18 +165,18 @@ class UniversalCompressor:
165
  def _get_default_compress_fn(self) -> Callable[[str], str]:
166
  """Get default compression function.
167
 
168
- Returns LLMLingua wrapper if available, else simple truncation.
169
  """
170
- if self.config.use_llmlingua:
171
  try:
172
- return self._llmlingua_compress
173
  except ImportError:
174
- logger.info("LLMLingua not available, using simple compression")
175
 
176
  return self._simple_compress
177
 
178
- def _llmlingua_compress(self, text: str) -> str:
179
- """Compress using LLMLingua.
180
 
181
  Args:
182
  text: Text to compress.
@@ -185,16 +185,15 @@ class UniversalCompressor:
185
  Compressed text.
186
  """
187
  try:
188
- from headroom.transforms.llmlingua_compressor import compress_with_llmlingua
189
 
190
- return compress_with_llmlingua(
191
- text,
192
- compression_rate=self.config.compression_ratio_target,
193
- )
194
  except ImportError:
195
  return self._simple_compress(text)
196
  except Exception as e:
197
- logger.warning("LLMLingua compression failed: %s", e)
198
  return self._simple_compress(text)
199
 
200
  def _simple_compress(self, text: str) -> str:
 
3
  This is the main entry point for compression. It:
4
  1. Detects content type using Magika (ML)
5
  2. Extracts structure using appropriate handler
6
+ 3. Compresses non-structural content with Kompress
7
  4. Optionally stores original in CCR for retrieval
8
 
9
  Usage:
 
51
 
52
  Attributes:
53
  use_magika: Use ML-based detection (requires magika package).
54
+ use_kompress: Use Kompress for content compression.
55
  use_entropy_preservation: Preserve high-entropy tokens (UUIDs, etc.).
56
  entropy_threshold: Threshold for entropy-based preservation.
57
  min_content_length: Minimum content length to compress.
 
60
  """
61
 
62
  use_magika: bool = True
63
+ use_kompress: bool = True
64
  use_entropy_preservation: bool = True
65
  entropy_threshold: float = 0.85
66
  min_content_length: int = 100
 
139
  config: Compression configuration.
140
  handlers: Custom handlers for content types.
141
  compress_fn: Custom compression function. If None, uses
142
+ Kompress when available, else simple truncation.
143
  """
144
  self.config = config or UniversalCompressorConfig()
145
 
 
165
  def _get_default_compress_fn(self) -> Callable[[str], str]:
166
  """Get default compression function.
167
 
168
+ Returns Kompress wrapper if available, else simple truncation.
169
  """
170
+ if self.config.use_kompress:
171
  try:
172
+ return self._kompress_compress
173
  except ImportError:
174
+ logger.info("Kompress not available, using simple compression")
175
 
176
  return self._simple_compress
177
 
178
+ def _kompress_compress(self, text: str) -> str:
179
+ """Compress using Kompress.
180
 
181
  Args:
182
  text: Text to compress.
 
185
  Compressed text.
186
  """
187
  try:
188
+ from headroom.transforms.kompress_compressor import KompressCompressor
189
 
190
+ compressor = KompressCompressor()
191
+ result = compressor.compress(text)
192
+ return result.compressed
 
193
  except ImportError:
194
  return self._simple_compress(text)
195
  except Exception as e:
196
+ logger.warning("Kompress compression failed: %s", e)
197
  return self._simple_compress(text)
198
 
199
  def _simple_compress(self, text: str) -> str:
headroom/config.py CHANGED
@@ -619,7 +619,7 @@ class HeadroomConfig:
619
  prefix_freeze: PrefixFreezeConfig = field(default_factory=PrefixFreezeConfig)
620
 
621
  # Content Router - intelligent content-type based compression
622
- # Routes content to appropriate compressor (LLMLingua for text, SmartCrusher for JSON,
623
  # CodeCompressor for code, LogCompressor for logs, etc.)
624
  content_router_enabled: bool = True
625
 
 
619
  prefix_freeze: PrefixFreezeConfig = field(default_factory=PrefixFreezeConfig)
620
 
621
  # Content Router - intelligent content-type based compression
622
+ # Routes content to appropriate compressor (Kompress for text, SmartCrusher for JSON,
623
  # CodeCompressor for code, LogCompressor for logs, etc.)
624
  content_router_enabled: bool = True
625
 
headroom/evals/batch_compression_eval.py CHANGED
@@ -353,7 +353,7 @@ def generate_factual_test_cases() -> list[BatchTestCase]:
353
  Context:
354
  The Headroom SDK is a context optimization layer for LLM applications. It was created
355
  by Anthropic in 2024. The main features include SmartCrusher for JSON compression,
356
- LLMLingua for text compression, and CCR (Compress-Cache-Retrieve) for reversible
357
  compression. The SDK supports Python 3.9+ and can save up to 70% of tokens on
358
  large JSON arrays.
359
 
@@ -363,7 +363,7 @@ Question: What percentage of tokens can the SDK save on large JSON arrays?""",
363
  ),
364
  ground_truth="70%",
365
  ground_truth_keywords=["70", "percent", "%"],
366
- context_facts=["70%", "SmartCrusher", "LLMLingua", "CCR", "Python 3.9"],
367
  ),
368
  BatchTestCase(
369
  id="factual_002",
 
353
  Context:
354
  The Headroom SDK is a context optimization layer for LLM applications. It was created
355
  by Anthropic in 2024. The main features include SmartCrusher for JSON compression,
356
+ Kompress for text compression, and CCR (Compress-Cache-Retrieve) for reversible
357
  compression. The SDK supports Python 3.9+ and can save up to 70% of tokens on
358
  large JSON arrays.
359
 
 
363
  ),
364
  ground_truth="70%",
365
  ground_truth_keywords=["70", "percent", "%"],
366
+ context_facts=["70%", "SmartCrusher", "Kompress", "CCR", "Python 3.9"],
367
  ),
368
  BatchTestCase(
369
  id="factual_002",
headroom/evals/html_extraction.py CHANGED
@@ -4,7 +4,6 @@ This module evaluates whether HTMLExtractor preserves the information
4
  that LLMs need to answer questions about web content. We compare:
5
  1. LLM answers from original HTML
6
  2. LLM answers from HTMLExtractor output
7
- 3. LLM answers from LLMLingua baseline (current fallback)
8
 
9
  Uses LLM-as-judge to score answer quality on a 1-5 scale.
10
  """
@@ -81,7 +80,7 @@ class HTMLEvalResult:
81
  # Answers from different methods
82
  answer_from_original: str
83
  answer_from_extracted: str
84
- answer_from_baseline: str | None = None # LLMLingua baseline
85
 
86
  # Judge scores (1-5 scale)
87
  extracted_score: float = 0.0
@@ -211,7 +210,7 @@ class HTMLExtractionEvaluator:
211
  Args:
212
  answer_model: Model for generating answers from content.
213
  judge_model: Model for judging answer quality.
214
- compare_baseline: Whether to also test LLMLingua baseline.
215
  provider: API provider ("openai", "anthropic", "litellm").
216
  """
217
  self.answer_model = answer_model
@@ -221,7 +220,7 @@ class HTMLExtractionEvaluator:
221
 
222
  # Lazy-loaded components
223
  self._extractor: HTMLExtractor | None = None
224
- self._llmlingua: Any = None
225
  self._judge_fn: Callable[[str, str, str], tuple[float, str]] | None = None
226
  self._answer_fn: Any = None
227
 
@@ -235,16 +234,16 @@ class HTMLExtractionEvaluator:
235
  return self._extractor
236
 
237
  @property
238
- def llmlingua(self) -> Any:
239
- """Lazy-load LLMLingua compressor for baseline."""
240
- if self._llmlingua is None and self.compare_baseline:
241
  try:
242
- from headroom.transforms.llmlingua_compressor import LLMLinguaCompressor
243
 
244
- self._llmlingua = LLMLinguaCompressor()
245
  except ImportError:
246
- logger.warning("LLMLingua not available for baseline comparison")
247
- return self._llmlingua
248
 
249
  @property
250
  def judge_fn(self) -> Callable[[str, str, str], tuple[float, str]]:
@@ -429,14 +428,14 @@ Answer concisely and factually based only on the content provided."""
429
  answer_from_extracted,
430
  )
431
 
432
- # Optionally compare with LLMLingua baseline
433
  baseline_answer = None
434
  baseline_score = None
435
  baseline_reasoning = None
436
 
437
- if self.compare_baseline and self.llmlingua:
438
  try:
439
- baseline_result = self.llmlingua.compress(case.html)
440
  baseline_content = baseline_result.compressed
441
  baseline_answer = self._get_answer(baseline_content, case.question)
442
  baseline_score, baseline_reasoning = self.judge_fn(
 
4
  that LLMs need to answer questions about web content. We compare:
5
  1. LLM answers from original HTML
6
  2. LLM answers from HTMLExtractor output
 
7
 
8
  Uses LLM-as-judge to score answer quality on a 1-5 scale.
9
  """
 
80
  # Answers from different methods
81
  answer_from_original: str
82
  answer_from_extracted: str
83
+ answer_from_baseline: str | None = None # Baseline comparison
84
 
85
  # Judge scores (1-5 scale)
86
  extracted_score: float = 0.0
 
210
  Args:
211
  answer_model: Model for generating answers from content.
212
  judge_model: Model for judging answer quality.
213
+ compare_baseline: Whether to also test Kompress baseline.
214
  provider: API provider ("openai", "anthropic", "litellm").
215
  """
216
  self.answer_model = answer_model
 
220
 
221
  # Lazy-loaded components
222
  self._extractor: HTMLExtractor | None = None
223
+ self._kompress: Any = None
224
  self._judge_fn: Callable[[str, str, str], tuple[float, str]] | None = None
225
  self._answer_fn: Any = None
226
 
 
234
  return self._extractor
235
 
236
  @property
237
+ def kompress(self) -> Any:
238
+ """Lazy-load Kompress compressor for baseline."""
239
+ if self._kompress is None and self.compare_baseline:
240
  try:
241
+ from headroom.transforms.kompress_compressor import KompressCompressor
242
 
243
+ self._kompress = KompressCompressor()
244
  except ImportError:
245
+ logger.warning("Kompress not available for baseline comparison")
246
+ return self._kompress
247
 
248
  @property
249
  def judge_fn(self) -> Callable[[str, str, str], tuple[float, str]]:
 
428
  answer_from_extracted,
429
  )
430
 
431
+ # Optionally compare with Kompress baseline
432
  baseline_answer = None
433
  baseline_score = None
434
  baseline_reasoning = None
435
 
436
+ if self.compare_baseline and self.kompress:
437
  try:
438
+ baseline_result = self.kompress.compress(case.html)
439
  baseline_content = baseline_result.compressed
440
  baseline_answer = self._get_answer(baseline_content, case.question)
441
  baseline_score, baseline_reasoning = self.judge_fn(
headroom/models/config.py CHANGED
@@ -12,7 +12,6 @@ Usage:
12
  # Or use environment variables to override at runtime:
13
  # HEADROOM_SENTENCE_TRANSFORMER=intfloat/e5-small-v2
14
  # HEADROOM_SIGLIP=google/siglip-base-patch16-224
15
- # HEADROOM_LLMLINGUA=microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank
16
  """
17
 
18
  from __future__ import annotations
@@ -31,7 +30,6 @@ class MLModelConfig:
31
  Environment variables can override any default:
32
  - HEADROOM_SENTENCE_TRANSFORMER
33
  - HEADROOM_SIGLIP
34
- - HEADROOM_LLMLINGUA
35
  - HEADROOM_SPACY
36
  - HEADROOM_TECHNIQUE_ROUTER
37
 
@@ -47,10 +45,6 @@ class MLModelConfig:
47
  Default: google/siglip-base-patch16-224 (~400MB)
48
  Alternative: google/siglip-so400m-patch14-384 (larger, more accurate)
49
 
50
- llmlingua: Model for ML-based prompt compression.
51
- Default: microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank (~350MB)
52
- Alternative: microsoft/llmlingua-2-xlm-roberta-large-meetingbank (~1GB, slightly more accurate)
53
-
54
  spacy: Model for named entity recognition.
55
  Default: en_core_web_sm (~40MB)
56
  Alternative: en_core_web_md (~120MB, more accurate)
@@ -70,13 +64,6 @@ class MLModelConfig:
70
  default_factory=lambda: os.environ.get("HEADROOM_SIGLIP", "google/siglip-base-patch16-224")
71
  )
72
 
73
- # Prompt Compression (LLMLingua-2)
74
- llmlingua: str = field(
75
- default_factory=lambda: os.environ.get(
76
- "HEADROOM_LLMLINGUA", "microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank"
77
- )
78
- )
79
-
80
  # Named Entity Recognition (spaCy)
81
  spacy: str = field(default_factory=lambda: os.environ.get("HEADROOM_SPACY", "en_core_web_sm"))
82
 
@@ -99,9 +86,6 @@ class MLModelConfig:
99
  "google/siglip-base-patch16-224": 400,
100
  "google/siglip-so400m-patch14-384": 900,
101
  "google/siglip-large-patch16-384": 1200,
102
- # LLMLingua
103
- "microsoft/llmlingua-2-xlm-roberta-large-meetingbank": 1000,
104
- "microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank": 350,
105
  # spaCy
106
  "en_core_web_sm": 40,
107
  "en_core_web_md": 120,
@@ -131,7 +115,6 @@ class MLModelConfig:
131
  return (
132
  self.get_memory_estimate(self.sentence_transformer)
133
  + self.get_memory_estimate(self.siglip)
134
- + self.get_memory_estimate(self.llmlingua)
135
  + self.get_memory_estimate(self.spacy)
136
  + self.get_memory_estimate(self.technique_router)
137
  )
@@ -160,8 +143,3 @@ def get_default_spacy_model() -> str:
160
  def get_default_siglip_model() -> str:
161
  """Get the default SIGLIP model name."""
162
  return ML_MODEL_DEFAULTS.siglip
163
-
164
-
165
- def get_default_llmlingua_model() -> str:
166
- """Get the default LLMLingua model name."""
167
- return ML_MODEL_DEFAULTS.llmlingua
 
12
  # Or use environment variables to override at runtime:
13
  # HEADROOM_SENTENCE_TRANSFORMER=intfloat/e5-small-v2
14
  # HEADROOM_SIGLIP=google/siglip-base-patch16-224
 
15
  """
16
 
17
  from __future__ import annotations
 
30
  Environment variables can override any default:
31
  - HEADROOM_SENTENCE_TRANSFORMER
32
  - HEADROOM_SIGLIP
 
33
  - HEADROOM_SPACY
34
  - HEADROOM_TECHNIQUE_ROUTER
35
 
 
45
  Default: google/siglip-base-patch16-224 (~400MB)
46
  Alternative: google/siglip-so400m-patch14-384 (larger, more accurate)
47
 
 
 
 
 
48
  spacy: Model for named entity recognition.
49
  Default: en_core_web_sm (~40MB)
50
  Alternative: en_core_web_md (~120MB, more accurate)
 
64
  default_factory=lambda: os.environ.get("HEADROOM_SIGLIP", "google/siglip-base-patch16-224")
65
  )
66
 
 
 
 
 
 
 
 
67
  # Named Entity Recognition (spaCy)
68
  spacy: str = field(default_factory=lambda: os.environ.get("HEADROOM_SPACY", "en_core_web_sm"))
69
 
 
86
  "google/siglip-base-patch16-224": 400,
87
  "google/siglip-so400m-patch14-384": 900,
88
  "google/siglip-large-patch16-384": 1200,
 
 
 
89
  # spaCy
90
  "en_core_web_sm": 40,
91
  "en_core_web_md": 120,
 
115
  return (
116
  self.get_memory_estimate(self.sentence_transformer)
117
  + self.get_memory_estimate(self.siglip)
 
118
  + self.get_memory_estimate(self.spacy)
119
  + self.get_memory_estimate(self.technique_router)
120
  )
 
143
  def get_default_siglip_model() -> str:
144
  """Get the default SIGLIP model name."""
145
  return ML_MODEL_DEFAULTS.siglip
 
 
 
 
 
headroom/models/ml_models.py CHANGED
@@ -252,34 +252,6 @@ class MLModelRegistry:
252
  result: tuple[Any, Any] = instance._models[key]
253
  return result
254
 
255
- # =========================================================================
256
- # LLMLingua (uses existing singleton pattern)
257
- # =========================================================================
258
-
259
- @classmethod
260
- def get_llmlingua(cls, device: str | None = None, model_name: str | None = None) -> Any:
261
- """Get the LLMLingua compressor.
262
-
263
- Note: LLMLingua already has its own singleton in llmlingua_compressor.py.
264
- This method delegates to that implementation.
265
-
266
- Args:
267
- device: Device to use. Auto-detected if None.
268
- model_name: Model name (default: microsoft/llmlingua-2-xlm-roberta-large-meetingbank).
269
-
270
- Returns:
271
- PromptCompressor instance.
272
- """
273
- from headroom.transforms.llmlingua_compressor import _get_llmlingua_compressor
274
-
275
- if device is None:
276
- device = cls._detect_device()
277
-
278
- if model_name is None:
279
- model_name = ML_MODEL_DEFAULTS.llmlingua
280
-
281
- return _get_llmlingua_compressor(model_name=model_name, device=device)
282
-
283
  # =========================================================================
284
  # Utility Methods
285
  # =========================================================================
 
252
  result: tuple[Any, Any] = instance._models[key]
253
  return result
254
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  # =========================================================================
256
  # Utility Methods
257
  # =========================================================================
headroom/perf/analyzer.py CHANGED
@@ -562,7 +562,7 @@ def _generate_recommendations(report: PerfReport) -> list[str]:
562
  if len(slow) > len(report.perf_records) * 0.2:
563
  recs.append(
564
  f"{len(slow)} requests took >500ms for optimization — "
565
- "consider disabling LLMLingua or reducing transform pipeline"
566
  )
567
 
568
  if report.router_records:
 
562
  if len(slow) > len(report.perf_records) * 0.2:
563
  recs.append(
564
  f"{len(slow)} requests took >500ms for optimization — "
565
+ "consider reducing transform pipeline"
566
  )
567
 
568
  if report.router_records:
headroom/proxy/server.py CHANGED
@@ -93,7 +93,6 @@ from headroom.telemetry import get_telemetry_collector
93
  from headroom.telemetry.toin import get_toin
94
  from headroom.tokenizers import get_tokenizer
95
  from headroom.transforms import (
96
- _LLMLINGUA_AVAILABLE,
97
  CacheAligner,
98
  CodeAwareCompressor,
99
  CodeCompressorConfig,
@@ -127,10 +126,6 @@ def _get_image_compressor():
127
  return _image_compressor if _image_compressor else None
128
 
129
 
130
- # Conditionally import LLMLingua if available
131
- if _LLMLINGUA_AVAILABLE:
132
- from headroom.transforms import LLMLinguaCompressor, LLMLinguaConfig
133
-
134
  # Try to import LiteLLM for pricing
135
  try:
136
  import litellm
@@ -648,11 +643,6 @@ class ProxyConfig:
648
  ccr_proactive_expansion: bool = True # Proactively expand based on query relevance
649
  ccr_max_proactive_expansions: int = 2 # Max contexts to proactively expand per turn
650
 
651
- # LLMLingua ML-based compression (ON by default if installed)
652
- llmlingua_enabled: bool = True # Enable LLMLingua-2 for ML-based compression
653
- llmlingua_device: str = "auto" # Device: 'auto', 'cuda', 'cpu', 'mps'
654
- llmlingua_target_rate: float = 0.3 # Target compression rate (0.3 = keep 30%)
655
-
656
  # Code-aware compression (ON by default if installed)
657
  code_aware_enabled: bool = True # Enable AST-based code compression
658
 
@@ -1641,9 +1631,8 @@ class HeadroomProxy:
1641
 
1642
  if config.smart_routing:
1643
  # Smart routing: ContentRouter handles all content types intelligently
1644
- # It lazy-loads compressors (including LLMLingua) only when needed
1645
  router_config = ContentRouterConfig(
1646
- enable_llmlingua=config.llmlingua_enabled,
1647
  enable_code_aware=config.code_aware_enabled,
1648
  tool_profiles=config.tool_profiles,
1649
  read_lifecycle=ReadLifecycleConfig(enabled=config.read_lifecycle),
@@ -1656,7 +1645,6 @@ class HeadroomProxy:
1656
  ContentRouter(router_config),
1657
  context_manager,
1658
  ]
1659
- self._llmlingua_status = "lazy" if config.llmlingua_enabled else "disabled"
1660
  self._code_aware_status = "lazy" if config.code_aware_enabled else "disabled"
1661
  else:
1662
  # Legacy mode: sequential pipeline
@@ -1675,8 +1663,6 @@ class HeadroomProxy:
1675
  ),
1676
  context_manager,
1677
  ]
1678
- # Add LLMLingua if enabled and available
1679
- self._llmlingua_status = self._setup_llmlingua(config, transforms)
1680
  # Add CodeAware if enabled and available
1681
  self._code_aware_status = self._setup_code_aware(config, transforms)
1682
 
@@ -1893,38 +1879,6 @@ class HeadroomProxy:
1893
  self._compression_caches[session_id] = CompressionCache()
1894
  return self._compression_caches[session_id]
1895
 
1896
- def _setup_llmlingua(self, config: ProxyConfig, transforms: list) -> str:
1897
- """Set up LLMLingua compression if enabled.
1898
-
1899
- Args:
1900
- config: Proxy configuration
1901
- transforms: Transform list to append to
1902
-
1903
- Returns:
1904
- Status string for logging: 'enabled', 'disabled', 'available', 'unavailable'
1905
- """
1906
- if config.llmlingua_enabled:
1907
- if _LLMLINGUA_AVAILABLE:
1908
- llmlingua_config = LLMLinguaConfig(
1909
- device=config.llmlingua_device,
1910
- target_compression_rate=config.llmlingua_target_rate,
1911
- enable_ccr=config.ccr_inject_tool, # Link to CCR
1912
- )
1913
- # Insert before RollingWindow (which should be last)
1914
- # LLMLingua works best on individual tool outputs before windowing
1915
- transforms.insert(-1, LLMLinguaCompressor(llmlingua_config))
1916
- return "enabled"
1917
- else:
1918
- logger.warning(
1919
- "LLMLingua requested but not installed. "
1920
- "Install with: pip install headroom-ai[llmlingua]"
1921
- )
1922
- return "unavailable"
1923
- else:
1924
- if _LLMLINGUA_AVAILABLE:
1925
- return "available" # Available but not enabled - hint to user
1926
- return "disabled"
1927
-
1928
  def _setup_code_aware(self, config: ProxyConfig, transforms: list) -> str:
1929
  """Set up code-aware compression if enabled.
1930
 
@@ -2013,8 +1967,6 @@ class HeadroomProxy:
2013
  # Update internal status from eager loading results
2014
  if eager_status.get("kompress") == "enabled":
2015
  self._kompress_status = "enabled"
2016
- if eager_status.get("llmlingua") == "enabled":
2017
- self._llmlingua_status = "enabled"
2018
  if eager_status.get("code_aware") == "enabled":
2019
  self._code_aware_status = "enabled"
2020
 
@@ -2024,18 +1976,6 @@ class HeadroomProxy:
2024
  elif self.config.optimize:
2025
  logger.info("Kompress: not installed (pip install headroom-ai[ml] for ML compression)")
2026
 
2027
- if self._llmlingua_status == "enabled":
2028
- logger.info(
2029
- f"LLMLingua: ENABLED (device={self.config.llmlingua_device}, "
2030
- f"rate={self.config.llmlingua_target_rate})"
2031
- )
2032
- elif self._kompress_status == "enabled":
2033
- logger.info("LLMLingua: skipped (Kompress is active)")
2034
- elif self._llmlingua_status == "lazy":
2035
- logger.info("LLMLingua: LAZY (will load when prose content detected)")
2036
- elif self._llmlingua_status == "disabled":
2037
- logger.info("LLMLingua: DISABLED")
2038
-
2039
  if self._code_aware_status == "enabled":
2040
  logger.info("Code-Aware: ENABLED (AST-based compression)")
2041
  if "tree_sitter" in eager_status:
@@ -7969,21 +7909,6 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
7969
  return app
7970
 
7971
 
7972
- def _get_llmlingua_banner_status(config: ProxyConfig) -> str:
7973
- """Get LLMLingua status line for banner."""
7974
- if config.llmlingua_enabled:
7975
- if _LLMLINGUA_AVAILABLE:
7976
- return (
7977
- f"ENABLED (device={config.llmlingua_device}, rate={config.llmlingua_target_rate})"
7978
- )
7979
- else:
7980
- return "NOT INSTALLED (pip install headroom-ai[llmlingua])"
7981
- else:
7982
- if _LLMLINGUA_AVAILABLE:
7983
- return "DISABLED (remove --no-llmlingua to enable)"
7984
- return "DISABLED"
7985
-
7986
-
7987
  def _get_code_aware_banner_status(config: ProxyConfig) -> str:
7988
  """Get code-aware compression status line for banner."""
7989
  if config.code_aware_enabled:
@@ -8016,7 +7941,6 @@ def run_server(
8016
  config = config or ProxyConfig()
8017
  app = create_app(config)
8018
 
8019
- llmlingua_status = _get_llmlingua_banner_status(config)
8020
  code_aware_status = _get_code_aware_banner_status(config)
8021
 
8022
  # Format connection pool info
@@ -8055,7 +7979,6 @@ def run_server(
8055
  ║ Rate Limiting: {"ENABLED " if config.rate_limit_enabled else "DISABLED"} ({config.rate_limit_requests_per_minute} req/min, {config.rate_limit_tokens_per_minute:,} tok/min) ║
8056
  ║ Retry: {"ENABLED " if config.retry_enabled else "DISABLED"} (max {config.retry_max_attempts} attempts) ║
8057
  ║ Cost Tracking: {"ENABLED " if config.cost_tracking_enabled else "DISABLED"} (budget: {"$" + str(config.budget_limit_usd) + "/" + config.budget_period if config.budget_limit_usd else "unlimited"}) ║
8058
- ║ LLMLingua: {llmlingua_status:<52}║
8059
  ║ Code-Aware: {code_aware_status:<52}║
8060
  ║ HTTP/2: {http2_status:<52}║
8061
  ║ Conn Pool: {pool_info:<52}║
@@ -8269,30 +8192,6 @@ if __name__ == "__main__":
8269
  help="Disable smart routing (use legacy sequential pipeline)",
8270
  )
8271
 
8272
- # LLMLingua ML-based compression
8273
- parser.add_argument(
8274
- "--llmlingua",
8275
- action="store_true",
8276
- help="Enable LLMLingua-2 ML-based compression (requires: pip install headroom-ai[llmlingua])",
8277
- )
8278
- parser.add_argument(
8279
- "--no-llmlingua",
8280
- action="store_true",
8281
- help="Disable LLMLingua compression",
8282
- )
8283
- parser.add_argument(
8284
- "--llmlingua-device",
8285
- choices=["auto", "cuda", "cpu", "mps"],
8286
- default="auto",
8287
- help="Device for LLMLingua model (default: auto)",
8288
- )
8289
- parser.add_argument(
8290
- "--llmlingua-rate",
8291
- type=float,
8292
- default=0.3,
8293
- help="LLMLingua target compression rate, 0.0-1.0 (default: 0.3 = keep 30%%)",
8294
- )
8295
-
8296
  # Code-aware compression
8297
  parser.add_argument(
8298
  "--code-aware",
@@ -8310,7 +8209,6 @@ if __name__ == "__main__":
8310
  # Environment variable defaults (HEADROOM_* prefix)
8311
  # CLI args override env vars, env vars override ProxyConfig defaults
8312
  env_smart_routing = _get_env_bool("HEADROOM_SMART_ROUTING", True)
8313
- env_llmlingua = _get_env_bool("HEADROOM_LLMLINGUA_ENABLED", True)
8314
  env_code_aware = _get_env_bool("HEADROOM_CODE_AWARE_ENABLED", True)
8315
  env_optimize = _get_env_bool("HEADROOM_OPTIMIZE", True)
8316
  env_cache = _get_env_bool("HEADROOM_CACHE_ENABLED", True)
@@ -8319,11 +8217,6 @@ if __name__ == "__main__":
8319
  # Determine settings: CLI flags override env vars
8320
  # --no-X explicitly disables, --X explicitly enables, neither uses env var
8321
  smart_routing = env_smart_routing if not args.no_smart_routing else False
8322
- llmlingua_enabled = (
8323
- env_llmlingua
8324
- if not (args.llmlingua or args.no_llmlingua)
8325
- else (args.llmlingua or not args.no_llmlingua)
8326
- )
8327
  code_aware_enabled = (
8328
  env_code_aware
8329
  if not (args.code_aware or args.no_code_aware)
@@ -8364,9 +8257,6 @@ if __name__ == "__main__":
8364
  else os.environ.get("HEADROOM_LOG_FILE"),
8365
  log_full_messages=args.log_messages or _get_env_bool("HEADROOM_LOG_MESSAGES", False),
8366
  smart_routing=smart_routing,
8367
- llmlingua_enabled=llmlingua_enabled,
8368
- llmlingua_device=_get_env_str("HEADROOM_LLMLINGUA_DEVICE", args.llmlingua_device),
8369
- llmlingua_target_rate=_get_env_float("HEADROOM_LLMLINGUA_RATE", args.llmlingua_rate),
8370
  code_aware_enabled=code_aware_enabled,
8371
  # Connection pool settings
8372
  max_connections=_get_env_int("HEADROOM_MAX_CONNECTIONS", args.max_connections),
 
93
  from headroom.telemetry.toin import get_toin
94
  from headroom.tokenizers import get_tokenizer
95
  from headroom.transforms import (
 
96
  CacheAligner,
97
  CodeAwareCompressor,
98
  CodeCompressorConfig,
 
126
  return _image_compressor if _image_compressor else None
127
 
128
 
 
 
 
 
129
  # Try to import LiteLLM for pricing
130
  try:
131
  import litellm
 
643
  ccr_proactive_expansion: bool = True # Proactively expand based on query relevance
644
  ccr_max_proactive_expansions: int = 2 # Max contexts to proactively expand per turn
645
 
 
 
 
 
 
646
  # Code-aware compression (ON by default if installed)
647
  code_aware_enabled: bool = True # Enable AST-based code compression
648
 
 
1631
 
1632
  if config.smart_routing:
1633
  # Smart routing: ContentRouter handles all content types intelligently
1634
+ # It lazy-loads compressors only when needed
1635
  router_config = ContentRouterConfig(
 
1636
  enable_code_aware=config.code_aware_enabled,
1637
  tool_profiles=config.tool_profiles,
1638
  read_lifecycle=ReadLifecycleConfig(enabled=config.read_lifecycle),
 
1645
  ContentRouter(router_config),
1646
  context_manager,
1647
  ]
 
1648
  self._code_aware_status = "lazy" if config.code_aware_enabled else "disabled"
1649
  else:
1650
  # Legacy mode: sequential pipeline
 
1663
  ),
1664
  context_manager,
1665
  ]
 
 
1666
  # Add CodeAware if enabled and available
1667
  self._code_aware_status = self._setup_code_aware(config, transforms)
1668
 
 
1879
  self._compression_caches[session_id] = CompressionCache()
1880
  return self._compression_caches[session_id]
1881
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1882
  def _setup_code_aware(self, config: ProxyConfig, transforms: list) -> str:
1883
  """Set up code-aware compression if enabled.
1884
 
 
1967
  # Update internal status from eager loading results
1968
  if eager_status.get("kompress") == "enabled":
1969
  self._kompress_status = "enabled"
 
 
1970
  if eager_status.get("code_aware") == "enabled":
1971
  self._code_aware_status = "enabled"
1972
 
 
1976
  elif self.config.optimize:
1977
  logger.info("Kompress: not installed (pip install headroom-ai[ml] for ML compression)")
1978
 
 
 
 
 
 
 
 
 
 
 
 
 
1979
  if self._code_aware_status == "enabled":
1980
  logger.info("Code-Aware: ENABLED (AST-based compression)")
1981
  if "tree_sitter" in eager_status:
 
7909
  return app
7910
 
7911
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7912
  def _get_code_aware_banner_status(config: ProxyConfig) -> str:
7913
  """Get code-aware compression status line for banner."""
7914
  if config.code_aware_enabled:
 
7941
  config = config or ProxyConfig()
7942
  app = create_app(config)
7943
 
 
7944
  code_aware_status = _get_code_aware_banner_status(config)
7945
 
7946
  # Format connection pool info
 
7979
  ║ Rate Limiting: {"ENABLED " if config.rate_limit_enabled else "DISABLED"} ({config.rate_limit_requests_per_minute} req/min, {config.rate_limit_tokens_per_minute:,} tok/min) ║
7980
  ║ Retry: {"ENABLED " if config.retry_enabled else "DISABLED"} (max {config.retry_max_attempts} attempts) ║
7981
  ║ Cost Tracking: {"ENABLED " if config.cost_tracking_enabled else "DISABLED"} (budget: {"$" + str(config.budget_limit_usd) + "/" + config.budget_period if config.budget_limit_usd else "unlimited"}) ║
 
7982
  ║ Code-Aware: {code_aware_status:<52}║
7983
  ║ HTTP/2: {http2_status:<52}║
7984
  ║ Conn Pool: {pool_info:<52}║
 
8192
  help="Disable smart routing (use legacy sequential pipeline)",
8193
  )
8194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8195
  # Code-aware compression
8196
  parser.add_argument(
8197
  "--code-aware",
 
8209
  # Environment variable defaults (HEADROOM_* prefix)
8210
  # CLI args override env vars, env vars override ProxyConfig defaults
8211
  env_smart_routing = _get_env_bool("HEADROOM_SMART_ROUTING", True)
 
8212
  env_code_aware = _get_env_bool("HEADROOM_CODE_AWARE_ENABLED", True)
8213
  env_optimize = _get_env_bool("HEADROOM_OPTIMIZE", True)
8214
  env_cache = _get_env_bool("HEADROOM_CACHE_ENABLED", True)
 
8217
  # Determine settings: CLI flags override env vars
8218
  # --no-X explicitly disables, --X explicitly enables, neither uses env var
8219
  smart_routing = env_smart_routing if not args.no_smart_routing else False
 
 
 
 
 
8220
  code_aware_enabled = (
8221
  env_code_aware
8222
  if not (args.code_aware or args.no_code_aware)
 
8257
  else os.environ.get("HEADROOM_LOG_FILE"),
8258
  log_full_messages=args.log_messages or _get_env_bool("HEADROOM_LOG_MESSAGES", False),
8259
  smart_routing=smart_routing,
 
 
 
8260
  code_aware_enabled=code_aware_enabled,
8261
  # Connection pool settings
8262
  max_connections=_get_env_int("HEADROOM_MAX_CONNECTIONS", args.max_connections),
headroom/transforms/__init__.py CHANGED
@@ -25,21 +25,6 @@ from .search_compressor import (
25
  from .smart_crusher import SmartCrusher, SmartCrusherConfig
26
  from .tool_crusher import ToolCrusher
27
 
28
- # ML-based compression (optional dependency)
29
- try:
30
- from .llmlingua_compressor import ( # noqa: F401
31
- LLMLinguaCompressor,
32
- LLMLinguaConfig,
33
- LLMLinguaResult,
34
- compress_with_llmlingua,
35
- is_llmlingua_model_loaded,
36
- unload_llmlingua_model,
37
- )
38
-
39
- _LLMLINGUA_AVAILABLE = True
40
- except ImportError:
41
- _LLMLINGUA_AVAILABLE = False
42
-
43
  # HTML content extraction (optional dependency - requires trafilatura)
44
  try:
45
  from .html_extractor import ( # noqa: F401
@@ -122,25 +107,10 @@ __all__ = [
122
  "MessageScorer",
123
  "MessageScore",
124
  "EmbeddingProvider",
125
- # ML-based compression (optional)
126
- "_LLMLINGUA_AVAILABLE",
127
  # HTML extraction (optional)
128
  "_HTML_EXTRACTOR_AVAILABLE",
129
  ]
130
 
131
- # Conditionally add LLMLingua exports
132
- if _LLMLINGUA_AVAILABLE:
133
- __all__.extend(
134
- [
135
- "LLMLinguaCompressor",
136
- "LLMLinguaConfig",
137
- "LLMLinguaResult",
138
- "compress_with_llmlingua",
139
- "is_llmlingua_model_loaded",
140
- "unload_llmlingua_model",
141
- ]
142
- )
143
-
144
  # Conditionally add HTML extractor exports
145
  if _HTML_EXTRACTOR_AVAILABLE:
146
  __all__.extend(
 
25
  from .smart_crusher import SmartCrusher, SmartCrusherConfig
26
  from .tool_crusher import ToolCrusher
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # HTML content extraction (optional dependency - requires trafilatura)
29
  try:
30
  from .html_extractor import ( # noqa: F401
 
107
  "MessageScorer",
108
  "MessageScore",
109
  "EmbeddingProvider",
 
 
110
  # HTML extraction (optional)
111
  "_HTML_EXTRACTOR_AVAILABLE",
112
  ]
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  # Conditionally add HTML extractor exports
115
  if _HTML_EXTRACTOR_AVAILABLE:
116
  __all__.extend(
headroom/transforms/code_compressor.py CHANGED
@@ -1,8 +1,8 @@
1
  """Code-aware compressor using AST parsing for syntax-preserving compression.
2
 
3
  This module provides AST-based compression for source code that guarantees
4
- valid syntax output. Unlike token-level compression (LLMLingua), this
5
- preserves structural elements while compressing function bodies.
6
 
7
  Key Features:
8
  - Syntax validity guaranteed (output always parses)
@@ -329,7 +329,7 @@ class CodeCompressorConfig:
329
  compress_comments: Remove non-docstring comments.
330
  min_tokens_for_compression: Minimum tokens to trigger compression.
331
  language_hint: Explicit language (None = auto-detect).
332
- fallback_to_llmlingua: Use LLMLingua for unknown languages.
333
  enable_ccr: Store originals for retrieval.
334
  ccr_ttl: TTL for CCR entries in seconds.
335
  """
@@ -351,7 +351,7 @@ class CodeCompressorConfig:
351
 
352
  # Language handling
353
  language_hint: str | None = None
354
- fallback_to_llmlingua: bool = True
355
 
356
  # Semantic analysis (symbol importance scoring)
357
  semantic_analysis: bool = True
@@ -615,7 +615,7 @@ class CodeAwareCompressor(Transform):
615
  - Syntax validity guaranteed
616
  - Preserves imports, signatures, types
617
  - Better compression ratios for code (5-8x vs 3-5x)
618
- - Lower latency (~20-50ms vs 50-200ms for LLMLingua)
619
  - Smaller memory footprint (~50MB vs ~1GB)
620
  - Thread-safe (no mutable instance state during compression)
621
 
@@ -947,9 +947,9 @@ class CodeAwareCompressor(Transform):
947
  else:
948
  detected_lang, confidence = detect_language(code)
949
 
950
- # If language unknown and fallback enabled, try LLMLingua
951
  if detected_lang == CodeLanguage.UNKNOWN:
952
- if self.config.fallback_to_llmlingua:
953
  return self._fallback_compress(code, original_tokens)
954
  else:
955
  return CodeCompressionResult(
@@ -966,7 +966,7 @@ class CodeAwareCompressor(Transform):
966
  # Check if tree-sitter is available
967
  if not _check_tree_sitter_available():
968
  logger.warning("tree-sitter not available. Install with: pip install headroom-ai[code]")
969
- if self.config.fallback_to_llmlingua:
970
  return self._fallback_compress(code, original_tokens)
971
  return CodeCompressionResult(
972
  compressed=code,
@@ -1062,7 +1062,7 @@ class CodeAwareCompressor(Transform):
1062
 
1063
  except Exception as e:
1064
  logger.warning("AST compression failed: %s, falling back", e)
1065
- if self.config.fallback_to_llmlingua:
1066
  return self._fallback_compress(code, original_tokens)
1067
  return CodeCompressionResult(
1068
  compressed=code,
@@ -1629,13 +1629,13 @@ class CodeAwareCompressor(Transform):
1629
  return False
1630
 
1631
  def _fallback_compress(self, code: str, original_tokens: int) -> CodeCompressionResult:
1632
- """Fall back to LLMLingua compression."""
1633
  try:
1634
- from .llmlingua_compressor import LLMLinguaCompressor, _check_llmlingua_available
1635
 
1636
- if _check_llmlingua_available():
1637
- compressor = LLMLinguaCompressor()
1638
- result = compressor.compress(code, content_type="code")
1639
  return CodeCompressionResult(
1640
  compressed=result.compressed,
1641
  original=code,
@@ -1644,7 +1644,7 @@ class CodeAwareCompressor(Transform):
1644
  compression_ratio=result.compression_ratio,
1645
  language=CodeLanguage.UNKNOWN,
1646
  language_confidence=0.0,
1647
- # LLMLingua does NOT guarantee syntax validity
1648
  syntax_valid=False,
1649
  )
1650
  except ImportError:
 
1
  """Code-aware compressor using AST parsing for syntax-preserving compression.
2
 
3
  This module provides AST-based compression for source code that guarantees
4
+ valid syntax output. Unlike token-level compression, this preserves
5
+ structural elements while compressing function bodies.
6
 
7
  Key Features:
8
  - Syntax validity guaranteed (output always parses)
 
329
  compress_comments: Remove non-docstring comments.
330
  min_tokens_for_compression: Minimum tokens to trigger compression.
331
  language_hint: Explicit language (None = auto-detect).
332
+ fallback_to_kompress: Use Kompress for unknown languages.
333
  enable_ccr: Store originals for retrieval.
334
  ccr_ttl: TTL for CCR entries in seconds.
335
  """
 
351
 
352
  # Language handling
353
  language_hint: str | None = None
354
+ fallback_to_kompress: bool = True
355
 
356
  # Semantic analysis (symbol importance scoring)
357
  semantic_analysis: bool = True
 
615
  - Syntax validity guaranteed
616
  - Preserves imports, signatures, types
617
  - Better compression ratios for code (5-8x vs 3-5x)
618
+ - Lower latency (~20-50ms vs 50-200ms for token-level compressors)
619
  - Smaller memory footprint (~50MB vs ~1GB)
620
  - Thread-safe (no mutable instance state during compression)
621
 
 
947
  else:
948
  detected_lang, confidence = detect_language(code)
949
 
950
+ # If language unknown and fallback enabled, try Kompress
951
  if detected_lang == CodeLanguage.UNKNOWN:
952
+ if self.config.fallback_to_kompress:
953
  return self._fallback_compress(code, original_tokens)
954
  else:
955
  return CodeCompressionResult(
 
966
  # Check if tree-sitter is available
967
  if not _check_tree_sitter_available():
968
  logger.warning("tree-sitter not available. Install with: pip install headroom-ai[code]")
969
+ if self.config.fallback_to_kompress:
970
  return self._fallback_compress(code, original_tokens)
971
  return CodeCompressionResult(
972
  compressed=code,
 
1062
 
1063
  except Exception as e:
1064
  logger.warning("AST compression failed: %s, falling back", e)
1065
+ if self.config.fallback_to_kompress:
1066
  return self._fallback_compress(code, original_tokens)
1067
  return CodeCompressionResult(
1068
  compressed=code,
 
1629
  return False
1630
 
1631
  def _fallback_compress(self, code: str, original_tokens: int) -> CodeCompressionResult:
1632
+ """Fall back to Kompress compression."""
1633
  try:
1634
+ from .kompress_compressor import KompressCompressor, is_kompress_available
1635
 
1636
+ if is_kompress_available():
1637
+ compressor = KompressCompressor()
1638
+ result = compressor.compress(code)
1639
  return CodeCompressionResult(
1640
  compressed=result.compressed,
1641
  original=code,
 
1644
  compression_ratio=result.compression_ratio,
1645
  language=CodeLanguage.UNKNOWN,
1646
  language_confidence=0.0,
1647
+ # Kompress does NOT guarantee syntax validity
1648
  syntax_valid=False,
1649
  )
1650
  except ImportError:
headroom/transforms/content_detector.py CHANGED
@@ -224,7 +224,7 @@ def _try_detect_html(content: str) -> DetectionResult | None:
224
  """Try to detect HTML content.
225
 
226
  HTML needs content extraction (removing scripts, styles, nav, etc.),
227
- not token-level compression like LLMLingua.
228
  """
229
  # Check first 3000 chars for HTML indicators
230
  sample = content[:3000]
 
224
  """Try to detect HTML content.
225
 
226
  HTML needs content extraction (removing scripts, styles, nav, etc.),
227
+ not token-level compression like Kompress.
228
  """
229
  # Check first 3000 chars for HTML indicators
230
  sample = content[:3000]
headroom/transforms/content_router.py CHANGED
@@ -9,7 +9,7 @@ Supported Compressors:
9
  - SmartCrusher: JSON arrays
10
  - SearchCompressor: grep/ripgrep results
11
  - LogCompressor: Build/test output
12
- - LLMLinguaCompressor: Plain text (ML-based)
13
  - Kompress: Plain text (ML-based, requires [ml] extra)
14
 
15
  Routing Strategy:
@@ -251,7 +251,6 @@ class CompressionStrategy(Enum):
251
  SEARCH = "search"
252
  LOG = "log"
253
  KOMPRESS = "kompress"
254
- LLMLINGUA = "llmlingua"
255
  TEXT = "text"
256
  DIFF = "diff"
257
  HTML = "html"
@@ -360,12 +359,11 @@ class ContentRouterConfig:
360
 
361
  Attributes:
362
  enable_code_aware: Enable AST-based code compression.
363
- enable_llmlingua: Enable ML-based text compression.
364
  enable_smart_crusher: Enable JSON array compression.
365
  enable_search_compressor: Enable search result compression.
366
  enable_log_compressor: Enable build/test log compression.
367
  enable_image_optimizer: Enable image token optimization.
368
- prefer_code_aware_for_code: Use CodeAware over LLMLingua for code.
369
  mixed_content_threshold: Min distinct types to consider "mixed".
370
  min_section_tokens: Minimum tokens for a section to compress.
371
  fallback_strategy: Strategy when no compressor matches.
@@ -376,8 +374,7 @@ class ContentRouterConfig:
376
 
377
  # Enable/disable specific compressors
378
  enable_code_aware: bool = True
379
- enable_kompress: bool = True # Kompress: ModernBERT token compressor (preferred over LLMLingua)
380
- enable_llmlingua: bool = True
381
  enable_smart_crusher: bool = True
382
  enable_search_compressor: bool = True
383
  enable_log_compressor: bool = True
@@ -647,7 +644,6 @@ class ContentRouter(Transform):
647
  self._diff_compressor: Any = None
648
  self._html_extractor: Any = None
649
  self._kompress: Any = None
650
- self._llmlingua: Any = None
651
  self._image_optimizer: Any = None
652
 
653
  # TOIN integration for cross-strategy learning
@@ -813,7 +809,7 @@ class ContentRouter(Transform):
813
  strategy == CompressionStrategy.CODE_AWARE
814
  and not self.config.prefer_code_aware_for_code
815
  ):
816
- strategy = CompressionStrategy.LLMLINGUA
817
 
818
  return strategy
819
 
@@ -960,9 +956,11 @@ class ContentRouter(Transform):
960
  result = compressor.compress(content, language=language, context=context)
961
  compressed, compressed_tokens = result.compressed, result.compressed_tokens
962
  if compressed is None:
963
- # Fallback to LLMLingua
964
- compressed, compressed_tokens = self._try_llmlingua(content, context, question)
965
- strategy = CompressionStrategy.LLMLINGUA # Update for TOIN
 
 
966
 
967
  elif strategy == CompressionStrategy.SMART_CRUSHER:
968
  # SmartCrusher handles its own TOIN recording
@@ -1013,12 +1011,9 @@ class ContentRouter(Transform):
1013
  elif strategy == CompressionStrategy.KOMPRESS:
1014
  compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
1015
 
1016
- elif strategy == CompressionStrategy.LLMLINGUA:
1017
- compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
1018
-
1019
  elif strategy == CompressionStrategy.TEXT:
1020
- # Prefer ML compressor (Kompress > LLMLingua) for text
1021
- # Passes through unchanged if neither Kompress nor LLMLingua available
1022
  compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
1023
 
1024
  except Exception as e:
@@ -1043,7 +1038,7 @@ class ContentRouter(Transform):
1043
  def _try_ml_compressor(
1044
  self, content: str, context: str, question: str | None = None
1045
  ) -> tuple[str, int]:
1046
- """ML-based compression: Kompress (primary), LLMLingua (fallback only).
1047
 
1048
  Kompress (ModernBERT, trained on 330K structured tool outputs)
1049
  auto-downloads from HuggingFace on first use. No heuristic fallback.
@@ -1090,19 +1085,6 @@ class ContentRouter(Transform):
1090
  except Exception as e:
1091
  logger.warning("Kompress failed: %s", e)
1092
 
1093
- # Fallback: LLMLingua (only if Kompress not installed)
1094
- if compressed is None and self.config.enable_llmlingua:
1095
- compressor = self._get_llmlingua()
1096
- if compressor:
1097
- try:
1098
- result = compressor.compress(
1099
- text_to_compress, context=context, question=question
1100
- )
1101
- compressed = result.compressed
1102
- compressed_tokens = result.compressed_tokens
1103
- except Exception as e:
1104
- logger.warning("LLMLingua failed: %s", e)
1105
-
1106
  if compressed is None:
1107
  return content, len(content.split())
1108
 
@@ -1113,9 +1095,6 @@ class ContentRouter(Transform):
1113
 
1114
  return compressed, compressed_tokens or len(compressed.split())
1115
 
1116
- # Backwards compatibility
1117
- _try_llmlingua = _try_ml_compressor
1118
-
1119
  def _strategy_from_detection_type(self, content_type: ContentType) -> CompressionStrategy:
1120
  """Get strategy from ContentType enum."""
1121
  mapping = {
@@ -1140,7 +1119,6 @@ class ContentRouter(Transform):
1140
  CompressionStrategy.HTML: ContentType.HTML,
1141
  CompressionStrategy.TEXT: ContentType.PLAIN_TEXT,
1142
  CompressionStrategy.KOMPRESS: ContentType.PLAIN_TEXT,
1143
- CompressionStrategy.LLMLINGUA: ContentType.PLAIN_TEXT,
1144
  CompressionStrategy.PASSTHROUGH: ContentType.PLAIN_TEXT,
1145
  }
1146
  return mapping.get(strategy, ContentType.PLAIN_TEXT)
@@ -1233,7 +1211,7 @@ class ContentRouter(Transform):
1233
  """
1234
  status: dict[str, str] = {}
1235
 
1236
- # 1. ML text compressor: Kompress or LLMLingua fallback
1237
  if self.config.enable_kompress:
1238
  compressor = self._get_kompress()
1239
  if compressor:
@@ -1241,20 +1219,6 @@ class ContentRouter(Transform):
1241
  status["kompress"] = "enabled"
1242
  else:
1243
  status["kompress"] = "unavailable"
1244
- if "kompress" not in status or status["kompress"] != "enabled":
1245
- if self.config.enable_llmlingua:
1246
- compressor = self._get_llmlingua()
1247
- if compressor:
1248
- try:
1249
- from .llmlingua_compressor import _get_llmlingua_compressor
1250
-
1251
- device = compressor._resolve_device()
1252
- _get_llmlingua_compressor(compressor.config.model_name, device)
1253
- logger.info("LLMLingua model pre-loaded at startup")
1254
- status["llmlingua"] = "enabled"
1255
- except Exception as e:
1256
- logger.warning("Failed to pre-load LLMLingua model: %s", e)
1257
- status["llmlingua"] = f"failed: {e}"
1258
 
1259
  # 2. Magika content detector (avoids 100-200ms on first content detection)
1260
  try:
@@ -1326,21 +1290,6 @@ class ContentRouter(Transform):
1326
  logger.debug("Kompress dependencies not available")
1327
  return self._kompress
1328
 
1329
- def _get_llmlingua(self) -> Any:
1330
- """Get LLMLinguaCompressor (lazy load). Fallback if Kompress unavailable."""
1331
- if self._llmlingua is None:
1332
- try:
1333
- from .llmlingua_compressor import (
1334
- LLMLinguaCompressor,
1335
- _check_llmlingua_available,
1336
- )
1337
-
1338
- if _check_llmlingua_available():
1339
- self._llmlingua = LLMLinguaCompressor()
1340
- except ImportError:
1341
- logger.debug("LLMLinguaCompressor not available")
1342
- return self._llmlingua
1343
-
1344
  def _get_image_optimizer(self) -> Any:
1345
  """Get ImageCompressor (lazy load).
1346
 
@@ -1558,7 +1507,7 @@ class ContentRouter(Transform):
1558
  "non_string": 0,
1559
  "content_blocks": 0,
1560
  }
1561
- compressed_details: list[str] = [] # e.g. ["code_aware:0.72", "llmlingua:0.65"]
1562
 
1563
  # Check for analysis intent in the most recent user message
1564
  analysis_intent = False
 
9
  - SmartCrusher: JSON arrays
10
  - SearchCompressor: grep/ripgrep results
11
  - LogCompressor: Build/test output
12
+ - KompressCompressor: Plain text (ML-based)
13
  - Kompress: Plain text (ML-based, requires [ml] extra)
14
 
15
  Routing Strategy:
 
251
  SEARCH = "search"
252
  LOG = "log"
253
  KOMPRESS = "kompress"
 
254
  TEXT = "text"
255
  DIFF = "diff"
256
  HTML = "html"
 
359
 
360
  Attributes:
361
  enable_code_aware: Enable AST-based code compression.
 
362
  enable_smart_crusher: Enable JSON array compression.
363
  enable_search_compressor: Enable search result compression.
364
  enable_log_compressor: Enable build/test log compression.
365
  enable_image_optimizer: Enable image token optimization.
366
+ prefer_code_aware_for_code: Use CodeAware over Kompress for code.
367
  mixed_content_threshold: Min distinct types to consider "mixed".
368
  min_section_tokens: Minimum tokens for a section to compress.
369
  fallback_strategy: Strategy when no compressor matches.
 
374
 
375
  # Enable/disable specific compressors
376
  enable_code_aware: bool = True
377
+ enable_kompress: bool = True # Kompress: ModernBERT token compressor
 
378
  enable_smart_crusher: bool = True
379
  enable_search_compressor: bool = True
380
  enable_log_compressor: bool = True
 
644
  self._diff_compressor: Any = None
645
  self._html_extractor: Any = None
646
  self._kompress: Any = None
 
647
  self._image_optimizer: Any = None
648
 
649
  # TOIN integration for cross-strategy learning
 
809
  strategy == CompressionStrategy.CODE_AWARE
810
  and not self.config.prefer_code_aware_for_code
811
  ):
812
+ strategy = CompressionStrategy.KOMPRESS
813
 
814
  return strategy
815
 
 
956
  result = compressor.compress(content, language=language, context=context)
957
  compressed, compressed_tokens = result.compressed, result.compressed_tokens
958
  if compressed is None:
959
+ # Fallback to Kompress
960
+ compressed, compressed_tokens = self._try_ml_compressor(
961
+ content, context, question
962
+ )
963
+ strategy = CompressionStrategy.KOMPRESS # Update for TOIN
964
 
965
  elif strategy == CompressionStrategy.SMART_CRUSHER:
966
  # SmartCrusher handles its own TOIN recording
 
1011
  elif strategy == CompressionStrategy.KOMPRESS:
1012
  compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
1013
 
 
 
 
1014
  elif strategy == CompressionStrategy.TEXT:
1015
+ # Prefer Kompress ML compressor for text
1016
+ # Passes through unchanged if Kompress not available
1017
  compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
1018
 
1019
  except Exception as e:
 
1038
  def _try_ml_compressor(
1039
  self, content: str, context: str, question: str | None = None
1040
  ) -> tuple[str, int]:
1041
+ """ML-based compression using Kompress.
1042
 
1043
  Kompress (ModernBERT, trained on 330K structured tool outputs)
1044
  auto-downloads from HuggingFace on first use. No heuristic fallback.
 
1085
  except Exception as e:
1086
  logger.warning("Kompress failed: %s", e)
1087
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1088
  if compressed is None:
1089
  return content, len(content.split())
1090
 
 
1095
 
1096
  return compressed, compressed_tokens or len(compressed.split())
1097
 
 
 
 
1098
  def _strategy_from_detection_type(self, content_type: ContentType) -> CompressionStrategy:
1099
  """Get strategy from ContentType enum."""
1100
  mapping = {
 
1119
  CompressionStrategy.HTML: ContentType.HTML,
1120
  CompressionStrategy.TEXT: ContentType.PLAIN_TEXT,
1121
  CompressionStrategy.KOMPRESS: ContentType.PLAIN_TEXT,
 
1122
  CompressionStrategy.PASSTHROUGH: ContentType.PLAIN_TEXT,
1123
  }
1124
  return mapping.get(strategy, ContentType.PLAIN_TEXT)
 
1211
  """
1212
  status: dict[str, str] = {}
1213
 
1214
+ # 1. ML text compressor: Kompress
1215
  if self.config.enable_kompress:
1216
  compressor = self._get_kompress()
1217
  if compressor:
 
1219
  status["kompress"] = "enabled"
1220
  else:
1221
  status["kompress"] = "unavailable"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1222
 
1223
  # 2. Magika content detector (avoids 100-200ms on first content detection)
1224
  try:
 
1290
  logger.debug("Kompress dependencies not available")
1291
  return self._kompress
1292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1293
  def _get_image_optimizer(self) -> Any:
1294
  """Get ImageCompressor (lazy load).
1295
 
 
1507
  "non_string": 0,
1508
  "content_blocks": 0,
1509
  }
1510
+ compressed_details: list[str] = [] # e.g. ["code_aware:0.72", "kompress:0.65"]
1511
 
1512
  # Check for analysis intent in the most recent user message
1513
  analysis_intent = False
headroom/transforms/intelligent_context.py CHANGED
@@ -501,7 +501,6 @@ class IntelligentContextManager(Transform):
501
  # Configure for aggressive compression in COMPRESS_FIRST context
502
  router_config = ContentRouterConfig(
503
  enable_code_aware=True,
504
- enable_llmlingua=True,
505
  enable_smart_crusher=True,
506
  enable_search_compressor=True,
507
  enable_log_compressor=True,
 
501
  # Configure for aggressive compression in COMPRESS_FIRST context
502
  router_config = ContentRouterConfig(
503
  enable_code_aware=True,
 
504
  enable_smart_crusher=True,
505
  enable_search_compressor=True,
506
  enable_log_compressor=True,
headroom/transforms/kompress_compressor.py CHANGED
@@ -1,7 +1,7 @@
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
  Requires the [ml] extra: pip install headroom-ai[ml]
7
 
@@ -222,7 +222,6 @@ class KompressCompressor(Transform):
222
  """Kompress: ModernBERT token compressor for structured tool outputs.
223
 
224
  Auto-downloads chopratejas/kompress-base from HuggingFace on first use.
225
- Drop-in replacement for LLMLinguaCompressor with identical interface.
226
  """
227
 
228
  name: str = "kompress_compressor"
@@ -242,9 +241,9 @@ class KompressCompressor(Transform):
242
 
243
  Args:
244
  content: Text to compress.
245
- context: Optional surrounding context (unused by model, kept for interface compat).
246
  content_type: Ignored — model decides importance per content type.
247
- question: Ignored — kept for LLMLingua interface compat.
248
  target_ratio: If None (default), model decides how much to keep using
249
  score threshold. If set (e.g. 0.3), forces that keep ratio.
250
  The proxy never sets this — only user-facing API does.
 
1
  """Kompress: ModernBERT token compressor for structured tool outputs.
2
 
3
+ Auto-downloads the model from HuggingFace (chopratejas/kompress-base)
4
+ on first use.
5
 
6
  Requires the [ml] extra: pip install headroom-ai[ml]
7
 
 
222
  """Kompress: ModernBERT token compressor for structured tool outputs.
223
 
224
  Auto-downloads chopratejas/kompress-base from HuggingFace on first use.
 
225
  """
226
 
227
  name: str = "kompress_compressor"
 
241
 
242
  Args:
243
  content: Text to compress.
244
+ context: Optional surrounding context (unused by model).
245
  content_type: Ignored — model decides importance per content type.
246
+ question: Ignored — reserved for future QA-aware compression.
247
  target_ratio: If None (default), model decides how much to keep using
248
  score threshold. If set (e.g. 0.3), forces that keep ratio.
249
  The proxy never sets this — only user-facing API does.
headroom/transforms/llmlingua_compressor.py DELETED
@@ -1,652 +0,0 @@
1
- """LLMLingua-2 compressor for ML-based prompt compression.
2
-
3
- This module provides integration with LLMLingua-2, a BERT-based token classifier
4
- trained via GPT-4 distillation. It achieves superior compression (up to 20x)
5
- while maintaining high fidelity on tool outputs and structured content.
6
-
7
- Key Features:
8
- - Token-level classification (keep/remove) using fine-tuned BERT
9
- - 3-6x faster than LLMLingua-1 with better results
10
- - Especially effective on tool outputs, code, and structured data
11
- - Reversible compression via CCR integration
12
-
13
- Reference:
14
- LLMLingua-2: Data Distillation for Efficient and Faithful Task-Agnostic Prompt Compression
15
- https://arxiv.org/abs/2403.12968
16
-
17
- Installation:
18
- pip install headroom-ai[llmlingua]
19
-
20
- Usage:
21
- >>> from headroom.transforms import LLMLinguaCompressor
22
- >>> compressor = LLMLinguaCompressor()
23
- >>> result = compressor.compress(long_tool_output)
24
- >>> print(result.compressed) # Significantly reduced output
25
- """
26
-
27
- from __future__ import annotations
28
-
29
- import logging
30
- import threading
31
- from dataclasses import dataclass, field
32
- from typing import Any
33
-
34
- from ..config import TransformResult
35
- from ..models.config import ML_MODEL_DEFAULTS
36
- from ..tokenizer import Tokenizer
37
- from .base import Transform
38
-
39
- logger = logging.getLogger(__name__)
40
-
41
- # Lazy import for optional dependency
42
- _llmlingua_available: bool | None = None
43
- _llmlingua_instance: Any = None
44
- _llmlingua_lock = threading.Lock() # Thread safety for model access
45
-
46
-
47
- def _check_llmlingua_available() -> bool:
48
- """Check if llmlingua package is available."""
49
- global _llmlingua_available
50
- if _llmlingua_available is None:
51
- try:
52
- import llmlingua # noqa: F401
53
-
54
- _llmlingua_available = True
55
- except ImportError:
56
- _llmlingua_available = False
57
- return _llmlingua_available
58
-
59
-
60
- def _get_llmlingua_compressor(model_name: str, device: str) -> Any:
61
- """Get or create the LLMLingua compressor instance.
62
-
63
- Uses lazy initialization and caches the instance to avoid repeated model loading.
64
- Thread-safe: uses lock to prevent race conditions during model initialization.
65
-
66
- Args:
67
- model_name: HuggingFace model name for the compressor.
68
- device: Device to run the model on ('cuda', 'cpu', or 'auto').
69
-
70
- Returns:
71
- PromptCompressor instance from llmlingua.
72
-
73
- Raises:
74
- ImportError: If llmlingua is not installed.
75
- RuntimeError: If model loading fails.
76
- """
77
- global _llmlingua_instance
78
-
79
- if not _check_llmlingua_available():
80
- raise ImportError(
81
- "llmlingua is not installed. Install with: pip install headroom-ai[llmlingua]\n"
82
- "Note: This requires ~2GB of disk space and ~1GB RAM for the model."
83
- )
84
-
85
- with _llmlingua_lock:
86
- # Double-check after acquiring lock
87
- if _llmlingua_instance is None or _llmlingua_instance._model_name != model_name:
88
- try:
89
- from llmlingua import PromptCompressor
90
-
91
- logger.info(
92
- "Loading LLMLingua-2 model: %s on device: %s "
93
- "(this may take 10-30s on first run)",
94
- model_name,
95
- device,
96
- )
97
- _llmlingua_instance = PromptCompressor(
98
- model_name=model_name,
99
- device_map=device,
100
- use_llmlingua2=True, # Use LLMLingua-2 (BERT classifier)
101
- )
102
- # Store model name for later comparison
103
- _llmlingua_instance._model_name = model_name
104
- logger.info("LLMLingua-2 model loaded successfully")
105
-
106
- except Exception as e:
107
- error_msg = str(e).lower()
108
- if "out of memory" in error_msg or "oom" in error_msg:
109
- raise RuntimeError(
110
- f"Out of memory loading LLMLingua model. Try:\n"
111
- f" 1. Use device='cpu' instead of 'cuda'\n"
112
- f" 2. Close other GPU applications\n"
113
- f" 3. Use a smaller model\n"
114
- f"Original error: {e}"
115
- ) from e
116
- elif "not found" in error_msg or "404" in error_msg:
117
- raise RuntimeError(
118
- f"Model '{model_name}' not found on HuggingFace. Try:\n"
119
- f" 1. Check the model name is correct\n"
120
- f" 2. Use default: 'microsoft/llmlingua-2-xlm-roberta-large-meetingbank'\n"
121
- f"Original error: {e}"
122
- ) from e
123
- else:
124
- raise RuntimeError(
125
- f"Failed to load LLMLingua model: {e}\n"
126
- f"Ensure you have sufficient disk space and memory."
127
- ) from e
128
-
129
- return _llmlingua_instance
130
-
131
-
132
- def unload_llmlingua_model() -> bool:
133
- """Unload the LLMLingua model to free memory.
134
-
135
- Use this when you're done with compression and want to reclaim GPU/CPU memory.
136
- The model will be reloaded automatically on the next compression call.
137
-
138
- Returns:
139
- True if a model was unloaded, False if no model was loaded.
140
-
141
- Example:
142
- >>> from headroom.transforms import LLMLinguaCompressor, unload_llmlingua_model
143
- >>> compressor = LLMLinguaCompressor()
144
- >>> result = compressor.compress(content) # Model loaded here
145
- >>> # ... do other work ...
146
- >>> unload_llmlingua_model() # Free ~1GB of memory
147
- """
148
- global _llmlingua_instance
149
-
150
- with _llmlingua_lock:
151
- if _llmlingua_instance is not None:
152
- model_name = getattr(_llmlingua_instance, "_model_name", "unknown")
153
- logger.info("Unloading LLMLingua model: %s", model_name)
154
-
155
- # Clear the instance
156
- _llmlingua_instance = None
157
-
158
- # Attempt to free GPU memory if torch is available
159
- try:
160
- import torch
161
-
162
- if torch.cuda.is_available():
163
- torch.cuda.empty_cache()
164
- logger.debug("Cleared CUDA cache")
165
- except ImportError:
166
- pass
167
-
168
- return True
169
-
170
- return False
171
-
172
-
173
- def is_llmlingua_model_loaded() -> bool:
174
- """Check if an LLMLingua model is currently loaded.
175
-
176
- Returns:
177
- True if a model is loaded in memory, False otherwise.
178
- """
179
- return _llmlingua_instance is not None
180
-
181
-
182
- @dataclass
183
- class LLMLinguaConfig:
184
- """Configuration for LLMLingua-2 compression.
185
-
186
- Attributes:
187
- model_name: HuggingFace model for the compressor. Default is the
188
- LLMLingua-2 xlm-roberta-large model fine-tuned for compression.
189
- device: Device to run on ('cuda', 'cpu', 'auto'). Auto will use CUDA if available.
190
- target_compression_rate: Target compression ratio (e.g., 0.3 = keep 30% of tokens).
191
- force_tokens: Tokens to always preserve (e.g., important keywords).
192
- drop_consecutive: Whether to drop consecutive punctuation/whitespace.
193
- min_tokens_for_compression: Minimum token count to trigger compression.
194
- Content below this threshold is passed through unchanged.
195
- enable_ccr: Whether to store originals in CCR for retrieval.
196
- ccr_ttl: TTL for CCR entries in seconds.
197
-
198
- GOTCHA: Lower target_compression_rate = more aggressive compression.
199
- A rate of 0.2 means keeping only 20% of tokens.
200
- """
201
-
202
- # Model configuration
203
- model_name: str = field(default_factory=lambda: ML_MODEL_DEFAULTS.llmlingua)
204
- device: str = "auto"
205
-
206
- # Compression parameters
207
- target_compression_rate: float = 0.3
208
- force_tokens: list[str] = field(default_factory=list)
209
- drop_consecutive: bool = True
210
-
211
- # Thresholds
212
- min_tokens_for_compression: int = 100
213
-
214
- # CCR integration
215
- enable_ccr: bool = True
216
- ccr_ttl: int = 300 # 5 minutes
217
-
218
- # Content type specific settings
219
- code_compression_rate: float = 0.5 # Conservative for code
220
- json_compression_rate: float = 0.4 # Somewhat conservative for JSON
221
- text_compression_rate: float = 0.5 # Balanced for plain text (higher = more accurate)
222
-
223
-
224
- @dataclass
225
- class LLMLinguaResult:
226
- """Result of LLMLingua-2 compression.
227
-
228
- Attributes:
229
- compressed: Compressed content.
230
- original: Original content before compression.
231
- original_tokens: Token count of original content.
232
- compressed_tokens: Token count after compression.
233
- compression_ratio: Actual compression ratio achieved.
234
- cache_key: CCR cache key if stored.
235
- model_used: Model that performed the compression.
236
- tokens_saved: Number of tokens saved.
237
- """
238
-
239
- compressed: str
240
- original: str
241
- original_tokens: int
242
- compressed_tokens: int
243
- compression_ratio: float
244
- cache_key: str | None = None
245
- model_used: str | None = None
246
-
247
- @property
248
- def tokens_saved(self) -> int:
249
- """Number of tokens saved by compression."""
250
- return max(0, self.original_tokens - self.compressed_tokens)
251
-
252
- @property
253
- def savings_percentage(self) -> float:
254
- """Percentage of tokens saved."""
255
- if self.original_tokens == 0:
256
- return 0.0
257
- return (self.tokens_saved / self.original_tokens) * 100
258
-
259
-
260
- class LLMLinguaCompressor(Transform):
261
- """LLMLingua-2 based prompt compressor.
262
-
263
- Uses a BERT-based token classifier trained via GPT-4 distillation to
264
- identify and remove non-essential tokens while preserving semantic meaning.
265
-
266
- Key advantages over statistical compression:
267
- - Learned token importance from LLM feedback
268
- - Better handling of context-dependent importance
269
- - More aggressive compression with less information loss
270
- - Especially effective on structured outputs (JSON, code, logs)
271
-
272
- Example:
273
- >>> compressor = LLMLinguaCompressor()
274
- >>> result = compressor.compress(long_tool_output)
275
- >>> print(f"Saved {result.tokens_saved} tokens ({result.savings_percentage:.1f}%)")
276
-
277
- >>> # Use as a Transform in pipeline
278
- >>> from headroom.transforms import TransformPipeline
279
- >>> pipeline = TransformPipeline([LLMLinguaCompressor()])
280
- >>> result = pipeline.apply(messages, tokenizer)
281
- """
282
-
283
- name: str = "llmlingua_compressor"
284
-
285
- def __init__(self, config: LLMLinguaConfig | None = None):
286
- """Initialize LLMLingua compressor.
287
-
288
- Args:
289
- config: Compression configuration. If None, uses defaults.
290
-
291
- Note:
292
- The underlying model is loaded lazily on first use to avoid
293
- startup overhead when the compressor isn't used.
294
- """
295
- self.config = config or LLMLinguaConfig()
296
- self._compressor: Any = None # Lazy loaded
297
-
298
- def compress(
299
- self,
300
- content: str,
301
- context: str = "",
302
- content_type: str | None = None,
303
- question: str | None = None,
304
- ) -> LLMLinguaResult:
305
- """Compress content using LLMLingua-2.
306
-
307
- Args:
308
- content: Content to compress.
309
- context: Optional context for relevance-aware compression.
310
- content_type: Type of content ('code', 'json', 'text').
311
- If None, auto-detected.
312
- question: Optional question for QA-aware compression. When provided,
313
- LLMLingua preserves tokens relevant to answering this question.
314
- This significantly improves accuracy for QA tasks.
315
-
316
- Returns:
317
- LLMLinguaResult with compressed content and metadata.
318
-
319
- Raises:
320
- ImportError: If llmlingua is not installed.
321
- """
322
- # Check availability
323
- if not _check_llmlingua_available():
324
- logger.warning(
325
- "LLMLingua not available. Install with: pip install headroom-ai[llmlingua]"
326
- )
327
- return LLMLinguaResult(
328
- compressed=content,
329
- original=content,
330
- original_tokens=len(content.split()), # Rough estimate
331
- compressed_tokens=len(content.split()),
332
- compression_ratio=1.0,
333
- )
334
-
335
- # Estimate token count (rough)
336
- estimated_tokens = len(content.split())
337
-
338
- # Skip compression for small content
339
- if estimated_tokens < self.config.min_tokens_for_compression:
340
- return LLMLinguaResult(
341
- compressed=content,
342
- original=content,
343
- original_tokens=estimated_tokens,
344
- compressed_tokens=estimated_tokens,
345
- compression_ratio=1.0,
346
- )
347
-
348
- # Get compression rate based on content type
349
- compression_rate = self._get_compression_rate(content, content_type)
350
-
351
- # Get or initialize compressor
352
- device = self._resolve_device()
353
- compressor = _get_llmlingua_compressor(self.config.model_name, device)
354
-
355
- # Prepare force tokens
356
- force_tokens = list(self.config.force_tokens)
357
-
358
- # Add context words as force tokens if provided
359
- if context:
360
- context_words = [w for w in context.split() if len(w) > 3]
361
- force_tokens.extend(context_words[:10]) # Limit to avoid overhead
362
-
363
- # Perform compression
364
- try:
365
- # Build compress_prompt kwargs
366
- compress_kwargs: dict[str, Any] = {
367
- "context": [content], # LLMLingua expects a list of context strings
368
- "rate": compression_rate,
369
- "force_tokens": force_tokens if force_tokens else [],
370
- "drop_consecutive": self.config.drop_consecutive,
371
- }
372
-
373
- # Add question for QA-aware token selection (LLMLingua-2 feature)
374
- # This enables relevance-aware compression where tokens relevant
375
- # to answering the question are preserved with higher probability
376
- if question:
377
- compress_kwargs["question"] = question
378
-
379
- result = compressor.compress_prompt(**compress_kwargs)
380
-
381
- compressed = result.get("compressed_prompt", content)
382
- original_tokens = result.get("origin_tokens", estimated_tokens)
383
- compressed_tokens = result.get("compressed_tokens", len(compressed.split()))
384
-
385
- except Exception as e:
386
- logger.warning("LLMLingua compression failed: %s", e)
387
- return LLMLinguaResult(
388
- compressed=content,
389
- original=content,
390
- original_tokens=estimated_tokens,
391
- compressed_tokens=estimated_tokens,
392
- compression_ratio=1.0,
393
- )
394
-
395
- # Calculate actual ratio
396
- ratio = compressed_tokens / max(original_tokens, 1)
397
-
398
- # Store in CCR if enabled
399
- cache_key = None
400
- if self.config.enable_ccr and ratio < 0.8:
401
- cache_key = self._store_in_ccr(content, compressed, original_tokens)
402
- if cache_key:
403
- # Use standard CCR marker format for CCRToolInjector detection
404
- compressed += f"\n[{original_tokens} items compressed to {compressed_tokens}. Retrieve more: hash={cache_key}]"
405
-
406
- return LLMLinguaResult(
407
- compressed=compressed,
408
- original=content,
409
- original_tokens=original_tokens,
410
- compressed_tokens=compressed_tokens,
411
- compression_ratio=ratio,
412
- cache_key=cache_key,
413
- model_used=self.config.model_name,
414
- )
415
-
416
- def apply(
417
- self,
418
- messages: list[dict[str, Any]],
419
- tokenizer: Tokenizer,
420
- **kwargs: Any,
421
- ) -> TransformResult:
422
- """Apply LLMLingua compression to messages.
423
-
424
- This method implements the Transform interface for use in pipelines.
425
- It compresses tool outputs and long assistant/user messages.
426
-
427
- Args:
428
- messages: List of message dicts to transform.
429
- tokenizer: Tokenizer for accurate token counting.
430
- **kwargs: Additional arguments (e.g., 'context' for relevance).
431
-
432
- Returns:
433
- TransformResult with compressed messages and metadata.
434
- """
435
- tokens_before = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages)
436
- context = kwargs.get("context", "")
437
-
438
- transformed_messages = []
439
- transforms_applied = []
440
- warnings: list[str] = []
441
-
442
- for message in messages:
443
- role = message.get("role", "")
444
- content = message.get("content", "")
445
-
446
- # Skip non-string content (multimodal messages with images)
447
- if not isinstance(content, str):
448
- transformed_messages.append(message)
449
- continue
450
-
451
- # Compress tool results (highest value compression)
452
- if role == "tool" and content:
453
- result = self.compress(content, context=context, content_type="json")
454
- if result.compression_ratio < 0.9:
455
- transformed_messages.append({**message, "content": result.compressed})
456
- transforms_applied.append(f"llmlingua:tool:{result.compression_ratio:.2f}")
457
- else:
458
- transformed_messages.append(message)
459
-
460
- # Compress long assistant messages (tool outputs often embedded)
461
- elif role == "assistant" and len(content) > 500:
462
- result = self.compress(content, context=context)
463
- if result.compression_ratio < 0.9:
464
- transformed_messages.append({**message, "content": result.compressed})
465
- transforms_applied.append(f"llmlingua:assistant:{result.compression_ratio:.2f}")
466
- else:
467
- transformed_messages.append(message)
468
-
469
- # Pass through other messages
470
- else:
471
- transformed_messages.append(message)
472
-
473
- tokens_after = sum(
474
- tokenizer.count_text(str(m.get("content", ""))) for m in transformed_messages
475
- )
476
-
477
- # Add warning if llmlingua not available
478
- if not _check_llmlingua_available():
479
- warnings.append(
480
- "LLMLingua not installed. Install with: pip install headroom-ai[llmlingua]"
481
- )
482
-
483
- return TransformResult(
484
- messages=transformed_messages,
485
- tokens_before=tokens_before,
486
- tokens_after=tokens_after,
487
- transforms_applied=transforms_applied if transforms_applied else ["llmlingua:noop"],
488
- warnings=warnings,
489
- )
490
-
491
- def should_apply(
492
- self,
493
- messages: list[dict[str, Any]],
494
- tokenizer: Tokenizer,
495
- **kwargs: Any,
496
- ) -> bool:
497
- """Check if LLMLingua compression should be applied.
498
-
499
- Returns True if:
500
- - LLMLingua is available, AND
501
- - Total token count exceeds minimum threshold
502
-
503
- Args:
504
- messages: Messages to check.
505
- tokenizer: Tokenizer for counting.
506
- **kwargs: Additional arguments.
507
-
508
- Returns:
509
- True if compression should be applied.
510
- """
511
- if not _check_llmlingua_available():
512
- return False
513
-
514
- total_tokens = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages)
515
- return total_tokens >= self.config.min_tokens_for_compression
516
-
517
- def _get_compression_rate(
518
- self,
519
- content: str,
520
- content_type: str | None,
521
- ) -> float:
522
- """Get appropriate compression rate based on content type.
523
-
524
- Args:
525
- content: Content to analyze.
526
- content_type: Explicit content type or None for auto-detection.
527
-
528
- Returns:
529
- Target compression rate for this content.
530
- """
531
- if content_type == "code":
532
- return self.config.code_compression_rate
533
- elif content_type == "json":
534
- return self.config.json_compression_rate
535
- elif content_type == "text":
536
- return self.config.text_compression_rate
537
-
538
- # Auto-detect content type
539
- if self._looks_like_json(content):
540
- return self.config.json_compression_rate
541
- elif self._looks_like_code(content):
542
- return self.config.code_compression_rate
543
- else:
544
- return self.config.text_compression_rate
545
-
546
- def _looks_like_json(self, content: str) -> bool:
547
- """Check if content appears to be JSON."""
548
- stripped = content.strip()
549
- return (stripped.startswith("{") and stripped.endswith("}")) or (
550
- stripped.startswith("[") and stripped.endswith("]")
551
- )
552
-
553
- def _looks_like_code(self, content: str) -> bool:
554
- """Check if content appears to be code."""
555
- code_indicators = [
556
- "def ",
557
- "class ",
558
- "function ",
559
- "import ",
560
- "from ",
561
- "const ",
562
- "let ",
563
- "var ",
564
- "public ",
565
- "private ",
566
- "async ",
567
- "await ",
568
- "return ",
569
- "if (",
570
- "for (",
571
- "while (",
572
- ]
573
- return any(indicator in content for indicator in code_indicators)
574
-
575
- def _resolve_device(self) -> str:
576
- """Resolve 'auto' device to actual device."""
577
- if self.config.device != "auto":
578
- return self.config.device
579
-
580
- try:
581
- import torch
582
-
583
- if torch.cuda.is_available():
584
- return "cuda"
585
- elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
586
- return "mps"
587
- except ImportError:
588
- pass
589
-
590
- return "cpu"
591
-
592
- def _store_in_ccr(
593
- self,
594
- original: str,
595
- compressed: str,
596
- original_tokens: int,
597
- ) -> str | None:
598
- """Store original content in CCR for later retrieval.
599
-
600
- Args:
601
- original: Original content before compression.
602
- compressed: Compressed content.
603
- original_tokens: Token count of original.
604
-
605
- Returns:
606
- Cache key if stored successfully, None otherwise.
607
- """
608
- try:
609
- from ..cache.compression_store import get_compression_store
610
-
611
- store = get_compression_store()
612
- return store.store(
613
- original,
614
- compressed,
615
- original_tokens=original_tokens,
616
- compressed_tokens=len(compressed.split()),
617
- compression_strategy="llmlingua2",
618
- )
619
- except ImportError:
620
- return None
621
- except Exception as e:
622
- logger.debug("CCR storage failed: %s", e)
623
- return None
624
-
625
-
626
- def compress_with_llmlingua(
627
- content: str,
628
- compression_rate: float = 0.3,
629
- context: str = "",
630
- model_name: str | None = None,
631
- ) -> str:
632
- """Convenience function for one-off compression.
633
-
634
- Args:
635
- content: Content to compress.
636
- compression_rate: Target compression rate (0.0-1.0).
637
- context: Optional context for relevance-aware compression.
638
- model_name: Optional model name override.
639
-
640
- Returns:
641
- Compressed content string.
642
-
643
- Example:
644
- >>> compressed = compress_with_llmlingua(long_output, compression_rate=0.2)
645
- """
646
- config = LLMLinguaConfig(target_compression_rate=compression_rate)
647
- if model_name:
648
- config.model_name = model_name
649
-
650
- compressor = LLMLinguaCompressor(config)
651
- result = compressor.compress(content, context=context)
652
- return result.compressed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
headroom/transforms/pipeline.py CHANGED
@@ -40,7 +40,7 @@ class TransformPipeline:
40
  Transform order:
41
  1. Cache Aligner - normalize prefix for cache hits
42
  2. Content Router - intelligent content-aware compression (routes to appropriate
43
- compressor: LLMLingua for text, SmartCrusher for JSON, CodeCompressor for code, etc.)
44
  3. SmartCrusher/ToolCrusher - fallback if ContentRouter disabled
45
  4. IntelligentContextManager/RollingWindow - enforce token limits
46
  """
 
40
  Transform order:
41
  1. Cache Aligner - normalize prefix for cache hits
42
  2. Content Router - intelligent content-aware compression (routes to appropriate
43
+ compressor: Kompress for text, SmartCrusher for JSON, CodeCompressor for code, etc.)
44
  3. SmartCrusher/ToolCrusher - fallback if ContentRouter disabled
45
  4. IntelligentContextManager/RollingWindow - enforce token limits
46
  """
headroom/transforms/tag_protector.py CHANGED
@@ -1,7 +1,7 @@
1
  """Protect workflow/custom XML tags from text compression.
2
 
3
  LLM workflows use XML-style tags (<system-reminder>, <tool_call>, <thinking>)
4
- as structural markers. Text compressors (Kompress, LLMLingua) treat these as
5
  droppable noise and silently remove them, breaking downstream tools.
6
 
7
  This module detects custom tags (anything NOT standard HTML), replaces entire
 
1
  """Protect workflow/custom XML tags from text compression.
2
 
3
  LLM workflows use XML-style tags (<system-reminder>, <tool_call>, <thinking>)
4
+ as structural markers. Text compressors (Kompress) treat these as
5
  droppable noise and silently remove them, breaking downstream tools.
6
 
7
  This module detects custom tags (anything NOT standard HTML), replaces entire
tests/test_ccr_tool_injection.py CHANGED
@@ -413,7 +413,7 @@ class TestAlternativeMarkerFormats:
413
  - TextCompressor: [N lines compressed to M. Retrieve more: hash=xxx]
414
  - LogCompressor: [N lines compressed to M. Retrieve more: hash=xxx]
415
  - SearchCompressor: [N matches compressed to M. Retrieve more: hash=xxx]
416
- - LLMLingua: [N items compressed to M. Retrieve more: hash=xxx]
417
 
418
  The CCRToolInjector should detect all these formats.
419
  """
 
413
  - TextCompressor: [N lines compressed to M. Retrieve more: hash=xxx]
414
  - LogCompressor: [N lines compressed to M. Retrieve more: hash=xxx]
415
  - SearchCompressor: [N matches compressed to M. Retrieve more: hash=xxx]
416
+ - Kompress: [N items compressed to M. Retrieve more: hash=xxx]
417
 
418
  The CCRToolInjector should detect all these formats.
419
  """
tests/test_compression/test_evals.py CHANGED
@@ -755,10 +755,10 @@ class TestJSONAPIResponseEval:
755
 
756
  @pytest.fixture
757
  def compressor(self):
758
- """Create compressor with simple compression (no LLMLingua for tests)."""
759
  config = UniversalCompressorConfig(
760
  use_magika=False, # Use fallback for consistent tests
761
- use_llmlingua=False,
762
  ccr_enabled=False,
763
  )
764
  return UniversalCompressor(config=config)
@@ -885,7 +885,7 @@ class TestCodeFileEval:
885
  """Create compressor."""
886
  config = UniversalCompressorConfig(
887
  use_magika=False,
888
- use_llmlingua=False,
889
  ccr_enabled=False,
890
  )
891
  return UniversalCompressor(config=config)
@@ -969,7 +969,7 @@ class TestLogOutputEval:
969
  """Create compressor."""
970
  config = UniversalCompressorConfig(
971
  use_magika=False,
972
- use_llmlingua=False,
973
  ccr_enabled=False,
974
  )
975
  return UniversalCompressor(config=config)
@@ -1031,7 +1031,7 @@ class TestMultiToolAgentScenario:
1031
  """Create compressor."""
1032
  config = UniversalCompressorConfig(
1033
  use_magika=False,
1034
- use_llmlingua=False,
1035
  ccr_enabled=False,
1036
  )
1037
  return UniversalCompressor(config=config)
@@ -1129,7 +1129,7 @@ class TestCompressionQualityMetrics:
1129
  """Create compressor."""
1130
  config = UniversalCompressorConfig(
1131
  use_magika=False,
1132
- use_llmlingua=False,
1133
  ccr_enabled=False,
1134
  compression_ratio_target=0.3, # Target 70% reduction
1135
  )
 
755
 
756
  @pytest.fixture
757
  def compressor(self):
758
+ """Create compressor with simple compression (no Kompress for tests)."""
759
  config = UniversalCompressorConfig(
760
  use_magika=False, # Use fallback for consistent tests
761
+ use_kompress=False,
762
  ccr_enabled=False,
763
  )
764
  return UniversalCompressor(config=config)
 
885
  """Create compressor."""
886
  config = UniversalCompressorConfig(
887
  use_magika=False,
888
+ use_kompress=False,
889
  ccr_enabled=False,
890
  )
891
  return UniversalCompressor(config=config)
 
969
  """Create compressor."""
970
  config = UniversalCompressorConfig(
971
  use_magika=False,
972
+ use_kompress=False,
973
  ccr_enabled=False,
974
  )
975
  return UniversalCompressor(config=config)
 
1031
  """Create compressor."""
1032
  config = UniversalCompressorConfig(
1033
  use_magika=False,
1034
+ use_kompress=False,
1035
  ccr_enabled=False,
1036
  )
1037
  return UniversalCompressor(config=config)
 
1129
  """Create compressor."""
1130
  config = UniversalCompressorConfig(
1131
  use_magika=False,
1132
+ use_kompress=False,
1133
  ccr_enabled=False,
1134
  compression_ratio_target=0.3, # Target 70% reduction
1135
  )
tests/test_compression/test_llm_eval.py CHANGED
@@ -301,7 +301,7 @@ class TestJSONDiscoverability:
301
  """Create compressor."""
302
  config = UniversalCompressorConfig(
303
  use_magika=False,
304
- use_llmlingua=False,
305
  ccr_enabled=False,
306
  )
307
  return UniversalCompressor(config=config)
@@ -418,7 +418,7 @@ class TestCodeUnderstanding:
418
  """Create compressor."""
419
  config = UniversalCompressorConfig(
420
  use_magika=False,
421
- use_llmlingua=False,
422
  ccr_enabled=False,
423
  )
424
  return UniversalCompressor(config=config)
@@ -528,7 +528,7 @@ class TestMultiContentAgent:
528
  """Create compressor."""
529
  config = UniversalCompressorConfig(
530
  use_magika=False,
531
- use_llmlingua=False,
532
  ccr_enabled=False,
533
  )
534
  return UniversalCompressor(config=config)
@@ -591,7 +591,7 @@ class TestCompressionEfficacy:
591
  """Create compressor."""
592
  config = UniversalCompressorConfig(
593
  use_magika=False,
594
- use_llmlingua=False,
595
  ccr_enabled=False,
596
  )
597
  return UniversalCompressor(config=config)
 
301
  """Create compressor."""
302
  config = UniversalCompressorConfig(
303
  use_magika=False,
304
+ use_kompress=False,
305
  ccr_enabled=False,
306
  )
307
  return UniversalCompressor(config=config)
 
418
  """Create compressor."""
419
  config = UniversalCompressorConfig(
420
  use_magika=False,
421
+ use_kompress=False,
422
  ccr_enabled=False,
423
  )
424
  return UniversalCompressor(config=config)
 
528
  """Create compressor."""
529
  config = UniversalCompressorConfig(
530
  use_magika=False,
531
+ use_kompress=False,
532
  ccr_enabled=False,
533
  )
534
  return UniversalCompressor(config=config)
 
591
  """Create compressor."""
592
  config = UniversalCompressorConfig(
593
  use_magika=False,
594
+ use_kompress=False,
595
  ccr_enabled=False,
596
  )
597
  return UniversalCompressor(config=config)
tests/test_compression/test_universal.py CHANGED
@@ -22,7 +22,7 @@ class TestUniversalCompressorConfig:
22
  config = UniversalCompressorConfig()
23
 
24
  assert config.use_magika is True
25
- assert config.use_llmlingua is True
26
  assert config.use_entropy_preservation is True
27
  assert config.entropy_threshold == 0.85
28
  assert config.min_content_length == 100
@@ -99,7 +99,7 @@ class TestUniversalCompressor:
99
  """Create compressor with fallback detector (no Magika required)."""
100
  config = UniversalCompressorConfig(
101
  use_magika=False, # Use fallback detector
102
- use_llmlingua=False, # Use simple compression
103
  ccr_enabled=False, # Skip CCR
104
  )
105
  return UniversalCompressor(config=config)
@@ -215,7 +215,7 @@ class TestUniversalCompressorBatch:
215
  """Create compressor with fallback detector."""
216
  config = UniversalCompressorConfig(
217
  use_magika=False,
218
- use_llmlingua=False,
219
  ccr_enabled=False,
220
  )
221
  return UniversalCompressor(config=config)
@@ -262,7 +262,7 @@ class TestStructurePreservation:
262
  """Create compressor."""
263
  config = UniversalCompressorConfig(
264
  use_magika=False,
265
- use_llmlingua=False,
266
  ccr_enabled=False,
267
  )
268
  return UniversalCompressor(config=config)
 
22
  config = UniversalCompressorConfig()
23
 
24
  assert config.use_magika is True
25
+ assert config.use_kompress is True
26
  assert config.use_entropy_preservation is True
27
  assert config.entropy_threshold == 0.85
28
  assert config.min_content_length == 100
 
99
  """Create compressor with fallback detector (no Magika required)."""
100
  config = UniversalCompressorConfig(
101
  use_magika=False, # Use fallback detector
102
+ use_kompress=False, # Use simple compression
103
  ccr_enabled=False, # Skip CCR
104
  )
105
  return UniversalCompressor(config=config)
 
215
  """Create compressor with fallback detector."""
216
  config = UniversalCompressorConfig(
217
  use_magika=False,
218
+ use_kompress=False,
219
  ccr_enabled=False,
220
  )
221
  return UniversalCompressor(config=config)
 
262
  """Create compressor."""
263
  config = UniversalCompressorConfig(
264
  use_magika=False,
265
+ use_kompress=False,
266
  ccr_enabled=False,
267
  )
268
  return UniversalCompressor(config=config)
tests/test_evals/test_html_extraction_eval.py CHANGED
@@ -262,7 +262,7 @@ class TestHTMLExtractionWithLLM:
262
 
263
  @pytest.mark.skipif(not os.environ.get("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set")
264
  class TestHTMLvsBaseline:
265
- """Tests comparing HTMLExtractor vs LLMLingua baseline."""
266
 
267
  @pytest.fixture
268
  def evaluator_with_baseline(self):
@@ -274,9 +274,9 @@ class TestHTMLvsBaseline:
274
  provider="openai",
275
  )
276
 
277
- @pytest.mark.skipif(True, reason="LLMLingua requires GPU, skip in CI")
278
  def test_extraction_beats_baseline(self, evaluator_with_baseline):
279
- """Test that HTMLExtractor outperforms LLMLingua on HTML."""
280
  cases = get_sample_eval_cases()[:2] # Just test 2 for speed
281
 
282
  results = evaluator_with_baseline.evaluate(cases)
@@ -286,9 +286,9 @@ class TestHTMLvsBaseline:
286
  print(f"Avg extraction score: {results.avg_extraction_score}/5")
287
  print(f"Avg baseline score: {results.avg_baseline_score}/5")
288
 
289
- # HTMLExtractor should beat LLMLingua on HTML content
290
  assert results.avg_extraction_score >= results.avg_baseline_score, (
291
- "HTMLExtractor should perform at least as well as LLMLingua on HTML"
292
  )
293
 
294
 
 
262
 
263
  @pytest.mark.skipif(not os.environ.get("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set")
264
  class TestHTMLvsBaseline:
265
+ """Tests comparing HTMLExtractor vs Kompress baseline."""
266
 
267
  @pytest.fixture
268
  def evaluator_with_baseline(self):
 
274
  provider="openai",
275
  )
276
 
277
+ @pytest.mark.skipif(True, reason="Kompress requires GPU, skip in CI")
278
  def test_extraction_beats_baseline(self, evaluator_with_baseline):
279
+ """Test that HTMLExtractor outperforms Kompress on HTML."""
280
  cases = get_sample_eval_cases()[:2] # Just test 2 for speed
281
 
282
  results = evaluator_with_baseline.evaluate(cases)
 
286
  print(f"Avg extraction score: {results.avg_extraction_score}/5")
287
  print(f"Avg baseline score: {results.avg_baseline_score}/5")
288
 
289
+ # HTMLExtractor should beat Kompress on HTML content
290
  assert results.avg_extraction_score >= results.avg_baseline_score, (
291
+ "HTMLExtractor should perform at least as well as Kompress on HTML"
292
  )
293
 
294
 
tests/test_proxy_llmlingua.py DELETED
@@ -1,468 +0,0 @@
1
- """Tests for LLMLingua opt-in mechanism in the proxy server.
2
-
3
- These tests verify:
4
- - ProxyConfig llmlingua settings
5
- - LLMLingua transform integration in pipeline
6
- - Status detection and logging hints
7
- - CLI flag parsing
8
- - DevEx: helpful messages when llmlingua unavailable
9
- """
10
-
11
- from unittest.mock import MagicMock, patch
12
-
13
- import pytest
14
-
15
- # Skip if fastapi not available
16
- pytest.importorskip("fastapi")
17
-
18
- from fastapi.testclient import TestClient
19
-
20
- from headroom.proxy.server import (
21
- HeadroomProxy,
22
- ProxyConfig,
23
- _get_llmlingua_banner_status,
24
- create_app,
25
- )
26
- from headroom.transforms import _LLMLINGUA_AVAILABLE
27
-
28
- # =============================================================================
29
- # Test Fixtures
30
- # =============================================================================
31
-
32
-
33
- @pytest.fixture
34
- def base_config():
35
- """Base config with optimization disabled for simpler tests."""
36
- return ProxyConfig(
37
- optimize=False,
38
- cache_enabled=False,
39
- rate_limit_enabled=False,
40
- cost_tracking_enabled=False,
41
- llmlingua_enabled=False, # Explicitly disable for base config
42
- smart_routing=False, # Use legacy mode for these tests
43
- )
44
-
45
-
46
- @pytest.fixture
47
- def llmlingua_config():
48
- """Config with LLMLingua enabled (legacy mode for explicit status testing)."""
49
- return ProxyConfig(
50
- optimize=True,
51
- cache_enabled=False,
52
- rate_limit_enabled=False,
53
- cost_tracking_enabled=False,
54
- llmlingua_enabled=True,
55
- llmlingua_device="cpu",
56
- llmlingua_target_rate=0.4,
57
- smart_routing=False, # Use legacy mode for explicit status testing
58
- )
59
-
60
-
61
- @pytest.fixture
62
- def client(base_config):
63
- """Create test client with base config."""
64
- app = create_app(base_config)
65
- with TestClient(app) as client:
66
- yield client
67
-
68
-
69
- # =============================================================================
70
- # TestProxyConfigLLMLingua
71
- # =============================================================================
72
-
73
-
74
- class TestProxyConfigLLMLingua:
75
- """Tests for LLMLingua settings in ProxyConfig."""
76
-
77
- def test_default_llmlingua_enabled(self):
78
- """LLMLingua is enabled by default (with smart routing)."""
79
- config = ProxyConfig()
80
-
81
- # LLMLingua is now enabled by default with smart routing
82
- assert config.llmlingua_enabled is True
83
- assert config.llmlingua_device == "auto"
84
- assert config.llmlingua_target_rate == 0.3
85
-
86
- def test_llmlingua_can_be_enabled(self):
87
- """LLMLingua can be enabled via config."""
88
- config = ProxyConfig(
89
- llmlingua_enabled=True,
90
- llmlingua_device="cuda",
91
- llmlingua_target_rate=0.5,
92
- )
93
-
94
- assert config.llmlingua_enabled is True
95
- assert config.llmlingua_device == "cuda"
96
- assert config.llmlingua_target_rate == 0.5
97
-
98
- def test_llmlingua_device_options(self):
99
- """LLMLingua device accepts valid options."""
100
- for device in ["auto", "cuda", "cpu", "mps"]:
101
- config = ProxyConfig(llmlingua_device=device)
102
- assert config.llmlingua_device == device
103
-
104
- def test_llmlingua_target_rate_range(self):
105
- """LLMLingua target rate accepts 0.0-1.0 range."""
106
- # Low rate (aggressive compression)
107
- config_low = ProxyConfig(llmlingua_target_rate=0.1)
108
- assert config_low.llmlingua_target_rate == 0.1
109
-
110
- # High rate (conservative compression)
111
- config_high = ProxyConfig(llmlingua_target_rate=0.8)
112
- assert config_high.llmlingua_target_rate == 0.8
113
-
114
-
115
- # =============================================================================
116
- # TestLLMLinguaSetup
117
- # =============================================================================
118
-
119
-
120
- class TestLLMLinguaSetup:
121
- """Tests for LLMLingua setup in HeadroomProxy."""
122
-
123
- def test_setup_returns_disabled_when_not_enabled(self, base_config):
124
- """Setup returns 'disabled' when llmlingua not enabled and not available."""
125
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
126
- proxy = HeadroomProxy(base_config)
127
- assert proxy._llmlingua_status == "disabled"
128
-
129
- def test_setup_returns_available_when_installed_but_not_enabled(self, base_config):
130
- """Setup returns 'available' when llmlingua installed but not enabled."""
131
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
132
- proxy = HeadroomProxy(base_config)
133
- assert proxy._llmlingua_status == "available"
134
-
135
- def test_setup_returns_enabled_when_enabled_and_available(self, llmlingua_config):
136
- """Setup returns 'enabled' when llmlingua enabled and available."""
137
- mock_compressor = MagicMock()
138
-
139
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
140
- with patch("headroom.proxy.server.LLMLinguaCompressor", mock_compressor):
141
- with patch("headroom.proxy.server.LLMLinguaConfig"):
142
- proxy = HeadroomProxy(llmlingua_config)
143
- assert proxy._llmlingua_status == "enabled"
144
-
145
- def test_setup_returns_unavailable_when_enabled_but_not_installed(self):
146
- """Setup returns 'unavailable' when enabled but llmlingua not installed."""
147
- config = ProxyConfig(
148
- llmlingua_enabled=True,
149
- optimize=False,
150
- cache_enabled=False,
151
- rate_limit_enabled=False,
152
- smart_routing=False, # Use legacy mode for explicit status testing
153
- )
154
-
155
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
156
- proxy = HeadroomProxy(config)
157
- assert proxy._llmlingua_status == "unavailable"
158
-
159
- def test_llmlingua_compressor_added_to_pipeline(self, llmlingua_config):
160
- """LLMLinguaCompressor is added to pipeline when enabled."""
161
- mock_compressor_class = MagicMock()
162
- mock_compressor_instance = MagicMock()
163
- mock_compressor_class.return_value = mock_compressor_instance
164
-
165
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
166
- with patch("headroom.proxy.server.LLMLinguaCompressor", mock_compressor_class):
167
- with patch("headroom.proxy.server.LLMLinguaConfig") as mock_config:
168
- HeadroomProxy(llmlingua_config)
169
-
170
- # Verify LLMLinguaCompressor was instantiated
171
- mock_compressor_class.assert_called_once()
172
-
173
- # Verify config was passed with correct device and rate
174
- call_args = mock_config.call_args
175
- assert call_args.kwargs["device"] == "cpu"
176
- assert call_args.kwargs["target_compression_rate"] == 0.4
177
-
178
- def test_llmlingua_not_added_when_disabled(self, base_config):
179
- """LLMLinguaCompressor is NOT added when disabled."""
180
- mock_compressor_class = MagicMock()
181
-
182
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
183
- with patch("headroom.proxy.server.LLMLinguaCompressor", mock_compressor_class):
184
- HeadroomProxy(base_config)
185
-
186
- # Should NOT be called when disabled
187
- mock_compressor_class.assert_not_called()
188
-
189
-
190
- # =============================================================================
191
- # TestBannerStatus
192
- # =============================================================================
193
-
194
-
195
- class TestBannerStatus:
196
- """Tests for banner status helper function."""
197
-
198
- def test_banner_disabled_when_not_available(self):
199
- """Banner shows DISABLED when llmlingua not available and not enabled."""
200
- config = ProxyConfig(llmlingua_enabled=False, smart_routing=False)
201
-
202
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
203
- status = _get_llmlingua_banner_status(config)
204
- assert status == "DISABLED"
205
-
206
- def test_banner_available_hint_when_installed(self):
207
- """Banner shows availability hint when installed but not enabled."""
208
- config = ProxyConfig(llmlingua_enabled=False, smart_routing=False)
209
-
210
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
211
- status = _get_llmlingua_banner_status(config)
212
- # Now shows DISABLED with hint to enable
213
- assert "DISABLED" in status
214
- assert "--no-llmlingua" in status # Hint to remove the flag to enable
215
-
216
- def test_banner_enabled_when_active(self):
217
- """Banner shows ENABLED with config when active."""
218
- config = ProxyConfig(
219
- llmlingua_enabled=True,
220
- llmlingua_device="cuda",
221
- llmlingua_target_rate=0.25,
222
- )
223
-
224
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
225
- status = _get_llmlingua_banner_status(config)
226
- assert "ENABLED" in status
227
- assert "cuda" in status
228
- assert "0.25" in status
229
-
230
- def test_banner_shows_install_hint_when_requested_but_missing(self):
231
- """Banner shows install hint when enabled but not installed."""
232
- config = ProxyConfig(llmlingua_enabled=True, smart_routing=False)
233
-
234
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
235
- status = _get_llmlingua_banner_status(config)
236
- assert "NOT INSTALLED" in status
237
- assert "pip install" in status
238
-
239
-
240
- # =============================================================================
241
- # TestHealthEndpointWithLLMLingua
242
- # =============================================================================
243
-
244
-
245
- class TestHealthEndpointWithLLMLingua:
246
- """Tests for health endpoint reflecting LLMLingua status."""
247
-
248
- def test_health_returns_llmlingua_in_config(self, client):
249
- """Health endpoint works regardless of LLMLingua status."""
250
- response = client.get("/health")
251
- assert response.status_code == 200
252
-
253
- data = response.json()
254
- assert data["status"] == "healthy"
255
- assert "config" in data
256
-
257
-
258
- # =============================================================================
259
- # TestStatsEndpointWithLLMLingua
260
- # =============================================================================
261
-
262
-
263
- class TestStatsEndpointWithLLMLingua:
264
- """Tests for stats endpoint with LLMLingua integration."""
265
-
266
- def test_stats_endpoint_works(self, client):
267
- """Stats endpoint works with any LLMLingua configuration."""
268
- response = client.get("/stats")
269
- assert response.status_code == 200
270
-
271
- data = response.json()
272
- assert "requests" in data
273
- assert "tokens" in data
274
-
275
-
276
- # =============================================================================
277
- # TestCLIArguments
278
- # =============================================================================
279
-
280
-
281
- class TestCLIArguments:
282
- """Tests for CLI argument parsing (without actually running server)."""
283
-
284
- def test_llmlingua_flag_defaults(self):
285
- """Default CLI values for LLMLingua settings."""
286
- import argparse
287
-
288
- parser = argparse.ArgumentParser()
289
- parser.add_argument("--llmlingua", action="store_true")
290
- parser.add_argument("--llmlingua-device", default="auto")
291
- parser.add_argument("--llmlingua-rate", type=float, default=0.3)
292
-
293
- args = parser.parse_args([])
294
-
295
- assert args.llmlingua is False
296
- assert args.llmlingua_device == "auto"
297
- assert args.llmlingua_rate == 0.3
298
-
299
- def test_llmlingua_flag_enabled(self):
300
- """CLI --llmlingua flag enables LLMLingua."""
301
- import argparse
302
-
303
- parser = argparse.ArgumentParser()
304
- parser.add_argument("--llmlingua", action="store_true")
305
- parser.add_argument("--llmlingua-device", default="auto")
306
- parser.add_argument("--llmlingua-rate", type=float, default=0.3)
307
-
308
- args = parser.parse_args(["--llmlingua"])
309
-
310
- assert args.llmlingua is True
311
-
312
- def test_llmlingua_device_flag(self):
313
- """CLI --llmlingua-device flag sets device."""
314
- import argparse
315
-
316
- parser = argparse.ArgumentParser()
317
- parser.add_argument("--llmlingua-device", default="auto")
318
-
319
- args = parser.parse_args(["--llmlingua-device", "cuda"])
320
-
321
- assert args.llmlingua_device == "cuda"
322
-
323
- def test_llmlingua_rate_flag(self):
324
- """CLI --llmlingua-rate flag sets compression rate."""
325
- import argparse
326
-
327
- parser = argparse.ArgumentParser()
328
- parser.add_argument("--llmlingua-rate", type=float, default=0.3)
329
-
330
- args = parser.parse_args(["--llmlingua-rate", "0.5"])
331
-
332
- assert args.llmlingua_rate == 0.5
333
-
334
-
335
- # =============================================================================
336
- # TestDevExMessages
337
- # =============================================================================
338
-
339
-
340
- class TestDevExMessages:
341
- """Tests for developer experience messages and hints."""
342
-
343
- def test_warning_logged_when_enabled_but_unavailable(self, caplog):
344
- """Warning is logged when llmlingua enabled but not installed."""
345
- import logging
346
-
347
- config = ProxyConfig(
348
- llmlingua_enabled=True,
349
- optimize=False,
350
- cache_enabled=False,
351
- rate_limit_enabled=False,
352
- smart_routing=False, # Use legacy mode for explicit status testing
353
- )
354
-
355
- with caplog.at_level(logging.WARNING):
356
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
357
- proxy = HeadroomProxy(config)
358
-
359
- # Should have logged a warning about missing llmlingua
360
- assert proxy._llmlingua_status == "unavailable"
361
- assert any("llmlingua" in r.message.lower() for r in caplog.records)
362
- assert any("pip install" in r.message for r in caplog.records)
363
-
364
-
365
- # =============================================================================
366
- # TestIntegrationWithActualLLMLingua
367
- # =============================================================================
368
-
369
-
370
- @pytest.mark.skipif(not _LLMLINGUA_AVAILABLE, reason="llmlingua not installed")
371
- class TestIntegrationWithActualLLMLingua:
372
- """Integration tests that require actual llmlingua installation.
373
-
374
- These tests verify the full integration path when llmlingua is installed.
375
- """
376
-
377
- def test_proxy_starts_with_llmlingua_enabled(self):
378
- """Proxy starts successfully with LLMLingua enabled."""
379
- config = ProxyConfig(
380
- llmlingua_enabled=True,
381
- llmlingua_device="cpu", # CPU for CI/test environments
382
- llmlingua_target_rate=0.3,
383
- optimize=True,
384
- cache_enabled=False,
385
- rate_limit_enabled=False,
386
- smart_routing=False, # Use legacy mode for explicit status testing
387
- )
388
-
389
- # Should not raise
390
- proxy = HeadroomProxy(config)
391
-
392
- assert proxy._llmlingua_status == "enabled"
393
-
394
- def test_app_creates_with_llmlingua(self):
395
- """FastAPI app creates successfully with LLMLingua enabled."""
396
- config = ProxyConfig(
397
- llmlingua_enabled=True,
398
- llmlingua_device="cpu",
399
- optimize=True,
400
- cache_enabled=False,
401
- rate_limit_enabled=False,
402
- )
403
-
404
- # Should not raise
405
- app = create_app(config)
406
-
407
- assert app is not None
408
-
409
- def test_health_endpoint_with_llmlingua_enabled(self):
410
- """Health endpoint works with LLMLingua enabled."""
411
- config = ProxyConfig(
412
- llmlingua_enabled=True,
413
- llmlingua_device="cpu",
414
- optimize=True,
415
- cache_enabled=False,
416
- rate_limit_enabled=False,
417
- )
418
-
419
- app = create_app(config)
420
- with TestClient(app) as client:
421
- response = client.get("/health")
422
- assert response.status_code == 200
423
-
424
-
425
- # =============================================================================
426
- # TestEdgeCases
427
- # =============================================================================
428
-
429
-
430
- class TestEdgeCases:
431
- """Edge cases for LLMLingua proxy integration."""
432
-
433
- def test_multiple_proxy_instances_independent(self):
434
- """Multiple proxy instances have independent LLMLingua status."""
435
- config_enabled = ProxyConfig(
436
- llmlingua_enabled=True,
437
- optimize=False,
438
- cache_enabled=False,
439
- rate_limit_enabled=False,
440
- smart_routing=False, # Use legacy mode for explicit status testing
441
- )
442
- config_disabled = ProxyConfig(
443
- llmlingua_enabled=False,
444
- optimize=False,
445
- cache_enabled=False,
446
- rate_limit_enabled=False,
447
- smart_routing=False, # Use legacy mode for explicit status testing
448
- )
449
-
450
- with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
451
- with patch("headroom.proxy.server.LLMLinguaCompressor"):
452
- with patch("headroom.proxy.server.LLMLinguaConfig"):
453
- proxy_enabled = HeadroomProxy(config_enabled)
454
- proxy_disabled = HeadroomProxy(config_disabled)
455
-
456
- assert proxy_enabled._llmlingua_status == "enabled"
457
- assert proxy_disabled._llmlingua_status == "available"
458
-
459
- def test_config_immutable_after_proxy_creation(self, base_config):
460
- """Config values are captured at proxy creation time."""
461
- proxy = HeadroomProxy(base_config)
462
-
463
- # Modifying config after creation doesn't affect proxy
464
- # (ProxyConfig is a dataclass, so this tests the pattern)
465
- original_status = proxy._llmlingua_status
466
-
467
- # Status should remain unchanged
468
- assert proxy._llmlingua_status == original_status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_transforms/test_code_compressor.py CHANGED
@@ -499,14 +499,14 @@ class TestFallbackCompression:
499
 
500
  # Should still return a result (fallback compression)
501
  assert result is not None
502
- # LLMLingua fallback does NOT guarantee syntax validity
503
- # If LLMLingua is unavailable, returns original (valid)
504
- # If LLMLingua IS available, syntax_valid=False (cannot guarantee)
505
 
506
  def test_fallback_preserves_structure(self, default_config):
507
  """Fallback compression preserves basic structure when no compressor available.
508
 
509
- When both tree-sitter and LLMLingua are unavailable, the fallback
510
  returns the original code unchanged - preserving all structure.
511
  """
512
  with (
@@ -515,7 +515,7 @@ class TestFallbackCompression:
515
  return_value=False,
516
  ),
517
  patch(
518
- "headroom.transforms.llmlingua_compressor._check_llmlingua_available",
519
  return_value=False,
520
  ),
521
  ):
 
499
 
500
  # Should still return a result (fallback compression)
501
  assert result is not None
502
+ # Kompress fallback does NOT guarantee syntax validity
503
+ # If Kompress is unavailable, returns original (valid)
504
+ # If Kompress IS available, syntax_valid=False (cannot guarantee)
505
 
506
  def test_fallback_preserves_structure(self, default_config):
507
  """Fallback compression preserves basic structure when no compressor available.
508
 
509
+ When both tree-sitter and Kompress are unavailable, the fallback
510
  returns the original code unchanged - preserving all structure.
511
  """
512
  with (
 
515
  return_value=False,
516
  ),
517
  patch(
518
+ "headroom.transforms.kompress_compressor.is_kompress_available",
519
  return_value=False,
520
  ),
521
  ):
tests/test_transforms/test_content_router.py CHANGED
@@ -156,7 +156,6 @@ class TestContentRouterConfig:
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
@@ -168,13 +167,11 @@ class TestContentRouterConfig:
168
  config = ContentRouterConfig(
169
  min_section_tokens=50,
170
  enable_code_aware=False,
171
- enable_llmlingua=False,
172
  fallback_strategy=CompressionStrategy.TEXT,
173
  )
174
 
175
  assert config.min_section_tokens == 50
176
  assert config.enable_code_aware is False
177
- assert config.enable_llmlingua is False
178
  assert config.fallback_strategy == CompressionStrategy.TEXT
179
 
180
  def test_all_strategies_in_enum(self):
@@ -184,7 +181,6 @@ class TestContentRouterConfig:
184
  "SMART_CRUSHER",
185
  "SEARCH",
186
  "LOG",
187
- "LLMLINGUA",
188
  "TEXT",
189
  "MIXED",
190
  "PASSTHROUGH",
 
156
 
157
  assert config.enable_code_aware is True
158
  assert config.enable_kompress 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
 
167
  config = ContentRouterConfig(
168
  min_section_tokens=50,
169
  enable_code_aware=False,
 
170
  fallback_strategy=CompressionStrategy.TEXT,
171
  )
172
 
173
  assert config.min_section_tokens == 50
174
  assert config.enable_code_aware is False
 
175
  assert config.fallback_strategy == CompressionStrategy.TEXT
176
 
177
  def test_all_strategies_in_enum(self):
 
181
  "SMART_CRUSHER",
182
  "SEARCH",
183
  "LOG",
 
184
  "TEXT",
185
  "MIXED",
186
  "PASSTHROUGH",
tests/test_transforms/test_llmlingua_compressor.py DELETED
@@ -1,941 +0,0 @@
1
- """Tests for LLMLingua-2 compressor integration.
2
-
3
- Comprehensive tests covering:
4
- - LLMLinguaConfig: Configuration validation and defaults
5
- - LLMLinguaCompressor: Core compression functionality
6
- - Transform interface: apply(), should_apply() methods
7
- - Content type detection: JSON, code, plain text
8
- - CCR integration: Reversible compression storage
9
- - Edge cases: Empty content, unavailable dependency, fallbacks
10
- """
11
-
12
- import json
13
- from unittest.mock import MagicMock, patch
14
-
15
- import pytest
16
-
17
- from headroom.transforms.llmlingua_compressor import (
18
- LLMLinguaCompressor,
19
- LLMLinguaConfig,
20
- LLMLinguaResult,
21
- compress_with_llmlingua,
22
- is_llmlingua_model_loaded,
23
- unload_llmlingua_model,
24
- )
25
-
26
- # Try to import for availability check
27
- try:
28
- import llmlingua # noqa: F401
29
-
30
- LLMLINGUA_INSTALLED = True
31
- except ImportError:
32
- LLMLINGUA_INSTALLED = False
33
-
34
-
35
- # =============================================================================
36
- # Test Fixtures
37
- # =============================================================================
38
-
39
-
40
- @pytest.fixture
41
- def default_config():
42
- """Default LLMLinguaConfig for testing."""
43
- return LLMLinguaConfig(
44
- min_tokens_for_compression=10, # Low threshold for tests
45
- enable_ccr=False, # Disable CCR for unit tests
46
- )
47
-
48
-
49
- @pytest.fixture
50
- def compressor(default_config):
51
- """LLMLinguaCompressor instance with default config."""
52
- return LLMLinguaCompressor(default_config)
53
-
54
-
55
- @pytest.fixture
56
- def mock_llmlingua():
57
- """Mock the llmlingua module and PromptCompressor."""
58
- mock_compressor = MagicMock()
59
- mock_compressor._model_name = "test-model"
60
-
61
- # Default compress_prompt return value
62
- mock_compressor.compress_prompt.return_value = {
63
- "compressed_prompt": "compressed content here",
64
- "origin_tokens": 100,
65
- "compressed_tokens": 30,
66
- }
67
-
68
- with patch(
69
- "headroom.transforms.llmlingua_compressor._check_llmlingua_available",
70
- return_value=True,
71
- ):
72
- with patch(
73
- "headroom.transforms.llmlingua_compressor._get_llmlingua_compressor",
74
- return_value=mock_compressor,
75
- ):
76
- yield mock_compressor
77
-
78
-
79
- @pytest.fixture
80
- def tokenizer():
81
- """Get a tokenizer for Transform interface tests."""
82
- from headroom.providers import OpenAIProvider
83
- from headroom.tokenizer import Tokenizer
84
-
85
- provider = OpenAIProvider()
86
- token_counter = provider.get_token_counter("gpt-4o")
87
- return Tokenizer(token_counter, "gpt-4o")
88
-
89
-
90
- # =============================================================================
91
- # Test Data Generators
92
- # =============================================================================
93
-
94
-
95
- def generate_long_text(n_words: int = 500) -> str:
96
- """Generate long text content for compression testing."""
97
- words = ["the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog"]
98
- return " ".join(words[i % len(words)] for i in range(n_words))
99
-
100
-
101
- def generate_long_json(n_items: int = 50) -> str:
102
- """Generate long JSON content for compression testing."""
103
- items = [
104
- {
105
- "id": i,
106
- "name": f"Item {i}",
107
- "description": f"This is a detailed description for item number {i}",
108
- "value": i * 10,
109
- "active": i % 2 == 0,
110
- }
111
- for i in range(n_items)
112
- ]
113
- return json.dumps(items)
114
-
115
-
116
- def generate_long_code(n_functions: int = 20) -> str:
117
- """Generate Python code content for compression testing."""
118
- lines = ['"""Module with many functions."""', "", "import os", "from typing import Any", ""]
119
- for i in range(n_functions):
120
- lines.extend(
121
- [
122
- f"def function_{i}(arg: Any) -> str:",
123
- f' """Process argument {i}."""',
124
- " result = str(arg)",
125
- f' return f"Function {i}: {{result}}"',
126
- "",
127
- ]
128
- )
129
- return "\n".join(lines)
130
-
131
-
132
- # =============================================================================
133
- # TestLLMLinguaConfig
134
- # =============================================================================
135
-
136
-
137
- class TestLLMLinguaConfig:
138
- """Tests for LLMLinguaConfig dataclass."""
139
-
140
- def test_default_values(self):
141
- """Default config values are sensible."""
142
- config = LLMLinguaConfig()
143
-
144
- assert config.model_name == "microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank"
145
- assert config.device == "auto"
146
- assert config.target_compression_rate == 0.3
147
- assert config.min_tokens_for_compression == 100
148
- assert config.enable_ccr is True
149
- assert config.drop_consecutive is True
150
-
151
- def test_custom_values(self):
152
- """Custom config values are applied."""
153
- config = LLMLinguaConfig(
154
- model_name="custom/model",
155
- device="cuda",
156
- target_compression_rate=0.5,
157
- min_tokens_for_compression=50,
158
- force_tokens=["important", "keep"],
159
- )
160
-
161
- assert config.model_name == "custom/model"
162
- assert config.device == "cuda"
163
- assert config.target_compression_rate == 0.5
164
- assert config.min_tokens_for_compression == 50
165
- assert "important" in config.force_tokens
166
-
167
- def test_content_type_rates(self):
168
- """Different content types have appropriate compression rates."""
169
- config = LLMLinguaConfig()
170
-
171
- # Code and text are equally conservative for accuracy
172
- assert config.code_compression_rate >= config.text_compression_rate
173
- # JSON can be slightly more aggressive since structure is preserved
174
- assert config.json_compression_rate <= config.code_compression_rate
175
- assert config.json_compression_rate <= config.text_compression_rate
176
-
177
-
178
- # =============================================================================
179
- # TestLLMLinguaResult
180
- # =============================================================================
181
-
182
-
183
- class TestLLMLinguaResult:
184
- """Tests for LLMLinguaResult dataclass."""
185
-
186
- def test_tokens_saved(self):
187
- """tokens_saved property calculates correctly."""
188
- result = LLMLinguaResult(
189
- compressed="short",
190
- original="long content here",
191
- original_tokens=100,
192
- compressed_tokens=30,
193
- compression_ratio=0.3,
194
- )
195
-
196
- assert result.tokens_saved == 70
197
-
198
- def test_tokens_saved_no_negative(self):
199
- """tokens_saved never returns negative."""
200
- result = LLMLinguaResult(
201
- compressed="expanded content",
202
- original="short",
203
- original_tokens=10,
204
- compressed_tokens=20, # Expanded (unusual case)
205
- compression_ratio=2.0,
206
- )
207
-
208
- assert result.tokens_saved == 0
209
-
210
- def test_savings_percentage(self):
211
- """savings_percentage property calculates correctly."""
212
- result = LLMLinguaResult(
213
- compressed="short",
214
- original="long content",
215
- original_tokens=100,
216
- compressed_tokens=25,
217
- compression_ratio=0.25,
218
- )
219
-
220
- assert result.savings_percentage == 75.0
221
-
222
- def test_savings_percentage_zero_original(self):
223
- """savings_percentage handles zero original tokens."""
224
- result = LLMLinguaResult(
225
- compressed="",
226
- original="",
227
- original_tokens=0,
228
- compressed_tokens=0,
229
- compression_ratio=1.0,
230
- )
231
-
232
- assert result.savings_percentage == 0.0
233
-
234
-
235
- # =============================================================================
236
- # TestLLMLinguaCompressor
237
- # =============================================================================
238
-
239
-
240
- class TestLLMLinguaCompressor:
241
- """Tests for LLMLinguaCompressor core functionality."""
242
-
243
- def test_init_with_default_config(self):
244
- """Compressor initializes with default config."""
245
- compressor = LLMLinguaCompressor()
246
-
247
- assert compressor.config is not None
248
- assert compressor.config.model_name is not None
249
-
250
- def test_init_with_custom_config(self, default_config):
251
- """Compressor initializes with custom config."""
252
- compressor = LLMLinguaCompressor(default_config)
253
-
254
- assert compressor.config == default_config
255
-
256
- def test_compress_returns_result_when_unavailable(self, compressor):
257
- """Compress returns passthrough result when llmlingua unavailable."""
258
- with patch(
259
- "headroom.transforms.llmlingua_compressor._check_llmlingua_available",
260
- return_value=False,
261
- ):
262
- content = generate_long_text(100)
263
- result = compressor.compress(content)
264
-
265
- # Should return unchanged content
266
- assert result.compressed == content
267
- assert result.compression_ratio == 1.0
268
-
269
- def test_compress_skips_small_content(self, compressor):
270
- """Small content is not compressed."""
271
- small_content = "short text"
272
- result = compressor.compress(small_content)
273
-
274
- assert result.compressed == small_content
275
- assert result.compression_ratio == 1.0
276
-
277
- def test_compress_with_llmlingua(self, default_config, mock_llmlingua):
278
- """Compression uses llmlingua when available."""
279
- compressor = LLMLinguaCompressor(default_config)
280
- content = generate_long_text(200)
281
-
282
- result = compressor.compress(content)
283
-
284
- # Should have called compress_prompt
285
- mock_llmlingua.compress_prompt.assert_called_once()
286
- assert result.compressed == "compressed content here"
287
- assert result.compression_ratio < 1.0
288
-
289
- def test_compress_with_context(self, default_config, mock_llmlingua):
290
- """Context words are used as force tokens."""
291
- compressor = LLMLinguaCompressor(default_config)
292
- content = generate_long_text(200)
293
- context = "important keywords here"
294
-
295
- compressor.compress(content, context=context)
296
-
297
- # Check force_tokens includes context words
298
- call_args = mock_llmlingua.compress_prompt.call_args
299
- force_tokens = call_args.kwargs.get("force_tokens", [])
300
- # Should include context words longer than 3 chars
301
- assert "important" in force_tokens or "keywords" in force_tokens
302
-
303
- def test_compress_handles_exception(self, default_config, mock_llmlingua):
304
- """Exceptions from llmlingua are handled gracefully."""
305
- mock_llmlingua.compress_prompt.side_effect = RuntimeError("Model error")
306
-
307
- compressor = LLMLinguaCompressor(default_config)
308
- content = generate_long_text(200)
309
-
310
- result = compressor.compress(content)
311
-
312
- # Should return original content on error
313
- assert result.compressed == content
314
- assert result.compression_ratio == 1.0
315
-
316
-
317
- # =============================================================================
318
- # TestContentTypeDetection
319
- # =============================================================================
320
-
321
-
322
- class TestContentTypeDetection:
323
- """Tests for content type auto-detection."""
324
-
325
- def test_detect_json_content(self, default_config, mock_llmlingua):
326
- """JSON content is detected and uses JSON compression rate."""
327
- compressor = LLMLinguaCompressor(default_config)
328
-
329
- rate = compressor._get_compression_rate(generate_long_json(50), None)
330
-
331
- assert rate == default_config.json_compression_rate
332
-
333
- def test_detect_code_content(self, default_config, mock_llmlingua):
334
- """Code content is detected and uses code compression rate."""
335
- compressor = LLMLinguaCompressor(default_config)
336
- code = generate_long_code(20)
337
-
338
- rate = compressor._get_compression_rate(code, None)
339
-
340
- assert rate == default_config.code_compression_rate
341
-
342
- def test_detect_plain_text(self, default_config, mock_llmlingua):
343
- """Plain text uses text compression rate."""
344
- compressor = LLMLinguaCompressor(default_config)
345
- text = generate_long_text(200)
346
-
347
- rate = compressor._get_compression_rate(text, None)
348
-
349
- assert rate == default_config.text_compression_rate
350
-
351
- def test_explicit_content_type(self, default_config, mock_llmlingua):
352
- """Explicit content_type overrides detection."""
353
- compressor = LLMLinguaCompressor(default_config)
354
- # JSON-looking content but marked as text
355
- json_content = generate_long_json(50)
356
-
357
- rate = compressor._get_compression_rate(json_content, content_type="text")
358
-
359
- assert rate == default_config.text_compression_rate
360
-
361
- def test_looks_like_json_detection(self, default_config):
362
- """JSON detection works for arrays and objects."""
363
- compressor = LLMLinguaCompressor(default_config)
364
-
365
- assert compressor._looks_like_json('[{"key": "value"}]')
366
- assert compressor._looks_like_json('{"key": "value"}')
367
- assert not compressor._looks_like_json("plain text")
368
- assert not compressor._looks_like_json("def function():")
369
-
370
- def test_looks_like_code_detection(self, default_config):
371
- """Code detection works for common patterns."""
372
- compressor = LLMLinguaCompressor(default_config)
373
-
374
- assert compressor._looks_like_code("def function():")
375
- assert compressor._looks_like_code("class MyClass:")
376
- assert compressor._looks_like_code("import os")
377
- assert compressor._looks_like_code("function test() {")
378
- assert compressor._looks_like_code("const x = 5")
379
- assert not compressor._looks_like_code("plain text content")
380
-
381
-
382
- # =============================================================================
383
- # TestTransformInterface
384
- # =============================================================================
385
-
386
-
387
- class TestTransformInterface:
388
- """Tests for Transform interface (apply, should_apply)."""
389
-
390
- def test_should_apply_returns_false_when_unavailable(self, compressor, tokenizer):
391
- """should_apply returns False when llmlingua unavailable."""
392
- messages = [{"role": "user", "content": generate_long_text(200)}]
393
-
394
- with patch(
395
- "headroom.transforms.llmlingua_compressor._check_llmlingua_available",
396
- return_value=False,
397
- ):
398
- assert not compressor.should_apply(messages, tokenizer)
399
-
400
- def test_should_apply_returns_false_for_small_content(self, default_config, tokenizer):
401
- """should_apply returns False for small content."""
402
- config = LLMLinguaConfig(min_tokens_for_compression=1000)
403
- compressor = LLMLinguaCompressor(config)
404
- messages = [{"role": "user", "content": "small"}]
405
-
406
- with patch(
407
- "headroom.transforms.llmlingua_compressor._check_llmlingua_available",
408
- return_value=True,
409
- ):
410
- assert not compressor.should_apply(messages, tokenizer)
411
-
412
- def test_should_apply_returns_true_for_large_content(self, default_config, tokenizer):
413
- """should_apply returns True for large content."""
414
- compressor = LLMLinguaCompressor(default_config)
415
- messages = [{"role": "user", "content": generate_long_text(500)}]
416
-
417
- with patch(
418
- "headroom.transforms.llmlingua_compressor._check_llmlingua_available",
419
- return_value=True,
420
- ):
421
- assert compressor.should_apply(messages, tokenizer)
422
-
423
- def test_apply_compresses_tool_messages(self, default_config, tokenizer, mock_llmlingua):
424
- """apply() compresses tool message content."""
425
- compressor = LLMLinguaCompressor(default_config)
426
- tool_content = generate_long_json(100)
427
- messages = [
428
- {"role": "user", "content": "Get data"},
429
- {"role": "tool", "tool_call_id": "call_1", "content": tool_content},
430
- ]
431
-
432
- result = compressor.apply(messages, tokenizer)
433
-
434
- # Tool content should be compressed
435
- assert result.messages[1]["content"] != tool_content
436
- assert "compressed content here" in result.messages[1]["content"]
437
- assert len(result.transforms_applied) > 0
438
-
439
- def test_apply_compresses_long_assistant_messages(
440
- self, default_config, tokenizer, mock_llmlingua
441
- ):
442
- """apply() compresses long assistant messages."""
443
- compressor = LLMLinguaCompressor(default_config)
444
- long_content = generate_long_text(1000)
445
- messages = [
446
- {"role": "user", "content": "Tell me a story"},
447
- {"role": "assistant", "content": long_content},
448
- ]
449
-
450
- result = compressor.apply(messages, tokenizer)
451
-
452
- # Assistant content should be compressed (>500 chars)
453
- assert result.messages[1]["content"] != long_content
454
-
455
- def test_apply_passes_through_short_messages(self, default_config, tokenizer, mock_llmlingua):
456
- """apply() passes through short messages unchanged."""
457
- compressor = LLMLinguaCompressor(default_config)
458
- messages = [
459
- {"role": "user", "content": "Hello"},
460
- {"role": "assistant", "content": "Hi there!"},
461
- ]
462
-
463
- result = compressor.apply(messages, tokenizer)
464
-
465
- # Short messages unchanged
466
- assert result.messages[0]["content"] == "Hello"
467
- assert result.messages[1]["content"] == "Hi there!"
468
-
469
- def test_apply_tracks_transform_metadata(self, default_config, tokenizer, mock_llmlingua):
470
- """apply() returns proper TransformResult metadata."""
471
- compressor = LLMLinguaCompressor(default_config)
472
- messages = [
473
- {"role": "tool", "tool_call_id": "call_1", "content": generate_long_json(100)},
474
- ]
475
-
476
- result = compressor.apply(messages, tokenizer)
477
-
478
- assert result.tokens_before > 0
479
- assert result.tokens_after > 0
480
- assert len(result.transforms_applied) > 0
481
- assert "llmlingua" in result.transforms_applied[0]
482
-
483
- def test_apply_adds_warning_when_unavailable(self, default_config, tokenizer):
484
- """apply() adds warning when llmlingua unavailable."""
485
- compressor = LLMLinguaCompressor(default_config)
486
- messages = [{"role": "user", "content": "test"}]
487
-
488
- with patch(
489
- "headroom.transforms.llmlingua_compressor._check_llmlingua_available",
490
- return_value=False,
491
- ):
492
- result = compressor.apply(messages, tokenizer)
493
-
494
- assert len(result.warnings) > 0
495
- assert "llmlingua" in result.warnings[0].lower()
496
-
497
-
498
- # =============================================================================
499
- # TestDeviceResolution
500
- # =============================================================================
501
-
502
-
503
- class TestDeviceResolution:
504
- """Tests for device resolution logic."""
505
-
506
- def test_resolve_explicit_device(self, default_config):
507
- """Explicit device is returned unchanged."""
508
- config = LLMLinguaConfig(device="cuda")
509
- compressor = LLMLinguaCompressor(config)
510
-
511
- assert compressor._resolve_device() == "cuda"
512
-
513
- def test_resolve_auto_to_cpu_no_torch(self, default_config):
514
- """Auto resolves to CPU when torch unavailable."""
515
- config = LLMLinguaConfig(device="auto")
516
- compressor = LLMLinguaCompressor(config)
517
-
518
- with patch.dict("sys.modules", {"torch": None}):
519
- with patch(
520
- "headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._resolve_device"
521
- ) as mock_resolve:
522
- mock_resolve.return_value = "cpu"
523
- assert compressor._resolve_device() == "cpu"
524
-
525
-
526
- # =============================================================================
527
- # TestCCRIntegration
528
- # =============================================================================
529
-
530
-
531
- class TestCCRIntegration:
532
- """Tests for CCR (Compress-Cache-Retrieve) integration."""
533
-
534
- def test_ccr_stores_original(self, mock_llmlingua):
535
- """Compressed content is stored in CCR when enabled."""
536
- config = LLMLinguaConfig(
537
- enable_ccr=True,
538
- min_tokens_for_compression=10,
539
- )
540
- compressor = LLMLinguaCompressor(config)
541
- content = generate_long_text(200)
542
-
543
- with patch(
544
- "headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr"
545
- ) as mock_store:
546
- mock_store.return_value = "hash123"
547
-
548
- result = compressor.compress(content)
549
-
550
- mock_store.assert_called_once()
551
- assert result.cache_key == "hash123"
552
-
553
- def test_ccr_skipped_when_disabled(self, mock_llmlingua):
554
- """CCR is not used when disabled in config."""
555
- config = LLMLinguaConfig(
556
- enable_ccr=False,
557
- min_tokens_for_compression=10,
558
- )
559
- compressor = LLMLinguaCompressor(config)
560
- content = generate_long_text(200)
561
-
562
- with patch(
563
- "headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr"
564
- ) as mock_store:
565
- result = compressor.compress(content)
566
-
567
- mock_store.assert_not_called()
568
- assert result.cache_key is None
569
-
570
- def test_ccr_handles_storage_error(self, mock_llmlingua):
571
- """CCR storage errors are handled gracefully."""
572
- config = LLMLinguaConfig(
573
- enable_ccr=True,
574
- min_tokens_for_compression=10,
575
- )
576
- compressor = LLMLinguaCompressor(config)
577
- content = generate_long_text(200)
578
-
579
- with patch(
580
- "headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr"
581
- ) as mock_store:
582
- # Return None to simulate storage failure (internal error handling)
583
- mock_store.return_value = None
584
-
585
- # Should not raise
586
- result = compressor.compress(content)
587
-
588
- # Storage failed, so cache_key should be None
589
- assert result.cache_key is None
590
-
591
-
592
- # =============================================================================
593
- # TestConvenienceFunction
594
- # =============================================================================
595
-
596
-
597
- class TestConvenienceFunction:
598
- """Tests for compress_with_llmlingua convenience function."""
599
-
600
- def test_compress_with_llmlingua_basic(self, mock_llmlingua):
601
- """compress_with_llmlingua works with default settings."""
602
- content = generate_long_text(200)
603
-
604
- # Disable CCR for this test to avoid hash suffix
605
- with patch(
606
- "headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr"
607
- ) as mock_store:
608
- mock_store.return_value = None
609
- result = compress_with_llmlingua(content)
610
-
611
- # Should contain the compressed content
612
- assert "compressed content here" in result
613
-
614
- def test_compress_with_llmlingua_custom_rate(self, mock_llmlingua):
615
- """compress_with_llmlingua accepts custom compression rate."""
616
- content = generate_long_text(200)
617
-
618
- compress_with_llmlingua(content, compression_rate=0.5)
619
-
620
- # Verify compress_prompt was called
621
- mock_llmlingua.compress_prompt.assert_called()
622
-
623
- def test_compress_with_llmlingua_with_context(self, mock_llmlingua):
624
- """compress_with_llmlingua passes context."""
625
- content = generate_long_text(200)
626
- context = "important keywords"
627
-
628
- compress_with_llmlingua(content, context=context)
629
-
630
- call_args = mock_llmlingua.compress_prompt.call_args
631
- force_tokens = call_args.kwargs.get("force_tokens", [])
632
- # Context words should be in force_tokens
633
- assert any("important" in str(t) for t in force_tokens) or len(force_tokens) > 0
634
-
635
-
636
- # =============================================================================
637
- # TestEdgeCases
638
- # =============================================================================
639
-
640
-
641
- class TestEdgeCases:
642
- """Edge case tests for LLMLingua compressor."""
643
-
644
- def test_empty_content(self, compressor):
645
- """Empty content is handled gracefully."""
646
- result = compressor.compress("")
647
-
648
- assert result.compressed == ""
649
- assert result.compression_ratio == 1.0
650
-
651
- def test_whitespace_only_content(self, compressor):
652
- """Whitespace-only content is handled gracefully."""
653
- result = compressor.compress(" \n\t\n ")
654
-
655
- assert result.compression_ratio == 1.0
656
-
657
- def test_unicode_content(self, default_config, mock_llmlingua):
658
- """Unicode content is handled correctly."""
659
- mock_llmlingua.compress_prompt.return_value = {
660
- "compressed_prompt": "compressed \u4e2d\u6587 content",
661
- "origin_tokens": 100,
662
- "compressed_tokens": 30,
663
- }
664
-
665
- compressor = LLMLinguaCompressor(default_config)
666
- content = "\u4e2d\u6587 \u65e5\u672c\u8a9e " * 100 # Chinese/Japanese text
667
-
668
- result = compressor.compress(content)
669
-
670
- assert "\u4e2d\u6587" in result.compressed
671
-
672
- def test_very_long_content(self, default_config, mock_llmlingua):
673
- """Very long content is compressed."""
674
- compressor = LLMLinguaCompressor(default_config)
675
- content = generate_long_text(10000)
676
-
677
- compressor.compress(content)
678
-
679
- mock_llmlingua.compress_prompt.assert_called_once()
680
-
681
- def test_mixed_content_types(self, default_config, mock_llmlingua):
682
- """Mixed content (JSON with text) is handled."""
683
- compressor = LLMLinguaCompressor(default_config)
684
- # JSON-like but with extra text
685
- content = 'Some preamble text\n{"key": "value"}\nMore text after'
686
-
687
- # Should not crash
688
- result = compressor.compress(content)
689
- assert result is not None
690
-
691
- def test_malformed_json_content(self, default_config, mock_llmlingua):
692
- """Malformed JSON is treated as text."""
693
- compressor = LLMLinguaCompressor(default_config)
694
- content = "{malformed: json, missing quotes" * 50
695
-
696
- rate = compressor._get_compression_rate(content, None)
697
-
698
- # Should not detect as JSON
699
- assert rate == default_config.text_compression_rate
700
-
701
- def test_force_tokens_list_handling(self, default_config, mock_llmlingua):
702
- """Force tokens list is properly passed."""
703
- config = LLMLinguaConfig(
704
- force_tokens=["keep", "these", "tokens"],
705
- min_tokens_for_compression=10,
706
- )
707
- compressor = LLMLinguaCompressor(config)
708
- content = generate_long_text(200)
709
-
710
- compressor.compress(content)
711
-
712
- call_args = mock_llmlingua.compress_prompt.call_args
713
- force_tokens = call_args.kwargs.get("force_tokens", [])
714
- assert "keep" in force_tokens
715
- assert "these" in force_tokens
716
- assert "tokens" in force_tokens
717
-
718
-
719
- # =============================================================================
720
- # Integration Tests (only run if llmlingua is installed)
721
- # =============================================================================
722
-
723
-
724
- @pytest.mark.skipif(not LLMLINGUA_INSTALLED, reason="llmlingua not installed")
725
- class TestLLMLinguaIntegration:
726
- """Integration tests that require actual llmlingua installation.
727
-
728
- These tests verify the actual compression behavior and should be run
729
- in environments where llmlingua is installed.
730
- """
731
-
732
- def test_actual_compression(self):
733
- """Test actual compression with real llmlingua."""
734
- config = LLMLinguaConfig(
735
- target_compression_rate=0.3,
736
- min_tokens_for_compression=50,
737
- enable_ccr=False,
738
- )
739
- compressor = LLMLinguaCompressor(config)
740
- content = generate_long_text(500)
741
-
742
- result = compressor.compress(content)
743
-
744
- # Should achieve actual compression
745
- assert result.compression_ratio < 1.0
746
- assert result.tokens_saved > 0
747
- assert len(result.compressed) < len(content)
748
-
749
- def test_actual_json_compression(self):
750
- """Test JSON content compression with real llmlingua."""
751
- config = LLMLinguaConfig(
752
- target_compression_rate=0.35,
753
- min_tokens_for_compression=50,
754
- enable_ccr=False,
755
- )
756
- compressor = LLMLinguaCompressor(config)
757
- content = generate_long_json(50)
758
-
759
- result = compressor.compress(content, content_type="json")
760
-
761
- assert result.compression_ratio < 1.0
762
-
763
- def test_actual_code_compression(self):
764
- """Test code content compression with real llmlingua."""
765
- config = LLMLinguaConfig(
766
- target_compression_rate=0.4,
767
- min_tokens_for_compression=50,
768
- enable_ccr=False,
769
- )
770
- compressor = LLMLinguaCompressor(config)
771
- content = generate_long_code(30)
772
-
773
- result = compressor.compress(content, content_type="code")
774
-
775
- assert result.compression_ratio < 1.0
776
-
777
-
778
- # =============================================================================
779
- # TestMemoryManagement
780
- # =============================================================================
781
-
782
-
783
- class TestMemoryManagement:
784
- """Tests for memory management functions (unload_llmlingua_model, is_llmlingua_model_loaded)."""
785
-
786
- def test_is_model_loaded_returns_false_initially(self):
787
- """is_llmlingua_model_loaded returns False when no model loaded."""
788
- # Ensure model is unloaded
789
- with patch(
790
- "headroom.transforms.llmlingua_compressor._llmlingua_instance",
791
- None,
792
- ):
793
- assert is_llmlingua_model_loaded() is False
794
-
795
- def test_is_model_loaded_returns_true_when_loaded(self):
796
- """is_llmlingua_model_loaded returns True when model is loaded."""
797
- mock_instance = MagicMock()
798
-
799
- with patch(
800
- "headroom.transforms.llmlingua_compressor._llmlingua_instance",
801
- mock_instance,
802
- ):
803
- assert is_llmlingua_model_loaded() is True
804
-
805
- def test_unload_returns_false_when_no_model(self):
806
- """unload_llmlingua_model returns False when no model loaded."""
807
- import headroom.transforms.llmlingua_compressor as module
808
-
809
- # Save original
810
- original = module._llmlingua_instance
811
-
812
- try:
813
- module._llmlingua_instance = None
814
- result = unload_llmlingua_model()
815
- assert result is False
816
- finally:
817
- module._llmlingua_instance = original
818
-
819
- def test_unload_clears_instance(self):
820
- """unload_llmlingua_model clears the global instance."""
821
- import headroom.transforms.llmlingua_compressor as module
822
-
823
- # Save original
824
- original = module._llmlingua_instance
825
-
826
- try:
827
- # Set a mock instance
828
- mock_instance = MagicMock()
829
- mock_instance._model_name = "test-model"
830
- module._llmlingua_instance = mock_instance
831
-
832
- # Unload
833
- result = unload_llmlingua_model()
834
-
835
- assert result is True
836
- assert module._llmlingua_instance is None
837
- finally:
838
- module._llmlingua_instance = original
839
-
840
- def test_unload_clears_cuda_cache(self):
841
- """unload_llmlingua_model attempts to clear CUDA cache."""
842
- import headroom.transforms.llmlingua_compressor as module
843
-
844
- original = module._llmlingua_instance
845
-
846
- try:
847
- mock_instance = MagicMock()
848
- mock_instance._model_name = "test-model"
849
- module._llmlingua_instance = mock_instance
850
-
851
- mock_torch = MagicMock()
852
- mock_torch.cuda.is_available.return_value = True
853
-
854
- with patch.dict("sys.modules", {"torch": mock_torch}):
855
- with patch(
856
- "headroom.transforms.llmlingua_compressor.torch",
857
- mock_torch,
858
- create=True,
859
- ):
860
- result = unload_llmlingua_model()
861
-
862
- assert result is True
863
- finally:
864
- module._llmlingua_instance = original
865
-
866
-
867
- # =============================================================================
868
- # TestThreadSafety
869
- # =============================================================================
870
-
871
-
872
- class TestThreadSafety:
873
- """Tests for thread safety of model loading."""
874
-
875
- def test_lock_exists(self):
876
- """Verify thread lock is available."""
877
- import headroom.transforms.llmlingua_compressor as module
878
-
879
- assert hasattr(module, "_llmlingua_lock")
880
- import threading
881
-
882
- assert isinstance(module._llmlingua_lock, type(threading.Lock()))
883
-
884
-
885
- # =============================================================================
886
- # TestErrorMessages
887
- # =============================================================================
888
-
889
-
890
- class TestErrorMessages:
891
- """Tests for improved error messages."""
892
-
893
- def test_import_error_message_includes_install_hint(self):
894
- """ImportError includes installation instructions."""
895
- with patch(
896
- "headroom.transforms.llmlingua_compressor._check_llmlingua_available",
897
- return_value=False,
898
- ):
899
- from headroom.transforms.llmlingua_compressor import _get_llmlingua_compressor
900
-
901
- with pytest.raises(ImportError) as exc_info:
902
- _get_llmlingua_compressor("test-model", "cpu")
903
-
904
- error_msg = str(exc_info.value)
905
- assert "pip install headroom-ai[llmlingua]" in error_msg
906
- assert "2GB" in error_msg or "disk space" in error_msg.lower()
907
-
908
- def test_oom_error_provides_helpful_suggestions(self):
909
- """Out of memory error provides helpful suggestions."""
910
- import headroom.transforms.llmlingua_compressor as module
911
-
912
- # Save original state
913
- original_instance = module._llmlingua_instance
914
- original_available = module._llmlingua_available
915
-
916
- try:
917
- module._llmlingua_instance = None
918
- module._llmlingua_available = True
919
-
920
- # Create a mock that raises OOM when called
921
- mock_prompt_compressor_class = MagicMock()
922
- mock_prompt_compressor_class.side_effect = RuntimeError("CUDA out of memory")
923
-
924
- with patch.dict("sys.modules", {"llmlingua": MagicMock()}):
925
- with patch(
926
- "llmlingua.PromptCompressor",
927
- mock_prompt_compressor_class,
928
- ):
929
- from headroom.transforms.llmlingua_compressor import (
930
- _get_llmlingua_compressor,
931
- )
932
-
933
- with pytest.raises(RuntimeError) as exc_info:
934
- _get_llmlingua_compressor("test-model", "cuda")
935
-
936
- error_msg = str(exc_info.value)
937
- # Should include helpful suggestions
938
- assert "cpu" in error_msg.lower() or "memory" in error_msg.lower()
939
- finally:
940
- module._llmlingua_instance = original_instance
941
- module._llmlingua_available = original_available