mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
* fix(ui): scope key models dropdown options to the key's team A teamless key no longer offers the all-team-models option in the create and edit forms; the backend expands that sentinel to the full proxy model list when no team is attached, which is rarely what the user intended. A team key no longer surfaces the all-proxy-models sentinel that leaks in verbatim when the team's own model list carries it; the dropdown keeps All Team Models plus the team's individual models. Adds browser coverage to the management e2e suite: playwright (an optional dependency behind importorskip) drives the proxy-served dashboard at /ui, asserts the dropdown options a real user sees for teamless and team keys on both create and edit, and walks the create modal end to end, reading the persisted key back through /key/info. * fix(ui): offer all-proxy-models on teamless keys in the models dropdown A teamless key has no team allowlist to inherit, so the dropdown now offers All Proxy Models in place of All Team Models on both the create and edit forms, with the same exclusive-selection handling. Component and browser e2e tests updated to pin the swapped option pair; the teamless create case now also walks the modal end to end and reads the persisted key back through /key/info. * test(ui): update no-team key creation spec to pick All Proxy Models The create modal no longer offers All Team Models without a team; the teamless path now offers All Proxy Models, which is what this spec exercises * fix(ui): gate All Team Models on the team object being loaded When a key has a team_id but the teams prop does not yet include the matching team, availableModels stays empty and the models dropdown rendered All Team Models on its own with nothing to compare against. Gate the option on the team object being present so it only appears once team models are known, and add a regression test for the loading state Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): filter all-proxy-models from teamless model fetch in key edit form The teamless fetch path stored modelAvailableCall results without excludeProxyWideSentinel, so an all-proxy-models entry in the response rendered a second option colliding with the hardcoded All Proxy Models sentinel. Apply the same filter used on the team path and add a regression test asserting the sentinel option is not duplicated Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Mubashir Osmani <mubashir@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
161 lines
7.5 KiB
Python
161 lines
7.5 KiB
Python
"""The dashboard's key create/edit Models dropdown scopes its options to the key's team.
|
|
|
|
A teamless key offers All Proxy Models but not the all-team-models sentinel (the
|
|
backend expands the latter to the full proxy model list when no team is attached),
|
|
and a team key offers all-team-models plus the team's own models but never the
|
|
all-proxy-models sentinel, even when the team's model list carries it. The create
|
|
cases also walk the full product path: submit the modal with the offered sentinel
|
|
and read the persisted key back through /key/info.
|
|
|
|
The tests drive gpt-5.5, one of the example models prewired in the proxy config in
|
|
tests/e2e/docker-compose.yml; the dropdown wait fails with a pointer there when the
|
|
proxy under test does not serve it.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from e2e_config import PROXY_BASE_URL, unique_marker
|
|
from lifecycle import ResourceManager
|
|
from management_client import ManagementClient
|
|
from models import KeyGenerateBody, TeamNewBody
|
|
|
|
pytest.importorskip("playwright.sync_api", reason="playwright not installed")
|
|
|
|
from playwright.sync_api import Locator, Page, expect # noqa: E402 # import must follow the importorskip guard above
|
|
|
|
|
|
def _form_item(page: Page, label: str) -> Locator:
|
|
return page.locator(".ant-form-item").filter(has=page.get_by_text(label, exact=True)).first
|
|
|
|
|
|
def _open_dropdown(page: Page, label: str) -> Locator:
|
|
_form_item(page, label).locator(".ant-select-selector").first.click()
|
|
dropdown = page.locator(".ant-select-dropdown:not(.ant-select-dropdown-hidden)").last
|
|
expect(dropdown).to_be_visible()
|
|
return dropdown
|
|
|
|
|
|
def _models_dropdown_texts(page: Page, must_contain: str) -> list[str]:
|
|
dropdown = _open_dropdown(page, "Models")
|
|
expect(
|
|
dropdown.locator(".ant-select-item-option-content", has_text=must_contain).first,
|
|
f"{must_contain!r} never appeared in the Models dropdown; the proxy must serve it "
|
|
f"(see the model_list in tests/e2e/docker-compose.yml)",
|
|
).to_be_visible()
|
|
return dropdown.locator(".ant-select-item-option-content").all_inner_texts()
|
|
|
|
|
|
def _open_create_key_modal(page: Page) -> None:
|
|
page.goto(f"{PROXY_BASE_URL}/ui/api-keys/?create=true")
|
|
expect(page.locator(".ant-modal").first).to_be_visible()
|
|
|
|
|
|
def _select_team(page: Page, alias: str) -> None:
|
|
dropdown = _open_dropdown(page, "Team")
|
|
dropdown.get_by_text(alias).first.click()
|
|
|
|
|
|
def _submit_create_modal(page: Page, sentinel_label: str) -> str:
|
|
dropdown = page.locator(".ant-select-dropdown:not(.ant-select-dropdown-hidden)").last
|
|
dropdown.locator(".ant-select-item-option-content", has_text=sentinel_label).first.click()
|
|
page.keyboard.press("Escape")
|
|
_form_item(page, "Key Name").locator("input").first.fill(f"e2e-ui-key-{unique_marker()}")
|
|
page.get_by_role("button", name="Create Key", exact=True).click()
|
|
|
|
expect(page.get_by_text("Save your Key")).to_be_visible()
|
|
key = page.locator(".ant-modal pre").last.inner_text().strip()
|
|
assert key.startswith("sk-"), f"expected the created key in the success modal, got {key!r}"
|
|
return key
|
|
|
|
|
|
def _open_key_edit_form(page: Page, key_alias: str) -> None:
|
|
page.goto(f"{PROXY_BASE_URL}/ui/api-keys/")
|
|
page.get_by_text(key_alias).first.click()
|
|
page.get_by_role("tab", name="Settings").click()
|
|
page.get_by_role("button", name="Edit Settings").click()
|
|
expect(_form_item(page, "Models")).to_be_visible()
|
|
|
|
|
|
def _provision_team(client: ManagementClient, resources: ResourceManager, alias: str) -> str:
|
|
team_id = client.create_team(TeamNewBody(team_alias=alias, models=["all-proxy-models", "gpt-5.5"]))
|
|
resources.defer(lambda: client.delete_team(team_id))
|
|
return team_id
|
|
|
|
|
|
def _provision_key(
|
|
client: ManagementClient, resources: ResourceManager, alias: str, team_id: str | None = None
|
|
) -> str:
|
|
key = client.gateway.generate_key(KeyGenerateBody(key_alias=alias, models=["gpt-5.5"], team_id=team_id))
|
|
resources.defer(lambda: client.gateway.delete_key(key))
|
|
return key
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestKeyModelsDropdownUI:
|
|
@pytest.mark.covers("mgmt.key.generate.happy_path", exercised_on=[])
|
|
def test_create_teamless_key_offers_proxy_scope_and_persists(
|
|
self, ui_page: Page, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
_open_create_key_modal(ui_page)
|
|
|
|
options = _models_dropdown_texts(ui_page, must_contain="gpt-5.5")
|
|
assert "All Proxy Models" in options, f"teamless create lost 'All Proxy Models': {options}"
|
|
assert "All Team Models" not in options, f"teamless create offered 'All Team Models': {options}"
|
|
|
|
key = _submit_create_modal(ui_page, sentinel_label="All Proxy Models")
|
|
resources.defer(lambda: client.gateway.delete_key(key))
|
|
|
|
info = client.gateway.key_info(key)
|
|
assert info.models == ["all-proxy-models"], f"persisted models {info.models}"
|
|
assert info.team_id is None, f"teamless key persisted with team {info.team_id}"
|
|
|
|
@pytest.mark.covers("mgmt.key.generate.happy_path", exercised_on=[])
|
|
def test_create_team_key_offers_team_scope_and_persists(
|
|
self, ui_page: Page, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
team_alias = f"e2e-ui-team-{unique_marker()}"
|
|
team_id = _provision_team(client, resources, team_alias)
|
|
|
|
_open_create_key_modal(ui_page)
|
|
_select_team(ui_page, team_alias)
|
|
|
|
options = _models_dropdown_texts(ui_page, must_contain="All Team Models")
|
|
assert "gpt-5.5" in options, f"team key create lost the team's own model: {options}"
|
|
assert "All Proxy Models" not in options, f"team key create offered 'All Proxy Models': {options}"
|
|
assert "all-proxy-models" not in options, f"team key create offered the raw sentinel: {options}"
|
|
|
|
key = _submit_create_modal(ui_page, sentinel_label="All Team Models")
|
|
resources.defer(lambda: client.gateway.delete_key(key))
|
|
|
|
info = client.gateway.key_info(key)
|
|
assert info.models == ["all-team-models"], f"persisted models {info.models}"
|
|
assert info.team_id == team_id, f"persisted team {info.team_id}, expected {team_id}"
|
|
|
|
@pytest.mark.covers("mgmt.key.update.happy_path", exercised_on=[])
|
|
def test_edit_teamless_key_offers_proxy_scope(
|
|
self, ui_page: Page, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
key_alias = f"e2e-ui-teamless-{unique_marker()}"
|
|
_provision_key(client, resources, key_alias)
|
|
|
|
_open_key_edit_form(ui_page, key_alias)
|
|
|
|
options = _models_dropdown_texts(ui_page, must_contain="gpt-5.5")
|
|
assert "All Proxy Models" in options, f"teamless edit lost 'All Proxy Models': {options}"
|
|
assert "All Team Models" not in options, f"teamless edit offered 'All Team Models': {options}"
|
|
|
|
@pytest.mark.covers("mgmt.key.update.happy_path", exercised_on=[])
|
|
def test_edit_team_key_offers_team_scope_only(
|
|
self, ui_page: Page, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
team_alias = f"e2e-ui-team-{unique_marker()}"
|
|
team_id = _provision_team(client, resources, team_alias)
|
|
key_alias = f"e2e-ui-teamkey-{unique_marker()}"
|
|
_provision_key(client, resources, key_alias, team_id=team_id)
|
|
|
|
_open_key_edit_form(ui_page, key_alias)
|
|
|
|
options = _models_dropdown_texts(ui_page, must_contain="All Team Models")
|
|
assert "gpt-5.5" in options, f"team key edit lost the team's own model: {options}"
|
|
assert "All Proxy Models" not in options, f"team key edit offered 'All Proxy Models': {options}"
|
|
assert "all-proxy-models" not in options, f"team key edit offered the raw sentinel: {options}"
|