DEV Community

Cover image for Near-Duplicate Model Strings Are Quietly Changing Your Bill
Cogumellum
Cogumellum

Posted on AI-assisted

Near-Duplicate Model Strings Are Quietly Changing Your Bill

TL;DR: Gateways expose model names that look almost identical but carry different cache-read prices, so a typo in a model string can silently change what you pay. Here's a script that fetches pricing.usd.json and flags near-duplicate names with divergent prices before you ship.

The bug that doesn't throw

You're wiring up an LLM call. You open the provider's model list, copy a string, paste it into your config, and move on. The request succeeds. The response looks fine. Nothing in your logs tells you that you picked claude-fable-5 when you meant claude-fable-5-1, or grok-4.5 when the team decided on grok-4.6.

The constraint is that model identifiers are opaque strings. There is no type system for them. Your editor won't autocomplete them unless you've built a constant. Your tests won't fail on a wrong-but-valid string, because it's a valid string. And the failure mode is not an error; it's a line item on a bill that's larger than you expected, or a cache-read price that's several times what you budgeted.

This is worse on a gateway than on a single provider, because a gateway aggregates many vendors' naming conventions into one namespace. You get claude-fable-5 next to claude-fable-5-1, grok-4.5 next to grok-4.6, glm-5.2 next to glm-5.3, gemini-3.7-flash next to gemini-3.8-flash. Each pair is one character apart. Each pair can have a different price for the same token category.

The usual advice is "read the pricing page carefully." That's not a control. It's a hope. The rest of this article is about turning it into a check you can run in CI.

cache-read price per 1M tokens

What the pricebook actually contains

The source is a JSON file at https://global.beefapi.com/pricing.usd.json, read at 2026-09-19T23:31:02Z. It lists 33 models. Each entry has an input price and an output price per 1M tokens, and most have a cache-read price. Here are the entries where the naming gets dangerous, copied exactly:

Model Input ($/1M) Output ($/1M) Cache read ($/1M)
claude-fable-5 4 20 0.4
claude-fable-5-1 4 20 0.1
grok-4.5 0.6 1.8 0.09
grok-4.6 0.6 1.8 0.15
glm-5.2 0.91 2.86 0.169
glm-5.3 1 3.2 0.22
gemini-3.7-flash 0.375 1.87 0.0375
gemini-3.8-flash 0.375 1.87 0.0375
claude-opus-4-6 2 10 0.2
claude-opus-4-7 2 10 0.2
claude-opus-4-8 2 10 0.2
claude-opus-5 2 10 0.2

Look at the first two rows. claude-fable-5 and claude-fable-5-1 have identical input and output prices. If you're scanning a table for cost, they look the same. But the cache-read price is 0.4 for one and 0.1 for the other. If your workload leans on prompt caching, that difference is the whole story, and it's invisible unless you read the right column.

grok-4.5 and grok-4.6 are the same shape: identical input and output, different cache read (0.09 vs 0.15). glm-5.2 and glm-5.3 differ in every field, including cache read (0.169 vs 0.22). gemini-3.7-flash and gemini-3.8-flash happen to match on all three fields shown here, which is its own trap: you can't tell them apart from price alone, so you need another reason to prefer one.

These are prices, not performance. A lower cache-read price does not mean a faster or better model. It means cached input tokens are billed at a lower rate. The table tells you nothing about latency, throughput, or quality, because the source doesn't contain those fields.

Identical columns hide the real cost

Why "just read the docs" fails

There are three reasons a careful human still ships the wrong string.

First, the wrong string is valid. If claude-fable-5-1 is a real entry and claude-fable-5 is a real entry, both requests return 200. There's no signal that you picked the one you didn't mean.

Second, the difference is often in a column you weren't optimizing. Most developers compare input and output prices, because that's what a naive cost estimate uses. Cache-read prices only matter if you use prompt caching, and if you don't use it today, you won't look at that column. Then you add caching later, and the model string that was fine becomes expensive.

Third, the naming is not consistent across vendors in the same namespace. Some entries use a hyphen before a version suffix (claude-fable-5-1), some use a dot (glm-5.2), some use a word (gpt-5.6-sol, gpt-5.6-terra). You can't write one rule that catches every near-duplicate. You need to compare strings against each other, not against a pattern you invented.

Flag near-duplicates in CI

A script that flags the dangerous pairs

The check is: fetch the pricebook, group model names by similarity, and for each near-duplicate pair, compare the price fields. If two names are close but their prices diverge in any field, print a warning. Here's an illustrative script. It uses only the standard library plus a similarity heuristic, and it treats the pricebook as the source of truth.

# Illustrative example. Not production code.
# Field names are placeholders; inspect the actual JSON shape before relying on them.

import json
import urllib.request
from difflib import SequenceMatcher

PRICEBOOK_URL = "https://global.beefapi.com/pricing.usd.json"
SIMILARITY_THRESHOLD = 0.85


def fetch_pricebook(url):
    with urllib.request.urlopen(url) as resp:
        return json.load(resp)


def normalize(entry):
    # Adjust these keys to match the real schema in your copy of the file.
    return {
        "input": entry.get("input"),
        "output": entry.get("output"),
        "cache_read": entry.get("cache_read"),
    }


def main():
    data = fetch_pricebook(PRICEBOOK_URL)
    models = data["models"] if isinstance(data, dict) else data

    names = [m["name"] for m in models]
    prices = {m["name"]: normalize(m) for m in models}

    for i, a in enumerate(names):
        for b in names[i + 1:]:
            ratio = SequenceMatcher(None, a, b).ratio()
            if ratio < SIMILARITY_THRESHOLD:
                continue
            pa, pb = prices[a], prices[b]
            diffs = [k for k in pa if pa[k] != pb[k]]
            if diffs:
                print(f"NEAR-DUPLICATE with divergent prices: {a} vs {b}")
                for k in diffs:
                    print(f"  {k}: {pa[k]} vs {pb[k]}")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run against a pricebook shaped like the one read at 2026-09-19T23:31:02Z, this would surface pairs such as claude-fable-5 vs claude-fable-5-1 (cache_read 0.4 vs 0.1), grok-4.5 vs grok-4.6 (cache_read 0.09 vs 0.15), and glm-5.2 vs glm-5.3 (input 0.91 vs 1, output 2.86 vs 3.2, cache_read 0.169 vs 0.22). It would also flag claude-opus-4-6 vs claude-opus-4-7 and similar siblings, but those happen to agree on all three fields, so the diffs list would be empty and nothing would print. That's the point: the script only shouts when the choice has a price consequence.

The threshold is a knob. At 0.85 you catch one-character suffixes and version bumps. Lower it and you'll get noise from unrelated names that share a prefix. Higher it and you'll miss pairs like gemini-3.7-flash vs gemini-3.8-flash if you consider those too far apart, even though they're adjacent in the list.

One key, many models

Wiring it into your workflow

A script that only runs when you remember to run it is not much better than reading the docs. Three places to put it:

As a pre-commit hook. If your repo contains a file listing the model strings you use, the hook can fetch the pricebook and check that every string you reference exists, and that no two strings in your config are near-duplicates with divergent prices. That catches the case where someone adds a second model for a fallback path and picks the wrong sibling.

As a scheduled job. Prices change. The pricebook is a snapshot. A daily job that diffs today's pricebook against yesterday's, and prints any field that moved for a model you use, turns a silent billing change into a notification.

As a review artifact. When someone proposes switching a model in a pull request, the diff should include the price fields for the old and new string, side by side, including cache read. If the PR description says "switch to the cheaper model" and the cache-read column went up, the reviewer sees it.

None of this requires the gateway to do anything special. It's a property of the data being published as JSON: you can fetch it, parse it, and assert on it.

Stop guessing model strings

A worked cost comparison, as an example

Numbers below are made up to show the arithmetic, not to describe any real workload.

# Example only. Token counts are invented.
# Prices below are copied from the pricebook read at 2026-09-19T23:31:02Z.

PER_MILLION = 1_000_000

# Invented workload: 50M cached input tokens, 10M uncached input, 2M output.
cached_input_tokens = 50 * PER_MILLION
uncached_input_tokens = 10 * PER_MILLION
output_tokens = 2 * PER_MILLION


def cost(input_price, output_price, cache_read_price):
    return (
        uncached_input_tokens / PER_MILLION * input_price
        + cached_input_tokens / PER_MILLION * cache_read_price
        + output_tokens / PER_MILLION * output_price
    )


# claude-fable-5: input 4, output 20, cache read 0.4
print("claude-fable-5  ", cost(4, 20, 0.4))
# claude-fable-5-1: input 4, output 20, cache read 0.1
print("claude-fable-5-1", cost(4, 20, 0.1))
Enter fullscreen mode Exit fullscreen mode

The two calls differ only in the cache-read price. The input and output prices are identical. If you never look at the cache-read column, the two model strings look interchangeable, and the script above is the difference between noticing and not noticing.

What the data does not tell you

The pricebook is a price list. It does not contain latency, throughput, uptime, context window, or quality. It does not say how often prices change. It does not say whether a near-duplicate name is an alias, a snapshot, or a genuinely different model. It does not resolve the discrepancy between the number of entries in the file and the number of models the product profile advertises; if you need that answer, check the source directly.

So the script is a guardrail, not a decision. It tells you that two strings you might confuse have different prices. It does not tell you which one to use. That depends on what the model does for your task, which you have to measure yourself.

Questions for the comments

  1. Have you ever shipped a model string that was valid but wrong, and only found out from a bill or a cache-hit-rate dashboard? What tipped you off?
  2. Do you keep model identifiers as free strings in config, or as constants in code? If constants, how do you keep them in sync with the pricebook?
  3. For near-duplicates that agree on every price field, like claude-opus-4-6 through claude-opus-5, how do you decide which one to standardize on when price gives you no signal?
  4. Would you rather the gateway reject unknown model strings loudly, or accept any string and let the upstream decide? What breaks in each case?
  5. If you run prompt caching, do you track cache-read price separately in your cost model, or do you fold it into an average input price? What made you choose that?

Disclosure: I work on BeefAPI.

Top comments (2)

Collapse
 
mthburnsbarberweb profile image
mthburnsbarber-web

The claude-fable-5 vs claude-fable-5-1 cache-read price gap (0.4 vs 0.1) is a perfect example of why opaque model strings are such a silent killer — same input/output pricing makes them look identical in a naive cost estimate. The SequenceMatcher approach is clever because it doesn't require you to know the naming conventions in advance; you're just comparing the namespace against itself. We've started treating model identifiers like dependency versions — pinned as constants, reviewed in PRs with the price delta shown in the description. The "scheduled job diffs pricebook against yesterday's" idea is particularly underrated. Prices do shift quietly, and without that job you're only finding out at billing time.

Collapse
 
cogumellum profile image
Cogumellum AI-assisted

The pinned-constant-plus-PR-delta pattern is the right shape, and it maps cleanly onto the pricebook: the diff you want in the PR description is exactly the diffs list the script prints, cache_read included. One caveat on the scheduled job — the file is a snapshot, so "yesterday" only exists if you're storing each fetch yourself. The article doesn't say anything about a history endpoint or change feed, so I'd persist the JSON on each run and diff against your own copy rather than assume the gateway keeps one. Also worth pinning the fetch timestamp alongside the file, since a diff between two snapshots is meaningless if you can't tell when each was read.