Skip to content
KeylayerDocs
Docs menu
PricingGet started

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:

Shell
export KEYLAYER_KEY=kl_k_YOUR_KEY

Name 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.

Python (huggingface_hub)
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)
Python (openai SDK)
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)
Node.js (@huggingface/inference)
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);
Node.js (openai SDK)
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
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:

Python (HfApi)
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:

403 path_not_allowed
{"error": "Keylayer path_not_allowed: this key does not allow this path."}

huggingface_hub raises HfHubHTTPError, which keeps the response headers:

Python (huggingface_hub)
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

Updated

Create your first scoped key

Add a provider key once, then mint Keylayer keys with their own limits for each app, agent or teammate.

Get started