curl --request POST \
--url https://{host}/api/llm-gateway/admin/rate-limits \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"scope_type": "<string>",
"change_note": "<string>",
"member_of_group": "<string>",
"requests_per_minute": 123,
"scope_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_value": "<string>",
"target_mcp_server_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_model_alias": "<string>",
"target_provider_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_upstream_model": "<string>",
"tokens_per_minute": 123,
"traffic_type": "<string>"
}
'import requests
url = "https://{host}/api/llm-gateway/admin/rate-limits"
payload = {
"name": "<string>",
"scope_type": "<string>",
"change_note": "<string>",
"member_of_group": "<string>",
"requests_per_minute": 123,
"scope_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_value": "<string>",
"target_mcp_server_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_model_alias": "<string>",
"target_provider_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_upstream_model": "<string>",
"tokens_per_minute": 123,
"traffic_type": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: '<string>',
scope_type: '<string>',
change_note: '<string>',
member_of_group: '<string>',
requests_per_minute: 123,
scope_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
scope_value: '<string>',
target_mcp_server_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
target_model_alias: '<string>',
target_provider_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
target_upstream_model: '<string>',
tokens_per_minute: 123,
traffic_type: '<string>'
})
};
fetch('https://{host}/api/llm-gateway/admin/rate-limits', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{host}/api/llm-gateway/admin/rate-limits",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'scope_type' => '<string>',
'change_note' => '<string>',
'member_of_group' => '<string>',
'requests_per_minute' => 123,
'scope_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'scope_value' => '<string>',
'target_mcp_server_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'target_model_alias' => '<string>',
'target_provider_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'target_upstream_model' => '<string>',
'tokens_per_minute' => 123,
'traffic_type' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://{host}/api/llm-gateway/admin/rate-limits"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"change_note\": \"<string>\",\n \"member_of_group\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_value\": \"<string>\",\n \"target_mcp_server_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_model_alias\": \"<string>\",\n \"target_provider_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_upstream_model\": \"<string>\",\n \"tokens_per_minute\": 123,\n \"traffic_type\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://{host}/api/llm-gateway/admin/rate-limits")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"change_note\": \"<string>\",\n \"member_of_group\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_value\": \"<string>\",\n \"target_mcp_server_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_model_alias\": \"<string>\",\n \"target_provider_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_upstream_model\": \"<string>\",\n \"tokens_per_minute\": 123,\n \"traffic_type\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://{host}/api/llm-gateway/admin/rate-limits")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"change_note\": \"<string>\",\n \"member_of_group\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_value\": \"<string>\",\n \"target_mcp_server_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_model_alias\": \"<string>\",\n \"target_provider_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_upstream_model\": \"<string>\",\n \"tokens_per_minute\": 123,\n \"traffic_type\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"enabled": true,
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"org_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_type": "org",
"traffic_type": "all",
"member_of_group": "<string>",
"requests_per_minute": 123,
"scope_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_value": "<string>",
"target_mcp_server_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_model_alias": "<string>",
"target_provider_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_upstream_model": "<string>",
"tokens_per_minute": 123,
"created_by_email": "<string>",
"created_by_name": "<string>",
"created_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"last_change_note": "<string>",
"last_modified_at": "2023-11-07T05:31:56Z",
"last_modified_by_email": "<string>",
"last_modified_by_name": "<string>",
"last_modified_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}Create a rate-limit policy
curl --request POST \
--url https://{host}/api/llm-gateway/admin/rate-limits \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"scope_type": "<string>",
"change_note": "<string>",
"member_of_group": "<string>",
"requests_per_minute": 123,
"scope_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_value": "<string>",
"target_mcp_server_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_model_alias": "<string>",
"target_provider_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_upstream_model": "<string>",
"tokens_per_minute": 123,
"traffic_type": "<string>"
}
'import requests
url = "https://{host}/api/llm-gateway/admin/rate-limits"
payload = {
"name": "<string>",
"scope_type": "<string>",
"change_note": "<string>",
"member_of_group": "<string>",
"requests_per_minute": 123,
"scope_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_value": "<string>",
"target_mcp_server_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_model_alias": "<string>",
"target_provider_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_upstream_model": "<string>",
"tokens_per_minute": 123,
"traffic_type": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: '<string>',
scope_type: '<string>',
change_note: '<string>',
member_of_group: '<string>',
requests_per_minute: 123,
scope_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
scope_value: '<string>',
target_mcp_server_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
target_model_alias: '<string>',
target_provider_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
target_upstream_model: '<string>',
tokens_per_minute: 123,
traffic_type: '<string>'
})
};
fetch('https://{host}/api/llm-gateway/admin/rate-limits', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{host}/api/llm-gateway/admin/rate-limits",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'scope_type' => '<string>',
'change_note' => '<string>',
'member_of_group' => '<string>',
'requests_per_minute' => 123,
'scope_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'scope_value' => '<string>',
'target_mcp_server_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'target_model_alias' => '<string>',
'target_provider_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'target_upstream_model' => '<string>',
'tokens_per_minute' => 123,
'traffic_type' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://{host}/api/llm-gateway/admin/rate-limits"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"change_note\": \"<string>\",\n \"member_of_group\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_value\": \"<string>\",\n \"target_mcp_server_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_model_alias\": \"<string>\",\n \"target_provider_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_upstream_model\": \"<string>\",\n \"tokens_per_minute\": 123,\n \"traffic_type\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://{host}/api/llm-gateway/admin/rate-limits")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"change_note\": \"<string>\",\n \"member_of_group\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_value\": \"<string>\",\n \"target_mcp_server_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_model_alias\": \"<string>\",\n \"target_provider_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_upstream_model\": \"<string>\",\n \"tokens_per_minute\": 123,\n \"traffic_type\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://{host}/api/llm-gateway/admin/rate-limits")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"change_note\": \"<string>\",\n \"member_of_group\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_value\": \"<string>\",\n \"target_mcp_server_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_model_alias\": \"<string>\",\n \"target_provider_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"target_upstream_model\": \"<string>\",\n \"tokens_per_minute\": 123,\n \"traffic_type\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"enabled": true,
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"org_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_type": "org",
"traffic_type": "all",
"member_of_group": "<string>",
"requests_per_minute": 123,
"scope_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_value": "<string>",
"target_mcp_server_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_model_alias": "<string>",
"target_provider_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"target_upstream_model": "<string>",
"tokens_per_minute": 123,
"created_by_email": "<string>",
"created_by_name": "<string>",
"created_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"last_change_note": "<string>",
"last_modified_at": "2023-11-07T05:31:56Z",
"last_modified_by_email": "<string>",
"last_modified_by_name": "<string>",
"last_modified_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}Authorizations
A Barndoor API key, sent as Authorization: Bearer <key>. Keys are prefixed bdai_ and are opaque — do not attempt to decode one. Interchangeable with an OAuth 2.0 access token.
Body
Optional free-text note explaining why this policy was created (BCP-3998). Surfaced on the row and mirrored into the audit event.
BCP-3814: narrow this policy to the members of one IdP group. Only
valid on a broad user-scoped policy (scope_type = "user" with neither
scope_id nor scope_value), where the window is already keyed per user — so each
member of the group gets their own allowance from this single row.
Handler-validated; V67 CHECK is the final safety net.
BCP-4056 Target dimension: when set, the policy only meters requests
routed to this MCP server. Replaces the old mcp_server scope — that
one did work, and moves here for consistency with the other two.
BCP-4056 Target dimension: matches the user-facing alias from
request.model. Replaces the old model scope, which could never
match because scope_keys() emits no (Model, _) pair.
BCP-4056 Target dimension: when set, the policy only meters traffic
routed to this provider. Replaces the old llm_provider scope, which
recorded usage the check path never read. May be combined with
target_upstream_model; mutually exclusive with target_model_alias
and target_mcp_server_id (handler-validated; V74 CHECKs are the
final safety net).
BCP-4056 Target dimension refining target_provider_id: matches the
route's resolved upstream model name. Requires target_provider_id — a
bare upstream name would string every provider serving that model into
one window (handler-enforced).
Response
The created record
An admin-API response for an entity whose core type lives in
ai_governance and so cannot carry portal-only columns.
TokenBudget and RateLimitPolicy are read by the enforcement path on
every request; hanging five display-only fields off them would put portal
concerns in the hot path's type and force every governance consumer to
carry them. Wrapping instead keeps the enforcement reads
(get_enabled_by_org and friends) returning the bare governance type,
while the admin reads return this.
Both halves are #[serde(flatten)], so the wire shape stays the flat
object the portal already consumes — same choice AdminModelPricing makes.
Scope is the identity axis of a budget or rate-limit policy — "who is
this policy for?". The what (LLM provider, upstream model name, model
alias) lives on dedicated target_* columns on the row (BCP-2813), not
as a scope value. LlmProvider and Model remain in the enum because
rate_limit_policies and model_access_policies still accept them in
their scope_type CHECK constraints (V12 / V18 / V22 widenings — a
separate scope of work to clean up). For token budgets the CHECK was
tightened back to identity-only by V41__budget_scope_target.sql, so
any LlmProvider/Model budget row that survived would simply fail to
match in BudgetEnforcer::matched_keys.
org, team, user, project, api_key, mcp_server, llm_provider, model, agent, role, group all, llm, mcp BCP-3814 membership filter: when set, this policy only applies to requesters whose IdP group membership includes this group name.
Only meaningful on a broad user-scoped policy (scope_type == User
with scope_id and scope_value both None), where the counter key resolves to
the requesting user's id — so one policy row gives every member of
the group their own RPM/TPM allowance, and a user joining the group
is limited on their next request with no admin action. DB-enforced by
rate_limit_policies_member_of_group_requires_broad_user (V66, tightened
to exclude scope_value by V67).
None ⇒ org-wide (the pre-BCP-3814 behaviour). Contrast with a
ScopeType::Group policy, which keys its counter by the group name
and so pools one limit across all members.
BCP-4056 Target dimension: when set, the policy only fires on requests
routed to this MCP server. The MCP-traffic counterpart of
target_provider_id, and mutually exclusive with all three LLM target
columns (DB-enforced by
rate_limit_policies_mcp_target_exclusive_check) — no single request
carries both an MCP server and an LLM route, so a row naming both
could never match.
Distinct from a ScopeType::McpServer policy, which puts the server on
the identity axis and so cannot be combined with a user or group.
Unlike the other two object-shaped scopes, that one does work — it
moves here for consistency and to make "this user's rpm on this
server" expressible, not to fix a defect.
BCP-4056 Target dimension: when set, the policy only fires when the
requested model alias (request.model) equals this value. Mutually
exclusive with the other two LLM target_* fields.
Together with target_provider_id/target_upstream_model this
replaces ScopeType::Model, which could never match anything:
GovernanceIdentity::scope_keys() deliberately emits no (Model, _)
pair, so a model-scoped policy rendered as Enabled while enforcing
nothing.
BCP-4056 Target dimension: when set, the policy only fires on routes
whose provider id equals this value. May be combined with
target_upstream_model to limit a single model on this provider;
mutually exclusive with target_model_alias (DB-enforced by
rate_limit_policies_alias_target_exclusive_check).
This replaces ScopeType::LlmProvider as the way to express "limit
this provider". That scope never worked on the check path — the
gateway middleware builds its identity with no llm_provider_id, so
no (LlmProvider, _) scope key exists to match against, while the
post-response record path does carry one and wrote usage to a
counter nothing read.
BCP-4056 Target dimension refining target_provider_id: when set,
the policy only fires on routes whose resolved upstream model name
equals this value (string match). Always set alongside a provider
(V74 CHECK-enforced) — a bare upstream name would string every
provider serving that model into one limit.
Optional free text explaining why the last change was made ("Raised for the Q3 batch job", "Vendor deprecated the old endpoint").
Admin-authored and rendered in the portal, so it is untrusted input: length-capped here, escaped on render.
When the change named by last_modified_by_* happened.
Its own column rather than the row's updated_at, because updated_at
is bumped by writes nobody is attributed for -- pairing the two would
date a named admin's edit to a background job's clock. See the V75
migration for the full reasoning.