no credentials for <provider>.
The screen writes to two scopes and never reads a secret back.
The two scopes and the resolution order
The runtime reads the agent override first and falls back to the
tenant default when the agent has no override for that provider. An
agent with no override is not broken — it simply inherits the org-level
key. In the This agent tab, a provider with no override shows
uses tenant default rather than not set.
Use an override when one agent must talk to a different OpenAI
organisation, a different Deepgram project, or a different ElevenLabs
voice library than the rest of the org.
Both scopes are org-scoped procedures. For the agent scope, the server
additionally reads agent_config and checks that agentId belongs to
orgId before any write or delete; a mismatch returns NOT_FOUND
(Agent not found in this org).
admin.resolveAgentProviderCredentials reports which scope actually
answered, via its source field.
Which providers accept a per-agent override
Only six providers are on the runtime’s per-agent resolve path:openai, anthropic, gemini, deepgram, elevenlabs, ollama
The This agent tab lists exactly these. Every other provider in the
table below is tenant-scope only — set it once under Tenant defaults
and all agents in the org use it.
Required fields per provider
Each provider writes a flatpayload object of string fields. Every
field listed for a provider is required: the screen blocks a save with
<field> required rather than letting a half-filled payload (an Azure
key with no region, a PlayHT key with no user id) be stored and then
fail later on a live call. The server independently validates the
payload with validateProviderPayload and returns BAD_REQUEST if it
does not pass.
The payload shape is shared: the contact-centre credentials screen and
the voice AI credentials screen write the same object for a given
provider. The server persists whatever validated object it is given, so
a client that invented its own field names would store credentials the
runtime cannot read.
Write-only storage and what the mask shows
Keys are write-only from this screen. The secret fields of the payload are sealed under the owning org (sealProviderPayload(payload, orgId))
before being written to Redis — including for a per-agent override,
which is sealed under the org, not the agent, so the engine (which
knows the agent’s org) can open the hydrated config derived from it.
The meta queries return only presence and a mask, never the secret:
admin.getProviderCredentialsMeta/admin.getProviderCredentialsMetaAll— tenant scopeadmin.getAgentProviderOverrideMeta/admin.getAgentProviderOverrideMetaAll— agent scope
…All variants: one batched query per tab returns a
map keyed by provider key, so a page load issues a single request rather
than one per provider row.
Each entry carries configured and, when available, masked. The
screen renders the mask string (first/last characters of the stored
value) as the row’s badge; if masked is an object it renders the first
string value; if it is absent but configured is true it renders
configured. You can recognise which key is stored, but you cannot read
it back. To change a key, save a new one — Replace overwrites the
stored value.
What happens when you save
For a tenant default (admin.setProviderCredentials):
validateProviderPayload(provider, payload)runs. On failure the call returnsBAD_REQUESTwith the validator’s message.- The filtered payload is sealed under
orgId. - It is written to
provider_creds:{orgId}:{provider}.
admin.setAgentProviderOverride):
- The agent is looked up in
agent_configbyid+org_id;NOT_FOUNDif it does not belong to the org. - The payload is validated and sealed under the owning org.
- It is written to
agent_provider_creds:{agentId}:{provider}. - Re-hydration.
hydrateAndPublishrepublishes the agent’s runtime config toagent:{agentId}:config, so the new key is in effect for the next call that originates. This step is best-effort — it is wrapped in a try/catch because theagent_configrow may not exist yet during early setup. A hydration miss is recovered by the next explicitadmin.hydrateAgentor by the next originate. - Audit. An entry is recorded with
action: 'set-credential',resourceType: 'agent_provider_override', andresourceId: "{agentId}:{provider}". Theafterpayload contains only the field names that were written (for example['openai_api_key']) — never the key bytes — so the trail proves a credential changed without exposing it to anyone with audit-read.
admin.hydrateAgent for the affected agent. That procedure returns
ready, missing_providers and hydrated_at_ms, and records an audit
entry with action: 'hydrate' so you can later correlate a save with the
hydration that followed it.
Removing a key or an override
Both removals are confirmed in the UI before they run.admin.removeProviderCredentials deletes
provider_creds:{orgId}:{provider}. Every agent that was relying on that
tenant default now has no credential for that provider unless it carries
its own override.
admin.removeAgentProviderOverride verifies the agent belongs to the org,
deletes agent_provider_creds:{agentId}:{provider}, re-hydrates the agent
(best-effort, as above), and records an audit entry with
action: 'delete-credential'. Removing an override does not remove the
tenant default — the agent falls back to it.
Deleting an agent entirely is handled by admin.unpublishAgent, which
wipes the hydrated config and sweeps the agent’s provider override keys
for every provider, so a future agent that reuses the same id does not
inherit ghost credentials.
Diagnosing missing credentials at hydrate
admin.resolveAgentProviderCredentials is the read-only check behind the
“is this agent ready” warning. It loads the agent’s llm_provider,
asr_provider and tts_provider, resolves credentials for the agent
(override first, tenant default second), and returns:
Only the providers the agent actually selected are required. The
procedure deliberately does not report every unpopulated provider —
telling an Anthropic user to fill in Gemini is noise.
The slot values map to required credential fields as follows:
Note that the Gemini credential is stored under the provider key
gemini, while the agent config slot that requires it is google.
To fix a no credentials for X failure:
- Confirm which providers the agent selected in its config (LLM, ASR, TTS).
- Open Credentials → Tenant defaults and check that the matching
provider row shows a mask rather than
not set. - If the agent should use different keys, set them under This agent — available only for the six overridable providers, and only once an agent is selected.
- Call
admin.hydrateAgent(the screen’s Save path on the agent config, or the explicit hydrate action) and checkreadyandmissing_providersin the response before starting a test call.