> ## Documentation Index
> Fetch the complete documentation index at: https://openworklabs.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> For agents: install the OpenWork desktop app with `brew install --cask openwork` (macOS) or from https://openworklabs.com/download. Do not run `npx openwork`; the npm package named `openwork` is a different project.
> Connect an agent to OpenWork with the MCP Gateway: `claude mcp add --transport http openwork https://api.openworklabs.com/mcp/agent`. Teams sign up at https://app.openworklabs.com?mode=sign-up.
> Step-by-step agent guide: https://openworklabs.com/docs/start-here/use-openwork-from-an-ai-agent.md
> OpenWork in Slack is private alpha for approved organizations. Setup guide: https://openworklabs.com/docs/slack/overview.md. Agent skill: https://openworklabs.com/.well-known/agent-skills/set-up-openwork-slack/SKILL.md.

# Per-member LLM credentials

> Bind a separate write-only provider credential to each organization member

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 the **legacy provider API integration pattern**, which requires your own provisioner against Den's generic provider and credential-binding routes plus the LiteLLM API. For the built-in AI Gateway integration, including model/team discovery and OpenWork-created per-person keys, use [Use your LiteLLM proxy](https://openworklabs.com/docs/ai-gateway/litellm.md) instead. See the [provider support matrix](https://openworklabs.com/docs/ai-gateway/provider-support.md) for the differences.

## Where this fits

* Per-member credentials apply to [legacy providers](https://openworklabs.com/docs/cloud/share-with-your-team/managed-llm-provider.md), the `/v1/llm-providers` API. The Cloud dashboard doesn't have a setting for it: set `credentialMode` through the API. Once created, the provider appears under **AI Gateway › AI Providers › Legacy Providers** like any other.
* Members can't enter their own key in the desktop app. Their key comes from your provisioner or from the member route below. Until a member has an active binding, the desktop app lists the provider as **Needs organization credential** and its models aren't available.
* If you need a per-member sign-in instead of a per-member key, AI Gateway providers support **Each member signs in** for [Google Vertex AI](https://openworklabs.com/docs/ai-gateway/google-agent-platform.md), [Amazon Bedrock](https://openworklabs.com/docs/ai-gateway/amazon-bedrock.md) and [Microsoft Foundry](https://openworklabs.com/docs/ai-gateway/microsoft-foundry.md). Keys stay on the server and usage is tracked per person.

## 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`:

```json theme={null}
{
  "name": "Per-member gateway",
  "source": "custom",
  "customConfig": {
    "id": "per-member-gateway",
    "name": "Per-member gateway",
    "npm": "@ai-sdk/openai-compatible",
    "env": ["GATEWAY_API_KEY"],
    "api": "https://gateway.example.com/v1",
    "models": [{ "id": "team-model", "name": "Team model" }]
  },
  "credentialMode": "per_member",
  "allMembers": true
}
```

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:

```json theme={null}
{
  "llmProvider": {
    "apiKey": null,
    "apiKeys": null,
    "memberCredential": { "state": "missing" }
  }
}
```

`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.

A member's OpenWork Web instance receives the same resolution: Den writes that member's own active binding into the instance, never a teammate's, and leaves the provider out while the member has no active binding or is not granted the provider. A rotated binding is written to the instance the next time it is used.

Cloud Automations run on their owner's OpenWork Web instance, so they use the owner's own binding too. When an Automation's model belongs to a per-member provider and the owner has no active binding, the run stops before the model is called, and the Automation needs attention with a reason that names the provider. Issue the owner's binding, then resume the Automation.

## 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`](https://github.com/different-ai/openwork/tree/dev/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:

```bash theme={null}
node provision.mjs reconcile
```

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:

```js theme={null}
const { memberCredentials } = await den.get(
  `/v1/llm-providers/${providerId}/member-credentials`,
)

for (const binding of memberCredentials) {
  if (binding.state !== "missing") continue

  const keyAlias = `openwork-${binding.orgMembershipId}`
  const minted = await liteLlm.post("/key/generate", {
    models,
    key_alias: keyAlias,
    metadata: {
      openwork_org_membership_id: binding.orgMembershipId,
    },
  })

  await den.put(
    `/v1/llm-providers/${providerId}/member-credentials/${binding.orgMembershipId}`,
    {
      apiKey: minted.key,
      externalCredentialId: minted.token_id,
    },
  )
}
```

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`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.