LLM gateway¶
The gateway is a single OpenAI-compatible endpoint that sits between your applications and every model behind them — self-hosted models on GRN.CLOUD GPUs, and external providers.
The gateway is powered by Bifrost and runs as an
ordinary workload in your project: a Deployment, a Service, and a values file you keep
in Git.
Without a gateway, every application carries its own provider keys, its own retry logic and its own idea of what a request costs. With one, those move to a single place your team can inspect, budget and audit.
What it handles¶
| Concern | What the gateway does |
|---|---|
| Keys | Issues virtual keys per team, app or user; provider keys stay server-side |
| Budgets | Per-key spend limits and rate limits, enforced before the request leaves |
| Routing | Sends a request to the right model by name, tag or policy |
| Failover | Falls back to a secondary provider or tier when the primary is unhealthy |
| Observability | Per-request logging of model, tokens, latency and cost |
| Compatibility | OpenAI-shaped API, so existing SDKs work unchanged |
How it fits together¶
flowchart TB
subgraph CALLERS["callers - each holds a virtual key"]
direction LR
APP1["app"]
APP2["agent"]
APP3["notebook"]
end
CALLERS --> GW["LLM gateway<br/>holds the provider keys"]
GW --> SELF["your own models<br/>vLLM on GRN GPUs"]
GW --> EXT["external providers"]
GW -.->|budgets · request logs| ADMIN["platform team"]
Calling the gateway¶
Because the API is OpenAI-compatible, most applications need a base URL change and nothing else:
from openai import OpenAI
client = OpenAI(
base_url="https://<your-gateway-host>/v1",
api_key="<your-virtual-key>", # issued by the gateway, not the provider
)
response = client.chat.completions.create(
model="<model-name>",
messages=[{"role": "user", "content": "Hello"}],
)
Registering a backend¶
Providers are declared in the values file. Each entry names the credential to use and the model names it answers for. A self-hosted engine is registered the same way an external vendor is:
bifrost:
providerSecrets: # (1)!
openai:
envVar: OPENAI_API_KEY
existingSecret: <provider-secret>
key: openai-api-key
providers:
openai:
keys:
- name: openai-key
value: env.OPENAI_API_KEY # (2)!
weight: 1
models:
- gpt-4o
- gpt-4o-mini
vllm: # (3)!
keys:
- name: <backend-name>
value: ""
weight: 1
models:
- <org>/<model>
vllm_key_config:
model_name: <org>/<model>
url: http://<model-slug>.<your-project>.svc.cluster.local:8000
-
Edit this. The Secret holding the upstream credential, created in the cluster and never committed. The gateway reads it as an environment variable.
-
Leave as is.
env.indirection is what keeps the key out of the values file. The literal credential never appears in Git. -
Edit this. A model you host yourself, registered as a provider like any other. The caller sees it as one more model name. See AI inference.
weight distributes traffic when several keys serve the same model name, which is also how
failover between tiers is expressed.
Budgets, rate limits and virtual keys¶
The three are separate objects, referenced by id, so a limit can be written once and attached to many keys:
bifrost:
governance:
budgets:
- id: standard-budget
max_limit: 10 # (1)!
reset_duration: 1M
rateLimits:
- id: standard-rate
request_max_limit: 100
request_reset_duration: 1h
token_max_limit: 100000
token_reset_duration: 1d
virtualKeys:
- id: <key-id>
name: <human-readable-name>
description: <what this key is for>
value: <the-secret-value> # (2)!
is_active: true
budget_id: standard-budget
rate_limit_id: standard-rate
provider_configs: # (3)!
- provider: vllm
allowed_models:
- <org>/<model>
weight: 1
-
Tune this. Spend ceiling for the period in
reset_duration. A budget caps the total; the rate limit caps the speed. A runaway loop needs the second one to stop quickly — the budget only notices once the money is gone. -
Edit this, and keep it out of Git. Read it from a secret store. A key committed to a repository is a key you have to rotate.
-
Optional. Scoping a key to specific providers and models is what makes it safe to hand to CI or a batch job. Without it the key can reach every backend registered above.
What is fixed after creation¶
| Changeable | |
|---|---|
| Which providers are registered | yes |
| What a model name routes to | yes |
| A virtual key's budget and rate limit | yes |
| The gateway's own hostname | no; callers have it configured |
| A model name callers already use | not safely |
Connecting. The gateway serves the OpenAI API on port 8080:
| What | Where |
|---|---|
| In-cluster endpoint | http://<gateway>.<your-project>.svc.cluster.local:8080/v1 |
| Model list | GET /v1/models |
| Chat completions | POST /v1/chat/completions |
| Authentication | Authorization: Bearer <your-virtual-key> |
Deleting. Removing the gateway takes every model name with it, and every caller configured against its hostname fails at once. Move callers off the hostname before you remove the workload.
A 200 response does not prove the virtual key was checked
Key enforcement on inference requests is a configuration option, and it is not on by
default in every deployment. A 200 proves the gateway is reachable, nothing more.
Confirm that enforcement is enabled before treating a key as an access control.
Every model call in your estate goes through one workload
Centralising requests is what makes budgets and auditing possible, and it also means a gateway outage stops every model call at once. Run more than one replica, and decide what your applications do when the gateway is unreachable before you need the answer.
How to¶
| Task | Guide |
|---|---|
| Stand one up | LLM gateways |
| Issue a scoped key with a budget | Virtual keys |
| Put a backend behind a model name | Model providers |
| Point a coding agent at it | Coding agents |
Related¶
- AI inference — the models behind the gateway
- Security & identity — secrets and RBAC around the credentials
- Observability & backup — what the request logs feed