curl --request PUT \
--url https://{host}/api/llm-gateway/admin/rate-limits/{id} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"change_note": "<string>",
"enabled": true,
"name": "<string>",
"requests_per_minute": 123,
"scope_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_type": "<string>",
"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/{id}"
payload = {
"change_note": "<string>",
"enabled": True,
"name": "<string>",
"requests_per_minute": 123,
"scope_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_type": "<string>",
"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.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
change_note: '<string>',
enabled: true,
name: '<string>',
requests_per_minute: 123,
scope_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
scope_type: '<string>',
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/{id}', 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/{id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'change_note' => '<string>',
'enabled' => true,
'name' => '<string>',
'requests_per_minute' => 123,
'scope_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'scope_type' => '<string>',
'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/{id}"
payload := strings.NewReader("{\n \"change_note\": \"<string>\",\n \"enabled\": true,\n \"name\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_type\": \"<string>\",\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("PUT", 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.put("https://{host}/api/llm-gateway/admin/rate-limits/{id}")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"change_note\": \"<string>\",\n \"enabled\": true,\n \"name\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_type\": \"<string>\",\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/{id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"change_note\": \"<string>\",\n \"enabled\": true,\n \"name\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_type\": \"<string>\",\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"
}Update a rate-limit policy
curl --request PUT \
--url https://{host}/api/llm-gateway/admin/rate-limits/{id} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"change_note": "<string>",
"enabled": true,
"name": "<string>",
"requests_per_minute": 123,
"scope_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_type": "<string>",
"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/{id}"
payload = {
"change_note": "<string>",
"enabled": True,
"name": "<string>",
"requests_per_minute": 123,
"scope_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scope_type": "<string>",
"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.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
change_note: '<string>',
enabled: true,
name: '<string>',
requests_per_minute: 123,
scope_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
scope_type: '<string>',
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/{id}', 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/{id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'change_note' => '<string>',
'enabled' => true,
'name' => '<string>',
'requests_per_minute' => 123,
'scope_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'scope_type' => '<string>',
'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/{id}"
payload := strings.NewReader("{\n \"change_note\": \"<string>\",\n \"enabled\": true,\n \"name\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_type\": \"<string>\",\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("PUT", 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.put("https://{host}/api/llm-gateway/admin/rate-limits/{id}")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"change_note\": \"<string>\",\n \"enabled\": true,\n \"name\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_type\": \"<string>\",\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/{id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"change_note\": \"<string>\",\n \"enabled\": true,\n \"name\": \"<string>\",\n \"requests_per_minute\": 123,\n \"scope_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scope_type\": \"<string>\",\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.
Path Parameters
Identifier, a UUID
Body
PATCH semantics for an optional numeric metric — see
[super::deserialize_explicit_null].
Optional free-text note explaining why this edit was made. Omitting it clears any previous note — deliberately not the tri-state merge-patch treatment the fields above get, because the note describes the latest change and carrying an old one forward would explain the wrong edit.
Tri-state, so an entity-bound policy can be unbound back to "all" (BCP-3955) rather than silently keeping its old binding.
BCP-4056: the Target columns are tri-state on update for the same reason the scope columns are — a policy that gained a target must be able to lose it again. Safe to retarget in place (unlike a budget, whose period counter would be orphaned) because a rate-limit window is 60s and self-heals.
Response
The requested 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.