> Raw Markdown twin (generated at build time from the source Markdown). Rendered page: https://docs.gatellm.io/en/reference/access-keys-groups-fields · Doc index: https://docs.gatellm.io/en/llms.txt


# Access key and key group fields

GateLLM has two kinds of "users"; be sure to distinguish them:

- **Console user (ConsoleUser)**: the admin account that logs into the console, with the role admin or monitor (`normal_user` is not a console login role). UI operations are in [Console user management](/en/console/user-management.md).
- **Access key (AccessKey)**: the credential a caller presents when calling the API (the key inside `Bearer <key>`). This chapter covers its fields.

The two are fully independent: console users govern "who can enter the console", access keys govern "who can call the API".

## Access keys and key groups (Access Keys)

Entry: Console → **Access keys**. This page has three tabs: **Access Keys**, **Key groups**, and **Access rules** (Header ACL, fields in [Header ACL rule fields](/en/reference/header-acl-rules.md)).

### Relation

```text
Console user ──created_by──→ access key ──group──→ key group ──┬→ models[]         (which models it can call)
                                                               ├→ load_balancers[] (which load balancers it can call)
                                                               └→ mcp_tool_acl     (which MCP tools it can call)
```

- An access key belongs to one or more **key groups** (the `groups` field, multi-select in the form; with multiple groups, each dimension takes the union and cost quota takes the widest — see "Cost quota").
- A key group's permissions split into three **mutually independent** dimensions — models, load balancers, MCP tools — each with its own "all" switch, none implying the others:
  - **Model list** decides which models are accessible: empty = no model access; `["*"]` = all models; specific model names = only those listed (exact match on model name, not alias).
  - **Load balancer list** decides which load balancers are accessible: empty = no load balancer access; `["*"]` = all load balancers; specific names = only those listed (exact match on load balancer name, not alias).
  - Checking "all models" does **not** grant load-balancer permission, and vice versa — the two dimensions are checked separately.
  - **A load balancer is an authorization unit**: once a load balancer is granted, the gateway no longer validates the members (entries) configured inside that load balancer one-by-one against the "model list" — keys in that group can reach **every** member of that load balancer, even if the "model list" is empty or doesn't include those models. Putting a sensitive or expensive model into a load balancer's members effectively opens it to all keys holding that load balancer's permission; to strictly limit a key to a single model, configure it as an **ordinary model** and add it to the "model list" instead of putting it inside an authorized load balancer.
- A key with no group = no access to any model or load balancer.

### Managing access keys

| Operation | How | Description |
|------|--------|------|
| Create | "Access Keys" tab → New | Fill name, generate or manually enter the API key, select groups, enable |
| View full key | "Copy" button in the list | Calls `/access-keys/{name}/api-key` to fetch the full value (the list only shows a mask) |
| Rotate | Edit → "Rotate" to the right of the API key | After confirming, a new value replaces the old, which becomes invalid immediately; the new key is shown only once in the success dialog — copy it immediately (other attributes unchanged) |
| Revoke | Edit → uncheck "Enabled" | Marked revoked, no longer effective, record retained |
| Delete | List → Delete | Fully removed (prefer "Revoke" to preserve audit) |

### Access-key fields

| Field | Description | Default |
|------|------|--------|
| Name | Key identifier, for management and audit; read-only on edit | (required) |
| API key | The credential the client actually presents; the list shows a mask (first/last 4 chars, e.g. `sk-a***xxxx`) | auto-generated by clicking "Generate" |
| Group | The owning key group (multi-select, can belong to multiple groups; with multiple groups, model/LB/MCP permissions take the union), determines model/MCP permission | no group (= no access to any model) |
| Tags | Free-form organization tags, multiple allowed; used for console filtering and search (the search box also matches tags), pure annotation with no permission effect. Select from historical tags or type a new one | empty |
| Anonymous | When checked, this key is the anonymous fallback (see below), and the API key field is emptied | `false` |
| Enabled | Unchecking revokes it | `true` |
| Created by | Which console user created it | the currently logged-in console user |
| `acl_default_action` | `allow` / `deny`, the per-key fallback action when no rule matches | unset (falls back to the group, then to global Allow) |
| Model mapping (model_mappings) | `from → to` rewrite rules specific to this key, can carry `when` request-header conditions for branching; higher priority than the owning key group's rules. Mapping targets must pass the egress gate (looked up in the corresponding list by real kind: ordinary models in `models`, load balancers in `load_balancers`), and source names must be in the corresponding list (the entry gate looks up real names by kind, and falls back to union for fictional names). See [Configure identity model mapping](/en/howto/configure-identity-model-mapping.md) | empty (no mapping) |
| Monthly quota (monthly_quota) | This key's monthly (UTC natural month) cost cap (USD amount). Empty = inherit lower level; a value must be > 0. See "Cost quota" below | empty (inherit lower level) |
| Daily quota (daily_quota) | This key's daily (UTC natural day) cost cap (USD amount). Same semantics as monthly, judged independently | empty (inherit lower level) |

> Access keys are viewed and managed by admin in the console (`normal_user` doesn't log into the console; it only holds/uses an API key). After a console user is deleted, the keys it created have `created_by` cleared and become legacy keys (visible only to admin).

### Managing key groups

| Operation | How | Description |
|------|--------|------|
| Create | "Key groups" tab → New | Fill name, select models, select load balancers, configure MCP tool ACL |
| Edit | List → Edit | Four sub-tabs: Models / Load balancers / MCP / Model mappings |
| Delete | List → Delete | If keys reference this group, change their group first |

### Key-group fields

| Field | Description | Default |
|------|--------|------|
| Name | Group identifier | (required) |
| Models | The accessible model list; displayed grouped by upstream, "All Models" = `*` select-all. Independent of the load-balancer dimension | empty (no permission) |
| Load balancers | The accessible load balancer list; "All Load Balancers" = `*` select-all. "All Models" does not include load balancers, which must be checked separately here | empty (no permission) |
| MCP | `allowed_tools` / `denied_tools` / `allowed_tags`, controlling which MCP tools are callable (see [MCP configuration](/en/reference/mcp-config.md)) | empty |
| `acl_default_action` | `allow` / `deny`, the per-group fallback action when no rule matches | unset (falls back to global Allow) |
| Model mapping (model_mappings) | `from → to` rewrite rules shared by this key group (can carry `when` request-header conditions), applied to all keys in the group; lower priority than the key's own rules, higher than passthrough. Mapping targets must pass the egress gate (looked up in the corresponding list by real kind), and source names must be in the corresponding list (entry gate looks up real names by kind, falls back to union for fictional names). See [Configure identity model mapping](/en/howto/configure-identity-model-mapping.md) | empty (no mapping) |
| Monthly quota (monthly_quota) | The monthly (UTC natural month) cost cap (USD amount) each key in the group inherits. **Not a shared ledger** — it's a per-key cap for each key in the group. Empty = this group doesn't participate in folding (not treated as infinite); a value must be > 0 | empty (doesn't participate in folding) |
| Daily quota (daily_quota) | The daily (UTC natural day) cost cap each key in the group inherits. Same semantics as monthly, judged independently | empty (doesn't participate in folding) |

## Anonymous access keys

Set an access key's "Anonymous" = true (globally only one can exist):

- When a client request carries **no** `Authorization` / `x-api-key` / `x-goog-api-key` header, it matches this anonymous key.
- The anonymous key still needs a group to gain model access; otherwise it can "enter but call nothing".
- Requests with an **invalid** key also fall to the anonymous branch (equivalent to no key).

## Cost quota

The cost quota limits each access key's monetary consumption. Quotas are uniformly enforced in **USD** — a system-level single exchange-rate source `FxCache` (exposed via the `/fx-rates` API, with three-level fallback: operator config override > Frankfurter ECB online > static fallback table) converts each currency's spend into USD and accumulates. There are three config layers, all in the Console **Access keys** page and the **Billing → Quota** tab:

- **per-key**: fill monthly / daily caps in the access key form's "Cost quota" area.
- **per-group**: fill in the key group form — semantics are "the cap each key in the group inherits", **not a shared ledger for the group**.
- **global default**: edit `monthly_limit` / `daily_limit` in the **Billing → Quota** tab, applying to keys that have no self-set limit.

### Three-layer priority

Each period resolves the effective limit independently:

```
per-key || max(caps of member groups that set a limit, groups without a limit don't participate) || global default
```

- **Monthly and daily are two independent periods**, each taking its own value and judging independently; hitting either rejects the key's subsequent requests (429).
- **Group-limit semantics = the per-key cap each key in the group inherits** (not a shared ledger). A key in multiple groups where several groups set limits takes the **widest (max)** — e.g. team-a=$100, team-b=$500, a key in both groups → effective $500.
- **Member groups without a limit don't participate in folding** (not treated as infinite) — otherwise a limited key could become infinite just by joining an unlimited group.
- All member groups unset → falls back to the global default; global also unset → unlimited for that period.
- All three layers unset → fully unlimited.

### Value and period boundaries

- Values: empty / `null` = inherit the lower level (key→group→global), **not** "this window is unlimited"; a value must be > 0 (`0` and negatives are rejected on write, with frontend sync validation). To express "a window is unlimited", set a limit at a higher level and leave this level empty — or accept the trade-off that three layers can't express "one window unlimited while other windows limited".
- Period boundaries: **UTC natural month** (resets on the 1st at 00:00 UTC) and **UTC natural day** (resets at 00:00 UTC each day). Consistent with statistics/billing.
- The limit dimension is **USD amount**: `record_cost` looks up `FxCache` by `pricing.currency`, converts to USD micro-units, and accumulates inflight; `refresh_once` reads `cost_breakdown` (per-currency) and sums each multiplied by its exchange rate. The frontend billing page's `≈USD` and quota enforcement share the same backend exchange-rate source. Excel export still shows per-currency columns (no cross-currency summing, unchanged).

### 429 and soft limits

- Hitting any period returns **429**, error type `quota_exceeded`, with the message indicating whether it's the monthly or daily window, the limit and used amount, and the reset time.
- **Only the daily window** carries a `Retry-After` header (≤86400s); monthly can reach ~31 days, which is meaningless to clients and may be mis-handled, so the message already includes the reset time and no such header is sent.
- Soft limits: usage = the TTL-cached DB period total spend (USD; `refresh_once` reads `cost_breakdown` per-currency and sums each multiplied by its rate) + the in-process inflight accumulation (USD, `record_cost` sync-converted), refreshed about every 30s (daily) / 5min (monthly); a minute-level overrun window is accepted. In multi-instance (PG mode), the DB snapshot is shared while inflight is per-instance local, so cross-instance overrun is also slight — all within the soft-limit semantics. ECB daily-frequency rates; models of the same currency convert at the day's rate all day; on a missing rate, fail-safe under-counts (skips accumulating that request, doesn't falsely reject).

### Notes

- **Anonymous key**: anonymous traffic is already merged into a single `"anonymous"` bucket in statistics, and all anonymous traffic shares one quota (the global default applies to anonymous).
- **Renaming a key**: usage aggregates by name, so after renaming a key its old usage can't be associated with the new name (consistent with existing statistics semantics).
- **`count_tokens` exemption**: not billed, not judged for quota.
- **Model try-me (tryme)**: doesn't enter the admission-check seam; consumption is recorded in the SystemDiagnostics fake bucket and never counts toward any real key's quota.
- **Unpriced models** (no pricing): cost 0, doesn't consume quota (consistent with billing).

## Relation to load balancers

The load balancer is an authorization dimension in the key group independent of models: the key group's `load_balancers` list must include the load balancer's name (or `"*"`) for a key to call through the LB. The `models` dimension only governs ordinary models; "all models" does not additionally grant load balancers:

```json
{
  "name": "full-access",
  "models": ["*"],
  "load_balancers": ["gpt-4o-ha"]
}
```

Console → Access keys → Key groups → Edit → the "Load balancers" sub-tab to check the corresponding LB. See [Load-balancing fields](/en/reference/load-balancing-fields.md).

## FAQ

**Q: The client gets 403 model_access_denied, but the key is right.**
The key's key group "Model" list doesn't include the requested model. Go to "Key groups", add that model, or change it to `*`. Calling a **load balancer** and getting 403 is the same — the load balancer is an independent dimension; check the group's "Load balancers" list ("All Models" does not additionally grant load balancers).

**Q: How do I restrict a key to only a few models?**
Create a new key group whose model list only contains those, and point the key's group at it.

**Q: The access-key list only shows `sk-a***xxxx` — how do I get the full value?**
Click that row's "Copy" button; it calls an API to fetch the full key and copy it to the clipboard. Anonymous keys don't support fetching the full value.

**Q: Who can view/manage access keys in the console?**
Access keys are viewed and managed by admin in the console. `normal_user` doesn't log into the console — it only holds/uses an API key (the credential presented when calling the API).

**Q: I want open, login-free access for internal services.**
Configure an anonymous access key (anonymous=true) and give it a permission-restricted key group. Note: anonymous means anyone who can reach the gateway can use it — only for trusted internal networks.

**Q: How do an access key's `acl_default_action` and a key group's `acl_default_action` work?**
They are the fallback action when no Header ACL rule matches. See [Header ACL rule fields](/en/reference/header-acl-rules.md).

**Next**: [Header ACL rule fields](/en/reference/header-acl-rules.md) for the full Header ACL rule field table; [Configure Header ACL access rules](/en/howto/configure-header-acl.md) for steps and common examples; [Upstream and model fields](/en/reference/upstreams-models-fields.md) for model configuration; [Load-balancing fields](/en/reference/load-balancing-fields.md) for the LB–key-group relation; [Configure identity model mapping](/en/howto/configure-identity-model-mapping.md) for the steps and operational contract of rewriting model names by key / group.
