Fit chat history into any model's context window: token estimation, sliding-window packing with a running summary, and overflow detection. Zero dependencies, no AI inside.
Project description
llm-context-budget
Fit chat history into any model's context window.
Every chat app eventually writes the same code: estimate tokens, keep recent turns verbatim, compress the old ones, reserve room for the response, and handle the overflow error anyway. This package is that code — extracted from a production on-device assistant where the context windows are small and an overflow doesn't return a 400, it crashes the runtime.
Zero dependencies. No AI inside — the summarizer is a callback you can point at your model, with a deterministic extractive fallback built in. Messages are plain dicts in the OpenAI/Anthropic shape, multimodal content included. Python sibling of @yanib/context-budget (npm).
from context_budget import pack_messages
result = pack_messages(
history, # [{"role": ..., "content": ...}, ...]
context_tokens=8192, # your model's window
response_reserve=600, # headroom for the reply
summary=conversation.summary, # carried from the last turn
already_summarized=conversation.cursor, # ...so old turns aren't re-folded
system_blocks=[persona_prompt, rag_context],
)
reply = client.chat.completions.create(model=..., messages=result.messages)
conversation.summary = result.summary # persist for next turn
conversation.cursor = result.summarized_count
Async apps use apack_messages, which also accepts async summarizers:
result = await apack_messages(
history,
summarize=lambda prev, dropped: my_model_summary(prev, dropped), # sync or async
)
Install
pip install llm-context-budget
The import name is context_budget — matching the JS sibling. (PyPI's name-similarity rules block both context-budget and token-budget as distribution names; the code doesn't care.)
How it packs
- The most recent turns stay verbatim (window size adapts to the model: 8 turns at 4k context, 16 at 8k, 24 at 16k+ — or set your own).
- Older turns are represented by a running summary, carried between turns via
summary+summarized_countso nothing is summarized twice. - If the verbatim window still blows the budget, it's trimmed from the front and the trimmed turns are folded into the summary — one summarizer call per pack, not one per message.
- Final guard: if a single message + system blocks still overflow, the summary is hard-truncated into whatever room remains. If even that can't fit, you get
fits=Falseand you decide (the messages are still returned).
System blocks and the summary are merged into one system message — some providers accept only a single instructions block, and every other provider tolerates it.
Token estimation
The default estimator is the ~4-chars-per-token heuristic — deliberately dependency-free (real tokenizers cost megabytes and vary per model, and budgets carry headroom anyway). Have exact counts? Plug them in:
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
pack_messages(history, estimate_tokens=lambda t: len(enc.encode(t)))
Also exported: create_char_estimator(ratio), truncate_to_tokens(text, max, estimator, keep="head"|"tail"), estimate_tokens_of, context_limits_for, content_text.
The overflow retry
Estimates are estimates. When the provider still throws, detect it and retry aggressively — half the verbatim window:
from context_budget import is_context_overflow_error, pack_messages
try:
return call_model(result.messages)
except Exception as err:
if not is_context_overflow_error(err):
raise
retry = pack_messages(history, aggressive=True, **options)
return call_model(retry.messages)
The summarizer seam
summarize(previous_summary, dropped_turns) may call anything:
- Default:
create_extractive_summarizer()— one compact labeled line per dropped turn, capped total size keeping the tail. Deterministic, instant, offline. - Your model: semantic summaries when quality matters. If your call fails, return
previous_summary— a memory hiccup should never block a chat turn.
Extra message keys (ids, tool calls, timestamps) pass through packing untouched, and OpenAI-style multimodal list content is estimated by its text parts.
API
pack_messages(history, *, context_tokens=4096, response_reserve=600,
recent_window=None, aggressive=False, estimate_tokens=...,
summary="", already_summarized=0, system_blocks=(),
summarize=None) -> PackResult
PackResult:
messages # one merged system message (if any) + verbatim window
summary # persist and pass back next turn
summarized_count # pass back as already_summarized next turn
dropped # turns folded into the summary this call
used_tokens, budget, fits
Fully typed (py.typed), Python 3.9+.
License
MIT © Binaya Dhakal
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llm_context_budget-0.1.0.tar.gz.
File metadata
- Download URL: llm_context_budget-0.1.0.tar.gz
- Upload date:
- Size: 10.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d378da17e19f156f2a614a36b24af094b851984b1f1902a34297657520b8a1de
|
|
| MD5 |
353ae2377b339919c514af4c10f64623
|
|
| BLAKE2b-256 |
c858835c3cf6a9d367d97979563d5c64c9f3f1eb351f642638a8e11456d4a8aa
|
File details
Details for the file llm_context_budget-0.1.0-py3-none-any.whl.
File metadata
- Download URL: llm_context_budget-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4afaafd0763d88a60f4b550824a18f5b3bc7ab1241f093bf9b8f57ed263a67f5
|
|
| MD5 |
77028ac449b74d19f46038b6ad902934
|
|
| BLAKE2b-256 |
d4d7993388703000edf66e29878bd7a358a2d90c841141c6c3e75559ac46393c
|