Virtual keys
A virtual key is a revocable virt_ token that lets an agent call the LLM proxy without receiving an upstream credential. The token is shown once. AIControls stores only its hash.
Credential source
Choose exactly one source:
- Upstream (
upstreamRef) reuses the referenced upstream's URL, provider, and credential. AIControls fails closed if the upstream is missing or is a keyless caller-passthrough upstream. An upstream reference is pinned and does not fail over to another billing identity. - Provider key (
providerKeyId) keeps the agent credential separate. This mode remains compatible with existing virtual keys. Make sure a routable upstream uses the same provider.
A link to /settings?section=agents&tab=virtual-keys&create=1&upstream=<name> opens the issue form with that upstream preselected, when it holds a credential to lend. The setup wizard's Agent outside Kubernetes? link uses it with the model you connected.
Use virtual keys through the OpenAI-compatible or Anthropic Messages endpoints. Provider-native Bedrock and Vertex routes reject virtual keys because those routes use organization-managed cloud signing rather than virtual-key credential substitution.
Identity fields
agentId is required. When it matches a known Kubernetes service account, choose that identity's namespace and clusterId. These two fields must be supplied together. The issue form shows Converges with k8s://<namespace>/<service-account> only when the agent, namespace, and cluster all match. The displayed identity remains stable, while audit, budget, session, and usage attribution is qualified by the cluster's canonical ID so identical service-account names in different clusters stay separate.
clusterId is the UID of the cluster's kube-system namespace. When you enter a namespace and no known workload decides the cluster, the issue form fills in the cluster that AIControls itself runs in. Outside a cluster, or before AIControls has resolved its own cluster, the field stays empty. To look the UID up yourself:
kubectl get namespace kube-system -o jsonpath='{.metadata.uid}'
GET /api/v1/virtual-keys/issue-defaults returns the same default as {"clusterId": "<uid>"}, with an empty string when there is none. It requires an admin.
During an upgrade, AIControls leaves historical records without cluster evidence unqualified instead of assigning them to the first cluster that starts. Existing budget-window usage remains a conservative baseline for every matching cluster-qualified identity until that window expires; ambiguous audit and session history is not shown as belonging to a specific cluster.
Keys retired for a missing cluster ID
A namespace-scoped virtual key with no clusterId can't be matched to a cluster, so it can't authenticate. When AIControls starts, and again when it first resolves its cluster after a degraded start, it expires every such key that is still active with the cause no_cluster_id. A key whose TTL has already passed, or that has already reached its spend cap, expires with that cause instead (ttl or budget). Agents that use one of these keys stop working until you reissue it.
AIControls reports the retirement in three places:
- Logs. A warning with the number of keys, for example
expired 2 namespace-scoped virtual keys without a cluster ID; reissue them. A start that retires no keys logs nothing. - Virtual Keys page. The key's status is Expired · no cluster ID (retired by upgrade).
- Agent responses. An LLM call with the key returns
401withvirtual key retired: issued without cluster ID; reissue with clusterId. An MCP call returns401withinvalid virtual key.
To reissue a key, open the row's actions menu on the Virtual Keys page and choose Reissue for cluster…. The issue form opens with the old key's label, agent, namespace, credential source, allowed models, spend cap, and budget period, plus the cluster ID. A key that had an expiry gets one too: the time it had left, or its original lifetime if that already ran out. Issue the key, then give the new token to the agent. The old key stays expired; revoke it once nothing uses it.
Limits and lifecycle
| Field | Meaning |
|---|---|
allowedModels | Exact models the key can use. An empty list allows every model available through its credential source. |
expiresAt | Optional terminal TTL. A TTL-expired key never reactivates. |
spendCapUsd | Optional USD cap for the current budget period. Spend is recorded after each response, so requests already in flight may complete after the cap is reached. |
budgetPeriod | none, daily, or monthly. budgetWindow and budget_window are accepted compatibility aliases. |
spentUsd | Spend charged in the current period. |
expirationCause | Why an expired key stopped working; see Expiration causes. Revocation is represented by status=revoked. |
Daily periods start at 00:00 UTC. Monthly periods start at 00:00 UTC on the first day of the month. On the first request in a new period, AIControls atomically advances the period and resets spend. It reactivates only a key that expired from the immediately preceding period's budget. Every other expired key, and every revoked key, stays terminal.
Expiration causes
The Status column on the Virtual Keys page names the cause, and so does the error an LLM call with the key returns. A budget expiry returns 402. The other causes return 401 when the key is presented, or 403 when it stops being valid while a request is already being routed. MCP calls with an expired key return 401 with invalid virtual key, whatever the cause. An expired key with no expiresAt shows — in the Expires column.
expirationCause | Status shown | LLM call error | Meaning |
|---|---|---|---|
ttl | Expired · TTL | virtual key is expired: TTL elapsed | expiresAt passed. |
budget | Expired · budget | virtual key budget exhausted | The key reached spendCapUsd. A key with a daily or monthly period works again when the next period starts. |
no_cluster_id | Expired · no cluster ID (retired by upgrade) | virtual key retired: issued without cluster ID; reissue with clusterId | A namespace-scoped key issued without clusterId. See Keys retired for a missing cluster ID. |
legacy_unknown | Expired · cause unknown | virtual key is expired (cause unknown); reissue it | The key expired before AIControls recorded expiration causes, so the cause can't be recovered. |
AIControls keeps legacy_unknown for an older expired key whose TTL had passed or whose spend had reached its cap, because either could have expired it. An older expired key that is namespace-scoped, has no clusterId, and has neither of those causes is reported as no_cluster_id.