{"slug": "near-duplicate-model-strings-are-quietly-changing-your-bill", "title": "Near-Duplicate Model Strings Are Quietly Changing Your Bill", "summary": "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.", "body_md": "**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.\n\nYou'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`.\n\nThe 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.\n\nThis 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.\n\nThe 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.\n\nThe 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:\n\n| Model | Input ($/1M) | Output ($/1M) | Cache read ($/1M) | \n|---|---|---|---|\n| `claude-fable-5` | 4 | 20 | 0.4 | \n| `claude-fable-5-1` | 4 | 20 | 0.1 | \n| `grok-4.5` | 0.6 | 1.8 | 0.09 | \n| `grok-4.6` | 0.6 | 1.8 | 0.15 | \n| `glm-5.2` | 0.91 | 2.86 | 0.169 | \n| `glm-5.3` | 1 | 3.2 | 0.22 | \n| `gemini-3.7-flash` | 0.375 | 1.87 | 0.0375 | \n| `gemini-3.8-flash` | 0.375 | 1.87 | 0.0375 | \n| `claude-opus-4-6` | 2 | 10 | 0.2 | \n| `claude-opus-4-7` | 2 | 10 | 0.2 | \n| `claude-opus-4-8` | 2 | 10 | 0.2 | \n| `claude-opus-5` | 2 | 10 | 0.2 | \n\nLook 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.\n\n`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.\n\nThese 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.\n\nThere are three reasons a careful human still ships the wrong string.\n\nFirst, 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.\n\nSecond, 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.\n\nThird, 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.\n\nThe 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.\n\n```\n# Illustrative example. Not production code.\n# Field names are placeholders; inspect the actual JSON shape before relying on them.\n\nimport json\nimport urllib.request\nfrom difflib import SequenceMatcher\n\nPRICEBOOK_URL = \"https://global.beefapi.com/pricing.usd.json\"\nSIMILARITY_THRESHOLD = 0.85\n\ndef fetch_pricebook(url):\n    with urllib.request.urlopen(url) as resp:\n        return json.load(resp)\n\ndef normalize(entry):\n    # Adjust these keys to match the real schema in your copy of the file.\n    return {\n        \"input\": entry.get(\"input\"),\n        \"output\": entry.get(\"output\"),\n        \"cache_read\": entry.get(\"cache_read\"),\n    }\n\ndef main():\n    data = fetch_pricebook(PRICEBOOK_URL)\n    models = data[\"models\"] if isinstance(data, dict) else data\n\n    names = [m[\"name\"] for m in models]\n    prices = {m[\"name\"]: normalize(m) for m in models}\n\n    for i, a in enumerate(names):\n        for b in names[i + 1:]:\n            ratio = SequenceMatcher(None, a, b).ratio()\n            if ratio < SIMILARITY_THRESHOLD:\n                continue\n            pa, pb = prices[a], prices[b]\n            diffs = [k for k in pa if pa[k] != pb[k]]\n            if diffs:\n                print(f\"NEAR-DUPLICATE with divergent prices: {a} vs {b}\")\n                for k in diffs:\n                    print(f\"  {k}: {pa[k]} vs {pb[k]}\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\nRun 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.\n\nThe 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.\n\nA script that only runs when you remember to run it is not much better than reading the docs. Three places to put it:\n\n**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.\n\n**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.\n\n**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.\n\nNone 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.\n\nNumbers below are made up to show the arithmetic, not to describe any real workload.\n\n```\n# Example only. Token counts are invented.\n# Prices below are copied from the pricebook read at 2026-09-19T23:31:02Z.\n\nPER_MILLION = 1_000_000\n\n# Invented workload: 50M cached input tokens, 10M uncached input, 2M output.\ncached_input_tokens = 50 * PER_MILLION\nuncached_input_tokens = 10 * PER_MILLION\noutput_tokens = 2 * PER_MILLION\n\ndef cost(input_price, output_price, cache_read_price):\n    return (\n        uncached_input_tokens / PER_MILLION * input_price\n        + cached_input_tokens / PER_MILLION * cache_read_price\n        + output_tokens / PER_MILLION * output_price\n    )\n\n# claude-fable-5: input 4, output 20, cache read 0.4\nprint(\"claude-fable-5  \", cost(4, 20, 0.4))\n# claude-fable-5-1: input 4, output 20, cache read 0.1\nprint(\"claude-fable-5-1\", cost(4, 20, 0.1))\n```\n\nThe 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.\n\nThe 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.\n\nSo 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.\n\n`claude-opus-4-6` through `claude-opus-5`, how do you decide which one to standardize on when price gives you no signal?\n*Disclosure: I work on BeefAPI.*", "url": "https://wpnews.pro/news/near-duplicate-model-strings-are-quietly-changing-your-bill", "canonical_source": "https://dev.to/cogumellum/near-duplicate-model-strings-are-quietly-changing-your-bill-3k1n", "published_at": "2026-09-20 17:22:19+00:00", "updated_at": "2026-09-20 17:54:38.238968+00:00", "lang": "en", "topics": ["ai-infrastructure", "ai-tools", "large-language-models", "mlops", "developer-tools"], "entities": ["claude-fable-5", "claude-fable-5-1", "grok-4.5", "grok-4.6", "glm-5.2", "glm-5.3", "gemini-3.7-flash", "beefapi"], "alternates": {"html": "https://wpnews.pro/news/near-duplicate-model-strings-are-quietly-changing-your-bill", "markdown": "https://wpnews.pro/news/near-duplicate-model-strings-are-quietly-changing-your-bill.md", "text": "https://wpnews.pro/news/near-duplicate-model-strings-are-quietly-changing-your-bill.txt", "jsonld": "https://wpnews.pro/news/near-duplicate-model-strings-are-quietly-changing-your-bill.jsonld"}}