Directives
Directives are your team's rules: which models and providers may be used, how fast, and how much may be spent.
Scopes
Set Directives on the team, a project, an environment, or a single key. Owners and admins edit them on a project's Directives page, whose scope switch runs Team, the project, Environments and Keys; under Environments and Keys, open one (say the inference-bureau-api key in the inference-bureau project) to edit its own budgets, limits, model lists and alerts. Each level shows the ceiling it tightens, what it sets, and the effective result, and every change is versioned and recorded in the audit log: a request's receipt records the versions it ran under. Settings the table does not show, such as routing, are edited with Edit as JSON and are kept as they are when you publish from the table.
How scopes combine
- Allowed models and providers are intersected: a model must be allowed at every level. A
vendor/*entry counts as every model of that vendor, so a team'sopenai/*and a project'sopenai/gpt-5allow justopenai/gpt-5. - Numeric limits take the lowest value that applies.
- Every budget applies at once; unused project budget never lets the team exceed its own.
- Each
endUservalue takes the lowest any level sets, and applies to each end user on its own. - Tag budgets are like budgets: each applies at the level that sets it, and a request carrying the tag must fit all of them.
- Denied models and providers add up: a model denied at any level is denied below it, and a denial wins over the allowed lists.
- Aliases merge by name: the most specific level that defines
org/fastdecides what it runs. requireZdrandrequireNoTrainingset at any level apply to everything below it.routing.minPrecisiontakes the highest any level sets.logContentis on only where a level turns it on and none above turns it off;contentRetentionDaystakes the shortest any level sets. WhererequireZdralso applies, nothing is stored.- Where
requireZdrapplies, stored responses (store: trueon/v1/responses) and/v1/responses/compactare refused, and turning it on deletes the responses already stored under that level. - Model fallback is decided from the top: a team that turns it on lets a project turn it off, never the other way round. The most specific level that names
fallbackModelschooses them. - A missing field inherits. An explicitly empty allowlist denies everything.
- Changes apply to new requests. Requests already admitted keep the rules they started with.
Fields
| Field | Effect |
|---|---|
allowedModels | Models that may be requested, as ids or vendor/* for every model of a vendor. See Allowing a vendor. |
allowedProviders | Providers that may serve them. |
deniedModels | Models never used, as ids or vendor/*; a denial wins over allowedModels. See Denied models. |
deniedProviders | Providers never used, whatever allowedProviders says. |
aliases | Names such as org/fast that requests send as the model, each naming a model, a preset or a routing profile. See Model aliases. |
profiles | Routing profiles, keyed by id: a pool of models a profile alias chooses from per request, cheapest or best scoring, and a baseline. See Routing profiles. |
limits | Requests and tokens per minute and concurrent requests (see Rate limits); maxInputTokens and maxOutputTokens; and maxRequestChargeUsd, a ceiling on what one request may reserve. Input over maxInputTokens returns 400 context_limit_exceeded. maxOutputTokens lowers max_tokens silently and is the default when a request sends none. A request whose worst case is over maxRequestChargeUsd returns 403 policy_denied; lower max_tokens to fit. |
budget | monthlyUsd and dailyUsd: spend caps in USD on UTC calendar months and days. A request is admitted only if its reservation fits every budget that applies; otherwise it returns 402 budget_exceeded. Settled charges count, and so do holds of requests still running. |
endUser | Limits, daily quotas and budgets for each of your own end users, and required to refuse requests that name none. See Per end user. |
tagBudgets | Daily and monthly budgets for requests carrying a tag, keyed by the tag. See Tag budgets. |
routing | Whether model fallback is allowed, which models may stand in (fallbackModels, up to five, in order), how many attempts a request may make (1–3), and minPrecision: "fp8" or "bf16" keeps requests off providers that serve a model at lower weight precision. See Routing. |
data | With requireZdr, requests go only to provider endpoints listed upstream as zero data retention, and each one tells the upstream gateway to refuse any other. A model with no such provider returns 403 policy_denied and is left out of /v1/models. Set at any level, it cannot be lifted below. With requireNoTraining, only provider endpoints listed as not training on prompts; see No training. |
alerts | budgetPercentages: the percentages of a budget that send an alert. For the team only, lowBalanceUsd: the available balance below which a low-balance alert is sent. See Alerts. |
Unknown fields are rejected. Amounts are decimal strings, such as "0.50". Changes apply to the next request.
Rate limits
Every team has rate limits, even if nobody has set any. A team without its own values gets the defaults below. Projects, environments and keys have no rate limits until you set them, and all keys share the team's allowance.
| Field | Default (team) | Counts |
|---|---|---|
requestsPerMinute | 600 | Requests admitted per minute. |
tokensPerMinute | 2,000,000 | Tokens reserved per minute: the request's input plus its output budget (max_tokens, or 4,096 if unset, never more than the model's maximum or your maxOutputTokens). |
concurrentRequests | 10 | Requests in flight at once, including streams. |
The per-minute token count works from worst cases. When a request is admitted, its input plus its full output budget is reserved, and whatever goes unused is returned once the response finishes. So a high max_tokens takes up headroom even when the answers are short. Batch jobs, evals and agents that run many calls in parallel usually hit tokensPerMinute or concurrentRequests first.
A request over a limit is refused before it reaches a provider and isn't charged. The response is a 429 with:
error.code:rate_limit_exceeded(requests or tokens) orconcurrency_limit_exceeded.X-RateLimit-Scope: the level whose limit was hit, such asorganizationorkey.X-RateLimit-Dimension:requests,tokensorconcurrency.Retry-After: seconds until the request would fit (not sent for concurrency).
These 429s mean the service is working as configured. To stop seeing them, have an owner or admin raise the limit at the level the header names, or lower your concurrency or max_tokens. A single request that reserves more tokens than tokensPerMinute allows can never be admitted at that limit; the error message says so.
A 429 upstream_rate_limited is different. It means every allowed provider for the model was at capacity after failover (see Routing), and raising your limits won't help. Retry after a short wait. Use the Rate limited view in Logs to see which kind you got.
Per end user
When you serve your own users through one key, name the user on each request (the ib-user header, or user; see End users and tags) and endUser limits each of them on its own, so one heavy user can't use up the team's allowance or budget. End users are counted per environment: ib-user-1042 in production and in development are two end users.
| Field | Limits each end user to |
|---|---|
requestsPerMinute | Requests admitted per minute. |
tokensPerMinute | Tokens reserved per minute, counted as for rate limits. |
concurrentRequests | Requests in flight at once, including streams. |
requestsPerDay | Requests per UTC day. |
tokensPerDay | Tokens per UTC day: the worst case is counted when a request is admitted and what goes unused is given back when it finishes. |
dailyUsd, monthlyUsd | Spend in USD per UTC day and month, held and settled like budget. A request that doesn't fit returns 402 budget_exceeded naming the end user's budget. They send no threshold alerts, one per end user being too many; the first refusal of an end user in a day or month sends one end_user.budget_exhausted webhook. |
required | true refuses a request that names no end user with 400 invalid_request; Auditor's own calls are exempt. Otherwise such requests aren't limited by endUser at all. |
A request over an end user's limit or quota returns 429 rate_limit_exceeded with X-RateLimit-Scope: end_user, X-RateLimit-Dimension set to requests, tokens, daily_requests or daily_tokens, and Retry-After (until the next UTC day for a daily quota). Over concurrentRequests it is 429 concurrency_limit_exceeded with X-RateLimit-Dimension: concurrency and no Retry-After. Other end users, and the team's own limits, are unaffected. They apply on chat completions, messages, responses, embeddings, /v1/evaluate and /v1/suggest. Set them in the End users group of the Directives table, or as endUser in JSON.
Tag budgets
Cap what a feature or job can spend by budgeting the tag its requests carry (the ib-tags header or ib.tags; see Tags). tagBudgets maps a tag to dailyUsd and monthlyUsd, on UTC days and months:
{
"tagBudgets": {
"inference-bureau-search": { "dailyUsd": "20.00", "monthlyUsd": "300.00" }
}
}- A tag budget applies at the level that sets it: on the team it covers every request of the team carrying the tag, on a key only that key's. Budgets for the same tag at several levels all apply.
- A request with several budgeted tags is held against each, and must fit all of them. Requests without the tag, and tags with no budget, are unaffected.
- A request that doesn't fit returns
402 budget_exceedednaming the tag and the level, such astagBudgets["inference-bureau-search"].dailyUsd at project scope. - Tag budgets send
budget.thresholdalerts like other budgets, at the alert percentages of the level that sets them. - Up to 50 tag budgets at each level. Tags follow the request tag rules: letters, digits and
_ . : / -, up to 64 characters.
Add and remove them in the Tag budgets group of the Directives table, or with the management API.
Allowing a vendor
allowedModels takes model ids and vendor/*, the same patterns as deniedModels. anthropic/* allows every Anthropic model, including ones the catalog adds later, where a list of ids allows only those ids.
{
"allowedModels": ["anthropic/*", "openai/gpt-5-mini"]
}- Levels still intersect, as sets of models: team
openai/*with project["openai/gpt-5", "anthropic/claude-haiku-4.5"]allowsopenai/gpt-5; teamopenai/*with projectopenai/*allows every OpenAI model; teamopenai/*with projectanthropic/*allows nothing, and every request returns403 policy_denied. - A level below can narrow a vendor to some of its models, never widen a list of ids into a vendor.
- Denials still win:
allowedModels: ["openai/*"]withdeniedModels: ["openai/gpt-5"]allows every OpenAI model but that one. - In the allowlist drawer, tick
vendor/*under a vendor's heading to allow the whole vendor; its models show as included by the pattern. On a project, environment or key the box shows only for a vendor the level above allows whole. - An entry that is neither an id nor
vendor/*(such asgpt-5or*) is refused when you save.
Model aliases
An alias lets your code name a model by its job instead of its id. Define org/fast once and send it as the model; to move every caller to another model, change the alias, not the code. Aliases resolve before anything else looks at the request, so Directives (allowed and denied lists, requireZdr, requireNoTraining), certification and the model's own limits all apply to the model it names.
{
"aliases": {
"org/fast": "anthropic/claude-haiku-4.5",
"org/smart": {
"model": "anthropic/claude-sonnet-5",
"fallbackModels": ["openai/gpt-4o-mini"],
"params": { "maxOutputTokens": 2048, "reasoningEffort": "low" }
}
}
}- Names are
org/and up to 40 lowercase letters, digits and hyphens. Up to 50 aliases at each level. - Set them on the team or a project, in the Aliases group of the Directives table. An environment or a key can define its own too, with Edit as JSON or the management API. The most specific level's alias replaces one of the same name above it, so a project's replaces the team's for that project's keys.
- A target is a model id, or a preset:
model, up to fivefallbackModels, andparams. An alias may name an embedding model for/v1/embeddings. paramsare defaults that fill only what the request leaves out; a value the request sends wins.temperatureandtopPapply on every inference endpoint;maxOutputTokensismax_tokenson chat and messages andmax_output_tokenson responses (it can supply themax_tokensthat/v1/messagesotherwise requires);reasoningEffort(minimaltohigh) isreasoning.efforton chat and responses. YourmaxOutputTokenslimit still caps the result.- A preset's
fallbackModelsreplacerouting.fallbackModelsfor its requests, and turn model fallback on unless a level setrouting.allowModelFallbacktofalse. Each fallback model must pass your Directives too. - A name no level defines returns
400 invalid_request.GET /v1/modelslists the key's aliases withowned_by: "org"andalias_for. - The receipt keeps what you sent:
model_requestedisorg/fast, andaliasnames the model it resolved to and the preset defaults it filled. - With mixed-model sessions, suggestions are made for the model the alias names, and accepting one swaps that model for the turn; the alias itself is unchanged. When an alias's turns keep accepting the same cheaper model, the Savings page offers to point the alias at it; see Pointing an alias at a suggestion.
- An alias can also leave the choice of model to a routing profile:
{ "profile": "cheap-summaries" }.
With the management API, set one alias with the path aliases.org/fast in set, such as {"aliases.org/fast": "anthropic/claude-haiku-4.5"}; null removes it.
Retiring an alias
To move callers off an alias without breaking them on the day, give it a deprecatedAt date and, usually, a replacedBy: another alias or a model id. Both go in the alias's preset form.
{
"aliases": {
"org/fast": { "model": "anthropic/claude-haiku-4.5", "deprecatedAt": "2026-12-01", "replacedBy": "org/fast-v2" },
"org/fast-v2": "openai/gpt-5-mini"
}
}- Before the date, requests run as before, and every response carries
ib-alias-deprecated: 2026-12-01; replaced-by=org/fast-v2(just the date when there is no replacement). The request's Logs page shows the alias as deprecated. - From 00:00 UTC on the date, a request naming the alias runs as its replacement, with that alias's preset, and is recorded as a migration: the receipt's
aliaskeeps the name you sent and addsmigrated_to. Responses keep the header. A replacement that is itself retired is followed to the first current one, up to five deep. - Without a replacement, from the date the alias returns
400 invalid_requestnaming the date. So does a replacement no level defines, or replacements that loop. deprecatedAtis aYYYY-MM-DDdate in UTC;replacedByneeds it, and can't be the alias itself. A replacement model must be certified, and Directives apply to it like any alias target.- Set them under Deprecation in the alias's row of the Aliases group; the Effective column says when it retires and where requests go.
Routing profiles
A routing profile chooses the model for each request, so callers can send an alias without knowing which model is cheapest, or best, today. Define the profile under profiles, and point an alias at it with { "profile": "<id>" }.
{
"profiles": {
"cheap-summaries": {
"objective": "cost",
"pool": ["openai/gpt-5-mini", "google/gemini-3-flash", "anthropic/claude-haiku-4.5"],
"baseline": "anthropic/claude-sonnet-5",
"qualityFloor": 0.8,
"taskClass": "summarization"
}
},
"aliases": {
"org/summarize": { "profile": "cheap-summaries" }
}
}- Each pool model is checked as if the request named it: allowed and denied lists,
requireZdrandrequireNoTraining, the tools, structured output and streaming the request uses, its context window andmaxRequestChargeUsd. Models that can't take the request are left out. objectivecostpicks the cheapest of the rest, estimated from list prices for the request's input and a typical reply.qualitypicks the best evaluated score, cheaper first on a tie.qualityFloor(0.5 to 0.99) keeps only models whose evaluated score reaches it. Scores are read fortaskClass, or the request'sib.taskwhen it sends one (see mixed-model sessions for the task classes); without either, a model's mean across task classes. Withqualityor a floor, a model with no evaluation is left out.- The chosen model runs with provider failover, and the rest of the ranking, then the
baseline, become the request's fallback models, unless a level setrouting.allowModelFallbacktofalse. When no pool model qualifies, the request runs on the baseline. No classifier runs and nothing extra is billed. - Profile aliases serve chat completions, messages and responses.
/v1/embeddingsand/v1/suggestreturn400for them, and so does an alias naming a profile no level defines. Mixed-model suggestions don't apply to their requests. - The receipt's
routing_modeisprofile,model_requestedis the alias, andselectionlists each pool model with why it was chosen or left out. Logs show the profile on the request's page. - Savings counts these requests against the baseline, in a row per profile.
- Profile ids are up to 40 lowercase letters, digits and hyphens; up to 20 profiles at each level and 10 models in a pool, all certified chat models. Profiles merge by id like aliases, so a project's profile replaces the team's of the same id. A profile is versioned with its level's Directives.
- Edit them in the Routing profiles group of the Directives table, and set an alias to Routing profile in its row. With the management API, set one with the path
profiles.cheap-summariesinset;nullremoves it.
Denied models and providers
Block one model while allowing its family, or keep a provider out entirely. deniedModels takes model ids or vendor/* for every model of a vendor; deniedProviders takes provider ids.
{
"allowedModels": ["openai/gpt-4o", "openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"],
"deniedModels": ["openai/gpt-4o"],
"deniedProviders": ["azure"]
}- A denial wins over an allowed list, and denials from every level add up, so a project or key can only deny more.
- A denied model returns
403 policy_deniedand is left out of/v1/models. A denied provider is skipped in failover, as if it weren't allowed. - Set them in the Access group of the Directives table, comma-separated.
No training
Set data.requireNoTraining (Require no training in the Data group) to send requests only to provider endpoints listed upstream as not training on request content. It narrows the pool less than requireZdr, which also rules out providers that keep data for a while without training on it; set both to require both.
- It applies on chat completions, messages, responses, embeddings,
/v1/evaluate,/v1/suggestand the mixed-model classifier, and suggestions only name models that meet it./v1/messages/count_tokensanswers with a local estimate instead of sending the prompt out. - A model with no such provider returns
403 policy_deniedsaying so, and is left out of/v1/models. A provider that qualifies is still subject to your allowed and denied lists. - Set at any level, it applies to everything below and can't be lifted there. The Models catalog shows each provider's training policy.
- Each request also tells the upstream gateway to refuse a provider that may train on prompts, the way
requireZdrdoes for retention, so a provider that changes its terms is refused upstream even before Inference Bureau's own list catches up. - That list is checked against the current upstream listing every hour, for
requireZdrtoo. A provider the listing stops showing as not training (or as zero data retention) is dropped from routing within the hour; a newly listed one is used only once Inference Bureau has recorded it.
Alerts
The Alerts group of the Directives table sets when Inference Bureau tells you about spend. Alerts are sent by email to the team's owners, admins and billing contacts, and to your endpoints; see Alerts and webhooks.
| Setting | Field | Where | Effect |
|---|---|---|---|
| Budget alerts at | alerts.budgetPercentages | Every level | Percentages of a monthly or daily budget that send a budget.threshold alert, such as 50, 80, 95, 100 (the default). A budget follows the most specific level that sets them, so a key's budget can alert at other points than the team's. Type none to send no budget alerts; leave it blank to inherit. |
| Low balance alert | alerts.lowBalanceUsd | Team only | Sends balance.low once when the available balance (posted minus reserved) falls below this many dollars, and again after the next successful top-up. Off until set. The balance is the team's, so lower levels cannot set it. |
| Pause keys on unusual spend | alerts.pauseOnAnomaly | Every level | Checked hourly. A key whose last hour of spend was at least $1 and five times its average hour over the week before (counting the keys it was rotated from) is paused: its requests get 401 invalid_api_key until an admin resumes it on the API Keys page or with POST /v1/keys/:id/resume. Sends key.paused, and the alert email says which key. Off by default; the most specific level that sets it decides, so one key can opt out of its team's setting. Auditor's own key is never paused; it has its own caps. |
Log content
Off by default. Turn on Log content in the Data group (data.logContent) to keep what a request sent and what came back, so you can debug a bad completion from its request's page in Logs. Only requests admitted after the change are logged.
| Endpoint | What is kept |
|---|---|
/v1/chat/completions, /v1/messages, /v1/responses | The request body exactly as you sent it, and the response: the JSON returned, or for a stream the output assembled from the events relayed to you (text, tool calls, reasoning and usage). Capturing a stream never delays or changes it. |
/v1/embeddings | The request body, and the response with each vector replaced by its length. |
| A request that fails after admission | The request body and the error returned. |
- Headers are never kept, so neither is your API key. Each body is kept up to 256 KB and marked truncated beyond that.
- Content is encrypted at rest (AES-256-GCM) under a key held only by Inference Bureau's API and console, bound to your team and the request, and is stored after the response is sent, so it adds no latency. If storing fails, the request is unaffected.
contentRetentionDaysis7(the default) or30. The shortest any level sets applies, so a key can keep content for less time than its team, never more. Content is deleted when it expires, within minutes.- Turning Log content off at a level, or clearing it, deletes the content already stored under that level when you publish. Turning on
requireZdrdoes the same. Shortening retention from 30 to 7 days applies to what is already stored. - Zero data retention wins. A level can't set both
logContentandrequireZdr(the save is refused with400 invalid_request); when they meet from different levels, such as ZDR on the team and Log content on a key, nothing is stored and the Directives table says so. - Only owners and admins can read content, and each time they open it the audit log records
request.content.view. Members see that a request has content, not the content. It is not returned byGET /v1/requests/:idorGET /v1/usage. - A key with the Read logged content permission, off by default, can read its own project's content with
GET /v1/requests/:id/content; each read is audited asrequest.content.viewunder the key.
{
"data": { "logContent": true, "contentRetentionDays": 7 }
}Example
{
"allowedModels": ["openai/gpt-4o-mini", "anthropic/claude-sonnet-5"],
"allowedProviders": ["openai", "anthropic", "bedrock"],
"deniedModels": ["openai/gpt-4o"],
"aliases": {
"org/fast": "anthropic/claude-haiku-4.5",
"org/smart": { "model": "anthropic/claude-sonnet-5", "fallbackModels": ["openai/gpt-4o-mini"], "params": { "reasoningEffort": "low" } }
},
"limits": {
"requestsPerMinute": 600,
"tokensPerMinute": 2000000,
"concurrentRequests": 10,
"maxOutputTokens": 4096,
"maxRequestChargeUsd": "0.50"
},
"budget": { "monthlyUsd": "500.00" },
"endUser": { "requestsPerMinute": 20, "concurrentRequests": 2, "tokensPerDay": 200000, "dailyUsd": "1.00" },
"tagBudgets": {
"inference-bureau-search": { "dailyUsd": "20.00", "monthlyUsd": "300.00" },
"nightly-digest": { "monthlyUsd": "50.00" }
},
"routing": { "allowModelFallback": false, "fallbackModels": [], "maxApplicationAttempts": 3 },
"alerts": { "budgetPercentages": [50, 80, 95, 100], "lowBalanceUsd": "25" }
}GET /v1/models returns exactly the models a key may call after every scope is applied.