Resolution, not substitution

    An alias should resolve to one exact dated snapshot or be refused. Silently running a different model than the one requested is a billing bug and a trust bug.

    · 6 min read · Gatewayz

    Every model API has to answer a small question on every request: which model is this? The request contains a string. Somewhere, that string becomes a specific set of weights, a specific price, and a specific behavior. The rules for turning the string into the model are rarely documented, and they matter more than they appear to.

    There are two very different ways to handle a string that does not exactly match a catalog entry. Resolution maps it to exactly one model according to a published rule, or refuses. Substitution picks something close and runs it. This article argues that an inference layer should do the first and never the second.

    Aliases and dated snapshots

    Providers publish models under two kinds of names.

    A dated snapshot identifies a fixed model version, for example anthropic/claude-sonnet-4-5-20250929 or anthropic/claude-haiku-4-5-20251001 in the Gatewayz catalog. The date is part of the identity. The provider commits that the same name will keep pointing to the same model for as long as it is offered.

    An alias is a shorter, undated name such as claude-sonnet-4-5. Aliases exist because people do not want to type dates. An alias points to a snapshot, and the provider may move it to a newer snapshot over time.

    Aliases are convenient for exploration and risky for production. A job that pinned an alias gets whatever the alias points to on the day it runs.

    What resolution means

    Resolution is a deterministic function from a requested name to exactly one catalog entry. Gatewayz applies these rules:

    1. An exact id match runs that model.
    2. An undated alias resolves to its dated snapshot. claude-sonnet-4-5 becomes the dated claude-sonnet-4-5-20250929 snapshot when that is the single match.
    3. If two snapshots could match, the request is refused. The layer does not pick "the newest", because newest is a policy decision the caller did not make, and the answer changes the day a new snapshot is added.
    4. An unknown id returns 400 model_not_found. It never falls back to a nearest match.

    The important property is that each rule either produces one answer the caller could have predicted, or produces an error. There is no branch where the layer makes a judgment call on the caller's behalf.

    What substitution looks like

    Substitution tends to arrive with good intentions. A few common forms:

    • Nearest-name matching. A request for a model that is not in the catalog runs a model with a similar name, perhaps the same family at a different size.
    • Availability fallback. The requested model is temporarily unavailable, so the request runs on a different model that is.
    • Newest-wins. An ambiguous alias picks the most recent snapshot.
    • Silent retirement mapping. A model is removed from the catalog, and requests for it are quietly redirected to its successor.

    Each of these keeps a request from failing. Each of them also means the response did not come from the model the caller asked for, and the caller may not find out.

    Why substitution is a billing bug

    Models in the same family can differ in price by a large multiple between input and output tokens and between sizes. If a request for a small model runs on a large one, the invoice is wrong relative to what the caller budgeted. If it runs the other way, the output quality is wrong relative to what the caller tested.

    Either way, cost reports stop being explainable. When a team investigates why spend changed, "the gateway decided to run a different model for some of the requests" is a hard answer to find and a harder one to accept.

    Why substitution is a trust bug

    Evaluations are only meaningful if the model under evaluation is the model in production. Teams run test suites against a specific model, tune prompts for its behavior, and approve it for a use case. A substituted model has passed none of that.

    For unattended agents the problem is sharper. An agent cannot notice that its tool calls are now formatted slightly differently, or that refusals have become more frequent. It will keep running, and the first sign of a problem may be a downstream system acting on worse output.

    There is also a simpler trust argument: if a layer will quietly change the model in some cases, callers cannot rely on the model field in any response without verifying it.

    Refusal is the useful answer

    A refusal feels unhelpful in the moment. In practice it is the least costly outcome of a naming problem:

    • It fails at the first request, not after a week of degraded output.
    • It fails with a 4xx, so SDKs do not retry and circuit breakers do not trip. See Who reads the error?.
    • It names the problem, so the fix is a one-line config change.

    If you want fallback behavior, it belongs in your code, where it is explicit and reviewable. A caller that says "try model A, then model B if A returns 5xx" has made a decision. A layer that does the same thing silently has made it for them.

    Practical guidance

    For production agents:

    • Pin dated snapshots wherever the catalog offers them. Treat alias use as a development convenience.
    • Log the model field from the response, not just the one you sent, and alert when they differ in a way your resolution rules do not explain.
    • Fetch the catalog at deploy time. The Gatewayz catalog is at https://api.gatewayz.ai/v1/models. A deploy check that verifies every configured model id exists turns a runtime 400 into a failed deploy.
    • Handle 400 model_not_found as terminal. Do not retry it and do not count it toward provider health.
    • Write fallbacks explicitly if you need them, with each step logged.

    For anyone building a layer in the middle, the rule is short: resolve by published rules, refuse on ambiguity, never guess.