diff --git a/litellm/proxy/client/cli/commands/up.py b/litellm/proxy/client/cli/commands/up.py index 7023241cf06..e59f897bcc8 100644 --- a/litellm/proxy/client/cli/commands/up.py +++ b/litellm/proxy/client/cli/commands/up.py @@ -145,7 +145,8 @@ def resolve_api_key_helper(base_url: str) -> str: Code would otherwise use) makes `print-token` enforce that the cached token was actually issued for this proxy -- without it, a token minted for a different, previously-logged-into proxy would be handed to - whichever server `up` currently points at. + whichever server `up` currently points at. `--base-url` is an option on + the `lite` group itself, so it has to precede the subcommand. """ lite_path: Final = shutil.which("lite") if lite_path is None: @@ -153,7 +154,7 @@ def resolve_api_key_helper(base_url: str) -> str: "Could not find `lite` on your PATH. Claude Code's apiKeyHelper needs " "an absolute path to it, so `lite up` cannot continue." ) - return f"{shlex.quote(lite_path)} auth print-token --base-url {shlex.quote(base_url)}" + return f"{shlex.quote(lite_path)} --base-url {shlex.quote(base_url)} auth print-token" def _ensure_fresh_login(ctx: click.Context) -> None: diff --git a/tests/test_litellm/proxy/client/cli/test_up_commands.py b/tests/test_litellm/proxy/client/cli/test_up_commands.py index 1b182553644..d1b8bef6ab6 100644 --- a/tests/test_litellm/proxy/client/cli/test_up_commands.py +++ b/tests/test_litellm/proxy/client/cli/test_up_commands.py @@ -1,7 +1,9 @@ import json +import shlex import shutil import stat import sys +import time from unittest.mock import patch import click @@ -201,18 +203,38 @@ class TestResolveApiKeyHelper: def test_returns_helper_command_bound_to_the_selected_proxy(self, monkeypatch): monkeypatch.setattr(shutil, "which", lambda name: "/usr/local/bin/lite") helper = resolve_api_key_helper("http://localhost:4000") - assert helper == "/usr/local/bin/lite auth print-token --base-url http://localhost:4000" + assert helper == "/usr/local/bin/lite --base-url http://localhost:4000 auth print-token" def test_quotes_a_base_url_containing_shell_metacharacters(self, monkeypatch): monkeypatch.setattr(shutil, "which", lambda name: "/usr/local/bin/lite") helper = resolve_api_key_helper("http://example.com/path; rm -rf /") - assert helper == "/usr/local/bin/lite auth print-token --base-url 'http://example.com/path; rm -rf /'" + assert helper == "/usr/local/bin/lite --base-url 'http://example.com/path; rm -rf /' auth print-token" def test_raises_when_lite_not_on_path(self, monkeypatch): monkeypatch.setattr(shutil, "which", lambda name: None) with pytest.raises(UpError, match="Could not find `lite`"): resolve_api_key_helper("http://localhost:4000") + def test_generated_helper_command_is_runnable_and_prints_the_token(self, monkeypatch): + """Claude Code runs this command verbatim, so the argv `up` writes has to parse: `--base-url` + belongs to the `lite` group, and putting it after `auth print-token` made every request fail + with `No such option: --base-url`.""" + from litellm.proxy.client.cli import cli + + monkeypatch.setattr(shutil, "which", lambda name: "/usr/local/bin/lite") + helper = resolve_api_key_helper("http://localhost:4000") + token_data = { + "key": "sk-issued-for-this-proxy", + "base_url": "http://localhost:4000", + "timestamp": time.time(), + } + + with patch("litellm.proxy.client.cli.commands.auth.load_token", return_value=token_data): + result = CliRunner().invoke(cli, shlex.split(helper)[1:]) + + assert result.exit_code == 0, result.output + assert result.output.strip() == "sk-issued-for-this-proxy" + def _make_ctx(base_url): return click.Context(click.Command("test"), obj={"base_url": base_url})