prompt-cache-fit
Reorder LLM prompt blocks from least- to most-volatile so provider prefix caches stay hot, and measure whether it actually worked. This is the Python port of the JavaScript package of the same name.
The problem
Anthropic, OpenAI, xAI and DeepSeek all cache prompt prefixes. The cache is only reusable up to the first byte that differs between two requests. Teams that put a timestamp, a request id, or "today's date" near the top of the system prompt throw away the cache for every token below it, even though the rest of the prompt (the system message, the tool schema, the RAG chunks) did not actually change. The published advice is "order your prompt from least-variable to most-variable", but every team hand-rolls that ordering, and almost nobody measures whether the reordering actually raised the hit rate. This package does the reordering and the measurement, nothing else: no tokenizer, no provider SDK, no network calls.
Install
pip install prompt-cache-fit
Usage
from prompt_cache_fit import fit, hit_rate
static_block = {"text": "You are a helpful coding assistant.", "volatility": "static"}
tools_block = {"text": "Tools: search_docs(query), run_tests()", "volatility": "shared"}
session_block = {
"text": "User profile: senior backend engineer, prefers concise answers.",
"volatility": "session",
}
timestamp1 = {"text": "Timestamp: 2026-09-04T10:00:00Z", "volatility": "turn"}
question1 = {"text": "Turn: how do I retry a failed HTTP request?", "volatility": "turn"}
timestamp2 = {"text": "Timestamp: 2026-09-04T10:05:00Z", "volatility": "turn"}
question2 = {"text": "Turn: how do I cancel an in-flight fetch?", "volatility": "turn"}
# Hand-rolled: the volatile timestamp sits in the middle of the prompt.
hand_rolled1 = "\n\n".join(
b["text"] for b in [static_block, timestamp1, tools_block, session_block, question1]
)
hand_rolled2 = "\n\n".join(
b["text"] for b in [static_block, timestamp2, tools_block, session_block, question2]
)
# fit(): volatile blocks (rank 'turn') sort to the end automatically.
fitted1 = fit([static_block, timestamp1, tools_block, session_block, question1])
fitted2 = fit([static_block, timestamp2, tools_block, session_block, question2])
print(hit_rate([hand_rolled1, hand_rolled2]).mean) # 0.2916666666666667
print(hit_rate([fitted1.text, fitted2.text]).mean) # 0.7777777777777778
Moving the two turn-tier blocks to the end of the prompt takes the measured prefix hit rate on the second request from about 29% to about 78%, because the shared system prompt, tool schema and session block now form one unbroken prefix instead of being interrupted by the timestamp.
API
fit(blocks, *, separator="\n\n") -> FitResult
blocks: List[Dict[str, Any]], each dict with a requiredtext: strkey and optionalvolatility: str,id,pin: boolkeys. Plain dicts are accepted; no class is required.volatilityis one of"static" | "shared" | "session" | "turn", ranked in that order (staticsorts first,turnsorts last). Missing or unrecognized values default to"turn"; unknown strings never raise.pin=Trueon a block keeps that block at its original index. Every other block is sorted around it.- The sort is stable: two blocks with the same rank keep their original relative order.
separator(keyword-only, default"\n\n"): the string joined between rendered blocks.- Returns a frozen
FitResultdataclass:blocks: the input blocks, reordered.text: the reordered blocks joined withseparator.moved: count of blocks whose index differs from the input.stable_prefix_chars: characters from the start oftextthrough the end of the last block ranked belowturn, including the separator that follows it if another block comes after.0if every block isturn.
shared_prefix(a, b) -> int
Number of shared leading characters between two strings. shared_prefix("abc", "abd") is 2. Returns 0 for empty strings.
hit_rate(prompts) -> HitRateReport
prompts is a list of already-rendered prompt strings, in send order. For each prompt after the first, computes shared_prefix(prompts[i-1], prompts[i]) / len(prompts[i]). Returns a frozen HitRateReport dataclass:
per_request: one fraction (0..1) per request after the first.mean: mean ofper_request,0if fewer than 2 prompts were given.cached_chars: sum of shared prefix lengths across all requests after the first.total_chars: sum oflen(prompts[i])fori >= 1.
An empty prompt string contributes 0 instead of dividing by zero.
VOLATILITY
{"static": 0, "shared": 1, "session": 2, "turn": 3}, exported as a plain dict so forkers can add their own tiers or change the ranks.
How it works
fit() is a stable sort by volatility rank plus a reinsertion pass for pinned blocks; hit_rate() is a character-by-character common-prefix scan. That is the whole library. It does not know how any provider actually tokenizes or chunks its cache, so stable_prefix_chars and hit_rate are character-level proxies, not a guarantee of a provider-side cache hit: real caches key on token boundaries and have their own minimum prefix length and TTL. This package also does not inject provider-specific cache markers (Anthropic's cache_control, OpenAI's automatic prefix caching, etc.) into the output; ordering the blocks correctly is a precondition for those markers to help, but adding the markers themselves is left to the caller, since the marker format is provider-specific and out of scope here.
The JavaScript version of this package lives at the repository root: https://github.com/pjdurden/prompt-cache-fit
License
MIT
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 prompt_cache_fit-0.1.0.tar.gz.
File metadata
- Download URL: prompt_cache_fit-0.1.0.tar.gz
- Upload date:
- Size: 6.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1e38feb9f37f743e3ab987e0d83d15931915af5bc30f8f4c2207cc77257c68d
|
|
| MD5 |
4293bd1275bd848f925cf108dd60031d
|
|
| BLAKE2b-256 |
625553878b9602efcaf5dbb7c4c1f966080925db21154eecc2541d77a25caa75
|
File details
Details for the file prompt_cache_fit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: prompt_cache_fit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 5.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
100665d102d000c4cae05dedd6d22e3d846b3456b2a74f07f85cdad6cbf6b0ba
|
|
| MD5 |
b5f46f7d8f541c6fde81d27c0787e1d9
|
|
| BLAKE2b-256 |
70a218b2ffb06d48f709c4a335a04508e3b475605baee33ac55b35b60748b82d
|