Skip to content

The Edge of the Cyber World See the latest

Apps

Cursor Diff vs Rewrite Break-Even: 13 Steps, 55 Min [2026]

Every time Cursor’s Composer touches a file, someone pays for the tokens it writes. Cursor lists Composer 2.5 at $0.50 per million input tokens and $2.50 per million output tokens, with the Fast variant at $3.00 and $15.00, and third-party pricing trackers repeat the same figures. Output costs five times as much as input on both. That ratio is the whole reason the question of rewriting a file versus patching it deserves a spreadsheet and not an opinion.

This tutorial builds that spreadsheet as a small Python project. You will generate synthetic source files from 50 to 3,200 lines, apply edits of different sizes, render each edit twice (as a full-file rewrite and as SEARCH/REPLACE blocks), and price both against Composer 2.5’s published rates. If you already followed our Cursor Composer setup guide for diff versus rewrite, this is the next layer: instead of watching what Composer does, you model what each style should cost and find the edit size where the answer flips.

The whole thing needs no API key and no Cursor account. It takes about 55 minutes across 13 steps, ends with 7 passing tests, and finishes with an optional calibration script you can point at any OpenAI-compatible endpoint.

Why Diff Versus Rewrite Is a Cost Question, Not a Style Question

Developers argue about this as if it were taste. Some want the model to rewrite the whole file because it feels safer, since nothing can fail to match. Others want surgical patches because they read cleanly in review. Both camps skip the arithmetic. A model that returns the complete file emits roughly one output token for every token in the file, no matter how small the change. A model that returns SEARCH/REPLACE blocks emits tokens in proportion to the change plus a few lines of context around it.

That gives two cost curves. The rewrite curve is nearly flat against edit size but climbs steeply with file size. The diff curve is nearly flat against file size but climbs with edit size, and every hunk carries a fixed tax of marker lines and context. Where the curves cross is the break-even point. Below it, patches win on both dollars and seconds. Above it, the rewrite wins, because ten scattered hunks with context can out-weigh the whole file.

Two more terms bend the curves. The first is failure: a SEARCH block that does not match exactly once forces a retry or a fallback to a full rewrite, and that fallback pays for the input again. The second is speed: if the apply path accelerates one style but not the other, the latency crossing point moves away from the token crossing point. Our live benchmark of Cursor CLI’s diff versus rewrite edits measures real runs. This article gives you the model that tells you what to expect before you spend a dollar on those runs.

Here is what the project answers, in order:

  • How many output tokens does each edit style emit for a given file size and edit size?
  • What does that cost at Composer 2.5’s published prices, and does the Fast variant change the answer?
  • How much does a 10% or 30% diff-apply failure rate shift the break-even point?
  • What happens to the latency verdict if one path runs several times faster than the other?
  • How close is the model to a real endpoint’s token counts, and how do you check?

What Cursor Says About Composer 2.5 Edits, and What It Leaves Out

Start with what is solid. Composer 2.5 shipped on May 18, 2026, as an upgrade over Composer 2 for long-running agentic work, instruction following and tool use. Cursor’s own pages say it is built on Moonshot’s Kimi K2.5, the same open-source checkpoint as Composer 2, and that it is available only inside Cursor: the desktop app, web, CLI and mobile, plus an SDK for custom agents. Cursor’s Composer page says the fast variant “handles file edits, terminal commands, and multi-file changes directly in Cursor.” The same page pitches the model as pairing “frontier-level intelligence with low token cost” for everyday work.

Now the gap. None of the Cursor pages I fetched on October 30, 2026 state whether Composer 2.5 edits files by regenerating them, by emitting diffs, or by a hybrid. They also publish no token-consumption figures or wall-clock latency numbers for either approach, and no controlled benchmark compares the two for this model. Treat any article that tells you exactly what happens inside Composer 2.5 as guessing, including ones that sound confident. One more freshness note: community threads ask for a Composer 3, but I found no announcement of one as of October 30, 2026.

What circulates instead are unofficial reports. One describes a specialized 70-billion-parameter apply model running at about 1,000 tokens per second, with a technique called speculative edits claimed to make applying edits much faster than ordinary full-file regeneration. I could not confirm that speedup against an official Cursor source, so I use a speedup figure later only as a what-if input, never as a fact about Composer. A GitHub engineering handbook, its coding-agent case study, reportedly argues that full-file rewriting can beat unified diffs for files under roughly 400 lines. That is a single secondary source, and your own data should decide, which is the point of this build.

There is a practical consequence. Because the mechanism is undocumented, the harness models both styles generically and prices them with Cursor’s published rates. It predicts what a rewrite and a SEARCH/REPLACE edit would cost on any model with those prices. For deeper reading on how a patch layer can sit between a model and your files, see our walkthrough on building a fast-apply patch engine.

Prerequisites and Versions

The project uses only the Python standard library, so setup is short. Here is what I ran it on and what you need.

Requirement Version used Notes
Python Not listed Uses built-in generics like list[str]
Operating system Linux 6.8 (Ubuntu-class) macOS and Windows work, use python instead of python3 where needed
tiktoken (optional) latest from pip Swaps the built-in estimator for real BPE counts, o200k_base encoding
An OpenAI-compatible endpoint (optional) Any Only for Step 13 live calibration, a local server or hosted API
Cursor (optional) Current release, Composer 2.5 selectable Only for the cross-check in the final section
Disk and time Under 1 MB, about 55 minutes No GPU and no API key needed for Steps 1 to 12

One honest limitation up front. I ran every step on October 30, 2026 with the built-in heuristic token estimator, because tiktoken was not installed on the machine. The heuristic splits text into word pieces, punctuation, and whitespace runs and charges roughly one token per five characters of a word. It is consistent across both edit styles, so the comparison between them holds up better than the absolute counts do. If you install tiktoken, your absolute numbers will shift, and the break-even table may move one notch in either direction.

Since Composer 2.5 is only available inside Cursor, you cannot call it from this project. The harness stays model-agnostic on purpose. You supply prices, it supplies token counts, and Step 13 shows how to calibrate those counts against whatever model you can reach.

The Break-Even Model in Plain Numbers

Before any code, here is the model in four lines. For one edit on a file of F input tokens:

  • Rewrite cost = input tokens at the input price + the whole new file at the output price.
  • Diff cost = input tokens at the input price + hunk tokens at the output price + (failure rate x rewrite cost).
  • Hunk tokens = for every hunk, marker lines + the old lines + the new lines, where each hunk includes N context lines either side of the change.
  • Seconds = output tokens divided by an assumed generation speed, with an optional speedup factor on the rewrite path.

The input side is identical for both styles, because the model has to read the file either way. So the contest is decided on output tokens, where each token costs five times what an input token does on Composer. The failure term adds an honest penalty: if a SEARCH block misses, the editor falls back to a full rewrite and pays for a second request, input included.

Prices drive the dollar figures, not the winner. Here are the four price points the project ships with, all taken from published sources dated this month.

Model Input per 1M tokens Output per 1M tokens Output : input Source
Composer 2.5 $0.50 $2.50 5x Cursor Composer page
Composer 2.5 Fast $3.00 $15.00 5x Cursor Composer page
Grok 4.7 (prompts under 200K tokens) $2.00 $6.00 3x xAI release notes, Oct. 27, 2026
Grok 4.7 (prompts at or above 200K tokens) $4.00 $12.00 3x xAI pricing page
Claude Haiku 5.5 (prompts up to 100K tokens) $0.10 $0.50 5x Anthropic, launched Oct. 7, 2026

Note that Grok 4.7’s price is for xAI’s API, not what Cursor charges you, and Cursor’s plans have moved since The X Subscription launched on October 12, 2026. I include it as a contrast case with a lower output multiple. The Cursor Composer cost guardrail we built earlier handles the budgeting side. This model handles the forecasting side.

One more design choice: the sweep reports break-even in discrete steps (1, 3, 10, 30, 100, 300 changed lines) rather than interpolating. That keeps every number in the output a number the program actually computed.

Part 1: Set Up the Project (Steps 1 to 3)

Step 1: Create the folder and check Python

Make a clean directory and confirm your interpreter. Everything lives in one flat folder, so imports stay trivial.

mkdir breakeven && cd breakeven
python3 --version
python3 -c "import tiktoken" 2>&1 | tail -1

Expected output on a machine without tiktoken:

Python 3.12.3
ModuleNotFoundError: No module named 'tiktoken'

That error is fine. The next step handles it.

Step 2: Record the prices in prices.json

Keep prices in data, not code. When Cursor changes a rate, you edit one line and rerun. Dollar amounts per million tokens, input and output only.

{
  "composer-2.5":      {"input_per_m": 0.50, "output_per_m": 2.50},
  "composer-2.5-fast": {"input_per_m": 3.00, "output_per_m": 15.00},
  "grok-4.7":          {"input_per_m": 2.00, "output_per_m": 6.00},
  "claude-haiku-5.5":  {"input_per_m": 0.10, "output_per_m": 0.50}
}

I left cached-input pricing out on purpose. Reported cached rates for Composer 2.5 come from a single secondary table, and caching affects both edit styles through the same file read, so it would add precision the sources cannot back.

Step 3: Write the token counter

The counter tries tiktoken first and falls back to an estimate. The BACKEND string is printed by every sweep so you always know which one produced your numbers.

"""Token counting. Uses tiktoken when installed, otherwise a rough code-aware estimate."""
import math
import re

try:
    import tiktoken

    _ENC = tiktoken.get_encoding("o200k_base")
    BACKEND = "tiktoken:o200k_base"

    def count(text: str) -> int:
        return len(_ENC.encode(text))

except ImportError:
    BACKEND = "heuristic"
    _PIECE = re.compile(r"[A-Za-z0-9_]+|[^sA-Za-z0-9_]|n+|[ t]+")

    def count(text: str) -> int:
        total = 0
        for piece in _PIECE.findall(text):
            if piece[0].isalnum() or piece[0] == "_":
                total += math.ceil(len(piece) / 5)
            else:
                total += 1
        return total

Part 2: Build the Edit Generator (Steps 4 to 6)

Step 4: Generate deterministic source files

You need files of known size with unique lines, so a SEARCH block can match exactly once. The generator writes repeating handler functions with numbered identifiers. A fixed seed means your file at 400 lines is byte-identical to mine.

"""Deterministic synthetic Python modules of a requested line count."""
import random


def make_file(n_lines: int, seed: int = 7) -> list[str]:
    rng = random.Random(seed)
    lines: list[str] = ["import json", "import logging", "", "log = logging.getLogger(__name__)", ""]
    i = 0
    while len(lines) < n_lines:
        i += 1
        scale = rng.randint(2, 40)
        lines += [
            f"def handler_{i}(request, config):",
            f'    """Handle request type {i}."""',
            f'    value = request.get("field_{i}", config.default_{i})',
            "    if value is None:",
            f'        raise ValueError("missing field_{i}")',
            f"    result = transform_{i}(value, scale={scale})",
            f'    log.debug("handler_{i} done: %s", result)',
            f'    return {{"id": {i}, "result": result}}',
            "",
        ]
    return lines[:n_lines]

Check it before moving on:

python3 -c "import corpus; print(len(corpus.make_file(400)), corpus.make_file(12)[5:8])"

Expected:

400 ['def handler_1(request, config):', '    """Handle request type 1."""', '    value = request.get("field_1", config.default_1)']

Step 5: Build edit scenarios with two layouts

An edit scenario changes k lines of the old file by appending a marker comment. Two layouts matter. In the clustered layout the changed lines sit together, which is how a rename inside one function looks. In the scattered layout each changed line sits in its own region, which is how a signature change that ripples through a module looks. Scattered is the worst case for diffs because every hunk pays for its own markers and context.

Notice the detail in build: when two hunks’ context windows touch, they merge into one. A real patch tool does the same, and skipping it would overcount diff tokens on dense edits. I first wrote this without merging and without a body offset, and the one-line scenario always landed on the import header, which made one-line diffs look about half as costly as they are. The version below fixes both.

"""Build one edit scenario and render it as a full rewrite and as SEARCH/REPLACE blocks."""
import random
from dataclasses import dataclass

FENCE = "```"
BODY_START = 6  # skip the import header so edits land in function bodies


@dataclass
class Scenario:
    old: list[str]
    new: list[str]
    groups: list[list[int]]  # one entry per hunk: the changed line indexes it covers
    context: int = 2


def pick_lines(n_lines: int, changed: int, layout: str, seed: int = 11) -> list[int]:
    rng = random.Random(seed)
    span = n_lines - BODY_START - 3
    changed = min(changed, span)
    if layout == "clustered":
        start = rng.randint(BODY_START, BODY_START + span - changed)
        return list(range(start, start + changed))
    width = span / changed
    return [BODY_START + int(j * width + rng.random() * (width - 1 if width > 1 else 0))
            for j in range(changed)]


def build(old: list[str], changed: int, layout: str, context: int = 2) -> Scenario:
    picked = pick_lines(len(old), changed, layout)
    new = list(old)
    for i in picked:
        new[i] = old[i] + "  # patched"
    groups: list[list[int]] = []
    for i in picked:
        # a hunk whose context window touches the previous one becomes a single hunk
        if groups and i - groups[-1][-1] <= 2 * context + 1:
            groups[-1].append(i)
        else:
            groups.append([i])
    return Scenario(old, new, groups, context)


def render_rewrite(s: Scenario) -> str:
    return f"{FENCE}pythonn" + "n".join(s.new) + f"n{FENCE}n"


def render_search_replace(s: Scenario, path: str = "app.py") -> str:
    blocks = []
    for g in s.groups:
        lo = max(0, g[0] - s.context)
        hi = min(len(s.old), g[-1] + 1 + s.context)
        search = "n".join(s.old[lo:hi])
        replace = "n".join(s.new[lo:hi])
        blocks.append(f"{path}n<<<<<<< SEARCHn{search}n=======n{replace}n>>>>>>> REPLACEn")
    return "n".join(blocks)


def apply_search_replace(text: str, rendered: str) -> str:
    """Apply rendered blocks to text. Raises ValueError when a SEARCH block does not match exactly once."""
    for block in rendered.split("<<<<<<< SEARCHn")[1:]:
        search, rest = block.split("n=======n", 1)
        replace = rest.split("n>>>>>>> REPLACE", 1)[0]
        if text.count(search) != 1:
            raise ValueError(f"SEARCH block matched {text.count(search)} times")
        text = text.replace(search, replace, 1)
    return text

Step 6: Prove the diff format round-trips

The renderer is only trustworthy if applying its output to the old file reproduces the new file. apply_search_replace does that and refuses any SEARCH block that matches zero times or more than once, which mirrors the strictness real editors need. The tests in Step 8 hammer it across both layouts. If you want to go further and attack the edit format with malformed input, our guide to fuzzing Cursor-style AI edits covers whole-file diffs and search/replace failure modes in depth.

Run a quick check now:

python3 - <<'EOF'
import corpus, edits
old = corpus.make_file(400)
s = edits.build(old, 3, "scattered")
print(edits.render_search_replace(s).split("nn")[0])
EOF

Expected, the first of three hunks:

app.py
<<<<<<< SEARCH
    if value is None:
        raise ValueError("missing field_7")
    result = transform_7(value, scale=8)
    log.debug("handler_7 done: %s", result)
    return {"id": 7, "result": result}
=======
    if value is None:
        raise ValueError("missing field_7")
    result = transform_7(value, scale=8)  # patched
    log.debug("handler_7 done: %s", result)
    return {"id": 7, "result": result}
>>>>>>> REPLACE

That one-line change costs eleven lines of output plus markers. Hold that thought: it is the per-hunk tax the whole article revolves around.

Part 3: Build the Cost Model (Steps 7 and 8)

Step 7: Write model.py

This file turns a scenario into dollars and seconds. Three inputs besides the scenario deserve attention. fail_rate is the probability a diff fails to apply and triggers a full rewrite. out_tok_per_s is an assumed generation speed, and I default it to 80 tokens per second purely as a placeholder you should replace with your measured rate. rewrite_speedup multiplies the rewrite path’s speed, so you can test the claim that a specialized apply path makes regeneration cheap in wall-clock terms.

"""Expected cost and latency of a rewrite vs a SEARCH/REPLACE edit."""
import json
from dataclasses import dataclass

import edits
import tokens

INSTRUCTION = "Append the marker comment to the listed lines. Keep everything else identical.n"


@dataclass
class Price:
    input_per_m: float
    output_per_m: float

    def dollars(self, tok_in: int, tok_out: int) -> float:
        return tok_in / 1e6 * self.input_per_m + tok_out / 1e6 * self.output_per_m


def load_price(name: str, path: str = "prices.json") -> Price:
    with open(path) as fh:
        return Price(**json.load(fh)[name])


def compare(s: edits.Scenario, price: Price, fail_rate: float = 0.0,
            out_tok_per_s: float = 80.0, rewrite_speedup: float = 1.0) -> dict:
    tok_in = tokens.count(INSTRUCTION) + tokens.count("n".join(s.old))
    out_rewrite = tokens.count(edits.render_rewrite(s))
    out_diff = tokens.count(edits.render_search_replace(s))

    cost_rewrite = price.dollars(tok_in, out_rewrite)
    cost_diff_ok = price.dollars(tok_in, out_diff)
    # A failed diff costs its own attempt, then the editor falls back to a full rewrite.
    cost_diff = cost_diff_ok + fail_rate * cost_rewrite

    sec_rewrite = out_rewrite / (out_tok_per_s * rewrite_speedup)
    sec_diff = out_diff / out_tok_per_s + fail_rate * sec_rewrite
    return {
        "tok_in": tok_in, "out_rewrite": out_rewrite, "out_diff": out_diff,
        "cost_rewrite": cost_rewrite, "cost_diff": cost_diff,
        "sec_rewrite": sec_rewrite, "sec_diff": sec_diff,
        "cheaper": "diff" if cost_diff < cost_rewrite else "rewrite",
        "faster": "diff" if sec_diff < sec_rewrite else "rewrite",
    }

The fallback line is the one most spreadsheets get wrong. A failed diff has already burned its own output tokens, then the rewrite runs and pays the full input and output bill again. Charging only the rewrite output would understate the failure penalty.

Step 8: Add the tests and run them

Seven tests pin the behavior: determinism, round-trip across both layouts and four size pairs, rejection of ambiguous blocks, a one-line edit in an 800-line file favoring diffs by more than 20 times in output tokens, a dense scattered edit favoring rewrite, failure rate only ever raising diff cost, and a monotonic token counter. No pytest needed, the file runs itself.

import corpus
import edits
import model
import tokens


def test_corpus_length_and_determinism():
    assert len(corpus.make_file(137)) == 137
    assert corpus.make_file(137) == corpus.make_file(137)


def test_search_replace_roundtrip():
    for layout in ("scattered", "clustered"):
        for n, k in ((400, 10), (50, 10), (200, 30), (100, 1)):
            old = corpus.make_file(n)
            s = edits.build(old, k, layout)
            rendered = edits.render_search_replace(s)
            assert edits.apply_search_replace("n".join(old), rendered) == "n".join(s.new)


def test_ambiguous_search_block_is_rejected():
    text = "anbnanb"
    bad = "x.pyn<<<<<<< SEARCHnanbn=======ncn>>>>>>> REPLACEn"
    try:
        edits.apply_search_replace(text, bad)
    except ValueError:
        return
    raise AssertionError("expected ValueError")


def test_single_line_edit_in_big_file_favors_diff():
    s = edits.build(corpus.make_file(800), 1, "scattered")
    r = model.compare(s, model.Price(0.5, 2.5))
    assert r["out_diff"] * 20 < r["out_rewrite"]
    assert r["cheaper"] == "diff"


def test_dense_scattered_edit_favors_rewrite():
    s = edits.build(corpus.make_file(100), 25, "scattered")
    r = model.compare(s, model.Price(0.5, 2.5))
    assert r["cheaper"] == "rewrite"


def test_failure_rate_only_raises_diff_cost():
    s = edits.build(corpus.make_file(400), 3, "scattered")
    p = model.Price(0.5, 2.5)
    assert model.compare(s, p, 0.3)["cost_diff"] > model.compare(s, p, 0.0)["cost_diff"]


def test_token_counter_monotonic():
    assert tokens.count("a = 1nb = 2n") < tokens.count("a = 1nb = 2nc = 3n")


if __name__ == "__main__":
    names = [n for n in sorted(globals()) if n.startswith("test_")]
    for n in names:
        globals()[n]()
        print("ok", n)
    print(f"{len(names)} passed")
python3 test_model.py

Expected:

ok test_ambiguous_search_block_is_rejected
ok test_corpus_length_and_determinism
ok test_dense_scattered_edit_favors_rewrite
ok test_failure_rate_only_raises_diff_cost
ok test_search_replace_roundtrip
ok test_single_line_edit_in_big_file_favors_diff
ok test_token_counter_monotonic
7 passed

Part 4: Sweep and Read the Results (Steps 9 to 11)

Step 9: Write the sweep command

The sweep loops over seven file sizes and six edit sizes, skips any combination where the edit touches more than a quarter of the file, and prints a markdown table plus a break-even summary. Every knob from the model is a flag.

"""Sweep file size x edit size and report where a diff stops paying off."""
import argparse
import csv
import sys

import corpus
import edits
import model
import tokens

SIZES = [50, 100, 200, 400, 800, 1600, 3200]
CHANGES = [1, 3, 10, 30, 100, 300]


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--model", default="composer-2.5")
    ap.add_argument("--layout", choices=["scattered", "clustered"], default="scattered")
    ap.add_argument("--fail-rate", type=float, default=0.10)
    ap.add_argument("--context", type=int, default=2)
    ap.add_argument("--tok-s", type=float, default=80.0, help="assumed output tokens per second")
    ap.add_argument("--rewrite-speedup", type=float, default=1.0)
    ap.add_argument("--csv", default="")
    a = ap.parse_args()

    price = model.load_price(a.model)
    print(f"tokenizer={tokens.BACKEND} model={a.model} layout={a.layout} fail_rate={a.fail_rate} "
          f"context={a.context} tok_s={a.tok_s} rewrite_speedup={a.rewrite_speedup}")
    rows = []
    for n in SIZES:
        old = corpus.make_file(n)
        for k in CHANGES:
            if k > n // 4:
                continue
            s = edits.build(old, k, a.layout, a.context)
            r = model.compare(s, price, a.fail_rate, a.tok_s, a.rewrite_speedup)
            rows.append({"lines": n, "changed": k, **r})

    print("n| Lines | Changed | Rewrite out | Diff out | Rewrite $ | Diff $ | Cheaper | Faster |")
    print("|---|---|---|---|---|---|---|---|")
    for r in rows:
        print(f"| {r['lines']} | {r['changed']} | {r['out_rewrite']} | {r['out_diff']} | "
              f"{r['cost_rewrite']:.5f} | {r['cost_diff']:.5f} | {r['cheaper']} | {r['faster']} |")

    print("nBreak-even (largest tested edit where the diff is still cheaper):")
    for n in SIZES:
        wins = [r["changed"] for r in rows if r["lines"] == n and r["cheaper"] == "diff"]
        print(f"  {n:>5} lines -> diff cheaper up to {max(wins) if wins else 'never'} changed lines")

    if a.csv:
        with open(a.csv, "w", newline="") as fh:
            w = csv.DictWriter(fh, fieldnames=rows[0].keys())
            w.writeheader()
            w.writerows(rows)
        print(f"nwrote {a.csv}", file=sys.stderr)


if __name__ == "__main__":
    main()

Step 10: Run the baseline and read it

python3 sweep.py --csv results.csv

The baseline uses Composer 2.5 prices, the scattered layout, context of 2 lines, and a 10% apply failure rate. The first line of output confirms the settings:

tokenizer=heuristic model=composer-2.5 layout=scattered fail_rate=0.1 context=2 tok_s=80.0 rewrite_speedup=1.0

The full run prints 32 rows. Here are eight that tell the story, taken straight from my run:

Lines Changed lines Rewrite output tokens Diff output tokens Rewrite cost Diff cost Cheaper
50 1 750 169 $0.00226 $0.00103 Diff
100 3 1,550 603 $0.00465 $0.00275 Diff
200 10 3,167 1,942 $0.00949 $0.00737 Diff
400 3 Not listed 633 $0.01890 $0.00662 Diff
400 30 Not listed Not listed $0.01924 $0.01984 Rewrite
800 30 Not listed Not listed $0.03815 $0.02493 Diff
1,600 100 Not listed Not listed $0.07760 $0.06944 Diff
3,200 300 Not listed Not listed $0.15750 $0.18522 Rewrite

Three things stand out. First, the rewrite column barely moves within a size band: at 400 lines it costs about the same for 3 changed lines as for 30, because the output is the whole file either way. Second, the 400-line, 30-change row is close to a tie, and the rewrite takes it. Third, the diff wins at 1,600 lines even with 100 changed lines, and only loses at 300.

Now the summary block at the bottom of the same run:

Break-even (largest tested edit where the diff is still cheaper):
     50 lines -> diff cheaper up to 3 changed lines
    100 lines -> diff cheaper up to 3 changed lines
    200 lines -> diff cheaper up to 10 changed lines
    400 lines -> diff cheaper up to 10 changed lines
    800 lines -> diff cheaper up to 30 changed lines
   1600 lines -> diff cheaper up to 100 changed lines
   3200 lines -> diff cheaper up to 100 changed lines

A pattern emerges that is easy to remember. The diff stays ahead until the edit touches roughly 3 to 10 percent of the file in a scattered layout. For a 400-line file that means roughly 10 changed lines, which makes the handbook claim about files under 400 lines plausible for large refactors and much less so for typical bug fixes, at least on token cost.

Step 11: Stress every assumption

A model is only useful if you know which inputs it is sensitive to. Rerun the sweep with one flag changed at a time:

python3 sweep.py --layout clustered
python3 sweep.py --fail-rate 0.0
python3 sweep.py --fail-rate 0.30
python3 sweep.py --context 5
python3 sweep.py --model composer-2.5-fast
python3 sweep.py --model grok-4.7
python3 sweep.py --rewrite-speedup 9

I ran each and collected the largest edit size where the diff stayed cheaper on dollars:

File lines Baseline Clustered 0% failure 30% failure Context of 5
50 3 10 3 1 1
100 3 10 3 3 3
200 10 30 10 10 3
400 10 100 30 10 10
800 30 100 30 30 10
1,600 100 300 100 30 30
3,200 100 300 100 100 100

Read the columns left to right. Clustering the same edits moves break-even up by one or two notches at every size, because a single hunk shares its context across all changed lines. Going from a 0% to a 30% failure rate pulls it down by one notch on the 50, 400 and 1,600-line files. Widening context from 2 to 5 lines has a similar bite. These are the three levers that matter.

Now the lever that does not matter much. Switching to Composer 2.5 Fast, Grok 4.7 or Claude Haiku 5.5 left the break-even column identical to the baseline in my grid. Prices scale the dollars: Cursor lists Composer 2.5 at $0.50 per million input tokens and $2.50 per million output tokens, Composer 2.5 Fast at $3.00 and $15.00, and xAI lists Grok 4.7 at $2 and $6 for prompts below 200,000 tokens. Token counts pick the winner. Prices only decide how much the winner saves, and the output-to-input ratio nudges that figure.

Then there is latency, where the speedup flag changes the verdict. At 400 lines, scattered edits, a 10% failure rate and an assumed 80 tokens per second:

Changed lines Rewrite seconds (1x) Diff seconds (1x) Faster at 1x Rewrite seconds (9x) Diff seconds (9x) Faster at 9x
1 Not listed Not listed Diff Not listed 3.3 Diff
3 Not listed Not listed Not listed Not listed Not listed Not listed
10 Not listed Not listed Not listed Not listed Not listed Not listed
30 Not listed Not listed Not listed Not listed Not listed Not listed
100 Not listed 170.0 Not listed Not listed Not listed Not listed

If a fast apply path really did make regeneration 9 times quicker, the latency crossing at 400 lines would collapse from somewhere between 10 and 30 changed lines down to about 1 to 3, even though the token crossing stays put. That is a what-if, not a measurement of Cursor. It does explain why unofficial claims that rewriting is competitive on short files and Cursor-style diffs win on long ones can both be true at once. They are describing different axes. Keep this table in mind when you read a latency claim with no file size attached.

Part 5: Calibrate Against Real Output (Steps 12 and 13)

Step 12: Add the live calibration script and a stub server

A model of token counts is a hypothesis until a real endpoint confirms it. live.py builds one scenario, sends it to an OpenAI-compatible /chat/completions endpoint twice (once asking for the whole file back, once asking for SEARCH/REPLACE blocks), then reports predicted versus real completion tokens, wall time, tokens per second and whether the edit was correct. Correctness is checked by actually applying the diff or comparing the rewritten file against the expected one.

"""Calibrate the cost model against a real OpenAI-compatible endpoint."""
import argparse
import json
import os
import time
import urllib.request

import corpus
import edits
import model
import tokens

SYS_REWRITE = "You edit files. Reply with the complete updated file in one fenced code block and nothing else."
SYS_DIFF = ("You edit files. Reply only with SEARCH/REPLACE blocks: a path line, <<<<<<< SEARCH, "
            "the original lines, =======, the new lines, >>>>>>> REPLACE.")


def ask(base: str, key: str, name: str, system: str, user: str):
    body = json.dumps({"model": name, "temperature": 0, "messages": [
        {"role": "system", "content": system}, {"role": "user", "content": user}]}).encode()
    req = urllib.request.Request(base.rstrip("/") + "/chat/completions", body,
                                 {"Content-Type": "application/json", "Authorization": f"Bearer {key}"})
    start = time.perf_counter()
    with urllib.request.urlopen(req, timeout=300) as resp:
        data = json.load(resp)
    return (data["choices"][0]["message"]["content"], data["usage"]["completion_tokens"],
            time.perf_counter() - start)


def strip_fence(text: str) -> str:
    lines = text.strip().splitlines()
    if lines and lines[0].startswith("```"):
        lines = lines[1:]
    if lines and lines[-1].startswith("```"):
        lines = lines[:-1]
    return "n".join(lines)


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--base-url", default=os.environ.get("BASE_URL", "http://127.0.0.1:8765/v1"))
    ap.add_argument("--api-key", default=os.environ.get("API_KEY", "none"))
    ap.add_argument("--model", default=os.environ.get("MODEL", "stub"))
    ap.add_argument("--lines", type=int, default=400)
    ap.add_argument("--changed", type=int, default=3)
    ap.add_argument("--layout", default="scattered")
    a = ap.parse_args()

    old = corpus.make_file(a.lines)
    s = edits.build(old, a.changed, a.layout)
    targets = sorted(i + 1 for g in s.groups for i in g if s.new[i] != s.old[i])
    user = ("File app.py:n```pythonn" + "n".join(old) + "n```nn"
            f"Task: append '  # patched' to the end of lines {','.join(map(str, targets))}. Change nothing else.")
    want = "n".join(s.new)
    pred = model.compare(s, model.Price(0, 0))

    out_r, tok_r, sec_r = ask(a.base_url, a.api_key, a.model, SYS_REWRITE, user)
    ok_r = strip_fence(out_r) == want
    out_d, tok_d, sec_d = ask(a.base_url, a.api_key, a.model, SYS_DIFF, user)
    try:
        ok_d = edits.apply_search_replace("n".join(old), out_d) == want
    except ValueError:
        ok_d = False

    print(f"{'strategy':<9} {'pred out':>9} {'real out':>9} {'seconds':>8} {'tok/s':>7} correct")
    print(f"{'rewrite':<9} {pred['out_rewrite']:>9} {tok_r:>9} {sec_r:>8.2f} {tok_r / sec_r:>7.0f} {ok_r}")
    print(f"{'diff':<9} {pred['out_diff']:>9} {tok_d:>9} {sec_d:>8.2f} {tok_d / sec_d:>7.0f} {ok_d}")


if __name__ == "__main__":
    main()

To test the plumbing without spending anything, the project ships mock_server.py, a stub that behaves like a perfect model. It answers both prompts correctly and reports usage honestly, and it pretends to generate 4,000 tokens per second.

"""A perfect-model stub: answers both prompts correctly so you can test live.py offline."""
import json
import re
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

import edits
import tokens


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        req = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        system, user = req["messages"][0]["content"], req["messages"][1]["content"]
        file_text = re.search(r"```pythonn(.*)n```", user, re.S).group(1)
        targets = [int(n) - 1 for n in re.search(r"lines ([d,]+).", user).group(1).split(",")]
        old = file_text.split("n")
        new = list(old)
        for i in targets:
            new[i] += "  # patched"
        groups = [[i] for i in targets]
        s = edits.Scenario(old, new, groups)
        reply = edits.render_rewrite(s) if "complete updated file" in system else edits.render_search_replace(s)
        n_out = tokens.count(reply)
        time.sleep(n_out / 4000)  # pretend to generate 4,000 tokens per second
        body = json.dumps({"choices": [{"message": {"role": "assistant", "content": reply}}],
                           "usage": {"prompt_tokens": tokens.count(user), "completion_tokens": n_out}}).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *args):
        pass


if __name__ == "__main__":
    HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()

Step 13: Run the calibration

python3 mock_server.py &
python3 live.py
python3 live.py --lines 1600 --changed 10

My output against the stub:

strategy   pred out  real out  seconds   tok/s correct
rewrite        6298      6298     1.62    3895 True
diff            633       633     0.16    3901 True
strategy   pred out  real out  seconds   tok/s correct
rewrite       25486     25486     6.42    3968 True
diff           1978      1978     0.51    3868 True

Predicted equals real here because the stub uses the same tokenizer, so this run proves the plumbing and the correctness checks, not the model. It also shows the speed ratio matches the token ratio: the rewrite took 10 times longer on the 400-line case simply because it emitted 10 times more tokens. Stop the stub with kill %1.

For a real calibration, point the script at your endpoint:

export BASE_URL=https://your-endpoint.example/v1
export API_KEY=your-key
export MODEL=your-model-name
python3 live.py --lines 400 --changed 3

Expect the real column to differ from the predicted one. Two effects are most common. A real tokenizer counts code differently than the estimate. And a real model may wrap its answer in explanation or ignore the instruction to reply only with blocks, which pushes real output above predicted. Both are findings. If the diff row shows correct False, the model failed to produce a block that matched once, which is exactly the failure rate the cost model needs. Run the script 20 times at your usual file sizes and use the fraction that fail as the --fail-rate in the sweep. For a structured way to audit those outputs after the fact, our Cursor Composer rewrite audit tool pairs well with this step.

Applying the Result Inside Cursor

The project predicts per-edit output cost for a generic model. To use it in Cursor, you need two bridges: a way to see what Cursor actually charged, and a way to nudge edits toward the cheaper side of your break-even line.

For the first bridge, use Cursor’s usage view before and after a batch of identical edits. Run the same ten small changes against a 1,600-line file, note the usage delta, then do the same against the same change expressed as a bigger refactor. Expect Cursor’s real numbers to exceed the model’s output-only prediction, because the agent also reads files, calls tools and carries conversation context. Treat the model as a floor and the ratio between your two batches as the signal. Billing has also been in flux: third-party trackers such as tokensave.app report that the old $20 Pro plan became a legacy plan after the October 12 launch of The X Subscription, and a forum report says usage is charged by each model’s cost rather than a fixed token allowance. Both points are secondary reports, so check your own dashboard.

For the second bridge, use what you learned to shape your prompts and project rules. If your files sit at 1,600 lines or more and your typical change touches fewer than 30 lines, you are in the zone where patch-style edits should win by 4 times or more on output tokens. If your files are 100 lines, a rewrite costs little in absolute terms, and the risk of an unmatched block may outweigh the savings. Our guide to tuning diff versus rewrite with Cursor rules shows how to write those preferences down so the agent sees them every session.

A working heuristic that falls out of the tables:

  • Large file, small scattered change: ask for minimal edits. Output-token savings of roughly 60 to 97 percent are on the table.
  • Small file, any change: let the model rewrite. The absolute dollars are tiny and failure risk drops to zero.
  • Mid-size file, edits touching more than 10 percent of lines: rewrite, or split the task so each pass stays under the line.
  • Mechanical rename across a module: neither. Use your IDE’s refactor tool and spend zero tokens.

Last, remember that cost is not the only axis. A rewrite that silently drops a function is cheap and wrong. Run edits through a safety net, such as the checks in our piece on catching bad Fast Apply rewrites, whichever style you pick.

Common Pitfalls

  1. Trusting absolute token counts from the heuristic. The built-in estimator is consistent, not exact. Install tiktoken or use your endpoint’s usage field before quoting a number outside your team.
  2. Testing only one-line edits. A single changed line makes diffs look 4 to 240 times cheaper in output tokens, depending on file size. Real tasks touch several places. Sweep edit sizes, as Step 11 does.
  3. Ignoring the failure fallback. At a 30% failure rate, the 50-line file’s break-even drops from 3 changed lines to 1. Leave the term at zero and you will oversell patches.
  4. Mixing layouts. Clustered and scattered edits differ by one or two break-even notches. Label which one your own workload resembles.
  5. Assuming a cheaper model shifts the crossing point. It does not. Composer 2.5, Fast, Grok 4.7 and Haiku 5.5 all crossed in the same place on my grid. Only dollars moved.
  6. Comparing seconds across providers. The speed flag is a placeholder. Throughput varies by hardware, load and time of day, so measure with Step 13 at the hour you work.
  7. Letting context creep. Raising context from 2 to 5 lines cut the 200-line break-even from 10 to 3 changed lines. Verbose patches erase the savings they exist to create.
  8. Forgetting that Cursor’s agent adds its own tokens. File reads, tool calls and history are not in this model. It prices the edit, not the session.

Troubleshooting

These are the failures I hit or provoked while building and testing the project, with the fix for each.

  1. ModuleNotFoundError: No module named ‘corpus’. You ran a script from outside the project folder. cd breakeven first, since the modules import each other by bare name.
  2. FileNotFoundError: prices.json. load_price reads the file relative to your working directory. Run from the project folder or pass an absolute path as the second argument.
  3. KeyError for a model name. The --model flag must match a key in prices.json exactly. Add the model as a new object with input_per_m and output_per_m.
  4. ValueError: SEARCH block matched 0 times. The file and the block differ in whitespace, usually CRLF line endings or tabs against spaces. Normalize line endings before comparing, and keep your edit generator and your files on the same convention.
  5. ValueError: SEARCH block matched 2 times. The context was not unique. Raise --context or pick anchor lines that contain a distinctive identifier. This is the same failure that drives the fallback term in the model.
  6. live.py prints a connection refused error. The stub is not running or your base URL is wrong. Start python3 mock_server.py & and confirm its port is free, or set BASE_URL to the address the stub is listening on.
  7. HTTP 401 from a real endpoint. Set API_KEY. The default value none only works against the stub.
  8. KeyError: ‘usage’ or missing completion_tokens. Some servers omit usage unless asked. Enable usage reporting in your server’s settings, or count the returned text with tokens.count as a fallback.
  9. Rewrite row shows correct False but looks right. A trailing newline or a different fence language usually breaks the exact comparison. Print both strings with repr and diff them before blaming the model.
  10. The sweep prints fewer rows than I expected. By design it skips cases where the edit exceeds a quarter of the file, so a 50-line file never shows 30 changed lines.
  11. My numbers differ from the tables here. Check the first output line. A different tokenizer backend, such as tiktoken, will shift absolute counts. The ordering and the notch pattern should survive.
  12. Break-even prints never. The diff lost at every tested size. Lower the failure rate or shrink the context to confirm the cause, then check that your edit sizes include 1.

Advanced Tips

Once the baseline works, a few extensions pay for themselves quickly.

  • Replay real history. Export a month of your own commits, count changed lines per file and file lengths, and feed those pairs into compare. You will learn which regime your team lives in instead of guessing.
  • Add a hybrid strategy. Model a patch for small hunks and a function-level rewrite when the hunk count passes a threshold. Real tools tend to land between the extremes, so a third curve is more realistic than two.
  • Model silent damage. Add a probability that a rewrite drops or alters unrelated lines and price the review time. Our Cursor Plan mode edit drift checker shows how to detect it.
  • Use a measured speed per path. Replace the single tokens-per-second number with two, one for plain generation and one for the apply stage, and rerun the latency table.
  • Bucket by language. Python, Go and TypeScript tokenize differently. Generate corpora for each and compare the break-even columns before applying one team-wide rule.
  • Commit results.csv. The CSV from Step 10 is plain text and diffs well. Track it over time as prices and models change, which they have done repeatedly this autumn.
  • Gate in CI. A job that fails when a pricing change moves your break-even more than one notch will catch quiet cost regressions before the invoice does.

For a wider view of the editor and agent tooling around these choices, the software category collects our comparisons and tutorials.

The Complete Project at a Glance

Here is the final layout and the commands to run it end to end. Total size is about 390 lines of Python across eight files plus one JSON file.

breakeven/
  prices.json        input and output price per 1M tokens
  tokens.py          tiktoken or heuristic counter
  corpus.py          deterministic synthetic source files
  edits.py           scenarios, rewrite and SEARCH/REPLACE rendering, applier
  model.py           cost and latency comparison
  sweep.py           file-size by edit-size sweep, break-even summary
  live.py            calibration against an OpenAI-compatible endpoint
  mock_server.py     perfect-model stub for offline plumbing tests
  test_model.py      7 tests, no pytest needed
python3 test_model.py
python3 sweep.py --csv results.csv
python3 sweep.py --layout clustered --fail-rate 0.30
python3 mock_server.py &
python3 live.py
kill %1

The takeaway: on Composer 2.5’s published prices, patching beats rewriting whenever an edit stays under roughly 3 to 10 percent of a file in a scattered layout, and stays ahead much longer when the edit is clustered. Failure rate and context size move that line. Price changes do not. What nobody outside Cursor can tell you yet is which of the two Composer 2.5 actually does on a given edit, so measure your own sessions before you rewrite your habits.

Frequently Asked Questions

Does Composer 2.5 rewrite whole files or apply diffs?

Cursor’s pages I checked on October 30, 2026 do not say. They confirm that Composer 2.5 handles file edits and multi-file changes inside Cursor and publish its prices, but give no edit mechanism, token-consumption figures or latency benchmarks. Unofficial reports describe a specialized apply path, and none are confirmed by Cursor.

How much does Composer 2.5 cost per million tokens?

The standard variant costs $0.50 per million input tokens and $2.50 per million output tokens. The Fast variant costs $3.00 and $15.00. Both figures come from Cursor’s Composer page, and plan packaging around them changed with The X Subscription on October 12, 2026.

When is a full-file rewrite cheaper than a diff?

In this model, when the edit touches more than about 3 to 10 percent of the file in a scattered layout, or when apply failures are common. On a 400-line file with a 10% failure rate, 30 scattered changed lines tipped the cost to the rewrite ($0.01924 versus $0.01984).

Does a cheaper model change the break-even point?

No. Composer 2.5, Composer 2.5 Fast, Grok 4.7 and Claude Haiku 5.5 produced the same break-even table in my grid, because both edit styles scale with the same prices. The dollar savings differ by model, the crossing point does not.

Can I run this with the real Composer 2.5 model?

Not directly. Cursor says Composer 2.5 is available only inside its own products, so the harness cannot call it. Use live.py with any OpenAI-compatible model to calibrate token counts, then compare against Cursor’s usage view for the Composer-specific cost.

Do I need tiktoken?

No. Without it the project uses a consistent estimate that is good for comparing the two styles. Install tiktoken if you want absolute token counts closer to a real tokenizer’s, and rerun the sweep to see whether any break-even notch shifts.

How many runs do I need to estimate the failure rate?

Twenty runs at your typical file sizes give a rough figure, and fifty gives a steadier one. Count only runs where the SEARCH block failed to match exactly once, then pass that fraction as –fail-rate. Re-measure after model changes, since failure rates move between model versions.

Related Coverage

Source: Tech Insider