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.
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.
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 (cacheread 0.4 vs 0.1), grok-4.5 vs grok-4.6 (cacheread 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.
