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.
- {compresr-2.7.3 → compresr-2.7.6}/PKG-INFO +1 -1
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/engine.py +65 -49
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/types.py +1 -17
- {compresr-2.7.3 → compresr-2.7.6}/compresr/exceptions/__init__.py +1 -6
- {compresr-2.7.3 → compresr-2.7.6}/compresr/exceptions/exceptions.py +0 -14
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langchain/middleware.py +0 -13
- compresr-2.7.6/compresr/integrations/litellm/__init__.py +54 -0
- compresr-2.7.6/compresr/integrations/litellm/cli.py +41 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/litellm/guardrail.py +233 -181
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/litellm/initializer.py +5 -16
- compresr-2.7.6/compresr/integrations/litellm/shim_installer.py +105 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/litellm/types.py +37 -39
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/llamaindex/memory.py +0 -1
- {compresr-2.7.3 → compresr-2.7.6}/compresr/schemas/usage.py +0 -5
- {compresr-2.7.3 → compresr-2.7.6}/compresr/services/compression.py +12 -8
- {compresr-2.7.3 → compresr-2.7.6}/compresr/services/proxy.py +0 -11
- {compresr-2.7.3 → compresr-2.7.6}/compresr.egg-info/PKG-INFO +1 -1
- {compresr-2.7.3 → compresr-2.7.6}/compresr.egg-info/SOURCES.txt +3 -0
- compresr-2.7.6/compresr.egg-info/entry_points.txt +3 -0
- {compresr-2.7.3 → compresr-2.7.6}/pyproject.toml +5 -1
- compresr-2.7.3/compresr/integrations/litellm/__init__.py +0 -58
- {compresr-2.7.3 → compresr-2.7.6}/LICENSE +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/README.md +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/facades/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/facades/anthropic.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/facades/native.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/facades/openai.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/normalized.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/agent.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/facade.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/parser.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/research/prompts.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/schemas/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/schemas/anthropic.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/schemas/openai.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/tools/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/agents/tools/web_search.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/clients.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/config.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/client.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/compress.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/errors.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/filters.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/kernel.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/policy.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/query.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/_shared/tokens.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langchain/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langchain/retriever.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langchain/wrappers.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langgraph/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langgraph/checkpoint.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langgraph/handoff.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langgraph/nodes.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/langgraph/store.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/llamaindex/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/llamaindex/postprocessor.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/integrations/llamaindex/wrappers.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/schemas/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/schemas/base.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/schemas/inference.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/services/__init__.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr/services/base.py +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr.egg-info/dependency_links.txt +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr.egg-info/requires.txt +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/compresr.egg-info/top_level.txt +0 -0
- {compresr-2.7.3 → compresr-2.7.6}/setup.cfg +0 -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
|
-
"""
|
|
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 (
|
|
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)
|