Upstreams
An upstream is an LLM provider that AIControls routes traffic to on behalf of your developers. You configure API keys once — developers never touch them directly.
How upstreams work
When a developer's AI tool makes a request, AIControls:
- Authenticates the developer using their personal access token
- Evaluates policies against the request
- Forwards the request to the matching upstream using the upstream's API key
- Returns the response and logs the full audit event
Developers connect with workspace credentials — they don't need Anthropic or OpenAI accounts.
Supported providers
AIControls can route to four classes of upstream provider, each with its own identity and authentication model:
- Anthropic — authenticated with a single
sk-ant-...API key. AIControls supports all Claude models, including claude-opus-4-8, claude-sonnet-4-6, and claude-haiku-4-5. New models appear automatically as Anthropic releases them, so you don't need to update an upstream definition when a new model ships. - OpenAI — authenticated with a single
sk-...API key. An OpenAI account can have multiple organizations, so an upstream optionally carries an organization ID to disambiguate which org's models and billing a request should use. - Azure OpenAI — identity is resource-based rather than account-based: a request targets a specific Azure resource endpoint and a named deployment within it (rather than a model ID directly), and every request is versioned against a specific Azure API version.
- Amazon Bedrock — authenticated with AWS credentials (an access key/secret pair, or an assumed role) rather than a vendor-issued API key, and scoped by AWS region. Because Bedrock authentication is just AWS IAM, you can scope a credential tightly — for example, a dedicated IAM user with
bedrock:InvokeModelpermission limited to specific model ARNs — instead of using a single all-or-nothing key.
For the exact fields, console steps, and config values needed to add each provider, see Configure an Upstream.
Routing between upstreams
When you configure multiple upstreams, AIControls uses static routing — each upstream declares which requests it should receive, and the first match wins. Routing decisions are computed from this static configuration, but a policy can override the result for an individual request — see Overriding routing with a policy below.
Each upstream can match on any combination of three signals (all are AND-ed):
| Signal | Field | How it works |
|---|---|---|
| OIDC groups | groups | Routes requests from members of specific IdP groups. Use ["*"] as a catch-all that matches any caller. |
| Model globs | match.models | Routes by the model name in the request. Supports prefix, suffix, and substring wildcards — e.g. claude-*, *gpt-4*. |
| Identity labels | match.labels | Routes by identity source set server-side during token resolution. Common key: source with values pat, github, or passthrough. Cannot be forged by clients. |
Selection order
Upstreams are evaluated in order of specificity — more specific predicates always beat catch-alls, regardless of the order you define them in settings:
- Identity labels present — highest priority
- Model globs present
- Non-wildcard groups present
- Catch-all (
groups: ["*"]or no predicates) — lowest priority
Among upstreams with equal specificity, use the priority field as a numeric tiebreaker — lower value is evaluated first.
If specificity and priority are still tied, AIControls uses the requested model to break the tie — the tied upstream whose provider (Anthropic, OpenAI, or Google) matches the model wins. If the model's family can't be determined, or every tied upstream is equally unopinionated (e.g. Azure, LiteLLM, custom deployments), routing falls back to a fixed, deterministic order. If the model's family is known and no single tied upstream confidently matches it, AIControls refuses to guess — the request fails immediately instead of silently going to the wrong provider.
Routing rules are configured per upstream in Settings → Upstreams → Edit upstream. Use the Priority field as a numeric tiebreaker when two upstreams have the same specificity — lower value is evaluated first.
Overriding routing with a policy
The upstream this static routing selects is exposed to every policy as object.defaults.routing.upstreamName (with object.defaults.routing.matchedOn explaining why it was picked). A MutatingPolicy with patchType: Defaults can override this per request — for example, routing a call to a data-retention-compliant upstream once content scanning flags PII in the prompt. See Defaults mutations for the worked example and the override's fail-open behavior (an unknown upstream name is ignored, never a denial).
This override is evaluated on every request path, so it also applies to policies that only intend to affect LLM traffic — routing has no meaning for a plain MCP tool call and the override namespace is simply absent there.
The routing dry-run API (POST /api/v1/routing/resolve) previews only the static selection described above — it does not simulate a patchType: Defaults override a policy might apply at request time.
See also
For step-by-step setup instructions for each provider, see Configure an Upstream.