mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
test(proxy): cover fail-closed team budget without membership
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
0fe4028cd9
commit
200224e5d6
3 changed files with 37 additions and 0 deletions
|
|
@ -21,6 +21,7 @@
|
|||
- {id: quota_management.budget.organization.blocks_over_limit, module: quota_management, tier: P1, behavior: budget, variant: organization, assertions: [blocks_over_limit], exercised_on: [chat_completions], source: "proxy/auth/auth_checks.py", rationale: "An organization's max_budget blocks keys under its teams"}
|
||||
- {id: quota_management.budget.team_member.blocks_over_limit, module: quota_management, tier: P1, behavior: budget, variant: team_member, assertions: [blocks_over_limit], exercised_on: [chat_completions], source: "proxy/auth/auth_checks.py", rationale: "A member's per-team budget blocks independently of the team budget"}
|
||||
- {id: quota_management.budget.team_member.isolates_per_member, module: quota_management, tier: P1, behavior: budget, variant: team_member, assertions: [isolates_per_member], exercised_on: [chat_completions], source: "proxy/auth/auth_checks.py", rationale: "One team member's exhausted per-team budget does not block a different member on the same team"}
|
||||
- {id: quota_management.budget.team_member.missing_membership_counts_as_verified_zero_spend, module: quota_management, tier: P1, behavior: budget, variant: team_member, assertions: [missing_membership_counts_as_verified_zero_spend], exercised_on: [chat_completions], source: "proxy/auth/auth_checks.py + proxy_server.py:get_current_spend", rationale: "A key scoped to a team with a default member budget but no membership row treats the absent row as verified zero spend and admits the request"}
|
||||
- {id: quota_management.budget.model_access_group.blocks_over_limit, module: quota_management, tier: P1, behavior: budget, variant: model_access_group, assertions: [blocks_over_limit], exercised_on: [chat_completions], source: "proxy/auth/auth_checks.py", rationale: "A model access group's shared max_budget blocks further calls to deployments in the group once the pool is spent"}
|
||||
- {id: quota_management.budget.model_access_group.enforced_across_keys, module: quota_management, tier: P1, behavior: budget, variant: model_access_group, assertions: [enforced_across_keys], exercised_on: [chat_completions], source: "proxy/auth/auth_checks.py", rationale: "The pool is shared, so a key that spent nothing of its own is blocked once another key granted the same group drained it"}
|
||||
- {id: quota_management.budget.model_access_group.isolates_per_group, module: quota_management, tier: P1, behavior: budget, variant: model_access_group, assertions: [isolates_per_group], exercised_on: [chat_completions], source: "proxy/db/db_spend_update_writer.py", rationale: "A request is charged only to the granted groups that serve the model it called, so an exhausted group never blocks a sibling group"}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ class TeamNewBody(BaseModel):
|
|||
max_budget: float | None = None
|
||||
budget_duration: str | None = None
|
||||
organization_id: str | None = None
|
||||
team_member_budget: float | None = None
|
||||
budget_limits: list[BudgetWindow] | None = None
|
||||
|
||||
|
||||
|
|
@ -416,6 +417,7 @@ class BudgetClient:
|
|||
max_budget: float | None = None,
|
||||
budget_duration: str | None = None,
|
||||
organization_id: str | None = None,
|
||||
team_member_budget: float | None = None,
|
||||
budget_limits: list[BudgetWindow] | None = None,
|
||||
) -> str:
|
||||
team_id = unwrap(
|
||||
|
|
@ -427,6 +429,7 @@ class BudgetClient:
|
|||
max_budget=max_budget,
|
||||
budget_duration=budget_duration,
|
||||
organization_id=organization_id,
|
||||
team_member_budget=team_member_budget,
|
||||
budget_limits=budget_limits,
|
||||
),
|
||||
response_type=TeamNewResponse,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,25 @@ def member(client: BudgetClient) -> Iterator[_Member]:
|
|||
resources.teardown()
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def member_without_membership(client: BudgetClient) -> Iterator[_Member]:
|
||||
resources = ResourceManager(client=client.proxy)
|
||||
try:
|
||||
marker = unique_marker()
|
||||
team_id = client.create_team(
|
||||
alias=f"e2e-team-member-no-membership-{marker}",
|
||||
team_member_budget=TEAM_BUDGET,
|
||||
)
|
||||
resources.defer(lambda: client.delete_team(team_id))
|
||||
user_id = client.create_user(max_budget=TEAM_BUDGET)
|
||||
resources.defer(lambda: client.delete_user(user_id))
|
||||
key = client.generate_key(team_id=team_id, user_id=user_id)
|
||||
resources.defer(lambda: client.delete_key(key))
|
||||
yield _Member(team_id=team_id, user_id=user_id, key=key)
|
||||
finally:
|
||||
resources.teardown()
|
||||
|
||||
|
||||
def _send(client: BudgetClient, key: str) -> str | None:
|
||||
"""One member call; its response id (== the spend-log request_id) if it went
|
||||
through, else None."""
|
||||
|
|
@ -106,3 +125,17 @@ class TestTeamMemberBudget:
|
|||
require_successful_call(result)
|
||||
time.sleep(2)
|
||||
pytest.fail("per-member budget never enforced within the call budget")
|
||||
|
||||
|
||||
class TestFailClosedTeamMemberBudgetWithoutMembership:
|
||||
@pytest.mark.covers("quota_management.budget.team_member.missing_membership_counts_as_verified_zero_spend")
|
||||
def test_missing_membership_counts_as_verified_zero_spend(
|
||||
self, client: BudgetClient, member_without_membership: _Member
|
||||
) -> None:
|
||||
result = client.chat(
|
||||
member_without_membership.key,
|
||||
MODEL,
|
||||
f"missing membership {unique_marker()}",
|
||||
max_tokens=16,
|
||||
)
|
||||
assert result.status_code == 200, result.body
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue