Skip to content

Cache Breakpoint Planner — Python source

Find what your prompts share — common prefix and suffix blocks — and place prompt-cache breakpoints where they pay, with an estimated cost saving. 100% client-side.

This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.

"""Cache Breakpoint Planner — find the blocks a set of prompts share and
place cache breakpoints where they pay.

Language: Python (3.10+, standard library only)
Port of src/lib/cacheBreakpointPlanner.ts (the canonical TypeScript
implementation). javascript.js in this set carries the same port; this file
mirrors it for Python.
Tool page: https://dev.cosmolabs.org/tools/cache-breakpoint-planner
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import List, Sequence

#: Cached reads bill at ~0.1x — the saving on the cached share is ~90%.
CACHE_READ_DISCOUNT: float = 0.1


@dataclass(frozen=True)
class PromptSession:
    """One prompt session: an id plus its ordered blocks."""

    id: str
    blocks: List[str] = field(default_factory=list)


@dataclass(frozen=True)
class Breakpoint:
    """Place the cache breakpoint AFTER this block index (0-based)."""

    after_block: int
    label: str
    reason: str
    cached_tokens: int


@dataclass(frozen=True)
class PerSessionRow:
    id: str
    total_tokens: int
    unique_tokens: int
    cached_ratio: float


@dataclass(frozen=True)
class BreakpointPlan:
    prefix_blocks: List[str] = field(default_factory=list)
    prefix_tokens: int = 0
    suffix_blocks: List[str] = field(default_factory=list)
    suffix_tokens: int = 0
    breakpoints: List[Breakpoint] = field(default_factory=list)
    per_session: List[PerSessionRow] = field(default_factory=list)
    #: Estimated cost saving across the sessions vs no caching (0-1).
    estimated_savings: float = 0.0
    warnings: List[str] = field(default_factory=list)


def _tok(text: str) -> int:
    """The `type: 'prose'` path of the tokenEstimator, inlined: every
    non-empty line costs max(1, round(length / 4)) tokens; empty text is 0."""
    if not text:
        return 0
    tokens = 0
    for line in text.splitlines():
        if line:
            tokens += max(1, round(len(line) / 4))
    return tokens


def plan_breakpoints(sessions: Sequence[PromptSession]) -> BreakpointPlan:
    """Plan cache breakpoints for a set of prompt sessions.

    Finds the common leading/trailing blocks across every session and places
    breakpoints where the cache pays: everything stable before the
    breakpoint, everything per-request after it.
    """
    warnings: List[str] = []
    valid = [s for s in sessions if isinstance(s.blocks, list)]

    if not valid:
        return BreakpointPlan(
            warnings=["No sessions given — paste at least two prompts to compare."]
        )
    if len(valid) == 1:
        warnings.append("Only one session — a prefix needs at least two prompts to detect.")

    # Common leading blocks by position.
    shortest = min(len(s.blocks) for s in valid)
    prefix_end = 0
    while prefix_end < shortest and all(
        s.blocks[prefix_end] == valid[0].blocks[prefix_end] for s in valid
    ):
        prefix_end += 1

    # Common trailing blocks, matched from each session's own tail, never
    # overlapping the prefix.
    suffix_len = 0
    while suffix_len < shortest - prefix_end and all(
        s.blocks[len(s.blocks) - 1 - suffix_len]
        == valid[0].blocks[len(valid[0].blocks) - 1 - suffix_len]
        for s in valid
    ):
        suffix_len += 1

    prefix_blocks = valid[0].blocks[:prefix_end]
    suffix_blocks = valid[0].blocks[len(valid[0].blocks) - suffix_len:] if suffix_len > 0 else []
    prefix_tokens = _tok("\n".join(prefix_blocks))
    suffix_tokens = _tok("\n".join(suffix_blocks))

    breakpoints: List[Breakpoint] = []
    if prefix_blocks:
        breakpoints.append(
            Breakpoint(
                after_block=prefix_end - 1,
                label="after the shared prefix",
                reason=(
                    f"{len(prefix_blocks)} block(s) identical across every session — "
                    "cache once, hit on every request."
                ),
                cached_tokens=prefix_tokens,
            )
        )
    if suffix_len > 0:
        breakpoints.append(
            Breakpoint(
                after_block=-1,  # terminal: the shared tail sits at the end
                label="shared tail",
                reason=(
                    f"{suffix_len} trailing block(s) also identical — extend the cache "
                    "segment or accept the re-read."
                ),
                cached_tokens=suffix_tokens,
            )
        )
    if not breakpoints:
        warnings.append(
            "No shared leading or trailing blocks — nothing to cache across these sessions."
        )

    per_session: List[PerSessionRow] = []
    for s in valid:
        total_tokens = _tok("\n".join(s.blocks))
        unique_tokens = max(total_tokens - prefix_tokens - suffix_tokens, 0)
        cached_ratio = (
            min((prefix_tokens + suffix_tokens) / total_tokens, 1.0)
            if total_tokens > 0
            else 0.0
        )
        per_session.append(
            PerSessionRow(s.id, total_tokens, unique_tokens, cached_ratio)
        )

    avg_total = sum(p.total_tokens for p in per_session) / len(per_session)
    cached_share = (
        min((prefix_tokens + suffix_tokens) / avg_total, 1.0) if avg_total > 0 else 0.0
    )
    estimated_savings = cached_share * (1 - CACHE_READ_DISCOUNT)

    return BreakpointPlan(
        prefix_blocks=prefix_blocks,
        prefix_tokens=prefix_tokens,
        suffix_blocks=suffix_blocks,
        suffix_tokens=suffix_tokens,
        breakpoints=breakpoints,
        per_session=per_session,
        estimated_savings=estimated_savings,
        warnings=warnings,
    )

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →