Use Hugging Face with a scoped key and a spend cap
Use https://relay.keylayer.dev/-/router/v1 as the base URL and a Keylayer key as the token to get spend caps priced for 185 model and provider pairs. Keylayer forwards each call to router.huggingface.co with your real Hugging Face token and nothing else in your code changes; caps are estimated rather than hard, and a request can run for up to 900 seconds.
Set up
Add your Hugging Face token as a credential and mint a key for it, as in the quickstart. A fine-grained token with only the Make calls to Inference Providers permission is enough. Put the key in an environment variable:
export KEYLAYER_KEY=kl_k_YOUR_KEYName the provider in the model string, as model:provider, so Keylayer can price the call. In @huggingface/inference, leave the provider option unset, because the client refuses endpointUrl together with a third-party provider.
import os
from huggingface_hub import InferenceClient
client = InferenceClient(
base_url="https://relay.keylayer.dev/-/router/v1",
api_key=os.environ["KEYLAYER_KEY"],
)
completion = client.chat.completions.create(
model="openai/gpt-oss-120b:cerebras",
messages=[{"role": "user", "content": "Say hello in five words."}],
max_tokens=500,
)
print(completion.choices[0].message.content)import os
from openai import OpenAI
client = OpenAI(
base_url="https://relay.keylayer.dev/-/router/v1",
api_key=os.environ["KEYLAYER_KEY"],
)
completion = client.chat.completions.create(
model="openai/gpt-oss-120b:cerebras",
messages=[{"role": "user", "content": "Say hello in five words."}],
max_tokens=500,
)
print(completion.choices[0].message.content)import { InferenceClient } from "@huggingface/inference";
const client = new InferenceClient(process.env.KEYLAYER_KEY, {
endpointUrl: "https://relay.keylayer.dev/-/router/v1",
});
const completion = await client.chatCompletion({
model: "openai/gpt-oss-120b:cerebras",
messages: [{ role: "user", content: "Say hello in five words." }],
max_tokens: 500,
});
console.log(completion.choices[0].message.content);import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://relay.keylayer.dev/-/router/v1",
apiKey: process.env.KEYLAYER_KEY,
});
const completion = await client.chat.completions.create({
model: "openai/gpt-oss-120b:cerebras",
messages: [{ role: "user", content: "Say hello in five words." }],
max_tokens: 500,
});
console.log(completion.choices[0].message.content);curl https://relay.keylayer.dev/-/router/v1/chat/completions \
-H "Authorization: Bearer $KEYLAYER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-oss-120b:cerebras",
"messages": [{"role": "user", "content": "Say hello in five words."}],
"max_tokens": 500
}'Hub API calls go to huggingface.co through the relay's root, without the /-/router prefix. HfApi takes the relay as its endpoint:
import os
from huggingface_hub import HfApi
api = HfApi(endpoint="https://relay.keylayer.dev", token=os.environ["KEYLAYER_KEY"])
for model in api.list_models(author="openai", limit=5):
print(model.id)Keylayer matches a key's Only paths against the path you send, prefix included, so allow /-/router/v1/chat/completions rather than /v1/chat/completions. See methods and paths.
Spend caps on Hugging Face
Hugging Face keys take estimated spend caps only. A hard cap needs a way to force a provider's standard tier and the router has none, so a key with a hard cap can't be minted. Hard and estimated caps explains the difference.
Keylayer prices /v1/chat/completions and /v1/responses on the router from a table of 185 model and provider pairs across 104 models, taken from router.huggingface.co/v1/models on 2026-10-22. For example, openai/gpt-oss-120b:cerebras costs $0.35 per million input tokens and $0.75 per million output tokens. Keylayer reserves the worst case before forwarding and charges the usage Hugging Face reports afterwards. A 4xx or 5xx without usage is free, except a 504 or 524 timeout, which is charged the full reservation, and so is a cancelled request.
| Request | Reserved before forwarding | Charged after |
|---|---|---|
| Chat on openai/gpt-oss-120b:cerebras, short prompt, max_tokens 500 | About $0.00045 | The usage Hugging Face reports |
| The same chat without max_tokens | About $0.10, for 131,072 output tokens | The usage Hugging Face reports |
| Chat on a model without :provider, or with :fastest | Refused (403); with escape hatches, $0.01 plus $0.75 per MB of body | Nothing; with escape hatches, the full reservation |
| Hub GET, such as listing models | $0.000001 | Nothing |
| Hub paths-info or preupload, or a model or dataset commit whose files are all in its body | $0.000001 | $0.000001 |
| Any other Hub POST, PUT or PATCH, such as an LFS upload | Refused (403) | Nothing |
A chat or responses request Keylayer can't price up front, such as one with an image, is let through at a fallback estimate: $0.01 plus the body at the model's input price, counting 4 bytes as a token. Keylayer then charges the usage Hugging Face reports when it can price it, or else the fallback, so spend can pass the cap. Pin a provider from the table to pay only for what you use. A pair missing from the table, including a model without :provider or with :fastest or :cheapest, gets 403 unpriced_operation, unless the key has escape hatches, which let it through at the table's highest input price, $3 per million tokens. A request with built-in tools, meaning any tool whose type isn't function or custom, is charged the usage Hugging Face reports plus the full fallback, which stands in for the tool fees, even with a provider pinned. A body Keylayer can't read, such as invalid JSON, a duplicate key, more than 393,216 brackets and commas, or nesting past 128 levels, gets 403 unpriced_operation. So does any other POST, PUT or PATCH, such as a router route other than /v1/chat/completions and /v1/responses.
Hub calls cost nothing per request, but private repositories bill $18 per TB a month above the included storage. A model or dataset commit whose every file travels in its body adds at most the 16 MiB body limit, so Keylayer admits it at $0.000001, along with paths-info and preupload, the lookups huggingface_hub runs before a commit. A commit that names an LFS file, a git-LFS upload batch to /{repo}.git/info/lfs/objects/batch and lfs-files/duplicate add storage the request doesn't show, so a capped key refuses them with 403 unpriced_operation, and so does lfs-files/batch, which deletes LFS files for good. A commit to a Space is refused too, since each one rebuilds and restarts the Space on hardware billed by the minute. In practice, small text files commit under a cap and model weights don't.
When a streamed chat request under a spend cap doesn't ask for usage, Keylayer adds stream_options.include_usage so it can read the cost, and drops that extra usage chunk from the stream you get back.
Blocked routes
Keylayer refuses these huggingface.co routes with 403 path_not_allowed, so a key can't change your account or start paid compute:
- Jobs and scheduled jobs, under /api/jobs and /api/scheduled-jobs
- Creating, deleting and moving repos, under /api/repos
- Account settings under /api/settings, everything under /api/organizations, and each repo's settings
- A Space's hardware, sleep time, dev mode, volumes, secrets and variables
- Duplicating a repo, user access requests, and Xet write tokens for large file uploads
- OAuth, under /oauth
Keylayer also removes the X-HF-Bill-To header, so a request can't bill an organization. Escape hatches lift the route block for one key; see admin routes and escape hatches.
Errors
A Keylayer refusal has a keylayer-error header and a body in Hugging Face's own error shape:
{"error": "Keylayer path_not_allowed: this key does not allow this path."}huggingface_hub raises HfHubHTTPError, which keeps the response headers:
import os
from huggingface_hub import InferenceClient
from huggingface_hub.errors import HfHubHTTPError
client = InferenceClient(
base_url="https://relay.keylayer.dev/-/router/v1",
api_key=os.environ["KEYLAYER_KEY"],
)
try:
completion = client.chat.completions.create(
model="openai/gpt-oss-120b:cerebras",
messages=[{"role": "user", "content": "Say hello in five words."}],
max_tokens=500,
)
except HfHubHTTPError as err:
code = err.response.headers.get("keylayer-error")
if code is None:
raise
print(f"Keylayer refused the request: {err.response.status_code} {code}")@huggingface/inference throws an error whose httpResponse has the status and body but no headers. There, body.error holds either the bare code or a message that starts with Keylayer and the code. The error reference lists every code.
What doesn't work
- InferenceClient tasks other than chat, such as text_to_image and feature_extraction, post to the base URL itself instead of a task route. Use the relay for chat completions and responses.
- Large file downloads redirect to Hugging Face's CDN, such as us.aws.cdn.hf.co. Keylayer relays only huggingface.co and router.huggingface.co, so the download itself goes straight to the CDN, outside the key's limits.
- Hard spend caps, as above.
Next steps
- Make your first request: add a credential and mint a key with a spend cap.
- Set limits on a key: every limit a key can have and how spend caps count.
- Handle Keylayer errors: every code Keylayer returns and what to do about it.
Updated