Choose a credential mode
SetcredentialMode when you create or update an organization LLM provider:
sharedstores one provider credential. Every granted member receives that credential from the connect route.per_memberresolves a separate binding for the calling organization member. The provider can be granted before a binding exists.
POST /v1/llm-providers:
x-openwork-org-id header on these requests.
Member flow
A granted member can manage only their own write-only material:PUT /v1/llm-providers/:id/my-credentialwith exactly one of{ "apiKey": "..." }or{ "apiKeys": { "ENV_NAME": "..." } }DELETE /v1/llm-providers/:id/my-credentialGET /v1/llm-providers/:id/connectto obtain the provider configuration and their resolved credential
200 for a granted member. When the member has no active binding, it keeps the standard provider payload shape, leaves both credential fields null, and reports the member-specific state:
memberCredential.state is one of missing, active, blocked, stale, or error. An active response carries that member’s resolved apiKey or apiKeys; every other state carries null credentials. The missing or blocked state is payload data rather than an error status because published desktop clients fail the whole provider sync on any non-OK connect response. Older clients already degrade safely by skipping a provider whose connect payload has no usable credential.
Admin and provisioner flow
Organization owners and admins can operate a central provisioner with these routes:GET /v1/llm-providers/:id/member-credentialslists every granted membership’s state, version, and external identifiers. It never returns credential material.PUT /v1/llm-providers/:id/member-credentials/:orgMembershipIdstores one member’s credential. The body acceptsapiKeyorapiKeys, plus optionalexternalPrincipalId,externalCredentialId, andexpectedVersion.POST /v1/llm-providers/:id/member-credentials/:orgMembershipId/blockmarks an existing binding blocked.
expectedVersion when multiple provisioner workers may update the same binding. A mismatch returns HTTP 409 with { "error": "version_conflict" }.
A blocked binding is admin-owned. A member cannot overwrite or delete it: member writes and deletes return HTTP 409 with { "error": "credential_blocked" }. An admin PUT is the explicit unblock and replacement path.
LiteLLM example
The runnable example atexamples/litellm-per-member-keys uses LiteLLM virtual keys. Configure the Den and LiteLLM URLs, admin tokens, provider ID, and model IDs listed in its README, then run:
GET /v1/llm-providers?scope=manageable, verifies that it is a custom per_member provider, then queries LiteLLM GET /model_group/info with the master key. Every configured model must have an exact model_group match and finite, positive max_input_tokens and max_output_tokens. The provisioner uses those facts to update each Den model’s context, input, and output limits. When present, it maps LiteLLM’s function-calling, reasoning, vision, response-schema, and temperature facts to tool_call, reasoning, attachment, structured_output, and temperature. The replacement preserves the complete provider config, existing model fields, credential mode, and all member/team access.
This step fails closed before key creation if metadata or limits are missing. It never guesses a token limit or falls back to a generic value. The full-replacement Den PATCH omits apiKey and apiKeys, so Den preserves the write-only stored credential, and the example skips the PATCH entirely when the provider is already synchronized.
It then reconciles missing member credentials:
token_id that can address the virtual key without retaining its plaintext value. The example stores that identifier in externalCredentialId; Den’s admin list can return it safely to the provisioner later.
The /model_group/info call and field mapping are deliberately implemented in this LiteLLM-specific example. Den core remains vendor-neutral: its provider PATCH and per-member credential APIs accept the resulting generic model configuration without hardcoding LiteLLM behavior.
Offboard in the safe order
Always revoke or block the upstream credential before removing its local materialization. If the upstream call fails, leave the Den binding active so the failure remains visible and retryable rather than reporting a false local block. For the example:- Read the member’s
externalCredentialIdfromGET /v1/llm-providers/:id/member-credentials. - Call LiteLLM
POST /key/blockwith{ "key": "<token_id>" }and verify success. - Call Den
POST /v1/llm-providers/:id/member-credentials/:orgMembershipId/block. - Verify the member’s connect request returns HTTP
200, null credentials, andmemberCredential.state: "blocked".
node provision.mjs offboard <orgMembershipId> to perform that sequence. The integration is proved against a real LiteLLM database and a cold Den by evals/specs/litellm-per-member-credentials.e2e.test.ts.