mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-15 04:19:08 +00:00
* 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
222 lines
7.9 KiB
Python
222 lines
7.9 KiB
Python
# stdlib imports
|
|
|
|
# third party imports
|
|
from typing import Final, Protocol
|
|
|
|
import click
|
|
|
|
from litellm._version import version as litellm_version
|
|
from litellm.litellm_core_utils.cli_token_utils import load_cli_token
|
|
from litellm.proxy.client.health import HealthManagementClient
|
|
|
|
from .commands.agents import agent_commands
|
|
from .commands.auth import (
|
|
CliContextObj,
|
|
auth_group,
|
|
context_secret_vault,
|
|
get_stored_api_key,
|
|
keychain_unreadable_notice,
|
|
login,
|
|
logout,
|
|
whoami,
|
|
)
|
|
from .commands.autoroute.commands import autoroute_group
|
|
from .commands.chat import chat
|
|
from .commands.config import config_commands, get_config_value, hidden_command_names
|
|
from .commands.configure import configure_group, reconfigure_group, unconfigure_group
|
|
from .commands.credentials import credentials
|
|
from .commands.debug import debug
|
|
from .commands.encryption import encryption
|
|
from .commands.http import http
|
|
from .commands.keys import keys
|
|
from .commands.model_groups import model_groups
|
|
|
|
# local imports
|
|
from .commands.models import models
|
|
from .commands.teams import teams
|
|
from .commands.up import down, up
|
|
from .commands.users import users
|
|
from .interface import interactive_shell
|
|
|
|
|
|
class _CliContextObjView(Protocol):
|
|
@property
|
|
def obj(self) -> CliContextObj: ...
|
|
|
|
|
|
def _cli_context_obj(view: _CliContextObjView) -> CliContextObj:
|
|
return view.obj
|
|
|
|
|
|
def print_version(base_url: str, api_key: str | None):
|
|
"""Print CLI and server version info."""
|
|
click.echo(f"LiteLLM Proxy CLI Version: {litellm_version}")
|
|
if base_url:
|
|
click.echo(f"LiteLLM Proxy Server URL: {base_url}")
|
|
try:
|
|
health_client: Final = HealthManagementClient(base_url=base_url, api_key=api_key)
|
|
server_version: Final = health_client.get_server_version()
|
|
if server_version:
|
|
click.echo(f"LiteLLM Proxy Server Version: {server_version}")
|
|
else:
|
|
click.echo("LiteLLM Proxy Server Version: (unavailable)")
|
|
except Exception as e:
|
|
click.echo(f"Could not retrieve server version: {e}")
|
|
|
|
|
|
class HideConfiguredCommandsGroup(click.Group):
|
|
"""Group that omits operator-hidden commands from listings, still running them.
|
|
|
|
Deployments hand `lite` to users who should only see a curated subset of
|
|
commands (`lite config set hidden_commands codex,opencode`). Filtering the
|
|
listing rather than dropping the commands keeps anyone's existing scripts
|
|
working.
|
|
"""
|
|
|
|
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
hidden: Final = hidden_command_names()
|
|
return [name for name in super().list_commands(ctx) if name not in hidden]
|
|
|
|
|
|
@click.group(cls=HideConfiguredCommandsGroup, invoke_without_command=True)
|
|
@click.option(
|
|
"--version",
|
|
"-v",
|
|
"show_version",
|
|
is_flag=True,
|
|
help="Show the LiteLLM Proxy CLI and server version and exit.",
|
|
)
|
|
@click.option(
|
|
"--base-url",
|
|
envvar="LITELLM_PROXY_URL",
|
|
show_envvar=True,
|
|
default=None,
|
|
show_default="base_url from `lite config`, else the `lite login` server for a bare `lite` with no key, else http://localhost:4000",
|
|
help="Base URL of the LiteLLM proxy server",
|
|
)
|
|
@click.option(
|
|
"--api-key",
|
|
envvar="LITELLM_PROXY_API_KEY",
|
|
show_envvar=True,
|
|
help="API key for authentication",
|
|
)
|
|
@click.pass_context
|
|
def cli(ctx: click.Context, show_version: bool, base_url: str | None, api_key: str | None) -> None:
|
|
"""LiteLLM Proxy CLI - Manage your LiteLLM proxy server"""
|
|
ctx.ensure_object(dict)
|
|
|
|
stored_base_url: Final = get_config_value("base_url")
|
|
base_url_provided: Final = base_url is not None
|
|
shell: Final = ctx.invoked_subcommand is None and not show_version
|
|
login: Final = (
|
|
load_cli_token(vault=context_secret_vault(ctx))
|
|
if shell and base_url is None and not stored_base_url and api_key is None
|
|
else None
|
|
)
|
|
login_base_url: Final = login.base_url if login is not None and login.key else None
|
|
if login is not None and login.base_url and not login.key:
|
|
click.secho(
|
|
keychain_unreadable_notice(
|
|
context_secret_vault(ctx), login_command=f"lite --base-url {login.base_url} login"
|
|
),
|
|
fg="yellow",
|
|
err=True,
|
|
)
|
|
|
|
# Normalize once here so every downstream command (login, agents, http, ...) can safely
|
|
# do f"{base_url}/some/path" without producing a double slash.
|
|
base_url = (
|
|
(stored_base_url or login_base_url or "http://localhost:4000") if base_url is None else base_url
|
|
).rstrip("/")
|
|
|
|
# If no API key provided via flag or environment variable, try to load from saved token.
|
|
# Pass base_url so we only use the stored key when it was issued for this server.
|
|
setup_command: Final = ctx.invoked_subcommand in ("configure", "reconfigure", "unconfigure")
|
|
api_key_from_token_file: Final = api_key is None and not setup_command
|
|
resolved_api_key: Final = (
|
|
get_stored_api_key(expected_base_url=base_url, vault=context_secret_vault(ctx))
|
|
if api_key_from_token_file
|
|
else api_key
|
|
)
|
|
|
|
ctx.obj["base_url"] = base_url
|
|
ctx.obj["api_key"] = resolved_api_key
|
|
ctx.obj["api_key_from_token_file"] = api_key_from_token_file
|
|
# `--base-url` defaults to localhost:4000 for local dev convenience, but
|
|
# apiKeyHelper is invoked bare (no flags) -- commands that must work
|
|
# unattended (print-token) need to tell "user didn't say" apart from
|
|
# "user said localhost:4000 on purpose" so they can fall back to
|
|
# whatever server the stored token was actually issued for. A base_url
|
|
# saved via `lite config set` counts as the user saying it.
|
|
ctx.obj["base_url_explicit"] = base_url_provided or (bool(stored_base_url) and not setup_command)
|
|
|
|
if show_version:
|
|
print_version(base_url, resolved_api_key)
|
|
ctx.exit()
|
|
|
|
# If no subcommand was invoked, start interactive mode
|
|
if ctx.invoked_subcommand is None:
|
|
interactive_shell(ctx, cli)
|
|
|
|
|
|
@cli.command()
|
|
@click.pass_context
|
|
def version(ctx: click.Context):
|
|
"""Show the LiteLLM Proxy CLI and server version."""
|
|
ctx_obj: Final = _cli_context_obj(ctx)
|
|
print_version(ctx_obj.get("base_url"), ctx_obj.get("api_key"))
|
|
|
|
|
|
# Add authentication commands as top-level commands
|
|
cli.add_command(login)
|
|
cli.add_command(logout)
|
|
cli.add_command(whoami)
|
|
# Add the auth command group (e.g. `lite auth print-token`, used as Claude Code's apiKeyHelper)
|
|
cli.add_command(auth_group, name="auth")
|
|
# Add the models command group
|
|
cli.add_command(models)
|
|
# Add the credentials command group
|
|
cli.add_command(credentials)
|
|
# Add the encryption migration command group
|
|
cli.add_command(encryption)
|
|
# Add the chat command group
|
|
cli.add_command(chat)
|
|
# Add the http command group
|
|
cli.add_command(http)
|
|
cli.add_command(debug)
|
|
# Add the keys command group
|
|
cli.add_command(keys)
|
|
# Add the teams command group
|
|
cli.add_command(teams)
|
|
# Add the users command group
|
|
cli.add_command(users)
|
|
# Add a top-level command per coding agent (claude, codex, opencode, ...)
|
|
for agent_command in agent_commands():
|
|
cli.add_command(agent_command)
|
|
# Add the up/down commands (route Claude Code through the local LiteLLM proxy)
|
|
cli.add_command(up)
|
|
cli.add_command(down)
|
|
# Add the model-groups command group (discover models your key can access)
|
|
cli.add_command(model_groups)
|
|
# Add the autoroute command group (QA auto-routing against your real proxy)
|
|
cli.add_command(autoroute_group, name="autoroute")
|
|
cli.add_command(config_commands)
|
|
# Add configure/unconfigure (persistently wire a coding agent to the proxy with a virtual key)
|
|
cli.add_command(configure_group)
|
|
cli.add_command(reconfigure_group)
|
|
cli.add_command(unconfigure_group)
|
|
|
|
|
|
LITELLM_PROXY_DEPRECATION_NOTICE: Final = (
|
|
"The `litellm-proxy` command is deprecated and will be removed in a future release; "
|
|
"run `lite` instead, it takes the same commands and options."
|
|
)
|
|
|
|
|
|
def litellm_proxy_cli() -> None:
|
|
click.secho(LITELLM_PROXY_DEPRECATION_NOTICE, err=True, fg="yellow")
|
|
cli()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|