mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-16 04:29:31 +00:00
feat(cli): set display names on credentials and show them in the list
This commit is contained in:
parent
83e815b828
commit
d1277a94fc
4 changed files with 132 additions and 2 deletions
|
|
@ -18,6 +18,8 @@ class _CredentialInfo(TypedDict):
|
|||
|
||||
class _CredentialItem(TypedDict):
|
||||
credential_name: ReadOnly[NotRequired[str]]
|
||||
display_name: ReadOnly[NotRequired[str | None]]
|
||||
source: ReadOnly[NotRequired[str]]
|
||||
credential_info: ReadOnly[NotRequired[_CredentialInfo]]
|
||||
|
||||
|
||||
|
|
@ -61,6 +63,8 @@ def list(ctx: click.Context, output_format: Literal["table", "json"]):
|
|||
|
||||
# Add columns
|
||||
table.add_column("Credential Name", style="cyan")
|
||||
table.add_column("Display Name")
|
||||
table.add_column("Source")
|
||||
table.add_column("Custom LLM Provider", style="green")
|
||||
|
||||
# Add rows
|
||||
|
|
@ -69,6 +73,8 @@ def list(ctx: click.Context, output_format: Literal["table", "json"]):
|
|||
info = cred.get("credential_info", {})
|
||||
table.add_row(
|
||||
str(cred.get("credential_name", "")),
|
||||
cred.get("display_name") or "",
|
||||
str(cred.get("source", "")),
|
||||
str(info.get("custom_llm_provider", "")),
|
||||
)
|
||||
|
||||
|
|
@ -89,8 +95,9 @@ def list(ctx: click.Context, output_format: Literal["table", "json"]):
|
|||
help="JSON string containing credential values",
|
||||
required=True,
|
||||
)
|
||||
@click.option("--display-name", type=str, default=None, help="Optional label shown in the UI")
|
||||
@click.pass_context
|
||||
def create(ctx: click.Context, credential_name: str, info: str, values: str):
|
||||
def create(ctx: click.Context, credential_name: str, info: str, values: str, display_name: str | None):
|
||||
"""Create a new credential"""
|
||||
context: Final = cli_context_values(ctx)
|
||||
client: Final = CredentialsManagementClient(context["base_url"], context["api_key"])
|
||||
|
|
@ -101,7 +108,33 @@ def create(ctx: click.Context, credential_name: str, info: str, values: str):
|
|||
raise click.BadParameter(f"Invalid JSON: {e}")
|
||||
|
||||
try:
|
||||
response: Final = client.create(credential_name, credential_info["value"], credential_values["value"])
|
||||
response: Final = client.create(
|
||||
credential_name, credential_info["value"], credential_values["value"], display_name=display_name
|
||||
)
|
||||
rich.print_json(data=response)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
click.echo(f"Error: HTTP {e.response.status_code}", err=True)
|
||||
try:
|
||||
error_body: Final[_JsonBodyView] = {"body": e.response.json()}
|
||||
rich.print_json(data=error_body["body"])
|
||||
except json.JSONDecodeError:
|
||||
click.echo(e.response.text, err=True)
|
||||
raise click.Abort()
|
||||
|
||||
|
||||
@credentials.command()
|
||||
@click.argument("credential_name")
|
||||
@click.option("--display-name", type=str, default=None, help="New label shown in the UI")
|
||||
@click.option("--clear-display-name", is_flag=True, help="Remove the label so the UI shows the credential name")
|
||||
@click.pass_context
|
||||
def update(ctx: click.Context, credential_name: str, display_name: str | None, clear_display_name: bool):
|
||||
"""Change a credential's display name. The credential name itself cannot change"""
|
||||
if (display_name is None) == (not clear_display_name):
|
||||
raise click.UsageError("Pass exactly one of --display-name or --clear-display-name")
|
||||
context: Final = cli_context_values(ctx)
|
||||
client: Final = CredentialsManagementClient(context["base_url"], context["api_key"])
|
||||
try:
|
||||
response: Final = client.update_display_name(credential_name, None if clear_display_name else display_name)
|
||||
rich.print_json(data=response)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
click.echo(f"Error: HTTP {e.response.status_code}", err=True)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ class CredentialsManagementClient:
|
|||
credential_info: Mapping[str, object],
|
||||
credential_values: Mapping[str, object],
|
||||
return_request: bool = False,
|
||||
display_name: str | None = None,
|
||||
) -> dict[str, Any] | requests.Request:
|
||||
"""
|
||||
Create a new credential.
|
||||
|
|
@ -97,6 +98,7 @@ class CredentialsManagementClient:
|
|||
"credential_name": credential_name,
|
||||
"credential_info": credential_info,
|
||||
"credential_values": credential_values,
|
||||
**({} if display_name is None else {"display_name": display_name}),
|
||||
}
|
||||
|
||||
request: Final = requests.Request("POST", url, headers=self._get_headers(), json=data)
|
||||
|
|
@ -151,6 +153,30 @@ class CredentialsManagementClient:
|
|||
raise UnauthorizedError(e)
|
||||
raise
|
||||
|
||||
def update_display_name(
|
||||
self,
|
||||
credential_name: str,
|
||||
display_name: str | None,
|
||||
return_request: bool = False,
|
||||
) -> dict[str, Any] | requests.Request:
|
||||
url: Final = f"{self._base_url}/credentials/{credential_name}"
|
||||
data: Final = {"display_name": display_name, "credential_info": {}}
|
||||
|
||||
request: Final = requests.Request("PATCH", url, headers=self._get_headers(), json=data)
|
||||
|
||||
if return_request:
|
||||
return request
|
||||
|
||||
session: Final = requests.Session()
|
||||
try:
|
||||
response: Final = session.send(request.prepare(), timeout=self._timeout)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 401:
|
||||
raise UnauthorizedError(e)
|
||||
raise
|
||||
|
||||
def get(
|
||||
self,
|
||||
credential_name: str,
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ def test_acreate_credential_success(cli_runner, mock_credentials_client):
|
|||
"test-cred",
|
||||
{"custom_llm_provider": "azure"},
|
||||
{"api_key": "test-key"},
|
||||
display_name=None,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -215,3 +216,55 @@ def test_aget_credential_success(cli_runner, mock_credentials_client):
|
|||
output_data = json.loads(result.output)
|
||||
assert output_data == mock_response
|
||||
mock_instance.get.assert_called_once_with("test-cred")
|
||||
|
||||
|
||||
def test_list_credentials_table_shows_display_name_and_source(cli_runner, mock_credentials_client):
|
||||
mock_credentials_client.return_value.list.return_value = {
|
||||
"credentials": [
|
||||
{"credential_name": "openai-prod", "display_name": "Prod OpenAI", "source": "db", "credential_info": {}},
|
||||
{"credential_name": "from-yaml", "display_name": None, "source": "config", "credential_info": {}},
|
||||
]
|
||||
}
|
||||
|
||||
result = cli_runner.invoke(cli, ["credentials", "list"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Prod OpenAI" in result.output
|
||||
assert "config" in result.output
|
||||
assert "None" not in result.output
|
||||
|
||||
|
||||
def test_create_credential_forwards_the_display_name(cli_runner, mock_credentials_client):
|
||||
mock_instance = mock_credentials_client.return_value
|
||||
mock_instance.create.return_value = {"success": True}
|
||||
|
||||
result = cli_runner.invoke(
|
||||
cli,
|
||||
["credentials", "create", "openai-prod", "--info", "{}", "--values", "{}", "--display-name", "Prod OpenAI"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_instance.create.assert_called_once_with("openai-prod", {}, {}, display_name="Prod OpenAI")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("flags", "expected"),
|
||||
[(["--display-name", "Prod OpenAI"], "Prod OpenAI"), (["--clear-display-name"], None)],
|
||||
)
|
||||
def test_update_credential_sets_or_clears_the_display_name(cli_runner, mock_credentials_client, flags, expected):
|
||||
mock_instance = mock_credentials_client.return_value
|
||||
mock_instance.update_display_name.return_value = {"success": True}
|
||||
|
||||
result = cli_runner.invoke(cli, ["credentials", "update", "openai-prod", *flags])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_instance.update_display_name.assert_called_once_with("openai-prod", expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("flags", [[], ["--display-name", "Prod", "--clear-display-name"]])
|
||||
def test_update_credential_requires_exactly_one_display_name_flag(cli_runner, mock_credentials_client, flags):
|
||||
result = cli_runner.invoke(cli, ["credentials", "update", "openai-prod", *flags])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "exactly one" in result.output
|
||||
mock_credentials_client.return_value.update_display_name.assert_not_called()
|
||||
|
|
|
|||
|
|
@ -130,6 +130,24 @@ def test_create_request(client, base_url, api_key):
|
|||
}
|
||||
|
||||
|
||||
def test_create_request_includes_the_display_name_only_when_given(client):
|
||||
labeled = client.create("azure1", {}, {"api_key": "sk-123"}, return_request=True, display_name="Azure EU")
|
||||
unlabeled = client.create("azure1", {}, {"api_key": "sk-123"}, return_request=True)
|
||||
|
||||
assert labeled.json["display_name"] == "Azure EU"
|
||||
assert "display_name" not in unlabeled.json
|
||||
|
||||
|
||||
@pytest.mark.parametrize("display_name", ["Azure EU", None])
|
||||
def test_update_display_name_request(client, base_url, api_key, display_name):
|
||||
request = client.update_display_name("azure1", display_name, return_request=True)
|
||||
|
||||
assert request.method == "PATCH"
|
||||
assert request.url == f"{base_url}/credentials/azure1"
|
||||
assert request.headers["Authorization"] == f"Bearer {api_key}"
|
||||
assert request.json == {"display_name": display_name, "credential_info": {}}
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_create_mock_response(client):
|
||||
"""Test create with a mocked successful response"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue