Connect a model provider

Authenticate model calls

The edge authenticates provider calls. Nodes use a local proxy and do not receive provider keys.

For: tenant administrators and cost owners.

The edge authenticates provider calls. Nodes use a local proxy and do not receive provider keys.

How calls are signed

Most providers take a key in a header, Bedrock's and Azure's API keys included: that is auth.kind: header, the default. Provider records also support the standard authentication methods below. The edge authenticates each call; the key never leaves the edge, and neither does a token it gets for one.

auth.kindWhat the edge doeskey_secret holdsAlso set
headerSends the key in auth.header after auth.prefixThe keyauth.header, auth.prefix
oauth2_client_credentialsTrades the client's ID and secret for a bearer token at auth.token_url (RFC 6749, 4.4), as Microsoft Entra does for Azure OpenAI and AI FoundryThe client secretauth.token_url, auth.client_id, auth.scope; auth.client_auth post (the default) or basic
oauth2_jwt_bearerSigns an RS256 assertion with a service account's key and trades it for a bearer token (RFC 7523), as Google does for Vertex AIThe service account's key file (JSON), or a PEM key with auth.client_id as its issuerauth.token_url, auth.scope; auth.audience (the token URL when empty)
sigv4Signs every call with AWS Signature Version 4 over its method, path, headers and body, as Bedrock's runtime takes itThe secret access keyauth.service (bedrock), auth.region, auth.access_key_secret (the access key ID's secret), auth.session_token_secret for temporary credentials
federatedExchanges the calling agent's own token from the tenant's issuer at the cloud for a credential that lives an hour (cloud federation): an AWS session signs SigV4, a Google or Entra token goes in as a bearer tokenNothing: a federated record stores no key, and one with key_secret is refusedauth.cloud (aws, gcp or entra); AWS: auth.role, auth.region, auth.service; Google: auth.workload_provider, auth.service_account, auth.scope; Entra: auth.directory, auth.client_id, auth.scope; auth.audience when the cloud's trust names another

A token goes in Authorization: Bearer unless auth.header says otherwise. Each edge keeps a tenant's token for its provider for as long as the token endpoint says it lives, mints a new one five minutes before it ends, and never logs it, writes it down or sends it to a node.

A token endpoint's answer must be a bearer token of plain characters, or the call fails with 502 oz0_token and the endpoint's error code, never its description. Token requests follow no redirect, and a token URL is https, or http on the edge's own machine only. A tenant's own record dials its token URL under the same rule as its base URL: public addresses only, unless a cluster admin marks it --internal.

Presets set the rest:

PresetKindSet yourself
vertexoauth2_jwt_bearer at https://oauth2.googleapis.com/token, scope cloud-platform; Claude in anthropic-messages through rawPredict and streamRawPredict, Gemini through generateContent; the model is in the path--base-url https://<region>-aiplatform.googleapis.com, --region, the key file in GOOGLE_SERVICE_ACCOUNT_KEY
azure-openai-entraoauth2_client_credentials, scope https://ai.azure.com/.default; openai-chat and openai-responses--base-url https://<resource>.openai.azure.com/openai, --token-url https://login.microsoftonline.com/<directory>/oauth2/v2.0/token, --client-id, --region, AZURE_CLIENT_SECRET
azure-foundry-entraThe same, for Claude in AI Foundry, in anthropic-messages--base-url https://<resource>.services.ai.azure.com/anthropic, the token URL, client and region
bedrocksigv4 for bedrock; Claude through InvokeModel and its stream, and the OpenAI-compatible chat completions; the model is in the path--base-url https://bedrock-runtime.<region>.amazonaws.com, --aws-region, --region, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
bedrock-keyA Bedrock API key in Authorization: BearerThe base URL and AWS_BEARER_TOKEN_BEDROCK
bedrock-federatedfederated with AWS, SigV4 for bedrock; no stored keyThe base URL, --aws-role, --aws-region, --region
vertex-federatedfederated with Google Cloud, scope cloud-platform; no stored keyThe base URL, --gcp-workload-provider, --gcp-service-account if any, --region
azure-openai-federatedfederated with Entra, scope https://ai.azure.com/.default; openai-chat and openai-responses; no stored keyThe base URL, which the servers must name with --cloud-endpoint entra-resources= (Cloud federation), --entra-directory, --client-id, --region
azure-foundry-federatedThe same, for Claude in AI Foundry, in anthropic-messagesThe base URL, directory, client and region
Terminal
oz0 secret set GOOGLE_SERVICE_ACCOUNT_KEY --from-file vertex-sa.json \
  --host europe-west1-aiplatform.googleapis.com   # it mints tokens at oauth2.googleapis.com, for this address only
oz0 provider set vertex --preset vertex --base-url https://europe-west1-aiplatform.googleapis.com --region eu
printf %s "$AWS_ACCESS_KEY_ID" | oz0 secret set AWS_ACCESS_KEY_ID --host bedrock-runtime.eu-north-1.amazonaws.com
printf %s "$AWS_SECRET_ACCESS_KEY" | oz0 secret set AWS_SECRET_ACCESS_KEY --host bedrock-runtime.eu-north-1.amazonaws.com
oz0 provider set bedrock --preset bedrock --base-url https://bedrock-runtime.eu-north-1.amazonaws.com \
  --aws-region eu-north-1 --region eu
oz0 provider show bedrock

A federated record's credential names the agent whose call it is, so it is only for agents' model calls: the edge takes the agent from the call's binding, and a call that names none (an operator's, say) is refused with 403 oz0_federation. The edge keeps a job's credential for its next calls until a few minutes before it ends; each exchange is audited as token.exchanged. The key-based presets stay for clusters whose issuer no cloud can reach.

The gateway preserves each provider's API format and meters calls in that format while enforcing the tenant's controls. Through Vertex and Bedrock that is Anthropic's Messages (and OpenAI's chat completions on Bedrock), metered as they are elsewhere; Bedrock's streamed answers come in AWS's binary event-stream framing, which the gateway passes on byte for byte and reads only to count tokens.

A call in a format it cannot meter, such as Bedrock's Converse API, is refused with 400 oz0_unmetered: no setting lets calls past the meter.

A service account or an AWS access key reaches far more than models, so a record that signs with one (oauth2_jwt_bearer, sigv4, or federated with AWS or Google), or whose base URL is Vertex AI's or Bedrock's, passes only the model calls the gateway meters, and refuses everything else with oz0_unmetered before it gets a token or signs anything:

ThroughPassedRefused, for example
Vertex AI.../publishers/anthropic/models/<model>:rawPredict and :streamRawPredict; .../publishers/google/models/<model>:generateContent, :streamGenerateContent and :countTokens; .../endpoints/openapi/chat/completionsother publishers' models (publishers/meta/...:rawPredict), customJobs, batchPredictionJobs, deployed endpoints' :predict
Bedrockmodel/<id>/invoke and invoke-with-response-stream for Anthropic's models (anthropic.*, a cross-region profile such as eu.anthropic.*, or a foundation model's or system inference profile's ARN); openai/v1/chat/completionsother vendors' models (meta.*, amazon.*, cohere.*), Converse, async-invoke, guardrails, an application inference profile's or a provisioned model's ARN, which does not say its model

Any provider's key reaches more than its models, so on every record a call other than a GET that is no model call in a format the gateway meters is refused with oz0_unmetered too: a batch (v1/messages/batches, v1/batches), a file, a fine-tuning job, OpenAI's images, audio and moderations, or Gemini's embedContent and batchEmbedContents would run at $0, and a realtime session's key would leave the edge.

Messages, chat completions, embeddings, the Responses API and Gemini's generateContent are metered and pass. So is a Responses API call with "background": true, whose answer comes later and elsewhere. A GET, such as listing a provider's models, passes. A token count (messages/count_tokens, :countTokens) is free at the provider and reserves nothing on budgets.

In a Vertex AI path, the project, the location and the model may hold only letters, digits, ., _, @ and -. A path with any escape in it, such as an encoded ?, # or /, is refused. The gateway checks the path exactly as it sends it upstream.

Agents in Pydantic AI call a record's first format at <base_url>/v1/..., so they reach Azure and AI Foundry; Vertex's and Bedrock's own paths are for harness agents and SDKs that speak them, such as Claude Code pointed at the node's proxy with its Vertex or Bedrock sign-in skipped.

Two tenants, one key

Provider cache isolation follows the provider's account, project or workspace rules, which may span several API keys. For example, Claude caches are isolated by workspace on the Claude API and by organization on Bedrock and Google-managed Claude; cache hits require an identical prompt prefix. See Claude's cache storage and sharing rules.

Orchestrator Zero detects identical credentials reused between tenants. This check does not establish whether different keys share a provider cache. Each time an edge opens a tenant's key, it records a keyed hash of it (an HMAC under a key derived from the cluster's secrets key, which only the edges hold; never the key itself). A key that two tenants used in the last 30 days is a finding:

  • for cluster admins, in oz0 doctor (shared provider key, naming the tenants and their providers) and on the Models page;
  • for each tenant's admins, in oz0 doctor --tenant <t> and on the Models and Credentials pages, without naming the other tenant: another tenant of this cluster uses the same key; prompt caches may be shared.

The tenant's other members see the providers without the note.

Give each tenant credentials for its own provider isolation boundary, such as a separate project or workspace. A different key in a shared workspace may not separate caches. A pool's members are hashed one by one, as <provider>/<member>, so the finding names the member whose key is shared.

Why gateway provider keys stay on the edge

Each node runs a local LLM proxy. The agent runtime and plugins that use it see ANTHROPIC_BASE_URL and OPENAI_BASE_URL pointing at it, with a per-start token as their API key. Unmodified SDKs work: the proxy forwards calls to the edge with the node's certificate, and the edge keeps the gateway provider's key. When an edge cannot be reached, the proxy tries the next one; a call that an edge has already taken is never sent to a second edge, so a provider never runs it twice, and the activity's own retry decides what happens next.

Because the gateway preserves the provider's API format, provider features such as prompt caching and extended thinking work without translation. Its anthropic-* request headers, betas included, reach the provider, and the provider's rate-limit and retry headers come back: retry-after, retry-after-ms, x-should-retry, anthropic-ratelimit-* and x-ratelimit-*. The gateway follows no redirect: a provider's 307 or 308 comes back as its answer, so the tenant's key never goes on to the host it names.

This protection applies to gateway provider keys. Remote harnesses receive their provider credentials on the node and send requests to the provider themselves.

Why nodes never see keys: Gateway provider keys.

Copyright © 2026