Directives
BetaWe just launched Apps: hosted models, actions and pages, on the same bill as your inference.Read the Apps docs
Gateway/Governance/Directives

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's openai/* and a project's openai/gpt-5 allow just openai/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 endUser value 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/fast decides what it runs.
  • requireZdr and requireNoTraining set at any level apply to everything below it. routing.minPrecision takes the highest any level sets.
  • logContent is on only where a level turns it on and none above turns it off; contentRetentionDays takes the shortest any level sets. Where requireZdr also applies, nothing is stored.
  • Where requireZdr applies, stored responses (store: true on /v1/responses) and /v1/responses/compact are 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 fallbackModels chooses 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

FieldEffect
allowedModelsModels that may be requested, as ids or vendor/* for every model of a vendor. See Allowing a vendor.
allowedProvidersProviders that may serve them.
deniedModelsModels never used, as ids or vendor/*; a denial wins over allowedModels. See Denied models.
deniedProvidersProviders never used, whatever allowedProviders says.
aliasesNames such as org/fast that requests send as the model, each naming a model, a preset or a routing profile. See Model aliases.
profilesRouting 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.
limitsRequests 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.
budgetmonthlyUsd 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.
endUserLimits, daily quotas and budgets for each of your own end users, and required to refuse requests that name none. See Per end user.
tagBudgetsDaily and monthly budgets for requests carrying a tag, keyed by the tag. See Tag budgets.
routingWhether 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.
dataWith 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.
alertsbudgetPercentages: 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.

FieldDefault (team)Counts
requestsPerMinute600Requests admitted per minute.
tokensPerMinute2,000,000Tokens 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).
concurrentRequests10Requests 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) or concurrency_limit_exceeded.
  • X-RateLimit-Scope: the level whose limit was hit, such as organization or key.
  • X-RateLimit-Dimension: requests, tokens or concurrency.
  • 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.

FieldLimits each end user to
requestsPerMinuteRequests admitted per minute.
tokensPerMinuteTokens reserved per minute, counted as for rate limits.
concurrentRequestsRequests in flight at once, including streams.
requestsPerDayRequests per UTC day.
tokensPerDayTokens per UTC day: the worst case is counted when a request is admitted and what goes unused is given back when it finishes.
dailyUsd, monthlyUsdSpend 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.
requiredtrue 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_exceeded naming the tag and the level, such as tagBudgets["inference-bureau-search"].dailyUsd at project scope.
  • Tag budgets send budget.threshold alerts 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"] allows openai/gpt-5; team openai/* with project openai/* allows every OpenAI model; team openai/* with project anthropic/* allows nothing, and every request returns 403 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/*"] with deniedModels: ["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 as gpt-5 or *) 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 five fallbackModels, and params. An alias may name an embedding model for /v1/embeddings.
  • params are defaults that fill only what the request leaves out; a value the request sends wins. temperature and topP apply on every inference endpoint; maxOutputTokens is max_tokens on chat and messages and max_output_tokens on responses (it can supply the max_tokens that /v1/messages otherwise requires); reasoningEffort (minimal to high) is reasoning.effort on chat and responses. Your maxOutputTokens limit still caps the result.
  • A preset's fallbackModels replace routing.fallbackModels for its requests, and turn model fallback on unless a level set routing.allowModelFallback to false. Each fallback model must pass your Directives too.
  • A name no level defines returns 400 invalid_request. GET /v1/models lists the key's aliases with owned_by: "org" and alias_for.
  • The receipt keeps what you sent: model_requested is org/fast, and alias names 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 alias keeps the name you sent and adds migrated_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_request naming the date. So does a replacement no level defines, or replacements that loop.
  • deprecatedAt is a YYYY-MM-DD date in UTC; replacedBy needs 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, requireZdr and requireNoTraining, the tools, structured output and streaming the request uses, its context window and maxRequestChargeUsd. Models that can't take the request are left out.
  • objective cost picks the cheapest of the rest, estimated from list prices for the request's input and a typical reply. quality picks 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 for taskClass, or the request's ib.task when it sends one (see mixed-model sessions for the task classes); without either, a model's mean across task classes. With quality or 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 set routing.allowModelFallback to false. 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/embeddings and /v1/suggest return 400 for 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_mode is profile, model_requested is the alias, and selection lists 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-summaries in set; null removes 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_denied and 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/suggest and the mixed-model classifier, and suggestions only name models that meet it. /v1/messages/count_tokens answers with a local estimate instead of sending the prompt out.
  • A model with no such provider returns 403 policy_denied saying 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 requireZdr does 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 requireZdr too. 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.

SettingFieldWhereEffect
Budget alerts atalerts.budgetPercentagesEvery levelPercentages 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 alertalerts.lowBalanceUsdTeam onlySends 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 spendalerts.pauseOnAnomalyEvery levelChecked 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.

EndpointWhat is kept
/v1/chat/completions, /v1/messages, /v1/responsesThe 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/embeddingsThe request body, and the response with each vector replaced by its length.
A request that fails after admissionThe 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.
  • contentRetentionDays is 7 (the default) or 30. 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 requireZdr does 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 logContent and requireZdr (the save is refused with 400 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 by GET /v1/requests/:id or GET /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 as request.content.view under 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.