Skip to main content
Per-member credential bindings let an organization grant one managed LLM provider to many people without sharing one upstream key. Use this when an external gateway, such as LiteLLM, issues a distinct virtual key for each member. This page describes an API integration pattern. LiteLLM provisioning and metadata discovery are not built into Den; run your own provisioner against Den’s generic provider and credential-binding routes plus the LiteLLM API.

Choose a credential mode

Set credentialMode when you create or update an organization LLM provider:
  • shared stores one provider credential. Every granted member receives that credential from the connect route.
  • per_member resolves a separate binding for the calling organization member. The provider can be granted before a binding exists.
Create a per-member custom provider with POST /v1/llm-providers:
Send the organization bearer token and 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-credential with exactly one of { "apiKey": "..." } or { "apiKeys": { "ENV_NAME": "..." } }
  • DELETE /v1/llm-providers/:id/my-credential
  • GET /v1/llm-providers/:id/connect to obtain the provider configuration and their resolved credential
The connect route always returns HTTP 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-credentials lists every granted membership’s state, version, and external identifiers. It never returns credential material.
  • PUT /v1/llm-providers/:id/member-credentials/:orgMembershipId stores one member’s credential. The body accepts apiKey or apiKeys, plus optional externalPrincipalId, externalCredentialId, and expectedVersion.
  • POST /v1/llm-providers/:id/member-credentials/:orgMembershipId/block marks an existing binding blocked.
Use 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 at examples/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:
The example first reads the exact provider from 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:
LiteLLM v1.97 returns a 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:
  1. Read the member’s externalCredentialId from GET /v1/llm-providers/:id/member-credentials.
  2. Call LiteLLM POST /key/block with { "key": "<token_id>" } and verify success.
  3. Call Den POST /v1/llm-providers/:id/member-credentials/:orgMembershipId/block.
  4. Verify the member’s connect request returns HTTP 200, null credentials, and memberCredential.state: "blocked".
Run 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.