curl --request POST \
--url https://{host}/api/llm-gateway/admin/budgets \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"period": "<string>",
"scope_type": "<string>",
"token_limit": 123,
"action_on_exhaust": "<string>",
"alert_thresholds": [
123
],
"change_note": "<string>",
"cost_limit": 123,
"currency": "<string>",
"member_of_group": "<string>",
"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>",
"traffic_type": "<string>"
}
'import requests
url = "https://{host}/api/llm-gateway/admin/budgets"
payload = {
"name": "<string>",
"period": "<string>",
"scope_type": "<string>",
"token_limit": 123,
"action_on_exhaust": "<string>",
"alert_thresholds": [123],
"change_note": "<string>",
"cost_limit": 123,
"currency": "<string>",
"member_of_group": "<string>",
"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>",
"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>',
period: '<string>',
scope_type: '<string>',
token_limit: 123,
action_on_exhaust: '<string>',
alert_thresholds: [123],
change_note: '<string>',
cost_limit: 123,
currency: '<string>',
member_of_group: '<string>',
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>',
traffic_type: '<string>'
})
};
fetch('https://{host}/api/llm-gateway/admin/budgets', 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/budgets",
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>',
'period' => '<string>',
'scope_type' => '<string>',
'token_limit' => 123,
'action_on_exhaust' => '<string>',
'alert_thresholds' => [
123
],
'change_note' => '<string>',
'cost_limit' => 123,
'currency' => '<string>',
'member_of_group' => '<string>',
'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>',
'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/budgets"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"period\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"token_limit\": 123,\n \"action_on_exhaust\": \"<string>\",\n \"alert_thresholds\": [\n 123\n ],\n \"change_note\": \"<string>\",\n \"cost_limit\": 123,\n \"currency\": \"<string>\",\n \"member_of_group\": \"<string>\",\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 \"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/budgets")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"period\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"token_limit\": 123,\n \"action_on_exhaust\": \"<string>\",\n \"alert_thresholds\": [\n 123\n ],\n \"change_note\": \"<string>\",\n \"cost_limit\": 123,\n \"currency\": \"<string>\",\n \"member_of_group\": \"<string>\",\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 \"traffic_type\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://{host}/api/llm-gateway/admin/budgets")
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 \"period\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"token_limit\": 123,\n \"action_on_exhaust\": \"<string>\",\n \"alert_thresholds\": [\n 123\n ],\n \"change_note\": \"<string>\",\n \"cost_limit\": 123,\n \"currency\": \"<string>\",\n \"member_of_group\": \"<string>\",\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 \"traffic_type\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"action_on_exhaust": "block",
"alert_thresholds": [
123
],
"enabled": true,
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"org_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"period": "daily",
"scope_type": "org",
"token_limit": 123,
"traffic_type": "all",
"cost_limit": 123,
"created_at": "2023-11-07T05:31:56Z",
"currency": "<string>",
"member_of_group": "<string>",
"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>",
"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 token budget
curl --request POST \
--url https://{host}/api/llm-gateway/admin/budgets \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"period": "<string>",
"scope_type": "<string>",
"token_limit": 123,
"action_on_exhaust": "<string>",
"alert_thresholds": [
123
],
"change_note": "<string>",
"cost_limit": 123,
"currency": "<string>",
"member_of_group": "<string>",
"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>",
"traffic_type": "<string>"
}
'import requests
url = "https://{host}/api/llm-gateway/admin/budgets"
payload = {
"name": "<string>",
"period": "<string>",
"scope_type": "<string>",
"token_limit": 123,
"action_on_exhaust": "<string>",
"alert_thresholds": [123],
"change_note": "<string>",
"cost_limit": 123,
"currency": "<string>",
"member_of_group": "<string>",
"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>",
"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>',
period: '<string>',
scope_type: '<string>',
token_limit: 123,
action_on_exhaust: '<string>',
alert_thresholds: [123],
change_note: '<string>',
cost_limit: 123,
currency: '<string>',
member_of_group: '<string>',
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>',
traffic_type: '<string>'
})
};
fetch('https://{host}/api/llm-gateway/admin/budgets', 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/budgets",
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>',
'period' => '<string>',
'scope_type' => '<string>',
'token_limit' => 123,
'action_on_exhaust' => '<string>',
'alert_thresholds' => [
123
],
'change_note' => '<string>',
'cost_limit' => 123,
'currency' => '<string>',
'member_of_group' => '<string>',
'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>',
'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/budgets"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"period\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"token_limit\": 123,\n \"action_on_exhaust\": \"<string>\",\n \"alert_thresholds\": [\n 123\n ],\n \"change_note\": \"<string>\",\n \"cost_limit\": 123,\n \"currency\": \"<string>\",\n \"member_of_group\": \"<string>\",\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 \"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/budgets")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"period\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"token_limit\": 123,\n \"action_on_exhaust\": \"<string>\",\n \"alert_thresholds\": [\n 123\n ],\n \"change_note\": \"<string>\",\n \"cost_limit\": 123,\n \"currency\": \"<string>\",\n \"member_of_group\": \"<string>\",\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 \"traffic_type\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://{host}/api/llm-gateway/admin/budgets")
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 \"period\": \"<string>\",\n \"scope_type\": \"<string>\",\n \"token_limit\": 123,\n \"action_on_exhaust\": \"<string>\",\n \"alert_thresholds\": [\n 123\n ],\n \"change_note\": \"<string>\",\n \"cost_limit\": 123,\n \"currency\": \"<string>\",\n \"member_of_group\": \"<string>\",\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 \"traffic_type\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"action_on_exhaust": "block",
"alert_thresholds": [
123
],
"enabled": true,
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"org_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"period": "daily",
"scope_type": "org",
"token_limit": 123,
"traffic_type": "all",
"cost_limit": 123,
"created_at": "2023-11-07T05:31:56Z",
"currency": "<string>",
"member_of_group": "<string>",
"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>",
"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 budget was created (BCP-3998). Surfaced on the row and mirrored into the audit event.
BCP-3814: narrow this budget to the members of one IdP group. Only
valid on a broad user-scoped budget (scope_type = "user" with neither
scope_id nor scope_value), where the counter 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-4053 Target dimension: when set, the budget only fires on requests routed to this MCP server. Requires MCP-capable traffic and is mutually exclusive with the three LLM target dimensions (validated at the handler; the V73 CHECKs are the final safety net).
BCP-2813 Target dimension: matches the user-facing alias from
request.model. Mutually exclusive with the other two.
BCP-2813 Target dimension: when set, the budget only fires on
routes whose provider id matches. May be combined with
target_upstream_model; mutually exclusive with
target_model_alias (validated at the handler; the DB CHECK is
the final safety net).
BCP-2813 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 cap (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.
block, throttle, warn How often a budget's counter resets.
Every period boundary is UTC, with no per-org configuration:
key_suffix formats Utc::now(), so Daily rolls at 00:00 UTC, Weekly
at Monday 00:00 UTC (ISO-8601 week), and Monthly on the 1st at 00:00 UTC.
An admin in a western timezone therefore sees their "daily" cap reset in
the afternoon — [Self::period_end] exists so the UI can say so rather
than leave them to infer it (BCP-4204).
daily, weekly, monthly 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 Optional spending cap in the configured currency. When set, the budget is enforced on estimated cost in addition to tokens.
When the budget was created. Populated on the admin API path (where it
is surfaced in the dashboard); left None on the hot enforcement path
and in fixtures, which don't need it. #[serde(default)] keeps older
payloads without the field deserializing cleanly.
ISO 4217 currency code (e.g. "USD"). Defaults to "USD".
BCP-3814 membership filter: when set, this budget only applies to requesters whose IdP group membership includes this group name.
Only meaningful on a broad user-scoped budget (scope_type == User
with scope_id and scope_value both None), where the counter
qualifier already resolves to the requesting user's id — so one budget
row gives every member of the group their own independent allowance,
and a user joining the group is enforced on their next request with no
admin action. DB-enforced by
token_budgets_member_of_group_requires_broad_user (V65, tightened to
exclude scope_value by V67).
None ⇒ the budget applies org-wide (the pre-BCP-3814 behaviour).
Contrast with a ScopeType::Group budget, which keys its counter by
the group name and so pools one allowance across all members.
BCP-4053 Target dimension: when set, the budget 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 token_budgets_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 budget, which puts the server on
the identity axis and so cannot be combined with a user or group.
As a target it composes freely with any scope, which is what makes
"this user's cap on this server" expressible.
BCP-2813 Target dimension: when set, the budget only fires when the
requested model alias (request.model) equals this value. Mutually
exclusive with the other two target_* fields.
BCP-2813 Target dimension: when set, the budget only fires on routes
whose provider id equals this value. May be combined with
target_upstream_model to cap a single model on this provider;
mutually exclusive with target_model_alias (DB-enforced by
token_budgets_alias_target_exclusive_check).
BCP-2813 Target dimension refining target_provider_id: when set,
the budget only fires on routes whose resolved upstream model name
equals this value (string match). Always set alongside a provider
(V54 CHECK-enforced) — a bare upstream name would string every
provider serving that model into one cap.
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.