26382a7ac6
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
165 lines
5.9 KiB
Python
165 lines
5.9 KiB
Python
"""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
|