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
Condense a long transcript or log into a shorter digest using a fast, transparent extractive heuristic (paragraph length, keyword density, and position all factor into what gets kept):
ctxopt summarize transcript.txt --ratio 0.2
638 -> 126 tokens
This is intentionally local and deterministic rather than LLM-backed, so it costs nothing and behaves predictably. If you want an abstractive summary, pipe the condensed output into your own LLM call — you'll be sending it far fewer tokens than the original.
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
digest = summarize_text(transcript_text, target_ratio=0.25)
# 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.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ctxopt-0.2.0.tar.gz | 16.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ctxopt-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 31.6 kB
Release files / ctxopt-0.2.0.tar.gz
| Download URL | ctxopt-0.2.0.tar.gz |
|---|---|
| Size | 16.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e83c257327a7bfe6d6c1683a164347caa54c04242833bdfe053be9e3238e6e26
|
|
BLAKE2b-256 checksum How to use checksums |
a8e46397496de7c3295a67bb9acc4277feb7b1a08d61330268de080a269d2a17
|
| 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 22, 2026.
Transparency logRelease files / ctxopt-0.2.0-py3-none-any.whl
| Download URL | ctxopt-0.2.0-py3-none-any.whl |
|---|---|
| Size | 15.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
408c642cfceccc70673b7f372656d082decfb08760943603bf3ae9c4e7e5e63b
|
|
BLAKE2b-256 checksum How to use checksums |
28eee2ec429dc00354cd6e80206612cad61b6deaa137cfa1c4b3c6c120567625
|
| 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 22, 2026.
Transparency logSecurity for ctxopt 0.2.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.
Attestation for ctxopt-0.2.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 22, 2026.
Transparency logA 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.