litellm/tests/unit/proxy/client/cli/test_interface.py
Mateo Wang 95edbd3773
fix(cli): run group subcommands in the lite shell and default to the logged-in gateway (#46511)
* fix(cli): run group subcommands in the lite shell and default to the logged-in gateway

Typing a command group alone in the lite shell printed 'Usage: lite lite',
the next subcommand was unknown, quoted arguments were split on spaces, and
a bare lite connected to localhost:4000 even after lite login to a gateway.

The shell now names the group in usage text, keeps the group as the prompt's
scope so its subcommands run next, accepts a leading 'lite', parses lines
with shlex, and a bare lite falls back to the server the stored lite login
credential was issued for before localhost:4000.

* refactor(cli): walk the shell's group path without recursion

* fix(cli): keep backslashes and compact JSON in the lite shell and pair the login server with its key

* fix(cli): keep the login server to the bare lite shell and accept lite options and = quoting at its prompt

* fix(cli): keep the shell's server for a lite options line and name the right login in the keychain notice

* fix(cli): keep compact JSON with a trailing = intact in the shell and pin options lines to the shell's server over LITELLM_PROXY_URL
2026-10-14 18:25:34 -07:00

183 lines
6.4 KiB
Python

from collections.abc import Mapping
from typing import Final
import click
import pytest
from click.testing import CliRunner, Result
from litellm.proxy.client.cli import cli
from litellm.proxy.client.cli.interface import run_shell_line
GATEWAY: Final = "https://gateway.example.com"
def _shell(*lines: str, env: Mapping[str, str] | None = None) -> Result:
"""Type each line into a bare `lite` shell against the gateway, then quit"""
typed: Final = "".join(f"{line}\n" for line in (*lines, "quit"))
return CliRunner(env=env).invoke(
cli, ["--base-url", GATEWAY, "--api-key", "sk-shell"], input=typed, prog_name="lite"
)
@pytest.fixture
def credentials_endpoint(requests_mock):
return requests_mock.get(
f"{GATEWAY}/credentials",
json={"credentials": [{"credential_name": "shared-openai", "credential_info": {}}]},
)
def test_a_group_typed_alone_prints_its_own_usage_and_opens_a_prompt_inside_it(credentials_endpoint):
result: Final = _shell("credentials", "list")
assert "Usage: lite credentials [OPTIONS] COMMAND [ARGS]..." in result.output
assert "lite lite" not in result.output
assert "credentials > " in result.output
assert "Unknown command" not in result.output
assert credentials_endpoint.call_count == 1
assert credentials_endpoint.last_request.headers["Authorization"] == "Bearer sk-shell"
@pytest.mark.parametrize(
"lines",
[
("credentials list",),
("lite credentials list",),
("credentials", "list"),
("credentials", "credentials list"),
("credentials", "lite credentials list"),
],
)
def test_every_spelling_of_a_group_subcommand_runs_it(lines, credentials_endpoint):
result: Final = _shell(*lines)
assert "Unknown command" not in result.output
assert "shared-openai" in result.output
assert credentials_endpoint.call_count == 1
@pytest.mark.parametrize(
"create_line",
[
"""create shell-made --info '{"custom_llm_provider": "openai"}' --values '{"api_key": "YWJjZA==", "organization": "b3Jn"}'""",
"""create shell-made --info {"custom_llm_provider":"openai"} --values {"api_key":"YWJjZA==","organization":"b3Jn"}""",
"""create shell-made --info={"custom_llm_provider":"openai"} --values={"api_key":"YWJjZA==","organization":"b3Jn"}""",
"""create shell-made --info='{"custom_llm_provider": "openai"}' --values='{"api_key": "YWJjZA==", "organization": "b3Jn"}'""",
],
)
def test_quoted_and_compact_json_arguments_reach_the_gateway_as_typed(requests_mock, create_line):
create: Final = requests_mock.post(f"{GATEWAY}/credentials", json={"credential_name": "shell-made"})
_shell("credentials", create_line)
assert create.call_count == 1
assert create.last_request.json() == {
"credential_name": "shell-made",
"credential_info": {"custom_llm_provider": "openai"},
"credential_values": {"api_key": "YWJjZA==", "organization": "b3Jn"},
}
def test_an_unclosed_quote_runs_nothing_and_keeps_the_group(requests_mock, credentials_endpoint):
create: Final = requests_mock.post(f"{GATEWAY}/credentials", json={})
result: Final = _shell("credentials", "create shell-made --info '{", "list")
assert "Could not read that command" in result.output
assert create.call_count == 0
assert credentials_endpoint.call_count == 1
def test_an_unknown_command_inside_a_group_keeps_the_group(credentials_endpoint):
result: Final = _shell("credentials", "frobnicate", "list")
assert "Unknown command: frobnicate" in result.output
assert credentials_endpoint.call_count == 1
@pytest.mark.parametrize("leaving_line", ["models list", "help", "lite"])
def test_a_top_level_line_inside_a_group_leaves_the_group(leaving_line, requests_mock, credentials_endpoint):
requests_mock.get(f"{GATEWAY}/models", json={"data": []})
result: Final = _shell("credentials", leaving_line, "list")
assert "Unknown command: list" in result.output
assert credentials_endpoint.call_count == 0
def test_help_inside_a_group_shows_that_group_and_keeps_it(credentials_endpoint):
result: Final = _shell("credentials", "--help", "list")
assert "Unknown command" not in result.output
assert result.output.count("Usage: lite credentials [OPTIONS] COMMAND [ARGS]...") == 2
assert credentials_endpoint.call_count == 1
def test_lite_with_its_own_options_inside_the_shell_runs_against_the_server_it_names(
requests_mock, credentials_endpoint
):
other: Final = requests_mock.get(
"https://other-gateway.example.com/credentials", json={"credentials": [{"credential_name": "other-openai"}]}
)
result: Final = _shell("lite --base-url https://other-gateway.example.com --api-key sk-other credentials list")
assert "other-openai" in result.output
assert other.last_request.headers["Authorization"] == "Bearer sk-other"
assert credentials_endpoint.call_count == 0
def test_a_lite_line_that_names_only_a_key_stays_on_the_shell_server_over_litellm_proxy_url(credentials_endpoint):
result: Final = _shell(
"lite --api-key sk-typed credentials list", env={"LITELLM_PROXY_URL": "https://elsewhere.example.com"}
)
assert "shared-openai" in result.output
assert credentials_endpoint.last_request.headers["Authorization"] == "Bearer sk-typed"
def test_a_group_that_runs_on_its_own_does_not_become_the_scope(capsys):
@click.group()
def root() -> None:
pass
@root.group(invoke_without_command=True)
def wizard() -> None:
click.echo("wizard ran")
@wizard.command()
def undo() -> None:
pass
scope: Final = run_shell_line("wizard", (), click.Context(root, info_name="lite", obj={}), root)
assert scope == ()
assert "wizard ran" in capsys.readouterr().out
def test_a_line_splits_on_spaces_and_unwraps_quotes_that_open_a_word_or_follow_an_equals_sign(capsys):
@click.group()
def root() -> None:
pass
@root.command()
@click.argument("words", nargs=-1)
def echo(words: tuple[str, ...]) -> None:
click.echo("\n".join(words))
run_shell_line(
r"""echo C:\Users\me\notes.txt "two words" '{"a": 1}' info='{"a": 1}' {"b":2} it's issue#1""",
(),
click.Context(root, info_name="lite", obj={}),
root,
)
assert capsys.readouterr().out.splitlines() == [
r"C:\Users\me\notes.txt",
"two words",
'{"a": 1}',
'info={"a": 1}',
'{"b":2}',
"it's",
"issue#1",
]