Renaming one function saved up to two-thirds of the tokens for 14 models reading the same code

AI finds code the way grep does: a brute-force text scan. A vague name means hundreds of files to read.
By Modem07-25 · 12 min read⚑ Tutorial / how-to
🎧 Listen to the podcast0:00 / --:--
0:00 / --:--
Tutorial · Xiaohu's Take
The 1-minute Brief
  • The names you give your functions decide whether an AI flips through 19 files or 459 when it edits one. Modem ran 1,680 comparisons to get this number.
  • Why? Because AI finds code with plain-text search, not a compiler-drawn dependency graph or a language server that resolves symbols. The Claude Code team tried vector databases early on, then dropped them.
  • With search-friendly names, all 14 model-and-tool combos burned fewer tokens reading the same code. The biggest saver dropped from 70K to 24K average tokens. Median savings: 30%. Confidently wrong answers: zero.
  • Saved search budget becomes judgment. Give a weak model an eight-round bug-finding budget and it finds only 3 of 8 bugs in a monolith. Split and rename the code and it catches all 8. Most failed reviews don't even reach a conclusion—they burn the budget flipping through files.
⚑ Source: Modem's engineering blog. They designed and ran the experiments themselves. The post ends with a product pitch. It also notes several test limitations, which we list at the end.
1 A Basic Fact

How AI Actually Finds Your Code

Ask an AI to rename a function and its first move is a search of your repository.

That search is no magic. It scans every line of the repo for the exact string, same as pressing Ctrl+F in your editor. The command-line version is grep, decades old. AI uses its faster cousin ripgrep (evoked as rg).

It won't ask the compiler for a dependency graph, and no language server resolves symbols for it. It only sees the literal text. Search, read the surrounding lines, and if that's not enough, search a different word.

Claude Code searching a repository. It runs ripgrep, reads the context around a hit, and searches again if needed. Video: Modem

That's not a shortcut. The Claude Code team tried vector databases early on, then abandoned them because plain-text search simply worked better. The academic SWE-agent landed on the same approach: give the model a keyword search tool, nothing fancier.

File paths are search terms too. Ask how the session broker works, and the AI will scan filenames for session-broker, sessionBroker, and session_broker. A directory named session-broker/ counts as a hit before a single line of code is read.

So the loop is: search, read context, search again with a different term if needed, and only then start editing.

Search with rg Read the hits Enough? Else new term Start edits Not enough? New search term, next loop The vaguer the name, the more loops. Each loop reads files. The AI navigation loop The AI navigation loop Search with rg Read the hits Enough? Else new term Start edits Next loop The vaguer the name, the more loops. Each loop reads files.
How AI navigates your repository (illustration by Xiaohu)

So one input in this chain is entirely within the developer's control: can the words and filenames in your code serve as good search terms?

Modem did the math. The company decided in early 2025 to let AI write all its code. After a year, models had generated 99.9% of its 360K lines of TypeScript app code and 320K lines of tests. The findings below, and the 1,680 comparison runs, come from that year.

2 The Cost

What Does a Vague Name Really Cost?

How big is the difference between a good and a bad search term? They tested it with three functions.

All three do the exact same thing—create an API client for an external service. Only the names differ:

Three ways to name the same thing
export function create(apiKey: string) { ... }
export function createClient(apiKey: string) { ... }
export function createStripeClient(apiKey: string) { ... }

Searching each in Modem's ~2,900-file TypeScript repo:

Same repo, three names, search hits
create1,585 lines · 459 files
createClient466 lines · 23 files
createStripeClient43 lines · 19 files
All three searches took 40–50 ms; speed wasn't the issue. Bar length is proportional to hit lines. Data: Modem's measurements in their own repo. Chart by Xiaohu.

So the problem isn't search speed. grep returns matching lines, not answers. A line like const client = create(config) doesn't tell the AI if this is the client you meant, or just one of hundreds of unrelated creates.

To find out, it opens the file and reads around the hit, often expanding outward or opening other sections. A file can be tens to thousands of lines. At roughly 10 tokens per line (tokens are how AI counts text—they determine buffer limits and cost, roughly one token per English word), eliminating one wrong candidate costs hundreds to thousands of tokens. Eliminate a dozen and you've burned tens of thousands of tokens without touching the task.

Those 1,585 hits are scattered across a lot of noise: test fixtures, database inserts, background task registrations. The 43 hits for createStripeClient are its definition, call sites, and tests—all about the same client.

And the savings go beyond this one task. Context has a limit. Every token spent eliminating a fake create is a token you can't use for the real edit. Worse, when context fills with plausible-but-wrong matches, the model's reliability drops. A name that grep can find in one jump protects both the AI's attention and its budget.

3 Modules Don't Save You

Why Isn't the Import Enough?

At this point, someone will ask: aren't imports like a signpost? Find the definition, go straight there.

That works if you're moving from a call site to a definition. An import is a one-way arrow. But the task here is a rename, which works in reverse: start at the definition and find every call site. Plain-text search has no backlink index. The file containing the definition has no record of who references it.

① Three import styles, each misses something

Three call sites of the same create might import it via relative path, package name, or with a local alias. Searching the path only finds the first, since package names don't contain paths. Searching the function name finds two, misses the aliased one, and pulls back a pile of unrelated creates.

② In a monorepo, package names aren't paths

An import says the module comes from @modem/common, but that's not a path. The AI must check package.json or search the package name to locate it. That often lands on a barrel file full of export *—a directory listing at the package entrance that says "everything below is re-exported" but not what maps to what. The AI has reached a roster without names, so it has to grep inside the package again. Three hops to get back to the search it could have run at the start.

③ Methods have no import at all

A call like client.getUsers()—imports only cover modules and their top-level exports, not methods. This isn't a TypeScript quirk. The same gap exists in Python, Go, Ruby, and Java.

Modules point one way, they blur at barrels and package names, and they stay silent on methods. The name does the heavy lifting.

4 Types

What Can Precise Parameter Types Save?

Everything above covers *finding* your code. What happens next depends on types.

Types differ from comments and READMEs in a fundamental way: comments can be ignored, types are enforced by the compiler. A type error during compilation is concrete feedback and the AI often fixes it within one round.

Even before the compiler complains, types are already saving tokens. A signature like this says what goes in and what comes out—often the implementation doesn't need to be opened:

A one-line signature answers the question
function enrichUser(user: User): EnrichedUser;
Change it to function enrichUser(data) or data: any, and to learn what data is, the AI has to read the implementation. But the implementation passes it to something else, so it reads that too. Maybe it even checks a call site. One missing type annotation turns a one-line read into a multi-file investigation.

TypeScript's any is sneakier: the damage is the same, but it *looks* like a type.

Names for types matter too

Here's a function where all three parameters are strings. Pass them in the wrong order, and the compiler stays silent:

All strings
transferOwnership(
  userId: string,
  orgId: string,
  projectId: string
)

Swap the arguments and the build passes. The error shows up at runtime.

Branded types · a distinct identity for strings
transferOwnership(
  userId: UserId,
  orgId: OrgId,
  projectId: ProjectId
)

Swap them, and the build fails immediately. The error explicitly says OrgId can't be used as UserId.

Research on this self-correction loop—letting a model iterate on compiler errors—shows roughly a 3× success rate increase.

Here's the connection back to the beginning. The OrgId named in that compiler error is likely defined in another file. To fix the error, the AI needs to read that definition, and the only way to find it is the same search it's always used.

Type names and function names obey the same rule: OrgId and UserEnrichmentResult grep directly to their definitions. Names like Result, Data, or Config send the AI back into the haystack. The compiler tells it where to look; the name decides how long the look takes.

This is the real problem with any: the compiler has nothing to say, so the AI has no search term. A bad name, at least, leaves a trail.

5 Experiment 1

Does It Actually Work?

So far, all theory. Names should be specific, types precise. But what's the actual token savings? Modem turned it into an experiment.

They had Fable 5 compress this blog post into a one-page skill file to load into context. Then they gave two authoring models a spec unrelated to the post (a small notification pipeline library with retries, rate limiting, and signing). Each model wrote it twice: once without the skill file, once with. The authors were Haiku 4.5 (in Claude Code) and GPT-5.6 Sol (in Codex CLI). That gave them four versions of the same library.

Haiku without the skill produced exactly the code this post warns about:

Haiku 4.5 · Without skill
Haiku 4.5 · With skill
Withoutsigner.ts
Withhmac-payload-signer.ts
Withoutqueue.ts
Withchannel-router.ts
WithoutvalidateConfig()
WithvalidateNotificationDeliveryConfig()

Sol's no-skill output was already decent—calculateBackoffDelay in backoff.ts, TokenBucket in token-bucket.ts. With the skill, it went further: backoff.ts became notification-retry-backoff.ts. But the gap between Sol's two versions was visibly narrower than Haiku's.

Next, the reading test. Each of the four versions was dropped into the same 500 files from a real repository. Fresh sessions, only search-and-read tools, and questions like these:

Questions for the reading agents (excerpt)
How long is the wait before a retry, and where is it calculated?
At what point is the calculated HMAC signature (a tamper-evident seal) attached to the data being sent?
Given a notification ID, where can I find its full delivery attempt history?

14 model-and-tool combinations each answered 10 questions about all four versions, 3 times each. That's 1,680 runs, tracking tokens consumed and correctness.

70K → 24K
Biggest saver (DeepSeek v4 pro): average tokens per question, down 65.7%
14 / 14
All combinations burned fewer tokens reading Haiku's code, from 6% to 66%, median 30%

With Sol's code, 12 of 14 combinations improved, median 12%; the other two didn't get worse.

The pattern is consistent: rougher original code leaves more room to improve. Sol already wrote decent code, so there was less to save.

No Privilege for the Author

One data point stands out. When Sol read its own no-skill code, switching to the skill version still saved 13% to 38%, depending on the tool driving it.

The model that wrote the code has no memory of where things are. It runs the same search as every other model.

A Type of Failure That Only Appears in the Vague-Named Version

Wrong answers were almost exclusive to Haiku's generic-named code, and they were confidently wrong: asked where this notification library stores its delivery history, it pointed to a Slack delivery ledger for something else in the repo. Asked where the HMAC signature gets attached, it pointed twice to a URL signer in a media proxy service.

The skilled version had zero wrong answers. A file named hmac-payload-signer.ts doesn't look like anything else. These failures barely appeared in Sol's two versions, which were correct 834 out of 840 runs; the few errors were scattered on both sides.

6 Experiment 2

Does It Hold Up on a Real Project? (A 4,943-Line File)

A synthetic codebase only proves so much, so they picked a real challenge: Odysseus, a self-hosted AI workbench that gathered tens of thousands of GitHub stars in its first week.

The Odysseus AI workbench interface: dark theme, pink logo, left toolbar, and a chat input in Agent mode
The Odysseus interface. Image: Modem

Its routes/email_routes.py is 4,943 lines, about 3,900 of which are a single function. All 81 route handlers (each responding to a web request type) are jammed inside it. One function. An entire email client.

Modem refactored only this one file. No behavior change, no route changes, mechanically verified. They split it into concept-named modules:

Before
email_routes.py
  4,943 lines
  81 handlers in one function
After
email/
  smtp_settings.py
  message_rendering.py
  folder_fallbacks.py
  delivery_history.py

Then they asked four models the same six questions. Same pattern: the models lost in the monolith saved the most (Haiku 4.5 at 34.3%, Grok 4.5 at 31.5%). The ones that weren't lost barely saved anything (GPT-5.6 Sol at 5.9%, Sonnet 5 at 3.9%). Haiku used to burn 400K tokens flipping through the monolith; after the refactor, it stopped.

More important is the worst case. Those catastrophic runs—400K tokens burned, nothing found—happened almost entirely on the un-refactored version. *Almost*, because Haiku had one bad run on the refactored version, and the location explains it: they only refactored one file. Haiku got lost in the untouched 1,800-line email_helpers.py junk drawer next door. The refactor raised the floor, and the new floor is the worst file you didn't touch.

Xiaohu's verification (2026-07-25, 5 days after publication): pulling the same file from the same branch, routes/email_routes.py is now 6,032 lines, and a single function setup_email_routes() accounts for 4,580 lines. In five days, the file grew by over a thousand lines, and the giant function by about 700. This detail comes from our own source pull; it's not in the blog. Your pull today might give a different number.
7 Experiment 3

What Do the Saved Tokens Buy You?

The first two experiments measured cost. There's a more important question: Does spending less lead to getting it *right*?

They planted four identical, subtle bugs in both versions of the email code (byte-identical changes, so both versions have the exact same defects). Then four models were given a fixed budget of eight rounds to check the code against the spec. Each model checked each bug twice: 32 reviews per version, 64 total across both.

Model
Monolith
Refactored
Grok 4.5
Monolith8/8
Refactored8/8
GPT-5.6 Sol
Monolith8/8
Refactored8/8
Haiku 4.5
Monolith4/8
Refactored8/8
Sonnet 5
Monolith3/8
Refactored8/8
Each cell is one review; filled means the bug was found. Same four bugs, same eight-round budget. The only difference is how the code is organized. Data: Modem. Chart: redrawn by Xiaohu from the original grid.

Grok 4.5 and GPT-5.6 Sol found all of them in both versions. Eight rounds is plenty for a model with a search strategy, even in a 4,943-line file.

Haiku and Sonnet couldn't. In the monolith, they found 4/8 and 3/8 respectively. Most of those failed reviews didn't produce a wrong answer. They burned the whole budget flipping through files and never reached a conclusion. Give them the refactored code, and both go perfect.

Sonnet's row is especially telling. In the earlier retrieval experiment, it barely saved any tokens from this refactor. But add a budget limit, and its accuracy goes from 3/8 to 8/8. Being able to *afford* a search in a monolith and being able to *conclude* something there are two very different things.

8 What You Can Copy Right Now

Writing AI-Friendly Code (Actionable Advice)

We've covered the mechanism and the data. Now for what you can do today.

The skill file used in the experiment is MIT-licensed. We fetched it, read it, and found several rules that are easier to apply than the blog post suggests.

Three words is the turning point

Name your exported symbols with 2 to 4 words, at least one being a business term. Tested on a ~700K-line monorepo: one-word exports were globally unique only 61% of the time. Three words: 96%. Four or more: 98%. Use the shortest name that's uniquely searchable, and put the rest in a comment.

camelCase is invisible to phrase search

Humans and AI ask questions in natural language: "rate limit", "retry delay". But RateLimiter as an identifier won't match a search for "rate limit". So use a comment above each export to write out that phrase with spaces. A SessionExpiryChecker should sit above "Checks whether the user session has expired", so a search for "session expired" actually lands there.

Keep strings whole

Never build event names, feature flags, or error codes with template literals. github.${entity}.${action} makes github.pr.merged an unsearchable string. Write it out, even if it feels verbose.

Give error messages a unique prefix

Make any logged error traceable back to the line that threw it. Write Webhook signature mismatch for ${id}, not ${prefix}: mismatch.

Filenames are names too

config.ts, types.ts, utils.ts, and helpers.ts are content-free in search results, and they collide with every other module's config/types/utils/helpers. Prefix them with a business term: billing-plan-config.ts.

Use types, not comments, for privileged operations

Require an OrgScopedDb instead of a raw database connection. As the skill file puts it: a comment is a request; a required type is a law of physics.

Almost none of this is new. Descriptive naming, precise types, comments that explain "why" — we've been telling each other this for decades, and forgiving ourselves for not doing it, because a human can always ask a colleague or check git blame. AI can't. It has your words, your types, and a search box. — Modem Engineering Blog

Finally, the edges of these conclusions. The authors list three: the skill file changes naming, file layout, types, and structure all at once, so the token savings can't be credited to naming alone; the Odysseus token numbers are only comparable within a row, not across tools; and the Odysseus test refactored just one file. One more limitation they don't mention, in our view: the skill file was distilled from the blog post itself by Fable 5, and then used to validate the blog—a bit of self-confirmation. They also point to promising data from Cursor showing that embedded retrieval on top of grep does help, so plain-text search might not be the final word. But grep, repository maps, and embedding retrieval all feed on the same input: the words in your source code.

Also on Xiaohu
Anthropic cut Claude Code's system prompt by 80% and offered six new ways to write context
✅ A Searchability Health Check for Your Code
Materials you can copy directly: the measured search-hit comparison for three names, the Chinese translation of the skill file's naming and comment rules, and a six-point pre-commit checklist. The skill file itself is MIT-licensed and ready to drop into your project.
Source
How AI reads your code (and how to write for it)Modem·Original·2026-07-20
Notes
The video and Odysseus UI screenshot are from the original post. The bug hunt grid mirrors an embedded grid in the original, redrawn by us. The search-hit bars are a table in the original; the three import-failure cards are three subsections with code blocks, both converted to graphics by us. The navigation loop diagram is ours. The six rules in Section 8 are from the write-discoverable-code skill file (fetched and translated by us), not from the blog body. The current line count for email_routes.py is from our pull of the dev branch on 2026-07-25; the blog's numbers reflect the file at publication time.
↑ End of article
✓ This article is free to read in full

Members can copy or download the full Markdown, and let their AI subscribe and ask.

Read the original ↗
📮 New explainers, straight to your inbox

Found this useful? Leave your email — every new explainer, the moment it's out. Unsubscribe anytime.