Skip to content

Cache Breakpoint Planner — JavaScript 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 JavaScript 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.
 * Port of src/lib/cacheBreakpointPlanner.ts
 *
 * Language: JavaScript (ES2022+, ES module; runs unmodified in Node 18+
 *           and modern browsers)
 * Source:   CosmoDev polyglot showcase port of the Cache Breakpoint Planner
 *           tool (slug: cache-breakpoint-planner), ported from
 *           src/lib/cacheBreakpointPlanner.ts (the canonical TypeScript
 *           implementation).
 * Tool page: https://dev.cosmolabs.org/tools/cache-breakpoint-planner
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * A session is a label plus an ordered list of prompt blocks (system, docs,
 * history turns, user ask…). The planner finds the blocks identical across
 * every session — a shared prefix at the front and a shared tail at the end —
 * and places cache breakpoints so everything stable is billed at the
 * cached-read rate on every request after the first. Token figures use the
 * tokenEstimator prose heuristic (4 characters per token, at least one per
 * non-empty line), inlined below so the file stays self-contained.
 */

/**
 * @typedef {{ id: string, blocks: string[] }} PromptSession
 * @typedef {{ afterBlock: number, label: string, reason: string, cachedTokens: number }} Breakpoint
 * @typedef {{ id: string, totalTokens: number, uniqueTokens: number, cachedRatio: number }} SessionTokens
 * @typedef {{
 *   prefixBlocks: string[], prefixTokens: number,
 *   suffixBlocks: string[], suffixTokens: number,
 *   breakpoints: Breakpoint[], perSession: SessionTokens[],
 *   estimatedSavings: number, warnings: string[],
 * }} BreakpointPlan
 */

/** Cached reads bill at ~0.1× — the saving on the cached share is ~90%. */
export const CACHE_READ_DISCOUNT = 0.1;

/**
 * Plan cache breakpoints for a set of prompt sessions.
 *
 * Sessions whose `blocks` is not an array are ignored (the TS lib filters
 * with `Array.isArray`); zero usable sessions yields an empty plan carrying
 * an explanatory warning. `estimatedSavings` is the share of the average
 * session that repeats, times the cached-read discount.
 * @param {readonly PromptSession[]} sessions
 * @returns {BreakpointPlan}
 */
export function planBreakpoints(sessions) {
  const warnings = [];
  const list = Array.isArray(sessions) ? sessions : [];
  const valid = list.filter((s) => s && Array.isArray(s.blocks));

  if (valid.length === 0) {
    return {
      prefixBlocks: [],
      prefixTokens: 0,
      suffixBlocks: [],
      suffixTokens: 0,
      breakpoints: [],
      perSession: [],
      estimatedSavings: 0,
      warnings: ['No sessions given — paste at least two prompts to compare.'],
    };
  }
  if (valid.length === 1) {
    warnings.push('Only one session — a prefix needs at least two prompts to detect.');
  }

  // Common leading blocks by position.
  const shortest = Math.min(...valid.map((s) => s.blocks.length));
  let prefixEnd = 0;
  while (
    prefixEnd < shortest &&
    valid.every((s) => s.blocks[prefixEnd] === valid[0].blocks[prefixEnd])
  ) {
    prefixEnd++;
  }

  // Common trailing blocks, matched from each session's own tail, never
  // overlapping the prefix.
  let suffixLen = 0;
  while (
    suffixLen < shortest - prefixEnd &&
    valid.every(
      (s) =>
        s.blocks[s.blocks.length - 1 - suffixLen] ===
        valid[0].blocks[valid[0].blocks.length - 1 - suffixLen],
    )
  ) {
    suffixLen++;
  }

  const prefixBlocks = valid[0].blocks.slice(0, prefixEnd);
  const suffixBlocks = suffixLen > 0 ? valid[0].blocks.slice(valid[0].blocks.length - suffixLen) : [];
  const prefixTokens = tok(prefixBlocks.join('\n'));
  const suffixTokens = tok(suffixBlocks.join('\n'));

  const breakpoints = [];
  if (prefixBlocks.length > 0) {
    breakpoints.push({
      afterBlock: prefixEnd - 1,
      label: 'after the shared prefix',
      reason: `${prefixBlocks.length} block(s) identical across every session — cache once, hit on every request.`,
      cachedTokens: prefixTokens,
    });
  }
  if (suffixLen > 0) {
    breakpoints.push({
      afterBlock: -1, // terminal: the shared tail sits at the end of each request
      label: 'shared tail',
      reason: `${suffixLen} trailing block(s) also identical — extend the cache segment or accept the re-read.`,
      cachedTokens: suffixTokens,
    });
  }
  if (breakpoints.length === 0) {
    warnings.push('No shared leading or trailing blocks — nothing to cache across these sessions.');
  }

  const perSession = valid.map((s) => {
    const totalTokens = tok(s.blocks.join('\n'));
    const uniqueTokens = Math.max(totalTokens - prefixTokens - suffixTokens, 0);
    return {
      id: s.id,
      totalTokens,
      uniqueTokens,
      cachedRatio: totalTokens > 0 ? Math.min((prefixTokens + suffixTokens) / totalTokens, 1) : 0,
    };
  });

  const avgTotal = perSession.reduce((sum, p) => sum + p.totalTokens, 0) / perSession.length;
  const cachedShare = avgTotal > 0 ? Math.min((prefixTokens + suffixTokens) / avgTotal, 1) : 0;

  return {
    prefixBlocks,
    prefixTokens,
    suffixBlocks,
    suffixTokens,
    breakpoints,
    perSession,
    estimatedSavings: cachedShare * (1 - CACHE_READ_DISCOUNT),
    warnings,
  };
}

/**
 * The `type: 'prose'` path of the tokenEstimator, inlined: every non-empty
 * line costs max(1, round(length / 4)) tokens, where length is JavaScript's
 * UTF-16 code-unit count. Empty text is 0.
 * @param {string} text
 * @returns {number}
 */
function tok(text) {
  if (!text) return 0;
  let tokens = 0;
  for (const line of text.split(/\r?\n/)) {
    if (line.trim() === '') continue;
    tokens += Math.max(1, Math.round(line.length / 4));
  }
  return tokens;
}

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 →