Skip to main content

promptbudget

Deterministic context-window budgeting for LLM prompts — with a full audit trail of every token decision.

CI PyPI Python License: MIT Typed

Your system prompt, tool schemas, chat history and retrieved chunks are all competing for the same context window. Most codebases resolve that fight with a while total > limit: messages.pop(0) loop buried in a helper — which silently evicts the one document the answer depended on, and leaves you no way to find out.

promptbudget makes that allocation an explicit, testable, auditable decision.

from promptbudget import Section, Item, pack

result = pack(
    [
        Section("system", SYSTEM_PROMPT, priority=100, strategy="all_or_nothing"),
        Section("tools", tool_schemas, priority=90, min_tokens=2_000),
        Section("history", messages, priority=50, strategy="drop_oldest"),
        Section("retrieval", chunks, priority=40, strategy="lowest_score_first"),
        Section("scratchpad", notes, priority=10, max_tokens=1_000),
    ],
    budget=128_000,
    reserved_output=4_000,
)

send(result.text)

for d in result.dropped():
    log.info("%s[%d] %s: %s", d.section, d.index, d.action.value, d.reason)
retrieval[7] dropped: lowest relevance score, evicted first to fit the section allocation
scratchpad[0] truncated: cut to the tokens remaining in the section allocation

Why this exists

The usual approach What goes wrong
messages[-10:] Breaks the moment one message is huge
while tokens > limit: pop(0) No priorities, no guarantees, drops pinned context
Framework-native trimming Handles chat history only — not tools, RAG chunks and history together
Retry on context_length_exceeded Pays for a failed call, and still doesn't decide what to shed
All of the above Cannot answer "why is that chunk missing from my prompt?"

Existing tools each cover a slice: LangChain's trim_messages trims message lists, LLMLingua compresses text, tiktoken counts tokens. None of them arbitrate a fixed budget across several sections with declared guarantees and tell you what they did.

Install

pip install promptbudget                 # zero dependencies
pip install "promptbudget[tiktoken]"     # exact OpenAI-family token counts

Python 3.9+. The core has no runtime dependencies.

Core ideas

Sections compete; priority decides who wins

Sections are served highest priority first. Ties break on name, so identical inputs always produce an identical prompt — the same reason you pin dependency versions.

Rendering follows your list order, not priority order. Priority decides who gets tokens; you decide what the prompt reads like.

Guarantees are kept or the call fails

Section("tools", tool_schemas, priority=90, min_tokens=2_000)

min_tokens is reserved before any lower-priority section is served — a greedy high-priority section cannot eat tokens promised downstream. If the minimums collectively don't fit, you get BudgetTooSmallError rather than a prompt that quietly violates one:

BudgetTooSmallError: Guaranteed minimums require 6000 tokens but only 4000 are
available (budget=8000, reserved_output=4000). Lower a min_tokens value, raise
the budget, or reduce reserved_output.

max_tokens is the mirror image: a cap, so a runaway history can't crowd out retrieval even when the window is empty.

Reserve room for the reply

pack(sections, budget=128_000, reserved_output=4_000)

Forgetting the model's own output is the most common way to get a context-length error from a prompt that "obviously fits". Reserved tokens are never offered to a section.

Strategies decide what gets shed

Strategy Behaviour Use for
"truncate" (default) Cuts the tail of the overflowing item Prose, scratchpads
"drop_oldest" Evicts from the front Chat history
"drop_newest" Evicts from the back Append-only logs
"lowest_score_first" Evicts least-relevant items RAG chunks
"all_or_nothing" Delivers intact or raises System prompts, tool schemas

Pin anything that must survive:

Item("The user's actual question", pinned=True)

Pinned items are never dropped or cut. If a pinned item genuinely cannot fit, you get SectionOverflowError — a loud failure beats a mangled prompt.

Bring your own by subclassing Strategy:

from promptbudget import Strategy, StrategyResult, register_strategy


class DeduplicateThenTruncate(Strategy):
    name = "dedupe"

    def apply(
        self, section, items, allocated, tokenizer, separator
    ): ...  # return StrategyResult(items, decisions)


register_strategy("dedupe", DeduplicateThenTruncate())

Any tokenizer, or none

from promptbudget import TiktokenTokenizer, pack

pack(sections, budget=128_000, tokenizer=TiktokenTokenizer("gpt-4o"))
pack(sections, budget=8_000, tokenizer=lambda text: len(text) // 4)  # any str -> int
pack(sections, budget=8_000)  # built-in heuristic

The allocator never imports a tokenizer directly — it talks to the Tokenizer interface, so HuggingFace, a provider SDK, or your own estimator all drop straight in.

The result explains itself

result.text  # the fitted prompt
result.used_tokens  # what it actually costs
result.remaining_tokens  # headroom left
result.complete  # True when nothing was lost
result.dropped()  # every item that lost content, and why
result.section("rag")  # per-section accounting
result.to_json(indent=2)  # structured logging, ready to ship

Because packing is a pure function returning a plain value, you can assert on it:

def test_the_user_question_always_survives():
    result = pack(sections, budget=4_000)
    assert "user_question" not in {d.section for d in result.dropped()}

Guarantees

Verified by property-based tests across hundreds of generated inputs (tests/test_properties.py):

  • The output never exceeds the budget.
  • Reserved output tokens are never consumed.
  • max_tokens caps are never breached.
  • Identical inputs produce byte-identical prompts.
  • Every item receives exactly one decision — nothing vanishes unrecorded.
  • complete never lies about dropped content.

Development

git clone https://github.com/mathewOracle/promptbudget && cd promptbudget
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest && ruff check . && mypy

Contributions welcome — see CONTRIBUTING.md. New strategies are especially welcome, and shouldn't require touching the allocator at all.

See CHANGELOG.md for release history and RELEASING.md for how versions get published.

License

MIT — see LICENSE.

Metadata

Release files for promptbudget 0.1.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 promptbudget 0.1.0
File Size Uploaded
promptbudget-0.1.0.tar.gz 29.7 kB Details

Built distribution (wheel)

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

Total release size: 50.3 kB

Release files / promptbudget-0.1.0.tar.gz

Download URL promptbudget-0.1.0.tar.gz
Size 29.7 kB
Tags Source
SHA-256 checksum
How to use checksums
fc4f7b88ac786b78ee871af84e924f18b94151a8147b11ffd0db71b856b15c7c
BLAKE2b-256 checksum
How to use checksums
ac3dde84b42d9ade0d31c6e94bd0eae95188e35fc526d15fe028bd128390a527
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 9, 2026.

Transparency log

Release files / promptbudget-0.1.0-py3-none-any.whl

Download URL promptbudget-0.1.0-py3-none-any.whl
Size 20.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a2f7b65609e5425d61159370430934e2a9315a9c3147fdc5a4aafdb3f3ac1132
BLAKE2b-256 checksum
How to use checksums
0234fbd03e6773baf086f497879076d588a0a118c466f441fa33c96ba0b0dabb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 9, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.1

2 release files

0.2.0

2 release files

This release

0.1.0 This release

2 release files

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