Scope Overrides
The deployment has one catalogue of providers and one catalogue of models. An organization does not get a copy of it. It gets the same catalogue with a patch applied — a small record saying only what that scope changes, keyed on the scope and the provider slug.
That distinction is the whole design, and it is worth stating why before the mechanics.
Why a patch and not a copy
Had an override been a copy of the provider, every overriding organization would
get its own provider ID. Model configurations are keyed on
{providerConfigId, modelId} and carry no scope of their own, so the entire
model catalogue would have had to be duplicated beneath each organization, or
become unreachable from it. Routing configurations point at provider IDs too, so
they would have become per-organization as a side effect.
So there is exactly one provider document per slug, exactly one model document
per (provider, model), and a scope adjusts them.
It also means the ceiling is real: an organization cannot add a provider the deployment does not have, and a department cannot reach a provider its organization has turned off. A scope narrows what it inherits; it never widens it.
The two records
| Provider override | Model override | |
|---|---|---|
| Answers | How this scope reaches a provider | Whether this scope may use a model |
| Keyed on | scope + providerSlug | scope + providerSlug + modelId |
| Can set | credential, base URL, connection settings, rate limits, priority, enabled | enabled |
| Carries pricing | No | No |
They are deliberately not one endpoint. They look alike and are not: changing a credential and withdrawing a model are different decisions, and sharing a body shape would mean one payload carrying two meanings and a discriminator to tell them apart.
Neither carries pricing. A rate is a property of the model at the provider, so it lives on the catalogue entry once and does not vary by scope.
Sparse patch semantics
Only the keys you send change. An absent key means inherit.
Absent is not the same as false, and not the same as null:
- A provider override with only
auth.apiKeyset leaves the deployment’s rate limits, priority, timeouts and free-tier configuration exactly as they were. - A model override that is absent means the scope follows whatever the level
above decides.
enabled: falseis a scope deliberately withdrawing a model;enabled: trueis a scope taking one the deployment left off.
The route assigns field by field for the same reason. Replacing the document wholesale would make editing a rate limit silently drop the scope’s stored credential.
Resolution order
At request time the gateway resolves the provider the caller actually gets:
Base catalogue entry │ ▼ organization patch applied (only the fields it sets) │ ▼ department patch applied (only the fields it sets) │ ▼What this request usesThe narrowest scope is written last and wins, matching the settings cascade. Both patches are loaded in one query and then sorted by level explicitly — relying on the driver’s return order would make a department’s patch lose to its organization’s about half the time, intermittently.
If the resolved result is disabled, the request fails with
PROVIDER_NOT_CONFIGURED, including when it was a scope that disabled it. That
is how an organization withdraws a provider from its own people.
Model overrides resolve the same way, and only ever answer whether a model is reachable. The catalogue row itself is never duplicated.
Per-scope credentials
An organization can point a provider at its own key, so its traffic bills to its own account rather than the deployment’s.
The credential is encrypted with the scope bound into the encryption’s
associated data — scopeKind:scopeId. Provider keys on the base catalogue
document are bound to the tenant, and because there is exactly one tenant that
value is a constant, which authenticates nothing: a ciphertext lifted from one
organization’s override would otherwise decrypt cleanly under another’s.
Binding to the scope restores the property.
Two consequences worth knowing:
- A stored key is never returned. The API reports
auth.apiKeySet: trueand nothing else. Whether one is set is the part that tells an operator whether the field is safe to leave alone; the value itself has no reason to leave the server. - Decryption failure is loud. A key that cannot be decrypted raises rather than falling through with ciphertext in the credential field, which would present as a rejected key from the vendor and send someone to rotate a key that was fine.
Departments do not set credentials. They inherit whichever key their organization resolved.
In the admin console
The Providers and models panel appears on both the organization and the department detail pages. It is the same component at both levels, because it is the same rule twice.
Each provider row shows one of four states:
| State | Meaning |
|---|---|
| Inherited — on | No patch here; the deployment has it enabled |
| Inherited — off | No patch here; the deployment has it disabled |
| Turned on here | This scope enabled it |
| Turned off here | This scope disabled it |
Expand a row for its models and, on an organization, its credential field. Rows carrying a decision are highlighted, and Only what this scope changed filters the catalogue down to them — a provider counts as changed if it has a patch of its own or any of its models do.
Inherit again removes the patch and returns the row to whatever the level above decides.
These controls do not save with the surrounding form. Each toggle is its own request against its own record, so a change has either happened or it has not. Putting them behind the drawer’s Save button would imply the whole drawer commits atomically, which it does not.
Endpoints
Provider overrides
| Method | Path | Permission |
|---|---|---|
GET | /api/admin/provider-overrides | providers:read |
PUT | /api/admin/provider-overrides | providers:update |
DELETE | /api/admin/provider-overrides/:scopeKind/:scopeId/:providerSlug | providers:delete |
GET returns the patches visible to the scope the session is currently acting
in — a scoped session sees its own and no one else’s, and a deployment-scoped
session sees all of them. It filters on the acting scope, not on a query
parameter, which is what stops ?scopeId=<someone else's> being a way to read
another organization’s configuration.
PUT creates or updates the patch for one provider in one scope:
{ "scopeKind": "organization", "scopeId": "665f...", "providerSlug": "openai", "auth": { "apiKey": "sk-..." }, "enabled": true}scopeKind is organization or department. There is no system — the
deployment’s own values live on the catalogue entry itself, and a second place
to set the same thing is a second thing to disagree.
The remaining optional keys are baseUrl, priority, auth.headerName,
auth.headerPrefix, connectionConfig (timeout, maxRetries,
initialRetryDelay, maxConcurrency) and rateLimits (requestsPerMinute,
requestsPerDay, tokensPerMinute, tokensPerDay).
DELETE drops the patch entirely and the scope inherits again.
Model overrides
| Method | Path | Permission |
|---|---|---|
GET | /api/admin/model-overrides | models:read |
PUT | /api/admin/model-overrides | models:update |
DELETE | /api/admin/model-overrides/:scopeKind/:scopeId/:providerSlug/:modelId | models:delete |
{ "scopeKind": "department", "scopeId": "665f...", "providerSlug": "openai", "modelId": "gpt-4o", "enabled": false}The model must exist in the catalogue under the provider named. Model IDs are not unique across providers, so checking the ID alone would accept a decision about a model that provider does not serve — stored, applying to nothing, with no indication why.
What is bounded, and how
Two guards do different jobs, and both are needed.
- Permission —
providers:update,models:updateand so on decide whether the caller may perform the action at all. - Scope —
assertWithinScopedecides whether the target is theirs. An organization administrator genuinely holdsproviders:update, so the permission check passes on a body naming another organization’s ID. It is the target that has to be bounded, and a refusal is audited.
Both catalogues are guarded from the other direction too: creating, editing or
deleting a provider or a model requires deployment scope, whatever
permissions the caller holds. Without that, the models:update an organization
administrator needs for its own on/off decisions would also let it rewrite what
every other organization sees.
Cache invalidation
Saving or removing an override invalidates the routing cache immediately — the resolved chain was computed under the old answer. The next request re-resolves.
Related
- Providers — the deployment provider catalogue
- Models — the deployment model catalogue
- Organizations & Departments — where these panels live