cd /news/ai-infrastructure/near-duplicate-model-strings-are-qui… · home topics ai-infrastructure article
[ARTICLE · art-135246] src=dev.to ↗ pub= topic=ai-infrastructure verified=true sentiment=· neutral

Near-Duplicate Model Strings Are Quietly Changing Your Bill

A developer has published a CI-runnable script that fetches a gateway's pricing.usd.json file and flags near-duplicate model identifiers whose cache-read prices diverge, after finding that strings like claude-fable-5 and claude-fable-5-1 share identical input and output rates but differ sharply on cached input (0.4 vs 0.1 per 1M tokens). The writeup argues that because model names are opaque, untyped strings, a single-character typo returns a valid 200 response and only surfaces later as an unexpected line item on a bill. The script was demonstrated against a 33-model pricing snapshot read at 2026-09-19T23:31:02Z.

by read8 min views1 publishedSep 20, 2026

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.

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.

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.

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.

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.


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):
    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()

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.

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.

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


PER_MILLION = 1_000_000

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
    )

print("claude-fable-5  ", cost(4, 20, 0.4))
print("claude-fable-5-1", cost(4, 20, 0.1))

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.

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.

claude-opus-4-6 through claude-opus-5, how do you decide which one to standardize on when price gives you no signal? Disclosure: I work on BeefAPI.

── more in #ai-infrastructure 4 stories · sorted by recency
── more on @claude-fable-5 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/near-duplicate-model…] indexed:0 read:8min 2026-09-20 ·