Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions docs/my-website/docs/proxy/model_access.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,157 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \

### [API Reference](https://litellm-api.up.railway.app/#/team%20management/new_team_team_new_post)

## **Per-Member Model Overrides (Team-Scoped Defaults)**

:::info

Requires `TEAM_MODEL_OVERRIDES=true` environment variable or `litellm.team_model_overrides_enabled = True`.

:::

By default, every team member can access all models in `team.models`. With per-member model overrides, you can:

- Set **`default_models`** on a team — the models every member gets by default
- Set **`models`** on individual team members — additional models only they can access

A member's **effective models** = `default_models` ∪ `member.models`. If neither is set, falls back to `team.models` (full backward compatibility).

### Enable the Feature

Add to your `config.yaml`:

```yaml
environment_variables:
TEAM_MODEL_OVERRIDES: "true"
```

### 1. Create a Team with Default Models

```shell
curl -L 'http://localhost:4000/team/new' \
-H 'Authorization: Bearer <your-master-key>' \
-H 'Content-Type: application/json' \
-d '{
"team_alias": "engineering",
"models": ["gpt-4", "gpt-4o-mini", "gpt-4o"],
"default_models": ["gpt-4o-mini"]
}'
Comment on lines +144 to +150
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 access_group_ids silently bypasses per-member model restrictions

The docs describe the feature as providing fine-grained per-member model access control. However, can_team_access_model in auth_checks.py falls back to the team's access_group_ids after the per-member effective-models check fails — without any restriction from the user's effective_models set. The code comment acknowledges this:

access groups are a team-level concept and are NOT restricted by per-member model overrides. If a team has access_group_ids configured, any member can access models from those groups regardless of their effective_models set.

This means:

  • An admin grants User A only ["gpt-4o-mini"] via per-member overrides
  • The team also has access_group_ids = ["premium-models"] which includes ["gpt-4", "claude-3"]
  • User A can access gpt-4 and claude-3 through the access-group bypass even though they're not in their effective model set

The docs should explicitly call this out so admins understand that access_group_ids is an additive team-wide grant that overrides per-member restrictions. Without this note, an admin who configures per-member restrictions for compliance or cost-control may unknowingly leave a bypass path open.

```

Comment on lines +144 to +152
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Docs omit service-key behaviour and capping semantics

The docs state:

A member's effective models = default_modelsmember.models. If neither is set, falls back to team.models.

Two behaviours are not documented:

  1. Capping: the union is further intersected with team.models (the team pool). If default_models = ["gpt-4"] but team.models = ["gpt-4o-mini"], the effective set is [] → falls back to ["gpt-4o-mini"].
  2. Service/bot keys (team-level keys with no user_id): current code restricts them to default_models at runtime (see inline comment on get_effective_team_models). The docs claim "Zero extra database queries on the auth hot path" and "full backward compatibility", which is only accurate for keys that are user-scoped. Service key users enabling this feature will experience a silent regression (see related comment on auth_checks.py).

- `models` — the full pool of models the team is allowed to use
- `default_models` — the subset every member gets by default (must be a subset of `models`)

### 2. Add Members with Per-User Overrides

```shell
# Alice gets the default (gpt-4o-mini only)
curl -L 'http://localhost:4000/team/member_add' \
-H 'Authorization: Bearer <your-master-key>' \
-H 'Content-Type: application/json' \
-d '{
"team_id": "<team-id>",
"member": {"role": "user", "user_id": "alice"}
}'

# Bob gets gpt-4o in addition to the default
curl -L 'http://localhost:4000/team/member_add' \
-H 'Authorization: Bearer <your-master-key>' \
-H 'Content-Type: application/json' \
-d '{
"team_id": "<team-id>",
"member": {"role": "user", "user_id": "bob", "models": ["gpt-4o"]}
}'
```

| Member | Override | Effective Models |
|--------|----------|-----------------|
| Alice | none | `["gpt-4o-mini"]` |
| Bob | `["gpt-4o"]` | `["gpt-4o-mini", "gpt-4o"]` |

### 3. Generate Keys and Test

```shell
# Generate key for Bob
curl -L 'http://localhost:4000/key/generate' \
-H 'Authorization: Bearer <your-master-key>' \
-H 'Content-Type: application/json' \
-d '{"team_id": "<team-id>", "user_id": "bob"}'
```

<Tabs>
<TabItem label="Allowed (Bob → gpt-4o)" value="allowed">

```shell
curl -L 'http://localhost:4000/chat/completions' \
-H 'Authorization: Bearer <bob-key>' \
-H 'Content-Type: application/json' \
-d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}'
```

Returns `200 OK` — `gpt-4o` is in Bob's effective set.

</TabItem>
<TabItem label="Denied (Bob → gpt-4)" value="denied">

```shell
curl -L 'http://localhost:4000/chat/completions' \
-H 'Authorization: Bearer <bob-key>' \
-H 'Content-Type: application/json' \
-d '{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}'
```

Returns `401 Unauthorized` — `gpt-4` is in the team pool but not in Bob's effective set.

</TabItem>
</Tabs>

### 4. Update Member Overrides

```shell
# Add gpt-4 to Bob's overrides
curl -L 'http://localhost:4000/team/member_update' \
-H 'Authorization: Bearer <your-master-key>' \
-H 'Content-Type: application/json' \
-d '{
"team_id": "<team-id>",
"user_id": "bob",
"models": ["gpt-4o", "gpt-4"]
}'

# Remove all overrides (Bob falls back to default_models only)
curl -L 'http://localhost:4000/team/member_update' \
-H 'Authorization: Bearer <your-master-key>' \
-H 'Content-Type: application/json' \
-d '{
"team_id": "<team-id>",
"user_id": "bob",
"models": []
}'
```

### Validation Rules

| Rule | Error |
|------|-------|
| `default_models` must be a subset of `team.models` | `400` on `/team/new` and `/team/update` |
| Member `models` must be a subset of `team.models` | `400` on `/team/member_add` and `/team/member_update` |
| Key `models` must be a subset of effective models | `403` on `/key/generate` |
| Narrowing `team.models` auto-prunes stale `default_models` | Automatic on `/team/update` |

### Important Notes

- **Effective models are capped by `team.models`**: If `team.models` is later narrowed, any member overrides outside the new pool are silently excluded at runtime — no access is granted beyond the team's allowed list.
- **Service/bot keys** (keys without a `user_id`) are **not affected** by this feature. They always use the full `team.models` pool, preserving backward compatibility.
- **Access groups** (`access_group_ids`) are a team-level concept and are **not** restricted by per-member overrides. Models granted via access groups remain available to all team members.

### Backward Compatibility

When the feature flag is off **or** when neither `default_models` nor member `models` is configured:

- `get_effective_team_models()` returns `team.models` unchanged
- All existing teams and keys work exactly as before
- Zero extra database queries on the auth hot path


## **View Available Fallback Models**

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable: Add default_models to LiteLLM_TeamTable
ALTER TABLE "LiteLLM_TeamTable" ADD COLUMN IF NOT EXISTS "default_models" TEXT[] DEFAULT ARRAY[]::TEXT[];

-- AlterTable: Add models to LiteLLM_TeamMembership
ALTER TABLE "LiteLLM_TeamMembership" ADD COLUMN IF NOT EXISTS "models" TEXT[] DEFAULT ARRAY[]::TEXT[];
2 changes: 2 additions & 0 deletions litellm-proxy-extras/litellm_proxy_extras/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ model LiteLLM_TeamTable {
policies String[] @default([])
model_id Int? @unique // id for LiteLLM_ModelTable -> stores team-level model aliases
allow_team_guardrail_config Boolean @default(false) // if true, team admin can configure guardrails for this team
default_models String[] @default([]) // NEW: team-wide defaults
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
litellm_model_table LiteLLM_ModelTable? @relation(fields: [model_id], references: [id])
object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id])
Expand Down Expand Up @@ -595,6 +596,7 @@ model LiteLLM_TeamMembership {
team_id String
spend Float @default(0.0)
budget_id String?
models String[] @default([]) // NEW: per-user model overrides
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
@@id([user_id, team_id])
}
Expand Down
1 change: 1 addition & 0 deletions litellm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
os.getenv("LITELLM_USE_CHAT_COMPLETIONS_URL_FOR_ANTHROPIC_MESSAGES", False)
) # When True, routes OpenAI /v1/messages requests to chat/completions instead of the Responses API
retry = True
team_model_overrides_enabled = os.getenv("TEAM_MODEL_OVERRIDES", "").lower() == "true"
### AUTH ###
api_key: Optional[str] = None
openai_key: Optional[str] = None
Expand Down
17 changes: 17 additions & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1611,6 +1611,16 @@ class Member(MemberBase):
] = Field(
description="The role of the user within the team. 'admin' users can manage team settings and members, 'user' is a regular team member"
)
models: Optional[List[str]] = Field(
default=None,
description="Specific models this member can access within the team. If provided, these will be used in addition to the team's default models.",
)
tpm_limit: Optional[int] = Field(
default=None, description="Tokens per minute limit for this team member"
)
rpm_limit: Optional[int] = Field(
default=None, description="Requests per minute limit for this team member"
)


class OrgMember(MemberBase):
Expand Down Expand Up @@ -1642,6 +1652,7 @@ class TeamBase(LiteLLMPydanticObjectBase):
blocked: bool = False
router_settings: Optional[dict] = None
access_group_ids: Optional[List[str]] = None
default_models: List[str] = []


class NewTeamRequest(TeamBase):
Expand Down Expand Up @@ -1719,6 +1730,7 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase):
model_aliases: Optional[dict] = None
guardrails: Optional[List[str]] = None
policies: Optional[List[str]] = None
default_models: Optional[List[str]] = None
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None
team_member_budget: Optional[float] = None
team_member_budget_duration: Optional[str] = None
Expand Down Expand Up @@ -2387,6 +2399,8 @@ class LiteLLM_VerificationTokenView(LiteLLM_VerificationToken):
team_alias: Optional[str] = None
team_tpm_limit: Optional[int] = None
team_rpm_limit: Optional[int] = None
team_member_models: Optional[List[str]] = None
team_default_models: Optional[List[str]] = None
team_max_budget: Optional[float] = None
team_soft_budget: Optional[float] = None
team_models: List = []
Expand Down Expand Up @@ -3607,6 +3621,7 @@ class LiteLLM_TeamMembership(LiteLLMPydanticObjectBase):
team_id: str
budget_id: Optional[str] = None
spend: Optional[float] = 0.0
models: List[str] = []
litellm_budget_table: Optional[LiteLLM_BudgetTable]

def safe_get_team_member_rpm_limit(self) -> Optional[int]:
Expand Down Expand Up @@ -3729,6 +3744,7 @@ class TeamMemberDeleteRequest(MemberDeleteRequest):
class TeamMemberUpdateRequest(TeamMemberDeleteRequest):
max_budget_in_team: Optional[float] = None
role: Optional[Literal["admin", "user"]] = None
models: Optional[List[str]] = None
tpm_limit: Optional[int] = Field(
default=None, description="Tokens per minute limit for this team member"
)
Expand All @@ -3739,6 +3755,7 @@ class TeamMemberUpdateRequest(TeamMemberDeleteRequest):

class TeamMemberUpdateResponse(MemberUpdateResponse):
team_id: str
models: Optional[List[str]] = None
max_budget_in_team: Optional[float] = None
tpm_limit: Optional[int] = None
rpm_limit: Optional[int] = None
Expand Down
Loading
Loading