Use OpenAI with a scoped key and a spend cap
Point the OpenAI SDK at https://relay.keylayer.dev/v1 with a Keylayer key to put a hard or estimated spend cap on OpenAI per UTC hour, day, week or month. Only the base URL and the API key change: Keylayer swaps in your OpenAI key and forwards each request to api.openai.com with the same method, path and query. A request can run for up to 900 seconds.
Set up
Add your OpenAI provider key as a credential, then mint a Keylayer key from it; the quickstart walks through both. Make the provider key a project API key with restricted permissions, limited to the endpoints you call, in a project with its own budget. Never add an admin key.
Put the Keylayer key in an environment variable:
export KEYLAYER_KEY=kl_k_YOUR_KEYThen create the client with the relay as its base URL:
import os
from openai import OpenAI
client = OpenAI(
base_url="https://relay.keylayer.dev/v1",
api_key=os.environ["KEYLAYER_KEY"],
)
response = client.responses.create(
model="gpt-6-luna",
input="Write a haiku about rate limits.",
max_output_tokens=1000,
)
print(response.output_text)import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://relay.keylayer.dev/v1",
apiKey: process.env.KEYLAYER_KEY,
});
const response = await client.responses.create({
model: "gpt-6-luna",
input: "Write a haiku about rate limits.",
max_output_tokens: 1000,
});
console.log(response.output_text);curl https://relay.keylayer.dev/v1/responses \
-H "Authorization: Bearer $KEYLAYER_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-6-luna", "input": "Write a haiku about rate limits.", "max_output_tokens": 1000}'Both SDKs also read OPENAI_BASE_URL and OPENAI_API_KEY, so code that calls OpenAI() or new OpenAI() with no arguments switches over with two variables:
export OPENAI_BASE_URL="https://relay.keylayer.dev/v1"
export OPENAI_API_KEY="$KEYLAYER_KEY"Streams work as usual: Keylayer passes server-sent events through as they arrive. It drops the OpenAI-Organization and OpenAI-Project headers, so OpenAI takes the organization and project from your provider key alone.
Spend caps on OpenAI
Keylayer prices OpenAI requests at OpenAI's standard prices, and premium service tiers at their own. What a request reserves depends on its route:
| Request | Hard cap | Estimated cap |
|---|---|---|
POST /v1/responses, /v1/responses/compact, /v1/chat/completions or /v1/completions |
Worst-case cost | Worst-case cost |
POST /v1/embeddings or /v1/responses/input_tokens |
Input cost | Input cost |
POST /v1/alpha/search, Codex web search |
Refused (403) | Flat estimate |
POST /v1/moderations, and any GET, HEAD, DELETE or OPTIONS |
$0.000001 | $0.000001 |
POST /v1/files, or /v1/uploads and its parts |
$0.001 | $0.001 |
| POST batches, fine-tuning jobs, eval runs, containers or vector stores | Refused (403) | Refused (403) |
| POST cancels of batches, background responses, eval runs and fine-tuning jobs, and fine-tuning pauses | Refused (403) | $0.000001 |
| POST image generations and edits, transcriptions, translations and speech | By model | By model |
| Any other POST, such as image variations | Refused (403) | Refused (403) |
The worst case counts about one token per byte of the request body, plus the output limit the request sets, times n. Without an output limit (max_output_tokens on Responses, max_completion_tokens or max_tokens on chat completions), Keylayer reserves the model's maximum output: for gpt-6-luna that's 128,000 tokens, about $0.064 per request in flight, where the example above reserves about $0.0005. On models with long-context prices, such as gpt-6-luna, a body over about 272 KB is reserved at 2 times the input and 1.5 times the output price. On models that charge for cache writes, such as gpt-6.1-sol, the input is reserved at the cache-write price: 1.25 times the input price, or 2.5 times when prompt_cache_options or a prompt_cache_breakpoint asks for a 12h lifetime. When the response ends, Keylayer charges the cost from the usage OpenAI reports and frees the rest. A cancelled request, or a 504 or 524 timeout with no usage, is charged the full reservation; any other 5xx error with no usage is free.
Compaction, POST /v1/responses/compact, takes no output limit, so Keylayer always reserves the model's maximum output for it. A token count, POST /v1/responses/input_tokens, reserves its input alone and is charged the count OpenAI returns at the model's input price.
Under a hard cap, Keylayer refuses with 403 unpriced_operation any request it can't bound up front: models missing from its price table, a service_tier other than default, best_of, audio input or output, predicted outputs, web search and other built-in tools, previous_response_id, conversations, stored prompts, image URLs other than data: URLs and file inputs, also inside tool call outputs, more than 1,500 images, video, cache lifetimes other than 30m and 12h, and n above 128. It also sets service_tier to default on chat completions and Responses requests that name no tier, so a project default such as priority can't raise the price. If OpenAI still reports a premium tier, Keylayer suspends the key.
Under an estimated cap, those requests go through, except a model missing from the price table, a body Keylayer can't read, such as one with a duplicate key, and a Responses request with background: true, which get 403 unpriced_operation under either cap. Keylayer reserves a quarter token per byte of the body at the model's input price, plus $0.001, then charges the reported usage when it can price it, or else that reservation, so spend can pass the cap. A request with built-in tools, such as web search, is charged its reported usage plus its reservation, which stands in for the tool fees. One that doesn't finish, such as a stream cut short after your client hangs up, or that ends without usage Keylayer can price, is charged its reservation plus the worst case it would have without the tools, unless something else in it, such as previous_response_id, leaves that unknown. Without an output limit, that worst case counts the model's whole output: 128,000 tokens, or $1.28 for gpt-5. Image generation also counts its images, as Image generation in Responses shows. With escape hatches on, a model missing from the table goes through at the highest input price in it, $150 per million tokens.
A request on a premium service_tier is charged at that tier's price: priority and fast at the model's Fast rates, or 2.5 times its standard prices where OpenAI lists none, ultrafast at 6 times standard, and flex and scale at standard prices. For gpt-5-mini, Fast is $0.45 per million input tokens and $3.60 per million output tokens, against $0.25 and $2. Keylayer goes by the service_tier OpenAI reports, or the one the request names when the response has none, as with /v1/responses/compact, and charges a tier it doesn't know at the Fast rates.
With either cap, Keylayer adds stream_options.include_usage to streamed chat completions so it can read the cost, and removes that extra usage chunk unless you asked for it.
Codex web search, POST /v1/alpha/search, reports no usage, so a hard cap refuses it with 403 unpriced_operation. An estimated cap charges a flat estimate in full: $0.01, plus one token per byte of the body and 8,192 more at the model's input price, plus max_output_tokens at its output price, or 4,096 tokens when the body sets none. Codex sets max_output_tokens to 10,000, so a Codex search on gpt-5.6-luna with a 2 KB body comes to about $0.024. A model Keylayer doesn't know gets 403 unpriced_operation, unless the key has escape hatches, which let it through at the highest prices in its OpenAI table.
OpenAI images and audio covers what image generations and edits, transcriptions, translations and speech reserve and charge.
Blocked routes
These OpenAI routes return 403 path_not_allowed:
/v1/organization/**, the Admin API for users, projects, API keys, rate limits and usage/v1/realtime/client_secrets/**,/v1/realtime/translations/client_secrets/**,/v1/realtime/sessions/**and/v1/realtime/transcription_sessions/**, which mint short-lived Realtime client secrets/v1/chatkit/sessions/**, which mints ChatKit client secrets/v1/realtime/calls/**and/v1/live/sessions/**, which start Realtime and GPT-Live voice sessions that stream straight between your client and OpenAI, so Keylayer never sees their usage
A client secret works against OpenAI directly, so minting one through Keylayer would step around the key's limits. Path blocks are a backstop: the restricted project key is what limits your provider key. Escape hatches lift the block for one key; the key limits page explains how.
Errors
When Keylayer refuses a request, it answers in OpenAI's error format, with the code in error.code and in a keylayer-error header:
HTTP/1.1 403 Forbidden
content-type: application/json
cache-control: no-store
keylayer-error: budget_exceeded
retry-after: 3600
{
"error": {
"message": "Keylayer budget_exceeded: this key's spend cap is reached; retry after 60m.",
"type": "invalid_request_error",
"param": null,
"code": "budget_exceeded"
}
}Refusals before Keylayer knows the provider, such as 401 missing_token and invalid_token, have a plain body like {"error": "invalid_token"} instead, with the same keylayer-error header.
The SDKs raise their usual errors: PermissionDeniedError on 403 codes such as budget_exceeded, unpriced_operation and path_not_allowed, AuthenticationError on 401 codes such as invalid_token, and RateLimitError on 429. Errors from OpenAI itself pass through with no keylayer-error header, so check for it:
import openai
try:
response = client.responses.create(
model="gpt-6-luna",
input="Write a haiku about rate limits.",
max_output_tokens=1000,
)
except openai.APIStatusError as error:
reason = error.response.headers.get("keylayer-error")
if reason is None:
raise
print(f"Keylayer refused the request: {reason} (HTTP {error.status_code})")The SDKs retry a 429 twice by default. When the wait is over 60 seconds or unknown, Keylayer adds x-should-retry: false, so the SDK stops at once. The error reference lists every code.
What doesn't work
- The Realtime API over WebSocket. The relay doesn't carry WebSocket connections.
- Regional hosts such as eu.api.openai.com. Keylayer sends OpenAI requests only to api.openai.com.
- Request bodies over 16 MiB on a key with a spend cap. Keylayer reads the whole body to price it, so it answers 413
body_too_large_for_budget, and 415content_encoding_unsupportedto a compressed body. Keys without a spend cap stream the body straight through. - Requests that run past 900 seconds. Keylayer ends them with 504
deadline_exceeded, or cuts off a started response.
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