chore: import upstream snapshot with attribution
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:35:30 +08:00
commit 26382a7ac6
2353 changed files with 629146 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
# lean-ctx (Python SDK)
Context compression for AI agents — a thin, dependency-free client for the local
[lean-ctx](https://leanctx.com) daemon.
```bash
pip install lean-ctx-sdk
```
## Drop-in `compress(messages, model)`
Compress a chat-style `messages` array before sending it to any model. Only text
payloads are rewritten through lean-ctx's deterministic funnel; images,
tool-call blocks and ids pass through untouched, and the output is byte-stable so
it stays friendly to provider prompt caching.
```python
from lean_ctx import compress
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": large_log_or_file_dump},
]
messages = compress(messages, model="claude-sonnet-4")
# → send `messages` to your provider as usual
```
Works with both OpenAI-style (`content: "string"`) and Anthropic-style
(`content: [{type: "text", …}, {type: "tool_result", …}]`) messages.
### Token-savings stats
```python
from lean_ctx import ProxyClient
result = ProxyClient().compress(messages, model="gpt-4o")
print(result.saved_tokens, result.saved_pct) # e.g. 1840 63.1
messages = result.messages
```
## Configuration
The endpoint and session token are auto-discovered from the running daemon. Every
step is overridable:
| Setting | Env var | Default |
| --- | --- | --- |
| Proxy URL | `LEAN_CTX_PROXY_URL` | `http://127.0.0.1:<port>` |
| Proxy port | `LEAN_CTX_PROXY_PORT` | `config.toml` `proxy_port`, else UID-derived |
| Session token | `LEAN_CTX_PROXY_TOKEN` | `<data_dir>/session_token` |
Or pass them explicitly (useful in CI / against a remote proxy):
```python
compress(messages, base_url="http://127.0.0.1:4444", token="")
```
If the daemon is not running, `compress()` raises `LeanCtxConnectionError`; an
unauthenticated request raises `LeanCtxAuthError`. Both subclass `LeanCtxError`.
## Framework integrations
### LiteLLM
Compress requests transparently with a `CustomLogger` (`pip install lean-ctx-sdk[litellm]`):
```python
import litellm
from lean_ctx import LeanCtxLiteLLMHandler
litellm.callbacks = [LeanCtxLiteLLMHandler(model="gpt-4o")]
# every completion now sends compressed messages
```
For non-proxy code, `compress_request_data(data)` rewrites the `messages` of any
OpenAI-style request dict in place.
### LangChain
```python
from langchain_core.messages import HumanMessage, SystemMessage
from lean_ctx import compress_messages
messages = compress_messages(
[
SystemMessage(content="You are a helpful assistant."),
HumanMessage(content=large_log_or_file_dump),
],
model="gpt-4o",
)
```
In both adapters a compaction failure never breaks the call — the original
messages are kept.
## CLI helpers
`LeanCtxClient` wraps the `lean-ctx` binary for `read` / `search` / `shell` /
`gain` / `benchmark`. The `LeanCtxRetriever` (LangChain) and `LeanCtxNodeParser`
(LlamaIndex) retrieval adapters are available via the `langchain` / `llamaindex`
extras.
## Learn more
- [compress() SDK cookbook](https://github.com/yvgude/lean-ctx/blob/main/docs/guides/compress-sdk.md) — Python + TypeScript recipes
- [lean-ctx vs Headroom](https://github.com/yvgude/lean-ctx/blob/main/docs/comparisons/vs-headroom.md) — comparison + reproducible benchmark
## License
MIT
+2
View File
@@ -0,0 +1,2 @@
# Ensures the package root is importable so `import lean_ctx` resolves when
# pytest is run from anywhere inside packages/python-lean-ctx.
@@ -0,0 +1,30 @@
"""lean-ctx SDK — context compression for AI agents and frameworks.
Drop-in usage::
from lean_ctx import compress
messages = compress(messages, model="claude-sonnet-4")
"""
from lean_ctx.client import LeanCtxClient
from lean_ctx.errors import LeanCtxAuthError, LeanCtxConnectionError, LeanCtxError
from lean_ctx.langchain import LeanCtxRetriever, compress_messages
from lean_ctx.litellm import LeanCtxLiteLLMHandler, compress_request_data
from lean_ctx.llamaindex import LeanCtxNodeParser
from lean_ctx.proxy import CompressResult, ProxyClient, compress
__version__ = "0.3.0"
__all__ = [
"compress",
"ProxyClient",
"CompressResult",
"LeanCtxClient",
"LeanCtxRetriever",
"compress_messages",
"LeanCtxLiteLLMHandler",
"compress_request_data",
"LeanCtxNodeParser",
"LeanCtxError",
"LeanCtxConnectionError",
"LeanCtxAuthError",
]
@@ -0,0 +1,63 @@
"""Core client for interacting with the lean-ctx daemon."""
import json
import subprocess
from pathlib import Path
from typing import Optional
class LeanCtxClient:
"""Thin wrapper around the lean-ctx CLI for programmatic access."""
def __init__(self, binary: str = "lean-ctx", project_root: Optional[str] = None):
self.binary = binary
self.project_root = project_root or str(Path.cwd())
def read(self, path: str, mode: str = "auto") -> str:
return self._run(["read", path, "--mode", mode])
def search(self, pattern: str, path: Optional[str] = None) -> str:
args = ["grep", pattern]
if path:
args.append(path)
return self._run(args)
def shell(self, command: str) -> str:
return self._run(["-c", command])
def gain(self) -> dict:
output = self._run(["gain", "--json"])
try:
return json.loads(output)
except json.JSONDecodeError:
return {"raw": output}
def benchmark(self, path: Optional[str] = None, json_output: bool = True) -> dict:
args = ["benchmark", "eval"]
if path:
args.append(path)
if json_output:
args.append("--json")
output = self._run(args)
try:
return json.loads(output)
except json.JSONDecodeError:
return {"raw": output}
def _run(self, args: list[str]) -> str:
try:
result = subprocess.run(
[self.binary] + args,
capture_output=True,
text=True,
cwd=self.project_root,
timeout=30,
)
return result.stdout.strip()
except FileNotFoundError:
raise RuntimeError(
f"lean-ctx binary not found at '{self.binary}'. "
"Install: curl -fsSL https://leanctx.com/install.sh | sh"
)
except subprocess.TimeoutExpired:
raise RuntimeError("lean-ctx command timed out after 30s")
@@ -0,0 +1,113 @@
"""Zero-dependency discovery of the local lean-ctx proxy endpoint.
Mirrors the daemon's own resolution so ``compress()`` works out of the box once
``lean-ctx proxy enable`` has run, while every step stays overridable via the
same environment variables the CLI honours:
* URL — ``LEAN_CTX_PROXY_URL`` else ``http://127.0.0.1:<port>``
* Port — ``LEAN_CTX_PROXY_PORT`` → ``config.toml`` ``proxy_port`` → UID-derived
* Token — ``LEAN_CTX_PROXY_TOKEN`` → ``<data_dir>/session_token``
* Dirs — ``LEAN_CTX_DATA_DIR`` / XDG, matching the Rust ``data_dir`` rules
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import List, Optional
# Base port the daemon derives per-UID (see proxy_setup::uid_based_port).
_DEFAULT_PORT = 4444
def _candidate_dirs() -> List[Path]:
"""Ordered data/config directories the daemon may have written to."""
dirs: List[Path] = []
env = os.environ.get("LEAN_CTX_DATA_DIR", "").strip()
if env:
dirs.append(Path(env))
home = Path.home()
dirs.append(home / ".lean-ctx")
xdg_data = os.environ.get("XDG_DATA_HOME", "").strip()
dirs.append(Path(xdg_data) / "lean-ctx" if xdg_data else home / ".local" / "share" / "lean-ctx")
xdg_config = os.environ.get("XDG_CONFIG_HOME", "").strip()
dirs.append(Path(xdg_config) / "lean-ctx" if xdg_config else home / ".config" / "lean-ctx")
# De-duplicate while preserving order.
seen = set()
unique: List[Path] = []
for d in dirs:
if d not in seen:
seen.add(d)
unique.append(d)
return unique
def _uid_port() -> int:
"""Replicate proxy_setup::uid_based_port (UID 1000 → 4444, +offset, base for <1000)."""
getuid = getattr(os, "getuid", None)
if getuid is None: # Windows
return _DEFAULT_PORT
uid = getuid()
offset = (uid - 1000) % 1000 if uid >= 1000 else 0
return _DEFAULT_PORT + offset
def _config_port() -> Optional[int]:
"""Read a top-level ``proxy_port`` from the first config.toml found."""
for directory in _candidate_dirs():
try:
text = (directory / "config.toml").read_text(encoding="utf-8")
except OSError:
continue
for line in text.splitlines():
stripped = line.strip()
if not stripped.startswith("proxy_port"):
continue
try:
value = stripped.split("=", 1)[1].strip().strip("\"'")
return int(value)
except (IndexError, ValueError):
break
return None
def resolve_port() -> int:
env = os.environ.get("LEAN_CTX_PROXY_PORT", "").strip()
if env:
try:
return int(env)
except ValueError:
pass
cfg = _config_port()
if cfg is not None:
return cfg
return _uid_port()
def resolve_base_url(base_url: Optional[str] = None) -> str:
if base_url:
return base_url.rstrip("/")
env = os.environ.get("LEAN_CTX_PROXY_URL", "").strip()
if env:
return env.rstrip("/")
return f"http://127.0.0.1:{resolve_port()}"
def resolve_token(token: Optional[str] = None) -> Optional[str]:
if token:
return token
env = os.environ.get("LEAN_CTX_PROXY_TOKEN", "").strip()
if env:
return env
for directory in _candidate_dirs():
try:
value = (directory / "session_token").read_text(encoding="utf-8").strip()
except OSError:
continue
if value:
return value
return None
@@ -0,0 +1,21 @@
"""Exception hierarchy for the lean-ctx SDK."""
class LeanCtxError(Exception):
"""Base class for every error raised by the lean-ctx SDK."""
class LeanCtxConnectionError(LeanCtxError):
"""The local lean-ctx proxy could not be reached.
Usually means the daemon is not running (``lean-ctx proxy enable``) or the
discovered host/port is wrong (override with ``LEAN_CTX_PROXY_URL``).
"""
class LeanCtxAuthError(LeanCtxError):
"""The proxy rejected the request (missing or invalid session token).
Provide the token explicitly, or export ``LEAN_CTX_PROXY_TOKEN`` so the SDK
can authenticate against the loopback proxy.
"""
@@ -0,0 +1,92 @@
"""LangChain integration for lean-ctx."""
from typing import Any, List, Optional
from lean_ctx.client import LeanCtxClient
from lean_ctx.proxy import compress as _compress_dicts
def _reattach_content(originals: List[Any], compressed: List[Any]) -> List[Any]:
"""Return clones of ``originals`` with ``content`` taken from ``compressed``.
Pydantic ``model_copy`` preserves each message's type and metadata; only the
textual content is swapped. If the conversion changed the message count (e.g.
tool-call splitting) the mapping is ambiguous, so the originals are returned
unchanged rather than risk corrupting the transcript.
"""
if len(originals) != len(compressed):
return list(originals)
out: List[Any] = []
for original, comp in zip(originals, compressed):
new_content = comp.get("content") if isinstance(comp, dict) else None
if new_content is None or not hasattr(original, "model_copy"):
out.append(original)
else:
out.append(original.model_copy(update={"content": new_content}))
return out
def compress_messages(messages: List[Any], model: Optional[str] = None, **kwargs: Any) -> List[Any]:
"""Compress the textual content of a list of LangChain ``BaseMessage`` objects.
Converts to the OpenAI wire shape, runs the deterministic proxy compression,
and returns new messages with only their ``content`` rewritten. Requires
``langchain-core``. Extra keyword arguments (``base_url``, ``token``,
``timeout``) are forwarded to :func:`lean_ctx.compress`.
"""
try:
from langchain_core.messages import convert_to_openai_messages
except ImportError as exc: # pragma: no cover - optional dependency
raise ImportError("langchain-core is required: pip install langchain-core") from exc
openai_messages = convert_to_openai_messages(messages)
compressed = _compress_dicts(openai_messages, model, **kwargs)
return _reattach_content(messages, compressed)
try:
from langchain_core.retrievers import BaseRetriever
from langchain_core.documents import Document
from langchain_core.callbacks import CallbackManagerForRetrieverRun
class LeanCtxRetriever(BaseRetriever):
"""LangChain retriever backed by lean-ctx hybrid search."""
client: LeanCtxClient = None
top_k: int = 10
def __init__(self, project_root: Optional[str] = None, top_k: int = 10, **kwargs):
super().__init__(**kwargs)
self.client = LeanCtxClient(project_root=project_root)
self.top_k = top_k
def _get_relevant_documents(
self, query: str, *, run_manager: CallbackManagerForRetrieverRun
) -> list[Document]:
result = self.client.search(query)
documents = []
for line in result.split("\n"):
if not line.strip():
continue
parts = line.split(":", 2)
if len(parts) >= 3:
file_path, line_num, content = parts[0], parts[1], parts[2]
documents.append(
Document(
page_content=content.strip(),
metadata={"source": file_path, "line": line_num},
)
)
else:
documents.append(Document(page_content=line.strip()))
return documents[: self.top_k]
except ImportError:
class LeanCtxRetriever:
"""Stub: install langchain-core for full integration."""
def __init__(self, **kwargs):
raise ImportError(
"langchain-core is required: pip install langchain-core"
)
@@ -0,0 +1,105 @@
"""LiteLLM integration for lean-ctx.
Compresses the ``messages`` of an outbound LiteLLM request through the local
proxy before it is sent to the provider. Two entry points:
* :func:`compress_request_data` — framework-agnostic helper that rewrites a
request ``dict`` in place (testable without LiteLLM installed).
* :class:`LeanCtxLiteLLMHandler` — a LiteLLM ``CustomLogger`` that hooks
``async_pre_call_hook`` for use with LiteLLM Proxy or ``litellm.callbacks``.
Register programmatically::
import litellm
from lean_ctx.litellm import LeanCtxLiteLLMHandler
litellm.callbacks = [LeanCtxLiteLLMHandler(model="gpt-4o")]
"""
from __future__ import annotations
import asyncio
from typing import Any, Dict, Optional
from .errors import LeanCtxError
from .proxy import ProxyClient
_PRECALL_TYPES = ("completion", "text_completion")
def compress_request_data(
data: Dict[str, Any],
*,
client: Optional[ProxyClient] = None,
model: Optional[str] = None,
raise_on_error: bool = False,
) -> Dict[str, Any]:
"""Compress ``data["messages"]`` in place and return ``data``.
Mirrors the LiteLLM/OpenAI request shape. On a proxy failure the messages are
left untouched — a compaction hiccup must never block the LLM call — unless
``raise_on_error`` is set.
"""
messages = data.get("messages")
if not isinstance(messages, list) or not messages:
return data
proxy = client or ProxyClient()
try:
data["messages"] = proxy.compress(messages, model=model or data.get("model")).messages
except LeanCtxError:
if raise_on_error:
raise
return data
try: # pragma: no cover - import wiring, exercised by presence/absence of litellm
from litellm.integrations.custom_logger import CustomLogger
_BASE: type = CustomLogger
_LITELLM_AVAILABLE = True
except Exception: # noqa: BLE001 - any litellm import failure means "not available"
_BASE = object
_LITELLM_AVAILABLE = False
class LeanCtxLiteLLMHandler(_BASE): # type: ignore[misc,valid-type]
"""LiteLLM ``CustomLogger`` that compresses requests in ``async_pre_call_hook``.
The synchronous (urllib) :class:`ProxyClient` call is dispatched to a thread
so it never blocks the proxy event loop.
"""
def __init__(
self,
*,
model: Optional[str] = None,
raise_on_error: bool = False,
base_url: Optional[str] = None,
token: Optional[str] = None,
) -> None:
if not _LITELLM_AVAILABLE:
raise ImportError("litellm is required: pip install litellm")
super().__init__()
self._client = ProxyClient(base_url=base_url, token=token)
self._model = model
self._raise_on_error = raise_on_error
async def async_pre_call_hook(
self,
user_api_key_dict: Any,
cache: Any,
data: Dict[str, Any],
call_type: str,
) -> Dict[str, Any]:
if call_type in _PRECALL_TYPES and isinstance(data, dict):
loop = asyncio.get_running_loop()
await loop.run_in_executor(
None,
lambda: compress_request_data(
data,
client=self._client,
model=self._model,
raise_on_error=self._raise_on_error,
),
)
return data
@@ -0,0 +1,47 @@
"""LlamaIndex integration for lean-ctx."""
from typing import Optional
from lean_ctx.client import LeanCtxClient
try:
from llama_index.core.node_parser import NodeParser
from llama_index.core.schema import BaseNode, TextNode, Document
class LeanCtxNodeParser(NodeParser):
"""LlamaIndex node parser using lean-ctx compression modes."""
client: LeanCtxClient = None
mode: str = "map"
def __init__(self, project_root: Optional[str] = None, mode: str = "map", **kwargs):
super().__init__(**kwargs)
self.client = LeanCtxClient(project_root=project_root)
self.mode = mode
def _parse_nodes(self, nodes: list[BaseNode], **kwargs) -> list[BaseNode]:
result_nodes = []
for node in nodes:
if isinstance(node, Document) and hasattr(node, "metadata"):
file_path = node.metadata.get("file_path", "")
if file_path:
compressed = self.client.read(file_path, mode=self.mode)
result_nodes.append(
TextNode(
text=compressed,
metadata={**node.metadata, "compression_mode": self.mode},
)
)
continue
result_nodes.append(node)
return result_nodes
except ImportError:
class LeanCtxNodeParser:
"""Stub: install llama-index-core for full integration."""
def __init__(self, **kwargs):
raise ImportError(
"llama-index-core is required: pip install llama-index-core"
)
+164
View File
@@ -0,0 +1,164 @@
"""Drop-in ``compress(messages, model)`` over the local lean-ctx proxy.
Posts a chat-style ``messages`` array to the daemon's deterministic
``POST /v1/compress`` endpoint and returns the rewritten messages. Only text
payloads are compressed; images, tool-call blocks and ids pass through
untouched, and the output is byte-stable for provider prompt caching.
Stdlib-only (``urllib``) so ``pip install lean-ctx-sdk`` pulls in no transitive
dependencies.
"""
from __future__ import annotations
import json
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from . import discovery
from .errors import LeanCtxAuthError, LeanCtxConnectionError, LeanCtxError
Message = Dict[str, Any]
_DEFAULT_TIMEOUT = 30.0
@dataclass
class CompressResult:
"""Result of a ``/v1/compress`` call: rewritten messages plus savings."""
messages: List[Message]
stats: Dict[str, Any] = field(default_factory=dict)
@property
def original_tokens(self) -> int:
return int(self.stats.get("original_tokens", 0))
@property
def compressed_tokens(self) -> int:
return int(self.stats.get("compressed_tokens", 0))
@property
def saved_tokens(self) -> int:
return int(self.stats.get("saved_tokens", 0))
@property
def saved_pct(self) -> float:
return float(self.stats.get("saved_pct", 0.0))
class ProxyClient:
"""Reusable client for the local lean-ctx proxy ``/v1/compress`` endpoint.
Endpoint and token are auto-discovered (env → config → UID/data-dir) and may
be overridden explicitly for CI or remote proxies.
"""
def __init__(
self,
base_url: Optional[str] = None,
token: Optional[str] = None,
timeout: float = _DEFAULT_TIMEOUT,
) -> None:
self.base_url = discovery.resolve_base_url(base_url)
self.token = discovery.resolve_token(token)
self.timeout = timeout
def compress(
self,
messages: List[Message],
model: Optional[str] = None,
) -> CompressResult:
"""Compress ``messages`` and return the rewritten list plus stats."""
if not isinstance(messages, list):
raise TypeError("messages must be a list of chat-message dicts")
payload: Dict[str, Any] = {"messages": messages}
if model:
payload["model"] = model
data = self._post("/v1/compress", payload)
out = data.get("messages")
if not isinstance(out, list):
raise LeanCtxError("malformed /v1/compress response: 'messages' missing")
stats = data.get("stats")
return CompressResult(messages=out, stats=stats if isinstance(stats, dict) else {})
def resolve_reference(self, reference_id: str) -> str:
"""Return the original content behind a lean-ctx reference id.
lean-ctx replaces oversized omitted content with a durable reference; this
fetches it back via ``GET /v1/references/{id}``. Raises :class:`LeanCtxError`
when the reference is unknown or expired.
"""
if not reference_id:
raise ValueError("reference_id must be a non-empty string")
quoted = urllib.parse.quote(reference_id, safe="")
request = urllib.request.Request(f"{self.base_url}/v1/references/{quoted}", method="GET")
if self.token:
request.add_header("Authorization", f"Bearer {self.token}")
return self._send(request).decode("utf-8")
def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
body = json.dumps(payload).encode("utf-8")
request = urllib.request.Request(f"{self.base_url}{path}", data=body, method="POST")
request.add_header("Content-Type", "application/json")
if self.token:
request.add_header("Authorization", f"Bearer {self.token}")
raw = self._send(request)
try:
return json.loads(raw.decode("utf-8"))
except (ValueError, TypeError) as exc:
raise LeanCtxError(f"invalid JSON response from {request.full_url}: {exc}") from exc
def _send(self, request: urllib.request.Request) -> bytes:
try:
with urllib.request.urlopen(request, timeout=self.timeout) as response:
return response.read()
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace").strip()
if exc.code in (401, 403):
raise LeanCtxAuthError(
f"proxy rejected the request (HTTP {exc.code}). "
"Set LEAN_CTX_PROXY_TOKEN or pass token=…"
) from exc
if exc.code == 404:
raise LeanCtxError(
f"{request.full_url} not found (HTTP 404): {detail}"
) from exc
raise LeanCtxError(
f"{request.get_method()} {request.full_url} failed (HTTP {exc.code}): {detail}"
) from exc
except urllib.error.URLError as exc:
raise LeanCtxConnectionError(
f"could not reach the lean-ctx proxy at {self.base_url} ({exc.reason}). "
"Is the daemon running? Try: lean-ctx proxy enable"
) from exc
def compress(
messages: List[Message],
model: Optional[str] = None,
*,
base_url: Optional[str] = None,
token: Optional[str] = None,
timeout: float = _DEFAULT_TIMEOUT,
) -> List[Message]:
"""Compress a chat ``messages`` array, returning the rewritten messages.
Drop-in parity with library-style gateways::
from lean_ctx import compress
messages = compress(messages, model="claude-sonnet-4")
For token-savings stats, use :class:`ProxyClient` directly::
from lean_ctx import ProxyClient
result = ProxyClient().compress(messages)
print(result.saved_pct)
"""
client = ProxyClient(base_url=base_url, token=token, timeout=timeout)
return client.compress(messages, model=model).messages
+26
View File
@@ -0,0 +1,26 @@
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"
[project]
name = "lean-ctx-sdk"
version = "0.3.0"
description = "SDK for lean-ctx — context compression for AI agents"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
keywords = ["llm", "context", "compression", "ai", "mcp"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
]
[project.optional-dependencies]
langchain = ["langchain-core>=0.2"]
llamaindex = ["llama-index-core>=0.10"]
litellm = ["litellm>=1.0"]
all = ["langchain-core>=0.2", "llama-index-core>=0.10", "litellm>=1.0"]
[tool.setuptools.packages.find]
include = ["lean_ctx*"]
@@ -0,0 +1,81 @@
"""Endpoint/token discovery — isolated from the developer's real environment."""
import os
import pytest
from lean_ctx import discovery
_ENV_KEYS = (
"LEAN_CTX_PROXY_URL",
"LEAN_CTX_PROXY_PORT",
"LEAN_CTX_PROXY_TOKEN",
"LEAN_CTX_DATA_DIR",
"XDG_DATA_HOME",
"XDG_CONFIG_HOME",
)
@pytest.fixture
def isolated(tmp_path, monkeypatch):
"""Clear every discovery env var and root HOME at an empty tmp dir."""
for key in _ENV_KEYS:
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("HOME", str(tmp_path))
return tmp_path
def test_base_url_explicit_strips_trailing_slash():
assert discovery.resolve_base_url("http://host:9/") == "http://host:9"
def test_base_url_from_env(isolated, monkeypatch):
monkeypatch.setenv("LEAN_CTX_PROXY_URL", "http://h:1234/")
assert discovery.resolve_base_url() == "http://h:1234"
def test_base_url_defaults_to_loopback(isolated, monkeypatch):
monkeypatch.setattr(os, "getuid", lambda: 1000, raising=False)
assert discovery.resolve_base_url() == "http://127.0.0.1:4444"
def test_port_env_wins(isolated, monkeypatch):
monkeypatch.setenv("LEAN_CTX_PROXY_PORT", "5005")
assert discovery.resolve_port() == 5005
def test_uid_port_matches_rust_formula(isolated, monkeypatch):
monkeypatch.setattr(os, "getuid", lambda: 1000, raising=False)
assert discovery._uid_port() == 4444
monkeypatch.setattr(os, "getuid", lambda: 2999, raising=False)
assert discovery._uid_port() == 5443
monkeypatch.setattr(os, "getuid", lambda: 500, raising=False)
assert discovery._uid_port() == 4444
def test_port_from_config_toml(isolated, monkeypatch):
monkeypatch.setenv("LEAN_CTX_DATA_DIR", str(isolated))
(isolated / "config.toml").write_text("proxy_port = 4500\n", encoding="utf-8")
assert discovery.resolve_port() == 4500
def test_commented_proxy_port_is_ignored(isolated, monkeypatch):
monkeypatch.setenv("LEAN_CTX_DATA_DIR", str(isolated))
(isolated / "config.toml").write_text("# proxy_port = 3128\n", encoding="utf-8")
monkeypatch.setattr(os, "getuid", lambda: 1000, raising=False)
assert discovery.resolve_port() == 4444
def test_token_env_precedence(isolated, monkeypatch):
monkeypatch.setenv("LEAN_CTX_PROXY_TOKEN", "envtok")
assert discovery.resolve_token() == "envtok"
def test_token_from_session_file(isolated, monkeypatch):
monkeypatch.setenv("LEAN_CTX_DATA_DIR", str(isolated))
(isolated / "session_token").write_text("deadbeef\n", encoding="utf-8")
assert discovery.resolve_token() == "deadbeef"
def test_token_absent_returns_none(isolated):
assert discovery.resolve_token() is None
@@ -0,0 +1,104 @@
"""Tests for the LangChain compress integration.
The content-reattachment logic is unit-tested with a fake message (no LangChain
needed); the full ``compress_messages`` path runs against a loopback server when
``langchain-core`` is installed.
"""
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import pytest
from lean_ctx.langchain import _reattach_content, compress_messages
class _FakeMessage:
"""Minimal pydantic-v2-like message: ``content`` plus ``model_copy``."""
def __init__(self, content, role="user"):
self.content = content
self.role = role
def model_copy(self, update):
clone = _FakeMessage(self.content, self.role)
for key, value in update.items():
setattr(clone, key, value)
return clone
def __eq__(self, other):
return (
isinstance(other, _FakeMessage)
and other.content == self.content
and other.role == self.role
)
def test_reattach_content_swaps_only_content():
originals = [_FakeMessage("long original body", "user")]
out = _reattach_content(originals, [{"role": "user", "content": "short"}])
assert out[0].content == "short"
assert out[0].role == "user"
assert out[0] is not originals[0]
def test_reattach_content_preserves_originals_on_count_mismatch():
originals = [_FakeMessage("a"), _FakeMessage("b")]
out = _reattach_content(originals, [{"content": "x"}])
assert out == originals
def test_reattach_content_keeps_message_without_model_copy():
class _Plain:
content = "untouched"
originals = [_Plain()]
out = _reattach_content(originals, [{"content": "new"}])
assert out[0].content == "untouched"
@pytest.fixture
def base_url():
class _Handler(BaseHTTPRequestHandler):
def do_POST(self): # noqa: N802
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length).decode("utf-8"))
out = []
for message in body["messages"]:
rewritten = dict(message)
if isinstance(rewritten.get("content"), str):
rewritten["content"] = rewritten["content"][:8]
out.append(rewritten)
payload = json.dumps({"messages": out, "stats": {}}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, *args):
pass
httpd = HTTPServer(("127.0.0.1", 0), _Handler)
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()
try:
host, port = httpd.server_address
yield f"http://{host}:{port}"
finally:
httpd.shutdown()
thread.join(timeout=2)
def test_compress_messages_rewrites_content(base_url):
pytest.importorskip("langchain_core")
from langchain_core.messages import HumanMessage, SystemMessage
messages = [
SystemMessage(content="You are a helpful assistant."),
HumanMessage(content="this is a long message body"),
]
out = compress_messages(messages, model="gpt-4o", base_url=base_url)
assert type(out[1]).__name__ == "HumanMessage"
assert out[1].content == "this is "
@@ -0,0 +1,97 @@
"""Tests for the LiteLLM integration.
``compress_request_data`` is exercised end-to-end against a loopback server;
the ``LeanCtxLiteLLMHandler`` is tested via its import guard (and its async hook
when LiteLLM happens to be installed).
"""
import asyncio
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import pytest
from lean_ctx.errors import LeanCtxConnectionError
from lean_ctx.litellm import (
_LITELLM_AVAILABLE,
LeanCtxLiteLLMHandler,
compress_request_data,
)
from lean_ctx.proxy import ProxyClient
class _Handler(BaseHTTPRequestHandler):
def do_POST(self): # noqa: N802 (BaseHTTPRequestHandler API)
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length).decode("utf-8"))
out = []
for message in body["messages"]:
rewritten = dict(message)
if isinstance(rewritten.get("content"), str):
rewritten["content"] = rewritten["content"][:8]
out.append(rewritten)
payload = json.dumps({"messages": out, "stats": {}}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, *args): # silence test output
pass
@pytest.fixture
def base_url():
httpd = HTTPServer(("127.0.0.1", 0), _Handler)
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()
try:
host, port = httpd.server_address
yield f"http://{host}:{port}"
finally:
httpd.shutdown()
thread.join(timeout=2)
def test_compress_request_data_rewrites_messages_in_place(base_url):
client = ProxyClient(base_url=base_url)
data = {"model": "gpt-4o", "messages": [{"role": "user", "content": "a long message body"}]}
out = compress_request_data(data, client=client)
assert out is data
assert data["messages"] == [{"role": "user", "content": "a long m"}]
def test_compress_request_data_ignores_empty_messages():
assert compress_request_data({"messages": []}) == {"messages": []}
assert compress_request_data({}) == {}
def test_compress_request_data_passthrough_when_proxy_down():
client = ProxyClient(base_url="http://127.0.0.1:1", token="t")
data = {"messages": [{"role": "user", "content": "x" * 40}]}
out = compress_request_data(data, client=client)
assert out["messages"][0]["content"] == "x" * 40
def test_compress_request_data_raise_on_error():
client = ProxyClient(base_url="http://127.0.0.1:1", token="t")
data = {"messages": [{"role": "user", "content": "x" * 40}]}
with pytest.raises(LeanCtxConnectionError):
compress_request_data(data, client=client, raise_on_error=True)
def test_handler_requires_litellm():
if _LITELLM_AVAILABLE:
pytest.skip("litellm installed; the ImportError guard is not exercised")
with pytest.raises(ImportError):
LeanCtxLiteLLMHandler()
@pytest.mark.skipif(not _LITELLM_AVAILABLE, reason="litellm not installed")
def test_async_pre_call_hook_compresses(base_url):
handler = LeanCtxLiteLLMHandler(base_url=base_url)
data = {"messages": [{"role": "user", "content": "a long message body"}]}
out = asyncio.run(handler.async_pre_call_hook(None, None, data, "completion"))
assert out["messages"][0]["content"] == "a long m"
@@ -0,0 +1,191 @@
"""Transport tests for ProxyClient against a real loopback HTTP server.
The server is a faithful stand-in for the daemon's ``/v1/compress`` contract
(echoes the request, returns a stats block); it exercises request building,
auth headers, response parsing and error mapping — the compression itself is
covered by the Rust unit tests.
"""
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import pytest
from lean_ctx import ProxyClient, compress
from lean_ctx.errors import LeanCtxAuthError, LeanCtxConnectionError, LeanCtxError
EXPECTED_TOKEN = "secret-token"
class _CompressHandler(BaseHTTPRequestHandler):
def do_POST(self): # noqa: N802 (BaseHTTPRequestHandler API)
if self.path != "/v1/compress":
self.send_error(404)
return
auth = self.headers.get("Authorization", "")
if self.server.require_token and auth != f"Bearer {EXPECTED_TOKEN}":
self._json(401, {"error": "unauthorized"})
return
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length).decode("utf-8"))
self.server.last_request = {"headers": dict(self.headers), "body": body}
original = 0
compressed = 0
out = []
for message in body["messages"]:
rewritten = dict(message)
content = rewritten.get("content")
if isinstance(content, str):
original += len(content)
rewritten["content"] = content[:8]
compressed += len(rewritten["content"])
out.append(rewritten)
saved = original - compressed
self._json(
200,
{
"messages": out,
"stats": {
"original_tokens": original,
"compressed_tokens": compressed,
"saved_tokens": saved,
"saved_pct": round(saved / original * 100, 1) if original else 0.0,
"model": body.get("model"),
},
},
)
def do_GET(self): # noqa: N802 (BaseHTTPRequestHandler API)
if not self.path.startswith("/v1/references/"):
self.send_error(404)
return
reference_id = self.path.rsplit("/", 1)[-1]
if reference_id == "missing":
self._text(404, "Reference expired or not found")
return
self._text(200, f"ORIGINAL[{reference_id}]")
def _text(self, status, text):
data = text.encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def _json(self, status, payload):
data = json.dumps(payload).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *args): # silence test output
pass
@pytest.fixture
def server():
httpd = HTTPServer(("127.0.0.1", 0), _CompressHandler)
httpd.require_token = False
httpd.last_request = None
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()
try:
host, port = httpd.server_address
yield httpd, f"http://{host}:{port}"
finally:
httpd.shutdown()
thread.join(timeout=2)
def test_compress_function_returns_messages(server):
httpd, base_url = server
out = compress(
[{"role": "user", "content": "this is a long message body"}],
model="gpt-4o",
base_url=base_url,
token=EXPECTED_TOKEN,
)
assert out == [{"role": "user", "content": "this is "}]
def test_client_returns_stats_and_sends_model(server):
httpd, base_url = server
client = ProxyClient(base_url=base_url, token=EXPECTED_TOKEN)
result = client.compress(
[{"role": "user", "content": "abcdefghijklmnop"}],
model="claude-sonnet-4",
)
assert result.saved_tokens == len("abcdefghijklmnop") - 8
assert result.saved_pct > 0
assert httpd.last_request["body"]["model"] == "claude-sonnet-4"
assert httpd.last_request["headers"]["Content-Type"] == "application/json"
def test_auth_error_maps_to_exception(server):
httpd, base_url = server
httpd.require_token = True
with pytest.raises(LeanCtxAuthError):
compress(
[{"role": "user", "content": "needs a valid token here"}],
base_url=base_url,
token="wrong",
)
def test_connection_error_when_daemon_down():
# Port 1 is never an open lean-ctx proxy → URLError → ConnectionError.
with pytest.raises(LeanCtxConnectionError):
compress(
[{"role": "user", "content": "x" * 50}],
base_url="http://127.0.0.1:1",
token="t",
)
def test_non_list_messages_rejected():
with pytest.raises(TypeError):
ProxyClient(base_url="http://127.0.0.1:1", token="t").compress(
{"role": "user", "content": "not a list"}
)
def test_malformed_response_raises(server):
httpd, base_url = server
class _Bad(_CompressHandler):
def do_POST(self): # noqa: N802
self._json(200, {"unexpected": True})
httpd.RequestHandlerClass = _Bad
with pytest.raises(LeanCtxError):
compress(
[{"role": "user", "content": "y" * 40}],
base_url=base_url,
token=EXPECTED_TOKEN,
)
def test_resolve_reference_returns_content(server):
httpd, base_url = server
client = ProxyClient(base_url=base_url, token=EXPECTED_TOKEN)
assert client.resolve_reference("abc123") == "ORIGINAL[abc123]"
def test_resolve_reference_missing_raises(server):
httpd, base_url = server
client = ProxyClient(base_url=base_url, token=EXPECTED_TOKEN)
with pytest.raises(LeanCtxError):
client.resolve_reference("missing")
def test_resolve_reference_empty_id_rejected():
client = ProxyClient(base_url="http://127.0.0.1:1", token="t")
with pytest.raises(ValueError):
client.resolve_reference("")