Add a user to the gateway¶
By the end of this guide a teammate, application or agent has its own virtual key for the gateway, capped in spend and in rate, restricted to the models it needs, and revocable without touching anything else.
For what a virtual key is and why one shared key costs you both attribution and control, see the overview. For the same thing in the console, see the console walkthrough.
Prerequisites¶
- A running gateway - see Deploy an LLM gateway
- Access to the gateway's configuration
- The model names you intend to allow - see Add a model provider
Decide the scope before you create anything¶
A key answers four questions. Settle them before you create it. A key can be changed afterwards, but one that has already been handed out with no limits is hard to withdraw.
| Question | Setting |
|---|---|
| Who holds it? | one holder, named in name and description |
| How much may it spend? | budget_id |
| How fast? | rate_limit_id |
| Which models may it reach? | provider_configs.allowed_models |
1. Define the limits¶
Budgets and rate limits are named objects that keys point at, so define each once and reuse it. A budget caps total spend over a window; a rate limit caps how fast that window can be consumed.
bifrost:
governance:
budgets:
- id: standard-budget
max_limit: 10 # (1)!
reset_duration: 1M # (2)!
- id: premium-budget
max_limit: 100
reset_duration: 1M
rateLimits:
- id: standard-rate # (3)!
request_max_limit: 100
request_reset_duration: 1h
token_max_limit: 100000 # (4)!
token_reset_duration: 1d
- id: premium-rate
request_max_limit: 1000
request_reset_duration: 1h
token_max_limit: 1000000
token_reset_duration: 1d
-
Tune this. The spend ceiling for one period, in the gateway's billing units. This is the total damage a key can do before it stops working.
-
Tune this. How often the budget resets -
1Mis monthly,1ddaily. Shorter periods bound the damage more tightly and generate more interruptions. -
Leave as is. The id keys refer to. Writing a limit once and attaching it to many keys is the point of the split - changing the limit then changes every key at once.
-
Tune this. Requests and tokens are capped separately, over different windows, and both matter: a hundred requests an hour sounds modest until each one carries a 100k-token context.
A budget alone does not stop a runaway loop
A retry loop can spend a month's budget in minutes. The budget bounds the damage; the rate limit bounds how fast it arrives, and is what gives you time to notice.
2. Generate the key value¶
Generate it. Do not compose one by hand, and do not encode anything meaningful in it:
3. Create the virtual key¶
bifrost:
governance:
virtualKeys:
- id: vk-<holder> # (1)!
name: <Human readable name>
description: <who holds this and what it is for>
is_active: true # (2)!
budget_id: standard-budget # (3)!
rate_limit_id: standard-rate
value: <generated-key> # (4)!
-
Edit this. The key's stable identifier, and what you will use to revoke or deactivate it later. Name it after the holder, not the person who created it.
-
Leave as is for now. Setting it to
falsedisables the key without deleting it, which is the reversible way to cut someone off - see Change. -
Edit this. Which budget and rate limit apply, by id from step 1. Omit them and the key is unlimited, which is rarely what you want.
-
Edit this, and keep it out of Git. The credential itself. Read it from your secret store when you render the values file - a key committed to a repository is a key you have to rotate.
That key can reach every model the gateway offers. To restrict it - the usual case - add a model scope:
- id: vk-<holder> # (1)!
name: <Human readable name>
description: <who holds this and what it is for> # (2)!
is_active: true # (3)!
budget_id: standard-budget # (4)!
rate_limit_id: standard-rate
value: <generated-key> # (5)!
provider_configs:
- provider: <provider>
weight: 1
allowed_models: # (6)!
- <model-name>
-
Edit this. A stable identifier for the key. You will use it to find, deactivate and delete this key later, so make it say who holds it.
-
Edit this. Write it for whoever reads this file in six months trying to work out whether the key is still needed. "Ana's laptop, OpenCode" beats "dev key".
-
Leave as is. Setting this to
falselater disables the key without deleting it - reversible, and it keeps the spend history. Reach for that before deletion. -
Edit this. Must match an
idfrom the budgets you defined. Note that budgets are shared: two keys pointing atstandard-budgetconsume the same ceiling. -
Edit this. The generated value from step 2. This is the credential the holder uses - treat the file it lands in accordingly.
-
Edit this. The gateway's public model names this key may reach, not the provider's model ids. Omit the whole
provider_configsblock to allow everything - but a key that only needs a small model should not be able to reach an expensive one.
allowed_models is checked before routing, so a request for anything else is refused at the
gateway rather than billed.
Key values are credentials, wherever they live
A key written into a values file is a credential in that file. Keep gateway configuration out of public repositories, and prefer a secret store where one is available - see Security & identity.
4. Apply¶
Apply the configuration the way the gateway is deployed - through your GitOps pipeline, or directly while developing:
oc apply -k <gateway-config-path> --enable-helm
oc rollout status deploy/<gateway> -n <gateway-project>
5. Hand it over¶
The recipient needs exactly two things: the gateway base URL, and their key. Send the key the way you would send a password, and never in the same message as the URL if that channel is shared.
If the recipient is a CLI coding agent rather than a person, see Connect a coding agent.
Verify¶
Test the key from the recipient's position, not yours.
It is accepted:
It can reach what it should:
curl -sS <base-url>/chat/completions \
-H "Authorization: Bearer <the-key>" \
-H "Content-Type: application/json" \
-d '{"model":"<allowed-model>","messages":[{"role":"user","content":"ping"}]}'
It cannot reach what it should not - the test people skip, and the only one that proves
the scope works. Ask for a model outside allowed_models and confirm it is refused:
curl -sS <base-url>/chat/completions \
-H "Authorization: Bearer <the-key>" \
-H "Content-Type: application/json" \
-d '{"model":"<a-model-not-allowed>","messages":[{"role":"user","content":"ping"}]}'
The request was attributed - check the gateway logs show the call against this key with token counts:
A 200 response does not prove the virtual key was checked
Some gateway deployments disable authentication on inference during a proof of concept.
If that is set, an inference call succeeds whether or not the key is valid. Confirm
authentication is enforced before concluding anything from a 200.
List¶
The keys themselves come from the gateway's configuration - read that rather than the cluster, since it is the source of truth for what exists.
Change¶
Adjust a limit by pointing the key at a different budget or rate limit, or by changing the numbers on the one it uses. Remember that changing a shared budget changes it for every key referencing it.
Deactivate without deleting - reversible, and it keeps the spend history:
Rotate a key. Never edit a value in place; something is using it. Overlap instead:
- Add a second entry with a new
idand a freshvalue. - Update the holder's configuration.
- Confirm traffic arrives on the new key.
- Set the old key
is_active: falseand wait. - Remove the old entry once nothing has broken.
Delete¶
Find the holder first. A key with no obvious owner is usually configured somewhere rather than genuinely unused:
Then remove its entry from virtualKeys and apply. Deactivating first, waiting, and
deleting afterwards turns a possible outage into a reversible one.
Troubleshoot¶
401 or 403 on every request. The key is not active, the value does not match, or the
configuration did not reload. Confirm the rollout completed and the gateway restarted.
The model is refused but you expected it to work. allowed_models matches the gateway's
public model name, not the provider's model id. Check the two are the same string - see
Add a model provider.
Requests refused with a budget or rate error and the numbers look wrong. More than one key can share a budget. Check whether the ceiling is being consumed by another holder.
The key works but nothing appears in the logs. Request logging is not always durable - some deployments run the gateway without persistent storage, in which case logs do not survive a restart. Confirm before relying on them for attribution.
Next¶
Connect a coding agent → - hand this key to a CLI agent, or Add a model provider to widen what it can reach.