compresr 2.7.3__tar.gz → 2.7.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {compresr-2.7.3 → compresr-2.7.6}/PKG-INFO +1 -1
  2. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/engine.py +65 -49
  3. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/types.py +1 -17
  4. {compresr-2.7.3 → compresr-2.7.6}/compresr/exceptions/__init__.py +1 -6
  5. {compresr-2.7.3 → compresr-2.7.6}/compresr/exceptions/exceptions.py +0 -14
  6. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langchain/middleware.py +0 -13
  7. compresr-2.7.6/compresr/integrations/litellm/__init__.py +54 -0
  8. compresr-2.7.6/compresr/integrations/litellm/cli.py +41 -0
  9. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/litellm/guardrail.py +233 -181
  10. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/litellm/initializer.py +5 -16
  11. compresr-2.7.6/compresr/integrations/litellm/shim_installer.py +105 -0
  12. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/litellm/types.py +37 -39
  13. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/llamaindex/memory.py +0 -1
  14. {compresr-2.7.3 → compresr-2.7.6}/compresr/schemas/usage.py +0 -5
  15. {compresr-2.7.3 → compresr-2.7.6}/compresr/services/compression.py +12 -8
  16. {compresr-2.7.3 → compresr-2.7.6}/compresr/services/proxy.py +0 -11
  17. {compresr-2.7.3 → compresr-2.7.6}/compresr.egg-info/PKG-INFO +1 -1
  18. {compresr-2.7.3 → compresr-2.7.6}/compresr.egg-info/SOURCES.txt +3 -0
  19. compresr-2.7.6/compresr.egg-info/entry_points.txt +3 -0
  20. {compresr-2.7.3 → compresr-2.7.6}/pyproject.toml +5 -1
  21. compresr-2.7.3/compresr/integrations/litellm/__init__.py +0 -58
  22. {compresr-2.7.3 → compresr-2.7.6}/LICENSE +0 -0
  23. {compresr-2.7.3 → compresr-2.7.6}/README.md +0 -0
  24. {compresr-2.7.3 → compresr-2.7.6}/compresr/__init__.py +0 -0
  25. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/__init__.py +0 -0
  26. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/facades/__init__.py +0 -0
  27. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/facades/anthropic.py +0 -0
  28. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/facades/native.py +0 -0
  29. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/facades/openai.py +0 -0
  30. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/normalized.py +0 -0
  31. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/__init__.py +0 -0
  32. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/agent.py +0 -0
  33. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/facade.py +0 -0
  34. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/parser.py +0 -0
  35. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/prompts.py +0 -0
  36. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/schemas/__init__.py +0 -0
  37. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/schemas/anthropic.py +0 -0
  38. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/schemas/openai.py +0 -0
  39. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/tools/__init__.py +0 -0
  40. {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/tools/web_search.py +0 -0
  41. {compresr-2.7.3 → compresr-2.7.6}/compresr/clients.py +0 -0
  42. {compresr-2.7.3 → compresr-2.7.6}/compresr/config.py +0 -0
  43. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/__init__.py +0 -0
  44. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/__init__.py +0 -0
  45. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/client.py +0 -0
  46. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/compress.py +0 -0
  47. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/errors.py +0 -0
  48. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/filters.py +0 -0
  49. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/kernel.py +0 -0
  50. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/policy.py +0 -0
  51. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/query.py +0 -0
  52. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/tokens.py +0 -0
  53. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langchain/__init__.py +0 -0
  54. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langchain/retriever.py +0 -0
  55. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langchain/wrappers.py +0 -0
  56. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langgraph/__init__.py +0 -0
  57. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langgraph/checkpoint.py +0 -0
  58. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langgraph/handoff.py +0 -0
  59. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langgraph/nodes.py +0 -0
  60. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langgraph/store.py +0 -0
  61. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/llamaindex/__init__.py +0 -0
  62. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/llamaindex/postprocessor.py +0 -0
  63. {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/llamaindex/wrappers.py +0 -0
  64. {compresr-2.7.3 → compresr-2.7.6}/compresr/schemas/__init__.py +0 -0
  65. {compresr-2.7.3 → compresr-2.7.6}/compresr/schemas/base.py +0 -0
  66. {compresr-2.7.3 → compresr-2.7.6}/compresr/schemas/inference.py +0 -0
  67. {compresr-2.7.3 → compresr-2.7.6}/compresr/services/__init__.py +0 -0
  68. {compresr-2.7.3 → compresr-2.7.6}/compresr/services/base.py +0 -0
  69. {compresr-2.7.3 → compresr-2.7.6}/compresr.egg-info/dependency_links.txt +0 -0
  70. {compresr-2.7.3 → compresr-2.7.6}/compresr.egg-info/requires.txt +0 -0
  71. {compresr-2.7.3 → compresr-2.7.6}/compresr.egg-info/top_level.txt +0 -0
  72. {compresr-2.7.3 → compresr-2.7.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compresr
3
- Version: 2.7.3
3
+ Version: 2.7.6
4
4
  Summary: Python SDK for Compresr - Intelligent prompt compression service
5
5
  Author-email: Compresr Team <founders@compresr.ai>
6
6
  License-Expression: Apache-2.0
@@ -23,11 +23,6 @@ from .normalized import Citation, CompresrStats, NormalizedResult
23
23
  logger = logging.getLogger(__name__)
24
24
 
25
25
 
26
- # ---------------------------------------------------------------------------
27
- # Lazy imports — keep `import compresr.agents` cheap.
28
- # ---------------------------------------------------------------------------
29
-
30
-
31
26
  def _import_lc() -> tuple[Any, Any, Any]:
32
27
  """Resolve ``init_chat_model``, ``create_agent``, and middleware lazily."""
33
28
  try:
@@ -56,28 +51,9 @@ def _CompresrToolMiddleware(*args: Any, **kwargs: Any) -> Any:
56
51
  return _mw(*args, **kwargs)
57
52
 
58
53
 
59
- # ---------------------------------------------------------------------------
60
- # Helpers
61
- # ---------------------------------------------------------------------------
62
-
63
-
64
54
  _KNOWN_PROVIDERS = {"anthropic", "openai", "google_genai"}
65
55
 
66
56
 
67
- # ---------------------------------------------------------------------------
68
- # Solo-WebSearchTool auto-route
69
- # ---------------------------------------------------------------------------
70
- #
71
- # When a generic-agent facade (``client.messages.create`` / ``client.chat
72
- # .completions.create`` / ``client.run``) is invoked with exactly the
73
- # Compresr ``WebSearchTool`` and nothing else, the engine silently routes the
74
- # call through the deep-research loop instead of the standard ``create_agent``
75
- # middleware path. The caller still gets back their facade's expected shape
76
- # (Anthropic ``Message`` / OpenAI ``ChatCompletion`` / ``NormalizedResult``),
77
- # but the loop semantics are research-agent: ``tool_choice="none"`` on the
78
- # final step, per-snippet compression against the live query, 10-step cap,
79
- # manual Anthropic ``cache_control`` stamping.
80
- #
81
57
  # Detection is by tool name because ``WebSearchTool.__new__`` returns a
82
58
  # ``StructuredTool`` (not a ``WebSearchTool`` instance), so ``isinstance``
83
59
  # would always be False.
@@ -163,6 +139,13 @@ _LLM_BIND_KWARGS = frozenset(
163
139
  }
164
140
  )
165
141
 
142
+ # Constructor-only kwargs — transport, not per-call LLM knobs. Baked into the
143
+ # chat model at build time so a custom httpx client (corporate proxy / custom
144
+ # CA bundle / verify=False) reaches the provider SDK. Requires langchain-anthropic
145
+ # >= the release that adds ``http_client``/``http_async_client`` to ChatAnthropic;
146
+ # langchain-openai already supports both.
147
+ _LLM_CONSTRUCTOR_KWARGS = frozenset({"http_client", "http_async_client"})
148
+
166
149
 
167
150
  def _parse_llm(llm: str) -> tuple[str, Optional[str]]:
168
151
  """Split ``"provider[:model]"`` (or ``"provider[/model]"``) into
@@ -259,7 +242,6 @@ def _extract_citations(msg: Any) -> list:
259
242
  for block in blocks:
260
243
  if not isinstance(block, dict):
261
244
  continue
262
- # Anthropic: text block with `citations` list
263
245
  for cit in block.get("citations", []) or []:
264
246
  if not isinstance(cit, dict):
265
247
  continue
@@ -275,7 +257,6 @@ def _extract_citations(msg: Any) -> list:
275
257
  provider_metadata=dict(cit),
276
258
  )
277
259
  )
278
- # OpenAI Responses: annotations[] with type="url_citation"
279
260
  for ann in block.get("annotations", []) or []:
280
261
  if not isinstance(ann, dict):
281
262
  continue
@@ -291,7 +272,6 @@ def _extract_citations(msg: Any) -> list:
291
272
  provider_metadata=dict(ann),
292
273
  )
293
274
  )
294
- # Gemini: grounding_metadata on response_metadata
295
275
  response_metadata = getattr(msg, "response_metadata", None) or {}
296
276
  grounding = response_metadata.get("grounding_metadata") or {}
297
277
  for chunk in grounding.get("grounding_chunks", []) or []:
@@ -446,11 +426,6 @@ def _last_ai_message(messages: list) -> Any:
446
426
  return messages[-1] if messages else None
447
427
 
448
428
 
449
- # ---------------------------------------------------------------------------
450
- # _Engine
451
- # ---------------------------------------------------------------------------
452
-
453
-
454
429
  class _Engine:
455
430
  """Private engine shared by the provider-shape facades.
456
431
 
@@ -478,6 +453,8 @@ class _Engine:
478
453
  prompt_cache_ttl: str = "5m",
479
454
  prompt_cache_min_messages: int = 2,
480
455
  openai_prompt_cache_key: Optional[str] = None,
456
+ llm_http_client: Any = None,
457
+ llm_http_async_client: Any = None,
481
458
  ) -> None:
482
459
  self._compresr_client = compresr_client
483
460
  self._provider, self._default_model_name = _parse_llm(llm)
@@ -487,6 +464,11 @@ class _Engine:
487
464
  self._prompt_cache_ttl = prompt_cache_ttl
488
465
  self._prompt_cache_min_messages = int(prompt_cache_min_messages)
489
466
  self._openai_prompt_cache_key = openai_prompt_cache_key
467
+ # Custom httpx clients for the LLM provider transport (corporate proxy /
468
+ # custom CA / verify settings). Baked into every chat-model build; a
469
+ # per-call http_client passed to run() overrides these.
470
+ self._llm_http_client = llm_http_client
471
+ self._llm_http_async_client = llm_http_async_client
490
472
  # Chat-model cache keyed by effective model name. Construction is
491
473
  # deferred to the first ``run()`` so a provider-only client
492
474
  # (``llm="anthropic"``) doesn't try to resolve a model at init time.
@@ -494,10 +476,6 @@ class _Engine:
494
476
  # combinations get their own cached chat model.
495
477
  self._chat_models: dict[tuple[str, tuple], Any] = {}
496
478
 
497
- # ------------------------------------------------------------------
498
- # Public, read-only accessors
499
- # ------------------------------------------------------------------
500
-
501
479
  @property
502
480
  def provider(self) -> str:
503
481
  return self._provider
@@ -516,10 +494,6 @@ class _Engine:
516
494
  """
517
495
  return self._default_model_name
518
496
 
519
- # ------------------------------------------------------------------
520
- # Build helpers
521
- # ------------------------------------------------------------------
522
-
523
497
  def _get_or_build_chat(self, model_name: str, extra_kwargs: Optional[dict] = None) -> Any:
524
498
  """Return a cached chat model for ``model_name`` + ``extra_kwargs``,
525
499
  building on miss.
@@ -544,6 +518,14 @@ class _Engine:
544
518
  kwargs: dict = dict(extra_kwargs)
545
519
  if self._llm_api_key is not None:
546
520
  kwargs.setdefault("api_key", self._llm_api_key)
521
+ # Client-level custom httpx clients. setdefault so a per-call override
522
+ # from run(http_client=...) wins. Only Anthropic and OpenAI chat models
523
+ # accept these kwargs; google_genai uses a different transport.
524
+ if self._provider in ("anthropic", "openai"):
525
+ if self._llm_http_client is not None:
526
+ kwargs.setdefault("http_client", self._llm_http_client)
527
+ if self._llm_http_async_client is not None:
528
+ kwargs.setdefault("http_async_client", self._llm_http_async_client)
547
529
  # OpenAI caching is server-side; we only pass ``prompt_cache_key``
548
530
  # and ``prompt_cache_retention`` via ``model_kwargs`` so they survive
549
531
  # ``bind_tools``.
@@ -564,8 +546,50 @@ class _Engine:
564
546
  )
565
547
  except TypeError: # pragma: no cover - older langchain-openai
566
548
  logger.debug("init_chat_model rejected output_version; falling back without it")
549
+ if self._provider == "anthropic":
550
+ return self._build_anthropic_chat(model_name, kwargs)
567
551
  return init_chat_model(f"{self._provider}:{model_name}", **kwargs)
568
552
 
553
+ def _build_anthropic_chat(self, model_name: str, kwargs: dict) -> Any:
554
+ """Build an Anthropic chat model, shimming ``http_client`` support on
555
+ versions of langchain-anthropic that predate the native field.
556
+
557
+ Native field present (PR merged / future release) → pass through. Absent
558
+ (current PyPI) → strip the unknown kwargs so the constructor doesn't drop
559
+ them silently, then swap the cached anthropic SDK client for one built
560
+ with the caller's httpx client. The shim is a no-op once native support
561
+ ships, so it can be removed then. See langchain issue #36056."""
562
+ from langchain_anthropic import ChatAnthropic
563
+
564
+ native = "http_client" in ChatAnthropic.model_fields
565
+ sync_client = kwargs.get("http_client")
566
+ async_client = kwargs.get("http_async_client")
567
+ if not native:
568
+ kwargs.pop("http_client", None)
569
+ kwargs.pop("http_async_client", None)
570
+
571
+ chat = init_chat_model(f"anthropic:{model_name}", **kwargs)
572
+
573
+ if not native and (sync_client is not None or async_client is not None):
574
+ self._inject_anthropic_http_client(chat, sync_client, async_client)
575
+ return chat
576
+
577
+ @staticmethod
578
+ def _inject_anthropic_http_client(chat: Any, sync_client: Any, async_client: Any) -> None:
579
+ """Replace ChatAnthropic's cached SDK clients with ones built around the
580
+ caller's httpx client, reusing langchain's own ``_client_params`` so
581
+ api_key / base_url / retries / headers stay correct. Writes through
582
+ ``__dict__`` to seed the ``cached_property`` before first access."""
583
+ import anthropic
584
+
585
+ params = dict(chat._client_params)
586
+ if sync_client is not None:
587
+ chat.__dict__["_client"] = anthropic.Anthropic(**{**params, "http_client": sync_client})
588
+ if async_client is not None:
589
+ chat.__dict__["_async_client"] = anthropic.AsyncClient(
590
+ **{**params, "http_client": async_client}
591
+ )
592
+
569
593
  def _openai_cache_model_kwargs(self) -> dict:
570
594
  """Build the OpenAI-specific cache controls. ``prompt_cache_retention``
571
595
  is only set for ``ttl="1h"`` so older langchain-openai doesn't reject
@@ -669,7 +693,7 @@ class _Engine:
669
693
  """
670
694
  bind_kwargs: dict = {}
671
695
  for key in list(kw):
672
- if key in _LLM_BIND_KWARGS:
696
+ if key in _LLM_BIND_KWARGS or key in _LLM_CONSTRUCTOR_KWARGS:
673
697
  bind_kwargs[key] = kw.pop(key)
674
698
  if max_tokens is not None:
675
699
  # Explicit signature param always reflects customer intent.
@@ -720,10 +744,6 @@ class _Engine:
720
744
  # facades layer; for now just keep the original messages.
721
745
  return list(messages)
722
746
 
723
- # ------------------------------------------------------------------
724
- # run / arun
725
- # ------------------------------------------------------------------
726
-
727
747
  def run(
728
748
  self,
729
749
  *,
@@ -840,10 +860,6 @@ class _Engine:
840
860
  )
841
861
  return self._normalize({"messages": state.messages})
842
862
 
843
- # ------------------------------------------------------------------
844
- # Normalization
845
- # ------------------------------------------------------------------
846
-
847
863
  def _normalize(self, agent_result: Any) -> NormalizedResult:
848
864
  """Reshape ``{"messages": [...]}`` into a :class:`NormalizedResult`.
849
865
 
@@ -28,23 +28,7 @@ class Step:
28
28
 
29
29
  @dataclass(frozen=True)
30
30
  class ResearchUsage:
31
- """Raw usage counts aggregated across the research loop.
32
-
33
- The SDK exposes counts only — no USD pricing. Neither Anthropic / OpenAI
34
- inference responses nor Brave / Tavily search responses carry a per-call
35
- dollar field, so any cost rollup must be done by the caller against their
36
- own (mutable) price table. Compresr's benchmark suite owns the canonical
37
- table for its experiments; downstream apps are expected to do the same.
38
-
39
- Fields:
40
- * ``input_tokens`` / ``output_tokens`` — fresh tokens, as reported by
41
- the provider's ``usage`` block.
42
- * ``cache_read_tokens`` / ``cache_creation_tokens`` — Anthropic
43
- ephemeral cache classes (0 elsewhere).
44
- * ``calls`` — LLM round-trips (one per loop step that hit the model).
45
- * ``search_calls`` — number of times the search tool fired. Independent
46
- of ``calls`` because one LLM turn can request multiple tool calls.
47
- """
31
+ """Token + tool-call counts across the research loop."""
48
32
 
49
33
  input_tokens: int = 0
50
34
  output_tokens: int = 0
@@ -5,7 +5,7 @@ Exact copies of exceptions from backend.
5
5
  Single source of truth maintained in backend.
6
6
  """
7
7
 
8
- from .exceptions import ( # Response models; Exception classes
8
+ from .exceptions import (
9
9
  ApiKeyBudgetError,
10
10
  AuthenticationError,
11
11
  AuthenticationErrorResponse,
@@ -40,7 +40,6 @@ ConnectionError = CompresrConnectionError
40
40
  TimeoutError = CompresrTimeoutError
41
41
 
42
42
  __all__ = [
43
- # Response models
44
43
  "ErrorResponse",
45
44
  "ValidationErrorResponse",
46
45
  "AuthenticationErrorResponse",
@@ -49,7 +48,6 @@ __all__ = [
49
48
  "ServerErrorResponse",
50
49
  "NotFoundErrorResponse",
51
50
  "ConnectionErrorResponse",
52
- # Exception classes
53
51
  "CompresrError",
54
52
  "AuthenticationError",
55
53
  "TargetAuthenticationError",
@@ -59,16 +57,13 @@ __all__ = [
59
57
  "ServerError",
60
58
  "NotFoundError",
61
59
  "CompresrConnectionError",
62
- # Budget & Credits
63
60
  "InsufficientCreditsError",
64
61
  "BudgetLimitError",
65
62
  "DailyLimitError",
66
63
  "ApiKeyBudgetError",
67
- # Model & Input
68
64
  "ModelNotFoundError",
69
65
  "ContextWindowExceededError",
70
66
  "ContentPolicyError",
71
- # Service
72
67
  "CompresrTimeoutError",
73
68
  "ServiceUnavailableError",
74
69
  ]
@@ -8,8 +8,6 @@ from typing import Optional
8
8
 
9
9
  from pydantic import BaseModel
10
10
 
11
- # Response Models (for documentation)
12
-
13
11
 
14
12
  class ErrorResponse(BaseModel):
15
13
  """Generic error response."""
@@ -78,9 +76,6 @@ class ConnectionErrorResponse(BaseModel):
78
76
  code: str = "connection_error"
79
77
 
80
78
 
81
- # Exception Classes
82
-
83
-
84
79
  class CompresrError(Exception):
85
80
  """Base exception for all Compresr errors."""
86
81
 
@@ -167,9 +162,6 @@ class CompresrConnectionError(CompresrError):
167
162
  self.service = service
168
163
 
169
164
 
170
- # Budget & Credits Errors
171
-
172
-
173
165
  class InsufficientCreditsError(CompresrError):
174
166
  """User has insufficient credits to complete the request."""
175
167
 
@@ -230,9 +222,6 @@ class ApiKeyBudgetError(CompresrError):
230
222
  self.api_key_used = api_key_used
231
223
 
232
224
 
233
- # Model & Input Errors
234
-
235
-
236
225
  class ModelNotFoundError(CompresrError):
237
226
  """Requested model does not exist."""
238
227
 
@@ -276,9 +265,6 @@ class ContentPolicyError(CompresrError):
276
265
  self.provider = provider
277
266
 
278
267
 
279
- # Service Errors
280
-
281
-
282
268
  class CompresrTimeoutError(CompresrError):
283
269
  """Request timed out."""
284
270
 
@@ -195,11 +195,6 @@ class CompresrToolMiddleware(_CompresrMiddlewareBase):
195
195
  return _build_tool_message(new, result) if new != result.content else result
196
196
 
197
197
 
198
- # ---------------------------------------------------------------------------
199
- # Summarization-style history compression
200
- # ---------------------------------------------------------------------------
201
-
202
-
203
198
  SUMMARY_PREFIX = "[Earlier conversation summary]\n\n"
204
199
 
205
200
 
@@ -297,7 +292,6 @@ class CompresrSummarizationMiddleware(_CompresrMiddlewareBase):
297
292
  if total < self._max_tokens:
298
293
  return None
299
294
 
300
- # Split: everything but the last `keep` is "old".
301
295
  to_summarize = messages[: -self._keep]
302
296
  recent = messages[-self._keep :]
303
297
  if not to_summarize:
@@ -337,11 +331,6 @@ class CompresrSummarizationMiddleware(_CompresrMiddlewareBase):
337
331
  }
338
332
 
339
333
 
340
- # ---------------------------------------------------------------------------
341
- # CompresrPromptMiddleware — outbound prompt budget cap
342
- # ---------------------------------------------------------------------------
343
-
344
-
345
334
  def _rebuild_with_content(msg: "BaseMessage", new_content: str) -> "BaseMessage":
346
335
  """Recreate a message of the same class with new string content."""
347
336
  kwargs: dict = {"content": new_content}
@@ -530,14 +519,12 @@ def _rebuild_request(request: Any, messages: list) -> Any:
530
519
  side-effect free. Falls back to mutation only if neither the Pydantic
531
520
  ``model_copy`` path nor the ``__dict__`` copy path is available.
532
521
  """
533
- # Pydantic v2 models expose ``model_copy(update=...)``.
534
522
  model_copy = getattr(request, "model_copy", None)
535
523
  if callable(model_copy):
536
524
  try:
537
525
  return model_copy(update={"messages": messages})
538
526
  except Exception: # noqa: BLE001 — fall through to dict path
539
527
  pass
540
- # Generic objects: clone via ``__class__`` + ``__dict__`` where possible.
541
528
  cls = type(request)
542
529
  try:
543
530
  clone = cls.__new__(cls) # type: ignore[call-overload]
@@ -0,0 +1,54 @@
1
+ """LiteLLM integration for Compresr (proxy guardrail)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ try:
8
+ import litellm # noqa: F401
9
+ except ImportError as exc: # pragma: no cover
10
+ raise ImportError(
11
+ "compresr LiteLLM integration requires litellm. "
12
+ "Install with: pip install compresr[litellm]"
13
+ ) from exc
14
+
15
+ from .guardrail import (
16
+ CompresrGuardrail,
17
+ CompresrGuardrailError,
18
+ CompresrGuardrailMissingSecrets,
19
+ )
20
+ from .initializer import (
21
+ guardrail_class_registry,
22
+ guardrail_initializer_registry,
23
+ initialize_guardrail,
24
+ )
25
+ from .types import (
26
+ CompresrGuardrailConfigModel,
27
+ CompresrGuardrailConfigModelOptionalParams,
28
+ )
29
+
30
+
31
+ def _maybe_auto_install_shim() -> None:
32
+ if os.environ.get("COMPRESR_AUTO_INSTALL_SHIM", "").lower() not in ("1", "true", "yes"):
33
+ return
34
+ try:
35
+ from .shim_installer import install
36
+
37
+ install(verbose=False)
38
+ except Exception: # pragma: no cover - best-effort
39
+ pass
40
+
41
+
42
+ _maybe_auto_install_shim()
43
+
44
+
45
+ __all__ = [
46
+ "CompresrGuardrail",
47
+ "CompresrGuardrailConfigModel",
48
+ "CompresrGuardrailConfigModelOptionalParams",
49
+ "CompresrGuardrailError",
50
+ "CompresrGuardrailMissingSecrets",
51
+ "guardrail_class_registry",
52
+ "guardrail_initializer_registry",
53
+ "initialize_guardrail",
54
+ ]
@@ -0,0 +1,41 @@
1
+ """``compresr-litellm`` — stock ``litellm`` CLI with the Compresr guardrail pre-registered."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+
8
+ def register() -> None:
9
+ """Inject the Compresr guardrail into LiteLLM's in-memory registries."""
10
+ try:
11
+ from litellm.proxy.guardrails.guardrail_registry import (
12
+ guardrail_class_registry,
13
+ guardrail_initializer_registry,
14
+ )
15
+ except ImportError as exc: # pragma: no cover
16
+ raise SystemExit(
17
+ "compresr-litellm requires the LiteLLM proxy. "
18
+ "Install with: pip install 'compresr[litellm]'"
19
+ ) from exc
20
+
21
+ from . import CompresrGuardrail, initialize_guardrail
22
+
23
+ guardrail_initializer_registry["compresr"] = initialize_guardrail
24
+ guardrail_class_registry["compresr"] = CompresrGuardrail
25
+
26
+
27
+ def main() -> None:
28
+ register()
29
+ try:
30
+ from litellm.proxy.proxy_cli import run_server
31
+ except ImportError as exc: # pragma: no cover
32
+ raise SystemExit(
33
+ "compresr-litellm requires the LiteLLM proxy. "
34
+ "Install with: pip install 'compresr[litellm]'"
35
+ ) from exc
36
+ run_server()
37
+
38
+
39
+ if __name__ == "__main__": # pragma: no cover
40
+ main()
41
+ sys.exit(0)