← All posts
Mechanics5 min read

Dated model IDs, and the rates that don't resolve

The model id on the response is routinely not the id you requested — and a pricing engine has to resolve exactly the right aliases while refusing the tempting ones, or it fails closed on rates it has.

An OpenAI call priced to null on our dashboard with the reason "we do not have a rate for this model". The model was gpt-4o-mini-2024-07-18. Our rate table has gpt-4o-mini. Nothing was missing — the rate was sitting right there, under a spelling one date-suffix away, and the ledger was saying "insufficient data" about data we had.

That bug is small. The mechanism behind it is not, and it bites anyone who prices, routes, or aggregates by model id: the id on the response is routinely not the id you requested.

Why the ids drift

You ask for gpt-4o-mini; OpenAI resolves it to the dated snapshot gpt-4o-mini-2024-07-18 and echoes the dated form back. You ask Mistral for -latest; it answers with a dated build. Anthropic ships some models under a dated id (claude-haiku-4-5-20251001) and echoes that. Vertex dates with an @ (claude-sonnet-4-5@20250929) — and only for models before 4.6, after which ids go dateless. Three providers, three date grammars, and none of them matches the others:

OpenAI     gpt-4o-mini-2024-07-18          hyphenated ISO
Anthropic  claude-haiku-4-5-20251001       compact, hyphen-joined
Vertex     claude-sonnet-4-5@20250929      compact, @-joined

Which is how our canonicaliser failed: it stripped -\d{8}$ — written for Anthropic's compact form — and OpenAI's hyphenated -2024-07-18 sailed through untouched. A canonicaliser written against one provider's date format silently does nothing on another's, and "silently does nothing" here means a fail-closed alert firing on a keying mismatch instead of a missing rate. That inverts the entire point of failing closed: the alert becomes noise, the number is absent for no reason, and the dashboard under-reports real spend. "Insufficient data" is only honest when the data is genuinely insufficient.

The three transformations to refuse

The tempting fix is aggressive normalisation — strip anything date-shaped, trim prefixes, fuzzy-match the rest. Each of those converts this bug into a worse one, because a wrong rate is worse than a missing rate. Three transformations a pricing engine must refuse:

Bare 4-digit suffixes. deepseek-v4-pro-0813 on Together and Fireworks carries -0813 as part of the model id, not as a date decoration. Strip anything that looks date-ish and you fold a distinct model onto a different row's price. Only a full calendar date, in a grammar you have positively identified — strict on year, month and day, so -1234-56-78 is not a date — may come off.

Vendor and fine-tune path prefixes. accounts/yourco/models/your-tune is your fine-tune. Trim the path to "find" the base model and you price a customer's custom model at the base rate — a fabricated number wearing a real model's name.

-latest pointers. mistral-large-latest points at whatever Mistral says it points at today, and that answer changes without notice. Resolving it yourself, from memory, prices today's traffic at whatever the pointer meant when someone last looked. If the provider echoes the resolved dated id, price that; a -latest you cannot resolve from the response is insufficient data, honestly.

What survives is a short whitelist: exact match always wins; then the three positively identified date grammars; then aliases published by the provider as facts (OpenAI documents gpt-5.6 as a pointer to gpt-5.6-sol — that is data, not pattern-matching); then, last and weakest, case. And every resolved rate records which row answered, so a price that came through an alias is auditable rather than indistinguishable from an exact hit.

Make the failure actionable, then leave it closed

The second half of the fix is about the day the whitelist misses — a new namespace, a grammar we have not met. The call still fails closed, but the refusal now names the nearest known ids: "unknown gpt-4o-mini-2024-07-18; nearest on openai: gpt-4o-mini, …". With the miss sitting directly under its match, the diagnosis takes minutes instead of an inspection. A fail-closed system earns its keep exactly to the degree its refusals are actionable; a bare "insufficient data" trains people to ignore the one alert that matters. The rendered result of each refusal is the dash the dashboard shows instead of a number — the argument for which is its own post.

One aggregation note before the checklist, because ids are identity as well as price: the same dated/undated drift that broke pricing also splits grouping. A dashboard keyed on raw response ids shows gpt-4o-mini and gpt-4o-mini-2024-07-18 as two models with two costs — and on the cache side the stakes are higher still, since the same display name on two platforms is genuinely two different products with two separate caches that must not be folded together. Canonicalise for pricing, group by platform-qualified identity, and never let a display label be a join key. Which models resolve, on which platforms, is what the rate table behind Vigil pins per row.

What to do

Take one day of response traffic and diff the model ids you requested against the ids on the responses — anywhere they differ, your pricing and your grouping are exercising an alias path, deliberate or accidental. Check your canonicaliser against all three date grammars, not just your main provider's. And grep your pipeline for the three refusals: nothing should strip a bare 4-digit suffix, trim a fine-tune path, or resolve -latest from memory. If a model legitimately has two names, encode the alias as data with a source, never as a regex that will match the next thing it was not written for.