mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-29 01:42:19 +00:00
Merge remote-tracking branch 'origin/main' into litellm_mcp_client_allowlist
This commit is contained in:
commit
52914a06d2
12 changed files with 116 additions and 25 deletions
|
|
@ -3,7 +3,7 @@
|
|||
Example: Using CLI token with LiteLLM SDK
|
||||
|
||||
This example shows how to use the CLI authentication token
|
||||
in your Python scripts after running `litellm-proxy login`.
|
||||
in your Python scripts after running `lite login`.
|
||||
"""
|
||||
|
||||
from textwrap import indent
|
||||
|
|
@ -22,7 +22,7 @@ def main():
|
|||
api_key = litellm.get_litellm_gateway_api_key()
|
||||
|
||||
if not api_key:
|
||||
print("❌ No CLI token found. Please run 'litellm-proxy login' first.")
|
||||
print("❌ No CLI token found. Please run 'lite login' first.")
|
||||
return
|
||||
|
||||
print("✅ Found CLI token.")
|
||||
|
|
@ -58,6 +58,6 @@ if __name__ == "__main__":
|
|||
main()
|
||||
|
||||
print("\n💡 Tips:")
|
||||
print("1. Run 'litellm-proxy login' to authenticate first")
|
||||
print("1. Run 'lite login' to authenticate first")
|
||||
print("2. Replace 'https://your-proxy.com' with your actual proxy URL")
|
||||
print("3. The token is stored in your OS keychain, or in ~/.litellm/token.json when there is none")
|
||||
|
|
|
|||
|
|
@ -38680,8 +38680,7 @@
|
|||
"required": false,
|
||||
"schema": {
|
||||
"default": 10,
|
||||
"maximum": 100,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"title": "Count",
|
||||
"type": "integer"
|
||||
}
|
||||
|
|
@ -39385,8 +39384,7 @@
|
|||
"required": false,
|
||||
"schema": {
|
||||
"default": 10,
|
||||
"maximum": 100,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"title": "Count",
|
||||
"type": "integer"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""CLI package for LiteLLM Proxy Client."""
|
||||
|
||||
from .main import cli
|
||||
from .main import cli, litellm_proxy_cli
|
||||
|
||||
__all__ = ["cli"]
|
||||
__all__ = ["cli", "litellm_proxy_cli"]
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ def migrate(ctx: click.Context, check_only: bool, dry_run: bool):
|
|||
resumable; safe to re-run after an interruption.
|
||||
|
||||
Examples:
|
||||
litellm-proxy encryption migrate --check # attestation scan, no writes
|
||||
litellm-proxy encryption migrate # perform the migration
|
||||
lite encryption migrate --check # attestation scan, no writes
|
||||
lite encryption migrate # perform the migration
|
||||
"""
|
||||
client: Final = HTTPClient(ctx.obj["base_url"], ctx.obj["api_key"])
|
||||
|
||||
|
|
|
|||
|
|
@ -168,5 +168,16 @@ cli.add_command(configure_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()
|
||||
|
|
|
|||
|
|
@ -264,6 +264,8 @@ scim_router: Final = APIRouter(
|
|||
dependencies=[Depends(_premium_user_check)],
|
||||
)
|
||||
|
||||
SCIM_MAX_PAGE_SIZE: Final = 100
|
||||
|
||||
|
||||
# Helper functions for common operations
|
||||
async def _get_prisma_client_or_raise_exception():
|
||||
|
|
@ -1572,12 +1574,13 @@ def _parse_scim_eq_filter(scim_filter: str) -> tuple[str, str] | None:
|
|||
)
|
||||
async def get_users(
|
||||
startIndex: int = Query(1, ge=1),
|
||||
count: int = Query(10, ge=1, le=100),
|
||||
count: int = Query(10, ge=0),
|
||||
filter: str | None = Query(None),
|
||||
):
|
||||
"""
|
||||
Get a list of users according to SCIM v2 protocol
|
||||
"""
|
||||
page_size: Final = min(count, SCIM_MAX_PAGE_SIZE)
|
||||
verbose_proxy_logger.debug(
|
||||
"SCIM GET USERS request: startIndex=%s count=%s filter=%s",
|
||||
startIndex,
|
||||
|
|
@ -1607,7 +1610,7 @@ async def get_users(
|
|||
users: Final[Sequence[LiteLLM_UserTable]] = await _table(UserRepository(prisma_client)).find_many(
|
||||
where=where_conditions,
|
||||
skip=(startIndex - 1),
|
||||
take=count,
|
||||
take=page_size,
|
||||
order={"created_at": "desc"},
|
||||
)
|
||||
|
||||
|
|
@ -1623,7 +1626,7 @@ async def get_users(
|
|||
return SCIMListResponse(
|
||||
totalResults=total_count,
|
||||
startIndex=startIndex,
|
||||
itemsPerPage=min(count, len(scim_users)),
|
||||
itemsPerPage=len(scim_users),
|
||||
Resources=scim_users,
|
||||
)
|
||||
|
||||
|
|
@ -2399,12 +2402,13 @@ class _TeamWhereConditions(TypedDict, total=False):
|
|||
)
|
||||
async def get_groups(
|
||||
startIndex: int = Query(1, ge=1),
|
||||
count: int = Query(10, ge=1, le=100),
|
||||
count: int = Query(10, ge=0),
|
||||
filter: str | None = Query(None),
|
||||
):
|
||||
"""
|
||||
Get a list of groups according to SCIM v2 protocol
|
||||
"""
|
||||
page_size: Final = min(count, SCIM_MAX_PAGE_SIZE)
|
||||
verbose_proxy_logger.debug(
|
||||
"SCIM GET GROUPS request: startIndex=%s count=%s filter=%s",
|
||||
startIndex,
|
||||
|
|
@ -2425,7 +2429,7 @@ async def get_groups(
|
|||
teams: Final = await _table(TeamRepository(prisma_client)).find_many(
|
||||
where=where_conditions,
|
||||
skip=(startIndex - 1),
|
||||
take=count,
|
||||
take=page_size,
|
||||
order={"created_at": "desc"},
|
||||
)
|
||||
|
||||
|
|
@ -2462,7 +2466,7 @@ async def get_groups(
|
|||
return SCIMListResponse(
|
||||
totalResults=total_count,
|
||||
startIndex=startIndex,
|
||||
itemsPerPage=min(count, len(scim_groups)),
|
||||
itemsPerPage=len(scim_groups),
|
||||
Resources=scim_groups,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ def _get_cli_sso_flow_or_raise(login_id: str | None, cache: DualCache) -> dict:
|
|||
status_code=400,
|
||||
detail=(
|
||||
"Your litellm CLI is out of date and uses a login flow this proxy no longer supports. "
|
||||
"Upgrade it with `pip install -U 'litellm[proxy]'` and run `litellm-proxy login` again."
|
||||
"Upgrade it with `pip install -U 'litellm[proxy]'` and run `lite login` again."
|
||||
),
|
||||
)
|
||||
if not _is_valid_cli_sso_login_id(login_id):
|
||||
|
|
@ -375,7 +375,7 @@ def _get_cli_sso_flow_or_raise(login_id: str | None, cache: DualCache) -> dict:
|
|||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=(
|
||||
"CLI login session not found or expired. Run `litellm-proxy login` again. "
|
||||
"CLI login session not found or expired. Run `lite login` again. "
|
||||
"If this happens immediately after starting a login, the proxy is likely running multiple "
|
||||
"replicas without a shared cache; configure a Redis cache "
|
||||
"so every replica can see the login session."
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ proxy-runtime = [
|
|||
[project.scripts]
|
||||
litellm = "litellm:run_server"
|
||||
lite = "litellm.proxy.client.cli:cli"
|
||||
litellm-proxy = "litellm.proxy.client.cli:cli"
|
||||
litellm-proxy = "litellm.proxy.client.cli:litellm_proxy_cli"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ async def obtain_cli_sso_token_via_poll_flow(
|
|||
models: list[str],
|
||||
) -> str:
|
||||
"""
|
||||
Obtain a CLI SSO JWT through the same HTTP flow as `litellm-proxy login`:
|
||||
Obtain a CLI SSO JWT through the same HTTP flow as `lite login`:
|
||||
/sso/cli/start -> (SSO callback) -> /sso/cli/complete -> /sso/cli/poll.
|
||||
|
||||
When the proxy SSO session cache is not shared with the test runner (otel CI
|
||||
|
|
@ -551,7 +551,7 @@ async def test_team_budget_enforcement():
|
|||
@pytest.mark.asyncio
|
||||
async def test_team_budget_enforcement_cli_sso_token():
|
||||
"""
|
||||
Team budget enforcement for CLI SSO session tokens (litellm-proxy login JWT).
|
||||
Team budget enforcement for CLI SSO session tokens (lite login JWT).
|
||||
|
||||
1. Create team with a tiny max_budget and a user on that team
|
||||
2. Obtain a CLI SSO JWT (HTTP poll flow when Redis is shared, else mint)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""CLI tests for the ``litellm-proxy encryption migrate`` command.
|
||||
"""CLI tests for the ``lite encryption migrate`` command.
|
||||
|
||||
The HTTP client is mocked, so these assert the command's request routing (GET
|
||||
check vs POST migrate, dry-run param) and its residual-state messaging without a
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
# stdlib imports
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -9,7 +11,8 @@ from click.testing import CliRunner
|
|||
|
||||
import litellm.proxy.client.cli
|
||||
from litellm._version import version as litellm_version
|
||||
from litellm.proxy.client.cli import cli
|
||||
from litellm.proxy.client.cli import cli, litellm_proxy_cli
|
||||
from litellm.proxy.client.cli.main import LITELLM_PROXY_DEPRECATION_NOTICE
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -234,3 +237,32 @@ def test_version_flag_never_sends_api_key_to_unnamed_server(cli_runner, isolated
|
|||
assert all(url.startswith("https://flag-proxy.example.com") for url in requested_urls)
|
||||
sent_keys = [call.kwargs["headers"].get("Authorization") for call in mock_request.call_args_list]
|
||||
assert sent_keys == ["Bearer sk-intended-for-flag-proxy"] * len(requested_urls)
|
||||
|
||||
|
||||
def test_litellm_proxy_entrypoint_prints_deprecation_notice_on_stderr_and_still_runs(monkeypatch, capsys, requests_mock):
|
||||
requests_mock.get("http://localhost:4000/health/readiness", json={"litellm_version": "1.2.3"})
|
||||
monkeypatch.setattr(sys, "argv", ["litellm-proxy", "--version"])
|
||||
monkeypatch.setenv("LITELLM_PROXY_URL", "http://localhost:4000")
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
litellm_proxy_cli()
|
||||
|
||||
captured: Final = capsys.readouterr()
|
||||
assert exit_info.value.code == 0
|
||||
assert captured.err.strip() == LITELLM_PROXY_DEPRECATION_NOTICE
|
||||
assert f"LiteLLM Proxy CLI Version: {litellm_version}" in captured.out
|
||||
assert "LiteLLM Proxy Server Version: 1.2.3" in captured.out
|
||||
assert "deprecated" not in captured.out
|
||||
|
||||
|
||||
def test_lite_entrypoint_prints_nothing_on_stderr(monkeypatch, capsys, requests_mock):
|
||||
requests_mock.get("http://localhost:4000/health/readiness", json={"litellm_version": "1.2.3"})
|
||||
monkeypatch.setattr(sys, "argv", ["lite", "--version"])
|
||||
monkeypatch.setenv("LITELLM_PROXY_URL", "http://localhost:4000")
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
cli()
|
||||
|
||||
captured: Final = capsys.readouterr()
|
||||
assert exit_info.value.code == 0
|
||||
assert "LiteLLM Proxy Server Version: 1.2.3" in captured.out
|
||||
assert f"LiteLLM Proxy CLI Version: {litellm_version}" in captured.out
|
||||
assert captured.err == ""
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ from typing import Final
|
|||
from unittest.mock import AsyncMock, MagicMock, call
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from litellm.proxy._types import (
|
||||
|
|
@ -31,6 +32,7 @@ from litellm.proxy.management_endpoints.scim.scim_v2 import (
|
|||
_handle_group_membership_changes,
|
||||
_handle_team_membership_changes,
|
||||
_parse_member_entries,
|
||||
_premium_user_check,
|
||||
_process_group_patch_operations,
|
||||
_recompute_scim_member_roles,
|
||||
_resolve_group_member_ids,
|
||||
|
|
@ -45,8 +47,10 @@ from litellm.proxy.management_endpoints.scim.scim_v2 import (
|
|||
patch_group,
|
||||
patch_team_membership,
|
||||
patch_user,
|
||||
scim_router,
|
||||
update_group,
|
||||
update_user,
|
||||
user_api_key_auth,
|
||||
)
|
||||
from litellm.types.proxy.management_endpoints.scim_v2 import (
|
||||
SCIM_ENTERPRISE_USER_SCHEMA,
|
||||
|
|
@ -484,6 +488,48 @@ async def test_scim_create_user_respects_default_role_set_via_ui(mocker, monkeyp
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scim_test_client():
|
||||
"""An in-process SCIM application with authorization dependencies bypassed."""
|
||||
app = FastAPI()
|
||||
app.dependency_overrides[_premium_user_check] = lambda: None
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
|
||||
app.include_router(scim_router)
|
||||
return AsyncClient(transport=ASGITransport(app=app), base_url="http://test")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("endpoint", ["Users", "Groups"])
|
||||
@pytest.mark.parametrize(("requested_count", "effective_count"), [(0, 0), (200, 100), (1000, 100)])
|
||||
async def test_scim_collection_endpoints_clamp_requested_page_size(
|
||||
scim_test_client, endpoint, requested_count, effective_count, mocker
|
||||
):
|
||||
"""SCIM list endpoints accept zero and cap larger client page requests."""
|
||||
mock_prisma_client = MagicMock()
|
||||
mock_prisma_client.db = MagicMock()
|
||||
table = MagicMock()
|
||||
table.find_many = AsyncMock(return_value=[])
|
||||
table.count = AsyncMock(return_value=0)
|
||||
mock_prisma_client.db.litellm_usertable = table
|
||||
mock_prisma_client.db.litellm_teamtable = table
|
||||
mocker.patch( # test-quality-ok: HTTP validation requires an in-memory database boundary.
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mock_prisma_client),
|
||||
)
|
||||
|
||||
async with scim_test_client as client:
|
||||
response = await client.get(f"/scim/v2/{endpoint}?startIndex=1&count={requested_count}")
|
||||
|
||||
assert response.status_code == 200
|
||||
table.find_many.assert_awaited_once_with(
|
||||
where={},
|
||||
skip=0,
|
||||
take=effective_count,
|
||||
order={"created_at": "desc"},
|
||||
)
|
||||
assert response.json()["itemsPerPage"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_users_filters_username_by_exposed_scim_username_for_okta(mocker):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue