Skip to main content

ctxopt

A lightweight CLI for taming LLM context windows.

ctxopt helps you keep conversations with LLMs — and the agents built on them — lean. It does three things, each independently useful:

  • 🧹 Sandbox noisy tool output (logs, stack traces, diffs, JSON blobs) down to a compact form that fits an actual token budget, instead of dumping thousands of lines into your context.
  • 📝 Summarize long conversations or transcripts into a shorter digest, locally and deterministically — no API calls required.
  • 🧭 Route a prompt to the right model tier based on an estimate of how complex it actually is, so you're not paying frontier-model prices for "what's the capital of France?"

No heavy dependencies, no required API keys, no telemetry. It's a small tool that does a small number of things well.

Install

pip install ctxopt

Or from source:

git clone https://github.com/feralxr/ctxopt.git
cd ctxopt
pip install -e .

Requires Python 3.9+.

Usage

Sandbox noisy output

This is the core command: it collapses long, repetitive, or oversized tool output while preserving the signal that actually matters.

ctxopt sandbox build.log
# or pipe it in:
cat build.log | ctxopt sandbox

Token-budget fitting (recommended — tokens, not lines, are what fill a context window):

ctxopt sandbox giant_stacktrace.log --max-tokens 300
  File "app.py", line 14, in recurse
    return recurse(n-1)
RecursionError: maximum recursion depth exceeded
profile=stacktrace  5346 -> 118 tokens (5228 saved, 981 lines elided, fit to budget)

Near-duplicate collapsing. Lines that are identical after stripping volatile bits (timestamps, hex addresses, UUIDs, standalone counters) get folded together — this is what actually tames recursive stack traces and progress spam, where only a counter or timestamp changes line to line. Digits embedded inside an identifier (like line1 or v2) are left alone, so distinct content never silently merges:

ctxopt sandbox big_diff.patch
diff --git a/big_module.py b/big_module.py
--- a/big_module.py
+++ b/big_module.py
@@ -1,300 +1,300 @@
+added line 0
... [299 near-identical line(s) collapsed] ...
profile=diff  1216 -> 28 tokens (1188 saved, 0 lines elided)

Content profiles. ctxopt sandbox auto-detects the shape of the input (--profile auto by default) and adjusts behavior accordingly:

Profile Detected when Behavior
generic default fallback Head/tail elision + near-dup collapsing
json input parses as JSON Structural truncation: long strings and large arrays are trimmed at the value level, so the output stays valid JSON
stacktrace 3+ frame-like lines (File "...", line N / at foo(...)) Tighter head window, Traceback/Caused by lines always preserved
diff 2+ diff markers (+++, ---, @@, diff --git) File headers and hunk markers always preserved even when the body is elided

JSON example — a 300-user payload with 800-character bios collapses to a fraction of its size while staying parseable:

ctxopt sandbox users.json
{
  "users": [
    { "id": 0, "name": "user_0", "bio": "xxxx...xx... [300 chars truncated]" },
    ...
  ]
}
profile=json  2401 -> 306 tokens (2095 saved, 0 lines elided)

Signal preservation. Lines matching error, exception, fail(ed/ure), traceback, fatal, or panic are never elided, even inside the collapsed middle — up to a cap (max_kept_matches as a library param) so a pathological log full of "error" lines can't defeat the budget. Add your own patterns with --keep:

ctxopt sandbox app.log --keep "payment_id=\w+" --max-tokens 500

Flags:

Flag Default Description
--max-tokens — Shrink output to fit this token budget (shrinks head/tail/keep windows until it fits)
--max-lines 200 Collapse once output exceeds this many lines (used when --max-tokens isn't set)
--head / --tail profile-specific Lines to keep from the start/end
--profile auto auto, generic, json, stacktrace, or diff
--keep — Extra regex to always preserve (repeatable)
--out stdout Write result to a file instead

Summarize a conversation

This is conversation-aware, not just a document shortener. It detects two transcript formats — Role: message plain text, and JSON message lists ([{"role": ..., "content": ...}], the shape most chat APIs use) — and summarizes turn by turn. Anything else falls back to paragraph-based segmentation.

ctxopt summarize support_thread.txt --max-tokens 120

A 47-turn transcript — a real bug report, 39 repetitive "any update?" check-ins, and a resolution — collapses to the parts that matter:

User: Hey, the deploy script is failing on staging.

Assistant: Can you share the error output?

User: Sure, here it is: ValueError: missing config key DATABASE_URL

Assistant: Looks like the staging .env file is missing that key. I will check.

Assistant: still looking into it, item 10 is in progress.

Assistant: Found it, the key was renamed in the last config migration. We
decided to add a fallback default for it.

User: Perfect, that fixed it. Thanks!

Assistant: Glad it works now. Let me know if anything else comes up.

backend=local  640 -> 120 tokens  turns 8/47

The problem report, the diagnosis, the decision, and the resolution all survived; the 39 near-identical "checking in" turns didn't.

How segments are chosen, same philosophy as sandbox's keep patterns:

  • Lines matching error, exception, decided, or important are always kept, plus any --keep <regex> you add, however small the budget.
  • The most recent --keep-last turns (default 2) are always kept — only meaningful for conversations, ignored for plain documents.
  • Everything else is scored by length, keyword density, and position, then greedily added until the budget is met — skipping any candidate that's a near-duplicate (word-overlap above redundancy_threshold) of something already kept, so repetitive check-ins don't all survive just because each one scores decently on its own.

This local mode is fully offline and deterministic — no network calls, no cost, same input always gives the same output.

Abstractive summaries (optional). Pass --llm to use a real model instead of the local heuristic. It reads ANTHROPIC_API_KEY from your environment and does a small map-reduce: segments are grouped into token-sized chunks, each chunk gets summarized by the model, and — only if the combined chunk summaries still exceed the target — one final call reduces them into a single digest.

export ANTHROPIC_API_KEY=sk-...
ctxopt summarize huge_transcript.txt --llm --max-tokens 300

As a library, llm_call is just Callable[[str], str] — point it at anything, not just Anthropic:

from ctxopt import summarize_text, anthropic_backend

result = summarize_text(transcript, llm_call=anthropic_backend(), max_tokens=300)
print(result.backend, result.text)  # "llm", "..."

Route a prompt to a model tier

Score a prompt's complexity and get a model recommendation back:

ctxopt route "What is the capital of France?"
Score    0.00
Tier     simple
Model    claude-haiku-5-5
Reasons  2 simple-query keyword(s) matched
ctxopt route "Design a fault-tolerant distributed architecture for this payment system and debug the race condition below: ..."
Score    0.74
Tier     complex
Model    claude-opus-5-5
Reasons  contains code or code-like syntax; 4 complexity keyword(s) matched

The scoring heuristic weighs input length, presence of code, and complexity/simplicity keyword signals. Tiers and model names are fully configurable — see below.

Using it as a library

Every command is backed by a plain Python function with no CLI coupling:

from ctxopt import sandbox_text, summarize_text, route, RouterConfig

# Collapse noisy output to fit an actual token budget
result = sandbox_text(log_text, max_tokens=500)
print(result.profile, result.text, result.original_tokens, result.final_tokens)

# Always preserve your own markers (e.g. a request/trace id), on top of the
# built-in error/exception/fail defaults
result = sandbox_text(log_text, max_tokens=500, keep_patterns=[r"trace_id=\w+"])

# Summarize a transcript (conversation-aware, local and free)
digest = summarize_text(transcript_text, max_tokens=300, keep_last_n_turns=3)
print(digest.turns_kept, "of", digest.turns_total, "turns kept")

# Route to a model tier, with your own model names
config = RouterConfig(tiers={
    "simple": "gpt-4o-mini",
    "standard": "gpt-4o",
    "complex": "o1",
})
decision = route(user_prompt, config)
print(decision.tier, decision.model, decision.reasons)

Why this exists

Agent and chat pipelines accumulate context fast: verbose tool output, sprawling multi-turn history, and routing every request — simple or complex — through the same expensive model. ctxopt is a small set of composable primitives for the three most common sources of that bloat. It's not a framework and doesn't ask you to adopt an architecture — pipe things in, get leaner things out.

Development

git clone https://github.com/feralxr/ctxopt.git
cd ctxopt
pip install -e ".[dev]"
pytest

Every push/PR to main runs the test suite across Python 3.9–3.12 via .github/workflows/tests.yml.

Publishing

Releases are published to PyPI via Trusted Publishing (OIDC) — no PyPI API token is ever stored as a secret. One-time setup on PyPI's side, under Your project → Publishing → Add a new pending publisher:

Field Value
PyPI Project Name ctxopt
Owner feralxr
Repository name ctxopt
Workflow name workflow.yml
Environment name pypi

After that's linked, cutting a release is just:

# bump the version in pyproject.toml and src/ctxopt/__init__.py first
git tag v0.2.0
git push origin v0.2.0

.github/workflows/workflow.yml builds the sdist/wheel, checks the tag matches pyproject.toml's version, and publishes.

License

MIT — see LICENSE.

Metadata

Release files for ctxopt 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ctxopt 0.3.0
File Size Uploaded Provenance
ctxopt-0.3.0.tar.gz 24.4 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for ctxopt 0.3.0
File Interpreter ABI Platform
ctxopt-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 46.5 kB

Release files / ctxopt-0.3.0.tar.gz

Download URL ctxopt-0.3.0.tar.gz
Size 24.4 kB
Tags Source
SHA-256 checksum
How to use checksums
56782f5257adf913fa7db26940790ff58daf40fb73264e519ba4f5c54cdea7fd
BLAKE2b-256 checksum
How to use checksums
8d1d323ffad0bc902afbc215bbee1e42e29e674ac99b224db6c8e163ab2d103f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / ctxopt-0.3.0-py3-none-any.whl

Download URL ctxopt-0.3.0-py3-none-any.whl
Size 22.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d8896fe68a6ab2b39f4bdb32ff410aa93def8f5814d00285bdfc239dd14dea28
BLAKE2b-256 checksum
How to use checksums
af3684388e7a995db7bc4105b84289d7b086d8c5eaa2ffaee35fba244263ed19
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for ctxopt 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ctxopt-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page