Configure identity model mapping
When you want different clients requesting the same client-side model name to be routed to different upstreams, use identity-scoped model mapping: declare from → to rules on an access key or key group, and the gateway automatically rewrites the model name after the ingress gate and before routing, with the egress gate then validating the target. For example, team-a's claude-opus-5 actually goes to Tongyi Qianwen's qwen3.7-max, while clients without that rule go through the original route as usual. Rules can also route by request header: a claude-opus-5 carrying X-Tenant: vip goes to the premium line, and one without it goes to the normal line.
Entry point: Console → Access Keys (Access Keys / Key Groups tabs, only admin can edit mappings).
Ingress gate / egress gate (recurring below): two allowlist validations. The ingress gate checks whether "the model name the client requested" is in that identity's
models/load_balancersallowlist, and rejects if not; the egress gate checks whether "the model to actually route to" (including the mapping-rewritten target) is in the corresponding allowlist. A request is let through only if both gates pass. The mapping rule on this page does not itself grant permission; the target must still pass the egress gate.
How it differs from switch-route / script useModel
| Mechanism | Scope | Configuration | Trigger | Target passes access control? |
|---|---|---|---|---|
switch-route | Global | Declarative | Request body content | No (admin reroute) |
Script context.useModel() | Global | Programmatic | Script logic | No (admin reroute) |
| Identity model mapping | Per key / group | Declarative | Request identity + request header | Yes (egress gate) |
All three are "model-name swaps", but only identity mapping takes effect per tenant and its target must be in that client's permission allowlist — configuring a mapping rule is not the same as granting permission. The targets of switch-route / useModel are admin-forced reroutes, not subject to client permission (and their signatures don't take identity context).
Configuration steps
- Open the Access Keys page and edit the target access key or access key group.
- At the bottom of the form find the Model Mappings area and click Add rule.
- Each row selects two fields: Source model (
from, the name the client requests) and Target model (to, the name actually routed). Thetodropdown only lists names that this identity's egress gate is authorized to reach;fromis an input-with-suggestions dropdown — models / load balancers / aliases that actually exist in the gateway can be picked from the dropdown, while fictional access names (access labels that don't exist in the gateway but that clients habitually send) are typed in. A group-levelfromyou type is automatically merged into that group'smodelslist on save (so the ingress gate allows it); a key-levelfromnot in any of that key's groups' lists shows an inline warning "the ingress gate will reject". A rule that can be configured in the UI will always pass the egress gate at runtime. You can add multiple rows, and remove one by clicking the delete icon at the row's end. - (Optional) Click Add header condition to give a rule a
when: header name can be input or picked from a dropdown, operator picked from a dropdown, and depending on the operator decide whether to fill the condition value (input box). See "Header conditions" below. - Save. Rules hot-reload and take effect immediately, no restart needed.
Saving without touching the mapping area equals "keep existing rules"; only after you add/remove/modify within that area does it write the current list back (clearing all rows = explicitly clear the mapping). A new identity (not yet saved) has an empty dropdown — save the identity and its model / load balancer allowlist first, then come back to configure mapping, and the dropdown will populate.
Wildcards
from and to each have at most one *. from's * captures a middle segment, and to's * fills it back in:
claude-opus-5 → qwen3.7-max: exact match, whole-name replacement.claude-* → qwen-*:claude-opus-5becomesqwen-opus-5,claude-sonnet-4becomesqwen-sonnet-4.- When
tocontains*,frommust contain*; neitherfromnortocan have two*. Otherwise saving reports 422.
fromaccepts any input (a specific name, a wildcard pattern likeclaude-*, a fictional access name);tocan only be picked from the dropdown (a specific name this identity's egress gate is authorized to reach, no wildcard). If you need atowith a wildcard, configure it directly via config file / API — it takes effect and displays normally, but cannot be created in the console'stodropdown.
Two wildcard dialects (mind the difference):
Where written Meaning of *Example Mapping rule fromMiddle-segment capture claude-*-5matchesclaude-opus-5(capturesopus)Key group allowlist models/load_balancersExact name, "*"select-all, trailing-*prefix match; middle*unsupportedclaude-*matches everything starting withclaude-;claude-*-5is only treated literallySo for a
from = "claude-*-5"rule to pass the ingress gate, the allowlist must contain the specific name (e.g.claude-opus-5) or a trailing wildcard (claude-*). Writingclaude-*-5in the allowlist is treated literally and only exactly matches that string. This is fail-closed: afromname that can't be written into the allowlist 403s at the ingress gate.
Header conditions (route by request header)
Add a when to a rule to make the same from go to different targets by request header. Pick one of five operators:
| Operator | Semantics | Fill condition value? |
|---|---|---|
exact | Header value exactly equals | Yes |
prefix | Header value starts with this prefix | Yes |
regex | Header value matches the regex | Yes |
exists | The header exists (value ignored) | No |
absent | The header doesn't exist | No |
Header names are case-insensitive, condition values are case-sensitive. When the condition isn't satisfied, skip this rule and keep looking for the next from-matching rule in the same bucket — so put conditional rules first and unconditional rules as fallback:
1. claude-opus-5 → glm-4.6 when X-Tenant exact vip # conditional specific rule first
2. claude-opus-5 → qwen3.7-max # unconditional fallbackA request carrying X-Tenant: vip hits rule 1 → glm-4.6; one without it skips rule 1 and hits rule 2 → qwen3.7-max.
The header name is an input-with-suggestions dropdown: built-in common headers ∪ headers used by existing request-header access rules ∪ headers this identity has used, as candidate suggestions, and you can also directly type any header name not in the candidates. At runtime there is no allowlist on header names — when a header referenced by a rule is missing in the request, that rule is skipped (no 403), continuing to the next rule in the same bucket.
whentargets the request headers carried at the moment the request is made. A few reverse-inference landings (DashScope async task status queryGET /v1/services/{model}/tasks/{task_id}) cannot obtain the original request headers at the mapping point and pass an empty query — in that case only unconditional rules match, and conditional rules are skipped. This is fail-closed: if the only matching rule carries awhenthat isn't satisfied, the model name isn't rewritten and is resolved by the original name (possibly 404). The egress gate runs as usual.
Priority
Key-level rules > group-level rules (in the order of that access key's groups list) > passthrough. The access key's own rules are looked at first; if none match, each of its groups' rules are tried in order; if none match, the model name is unchanged. Within a bucket, order wins: the first from-matching rule whose header condition is satisfied wins (first-match-wins), so conditional rules must come before unconditional rules.
The order between groups = the order of that access key's groups list, and this order is constant between two saves. When multiple groups configure a mapping for the same source name (from), the group earlier in the list wins — the gateway does not merge all groups' rules into one big table, but tries group by group, returning on the first group that produces a match, and doesn't look at later groups at all. Because it is "group-by-group serial, first-matching group returns", group order beats within-bucket order: e.g. group A's rules are [m→X (vip only), m→Y (unconditional)] and group B's is [m→Z (unconditional)]; when the request carries no vip, the result is Y, not Z (A skips its first rule, hits its second, the whole group returns, and B never gets a turn).
This order is deterministic when the configuration is unchanged (every request hits the same group). But currently there is no conflict validation or warning for "multiple groups (or key vs group) configured rules for the same
from" — who takes effect is entirely determined by the implicit convention of the groups-list order. When you need determinism, the safest approach is to configure a rule for the samefromin only one place (or directly on the access key — key-level always comes first and is clearest); if it must be spread across multiple groups, put the group that should win earlier in that key's groups list.
Three operational contracts you must know
The mapping target must be in the allowlist, and in the list of the correct kind. The egress gate determines the kind of the real target name inside the mapping primitive: a target that is an ordinary model is checked against the group's
models; a target that is a load balancer is checked against the group'sload_balancers. A target not in the corresponding list → 403. So when configuringclaude-opus-5 → my-lb(wheremy-lbis a load balancer), writemy-lbinto the group'sload_balancers.The source name (
from) must be in the allowlist. The ingress gate checks, for names that exist in the routing table, the corresponding list by kind (load_balancers = ["*"]only allows load-balancer names, not ordinary model names); for fictional client names (access labels that don't exist in the gateway) it takes the union of the two lists as fallback, so a fictional name can be written into eithermodelsorload_balancers. When configuring a mapping rule at the group level, a fictionalfromyou type is automatically merged into that group'smodelslist on save (so the ingress gate allows it); when configuring at the key level, the key has no list of its own, so a fictionalfrommust be manually written into one of that key's groups'modelsorload_balancers(either list works), and the console warns whenfromis not in any owning group's list. This is an explicit authorization action, not a bypass.The
/v1/modelslist does not reflect the mapping. The list only lists models that actually exist in the configuration; a mapping's source name (e.g.claude-opus-5,gpt-4o) is usually not among them, so you won't see it in the dropdown — but a client directly requesting that name succeeds. This is expected behavior.
Which name do statistics and billing see?
The final, post-mapping model name. The mapping is fully transparent to statistics and billing — the logs, statistics page, and bill record the target model (e.g. qwen3.7-max), and a source-name row never appears. If you need per-client-source-name attribution, record it on the client side yourself.
How to confirm the mapping took effect in logs
Look at the MM tag in the logs page's model column: a request that hit a mapping rule gets MM before its model name, and hovering shows original → mapped (e.g. gpt-4o → glm-4.6), with the same mapping-source entry visible in the log details page's Overview area. No need to dig through the request body — the tag is the proof. When the mapping target is a load-balancer name, the same row carries both the MM and LB tags; when also rerouted by a script's useModel, both MM and SW appear.
Troubleshooting
- Save reports 422 "invalid model mapping rule": check whether the row's from/to is empty, whether
fromhas more than one*, whethertohas*whilefromdoesn't; if it carries awhen, check whether the header name is empty, whetherexact/prefix/regexfilled a value, whetherexists/absentwrongly filled a value, and whether theregexcompiles. - Request 403: either the source name (
from) isn't in any of that identity's lists (ingress gate rejects), or the target (to) isn't in the list of the corresponding kind (egress gate rejects). The 403 error body writes the client's original name. - Request 404 but the server warned the real target name: the mapping target model / load balancer doesn't exist or is disabled; the message returned to the client is redacted back to the source name.
- Header condition didn't route: confirm the conditional rule is ordered before the unconditional rule; confirm the client actually carried that header with a matching value.
- Mapping didn't take effect: confirm the rule is configured on the correct identity (key-level vs group-level) and that the client actually authenticated as that identity; being overridden by a higher-priority rule also makes it "look like it didn't take effect".
Next: Access key & key group fields for the model_mappings field definition and the allowlist lists; Access control design for the overall allowlist access-control approach; Logs viewer for how to read the MM / SW / LB tags.
