Skip to content

Cache Breakpoint Planner — Rust 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 Rust 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: Rust (edition 2021, standard library only)
//! Port of src/lib/cacheBreakpointPlanner.ts (the canonical TypeScript
//! implementation). javascript.js in this set carries the same port.
//! Tool page: https://dev.cosmolabs.org/tools/cache-breakpoint-planner



/// Cached reads bill at ~0.1x — the saving on the cached share is ~90%.
pub const CACHE_READ_DISCOUNT: f64 = 0.1;

/// One prompt session: an id plus its ordered blocks.
pub struct PromptSession<'a> {
    pub id: &'a str,
    pub blocks: &'a [&'a str],
}

/// Place the cache breakpoint AFTER this block index (0-based).
#[derive(Debug, Clone, PartialEq)]
pub struct Breakpoint {
    pub after_block: i64,
    pub label: &'static str,
    pub reason: String,
    pub cached_tokens: i64,
}

#[derive(Debug, Clone, PartialEq)]
pub struct PerSessionRow {
    pub id: String,
    pub total_tokens: i64,
    pub unique_tokens: i64,
    pub cached_ratio: f64,
}

/// The full plan: shared blocks, breakpoints, per-session rows, savings.
#[derive(Debug, Clone, Default)]
pub struct BreakpointPlan {
    pub prefix_blocks: Vec<String>,
    pub prefix_tokens: i64,
    pub suffix_blocks: Vec<String>,
    pub suffix_tokens: i64,
    pub breakpoints: Vec<Breakpoint>,
    pub per_session: Vec<PerSessionRow>,
    /// Estimated cost saving across the sessions vs no caching (0-1).
    pub estimated_savings: f64,
    pub warnings: Vec<String>,
}

/// The `type: 'prose'` path of the tokenEstimator, inlined: every non-empty
/// line costs max(1, round(len / 4)) tokens (UTF-16 code units ≈ bytes here
/// for ASCII display snippets); empty text is 0.
fn tok(text: &str) -> i64 {
    if text.is_empty() {
        return 0;
    }
    let mut tokens: i64 = 0;
    for line in text.split('\n') {
        if !line.is_empty() {
            tokens += std::cmp::max(1, (line.len() as f64 / 4.0).round() as i64);
        }
    }
    tokens
}

/// Plan cache breakpoints for a set of prompt sessions: find the common
/// leading/trailing blocks across every session and place breakpoints where
/// the cache pays. `sessions` with a non-array `blocks` are ignored at the
/// caller's boundary (TS filters with Array.isArray); pass only valid ones.
pub fn plan_breakpoints(sessions: &[PromptSession<'_>]) -> BreakpointPlan {
    let mut warnings: Vec<String> = Vec::new();

    if sessions.is_empty() {
        return BreakpointPlan {
            warnings: vec!["No sessions given — paste at least two prompts to compare.".into()],
            ..Default::default()
        };
    }
    if sessions.len() == 1 {
        warnings.push("Only one session — a prefix needs at least two prompts to detect.".into());
    }

    // Common leading blocks by position.
    let shortest = sessions.iter().map(|s| s.blocks.len()).min().unwrap_or(0);
    let mut prefix_end = 0usize;
    while prefix_end < shortest
        && sessions
            .iter()
            .all(|s| s.blocks[prefix_end] == sessions[0].blocks[prefix_end])
    {
        prefix_end += 1;
    }

    // Common trailing blocks, matched from each session's own tail, never
    // overlapping the prefix.
    let mut suffix_len = 0usize;
    while suffix_len < shortest - prefix_end
        && sessions.iter().all(|s| {
            let own = s.blocks.len() - 1 - suffix_len;
            let first = sessions[0].blocks.len() - 1 - suffix_len;
            s.blocks[own] == sessions[0].blocks[first]
        })
    {
        suffix_len += 1;
    }

    let prefix_blocks: Vec<String> = sessions[0].blocks[..prefix_end]
        .iter()
        .map(|s| s.to_string())
        .collect();
    let suffix_blocks: Vec<String> = if suffix_len > 0 {
        let n = sessions[0].blocks.len();
        sessions[0].blocks[n - suffix_len..]
            .iter()
            .map(|s| s.to_string())
            .collect()
    } else {
        Vec::new()
    };
    let prefix_tokens = tok(&prefix_blocks.join("\n"));
    let suffix_tokens = tok(&suffix_blocks.join("\n"));

    let mut breakpoints: Vec<Breakpoint> = Vec::new();
    if !prefix_blocks.is_empty() {
        breakpoints.push(Breakpoint {
            after_block: prefix_end as i64 - 1,
            label: "after the shared prefix",
            reason: format!(
                "{} block(s) identical across every session — cache once, hit on every request.",
                prefix_blocks.len()
            ),
            cached_tokens: prefix_tokens,
        });
    }
    if suffix_len > 0 {
        breakpoints.push(Breakpoint {
            after_block: -1, // terminal: the shared tail sits at the end of each request
            label: "shared tail",
            reason: format!(
                "{} trailing block(s) also identical — extend the cache segment or accept the re-read.",
                suffix_len
            ),
            cached_tokens: suffix_tokens,
        });
    }
    if breakpoints.is_empty() {
        warnings.push(
            "No shared leading or trailing blocks — nothing to cache across these sessions.".into(),
        );
    }

    let per_session: Vec<PerSessionRow> = sessions
        .iter()
        .map(|s| {
            let total_tokens = tok(&s.blocks.join("\n"));
            let unique_tokens = std::cmp::max(total_tokens - prefix_tokens - suffix_tokens, 0);
            let cached_ratio = if total_tokens > 0 {
                ((prefix_tokens + suffix_tokens) as f64 / total_tokens as f64).min(1.0)
            } else {
                0.0
            };
            PerSessionRow {
                id: s.id.to_string(),
                total_tokens,
                unique_tokens,
                cached_ratio,
            }
        })
        .collect();

    let avg_total = per_session.iter().map(|p| p.total_tokens).sum::<i64>() as f64
        / per_session.len() as f64;
    let cached_share = if avg_total > 0.0 {
        ((prefix_tokens + suffix_tokens) as f64 / avg_total).min(1.0)
    } else {
        0.0
    };
    let estimated_savings = cached_share * (1.0 - CACHE_READ_DISCOUNT);

    BreakpointPlan {
        prefix_blocks,
        prefix_tokens,
        suffix_blocks,
        suffix_tokens,
        breakpoints,
        per_session,
        estimated_savings,
        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 →