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
Metadata
Release files for prompt-cache-fit 0.1.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 | |
|---|---|---|---|---|
| prompt_cache_fit-0.1.0.tar.gz | 6.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| prompt_cache_fit-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 12.6 kB
Release files / prompt_cache_fit-0.1.0.tar.gz
| Download URL | prompt_cache_fit-0.1.0.tar.gz |
|---|---|
| Size | 6.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c1e38feb9f37f743e3ab987e0d83d15931915af5bc30f8f4c2207cc77257c68d
|
|
BLAKE2b-256 checksum How to use checksums |
625553878b9602efcaf5dbb7c4c1f966080925db21154eecc2541d77a25caa75
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Release files / prompt_cache_fit-0.1.0-py3-none-any.whl
| Download URL | prompt_cache_fit-0.1.0-py3-none-any.whl |
|---|---|
| Size | 5.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
100665d102d000c4cae05dedd6d22e3d846b3456b2a74f07f85cdad6cbf6b0ba
|
|
BLAKE2b-256 checksum How to use checksums |
70a218b2ffb06d48f709c4a335a04508e3b475605baee33ac55b35b60748b82d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Security for prompt-cache-fit 0.1.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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
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.