mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(deps): move pydantic-settings into the base dependencies
`import litellm` reaches litellm/integrations/otel/model/config.py via litellm_core_utils/litellm_logging.py, so pydantic-settings is needed at import time. It was declared only in the `proxy` extra, which left a plain `pip install litellm` unimportable on every platform. Adds tests/base_sdk_tests/check_base_sdk_install.py and a base_sdk_install CircleCI job that builds the wheel, installs it into a clean venv with no extras, and smoke-checks the import, a mock completion, a mock embedding, the bundled pricing metadata and the token counter. The check is stdlib-only on purpose; installing pytest into that venv would add packaging, pluggy and iniconfig and could mask the class of undeclared dependency it exists to catch. Previously the Windows job was the only one installing without extras, so this class of break was caught by accident rather than by design.
This commit is contained in:
parent
b1fd20f4cd
commit
7447f9babc
4 changed files with 155 additions and 4 deletions
|
|
@ -281,6 +281,32 @@ jobs:
|
|||
uv build --wheel --out-dir dist
|
||||
uv run --no-sync python tests/windows_tests/check_windows_wheel_install.py
|
||||
|
||||
base_sdk_install:
|
||||
docker:
|
||||
- image: cimg/python:3.12@sha256:9c796c23c84e84a66a964acb508d39dc5433c81a47e07efd56dccbbc2427e07c
|
||||
auth:
|
||||
username: ${DOCKERHUB_USERNAME}
|
||||
password: ${DOCKERHUB_PASSWORD}
|
||||
working_directory: ~/project
|
||||
steps:
|
||||
- checkout
|
||||
- setup_google_dns
|
||||
- install_uv
|
||||
- run:
|
||||
name: Build the wheel
|
||||
environment:
|
||||
UV_HTTP_TIMEOUT: "300"
|
||||
command: |
|
||||
uv build --wheel --out-dir dist
|
||||
- run:
|
||||
name: Install the wheel with no extras and smoke-check it
|
||||
environment:
|
||||
UV_HTTP_TIMEOUT: "300"
|
||||
command: |
|
||||
uv venv /tmp/base-sdk --python 3.12
|
||||
VIRTUAL_ENV=/tmp/base-sdk uv pip install dist/*.whl
|
||||
/tmp/base-sdk/bin/python tests/base_sdk_tests/check_base_sdk_install.py
|
||||
|
||||
local_testing_part1:
|
||||
docker:
|
||||
- &python312_image
|
||||
|
|
@ -3031,6 +3057,8 @@ workflows:
|
|||
only:
|
||||
- main
|
||||
- /litellm_.*/
|
||||
- base_sdk_install:
|
||||
filters: *main_branches
|
||||
- local_testing_part1:
|
||||
filters: *main_branches
|
||||
- local_testing_part2:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ dependencies = [
|
|||
"jinja2>=3.1.6,<4.0",
|
||||
"aiohttp>=3.14.2,<4.0",
|
||||
"pydantic>=2.10.0,<3.0.0",
|
||||
"pydantic-settings>=2.14.1,<3.0",
|
||||
"jsonschema>=4.0.0,<5.0",
|
||||
]
|
||||
|
||||
|
|
@ -70,7 +71,6 @@ proxy = [
|
|||
"polars>=1.38.1,<2.0",
|
||||
"soundfile>=0.12.1,<1.0",
|
||||
"pyroscope-io>=0.8.16,<1.0; sys_platform != 'win32'",
|
||||
"pydantic-settings>=2.14.1,<3.0",
|
||||
"expression>=5.6.0,<6.0",
|
||||
]
|
||||
# Thin client install for the `lite` CLI on developer laptops. The CLI's heavy
|
||||
|
|
|
|||
123
tests/base_sdk_tests/check_base_sdk_install.py
Normal file
123
tests/base_sdk_tests/check_base_sdk_install.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
"""Smoke-check that a base ``pip install litellm`` (no extras) is importable and usable.
|
||||
|
||||
Run against a virtualenv that has the built wheel installed with no extras, using
|
||||
that venv's own interpreter and nothing else. Deliberately stdlib-only: pytest would
|
||||
pull ``packaging``, ``pluggy`` and ``iniconfig`` into the environment and could mask
|
||||
the very class of undeclared-dependency bug this guards against.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import traceback
|
||||
from collections.abc import Callable
|
||||
|
||||
EXTRAS_ONLY_MODULES = ("fastapi", "boto3", "uvicorn")
|
||||
|
||||
|
||||
def _require(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def check_environment_is_base_only() -> str:
|
||||
present = tuple(name for name in EXTRAS_ONLY_MODULES if importlib.util.find_spec(name) is not None)
|
||||
_require(
|
||||
not present,
|
||||
f"{', '.join(present)} installed, so this environment is not base-only and the run proves nothing",
|
||||
)
|
||||
return f"no extras-only packages present ({', '.join(EXTRAS_ONLY_MODULES)})"
|
||||
|
||||
|
||||
def check_import() -> str:
|
||||
from importlib.metadata import version
|
||||
|
||||
import litellm
|
||||
|
||||
_require(bool(litellm.__file__), "litellm has no __file__")
|
||||
return f"imported litellm {version('litellm')}"
|
||||
|
||||
|
||||
def check_completion() -> str:
|
||||
import litellm
|
||||
|
||||
response = litellm.completion(
|
||||
model="gpt-4o",
|
||||
messages=[{"role": "user", "content": "ping"}],
|
||||
mock_response="pong",
|
||||
)
|
||||
content = response.choices[0].message.content
|
||||
_require(content == "pong", f"mock completion returned {content!r}")
|
||||
return "mock completion round-trips"
|
||||
|
||||
|
||||
def check_embedding() -> str:
|
||||
import litellm
|
||||
|
||||
response = litellm.embedding(
|
||||
model="text-embedding-3-small",
|
||||
input=["ping"],
|
||||
mock_response=[[0.1, 0.2]],
|
||||
)
|
||||
_require(len(response.data) == 1, f"mock embedding returned {len(response.data)} rows")
|
||||
return "mock embedding round-trips"
|
||||
|
||||
|
||||
def check_bundled_model_metadata() -> str:
|
||||
import litellm
|
||||
|
||||
max_input_tokens = litellm.get_model_info("gpt-4o")["max_input_tokens"]
|
||||
_require(
|
||||
isinstance(max_input_tokens, int) and max_input_tokens > 0,
|
||||
f"get_model_info returned max_input_tokens={max_input_tokens!r}",
|
||||
)
|
||||
prompt_cost, completion_cost = litellm.cost_per_token(model="gpt-4o", prompt_tokens=1000, completion_tokens=1000)
|
||||
_require(
|
||||
prompt_cost > 0 and completion_cost > 0,
|
||||
f"cost_per_token returned ({prompt_cost}, {completion_cost})",
|
||||
)
|
||||
return f"bundled pricing metadata readable (gpt-4o max_input_tokens={max_input_tokens})"
|
||||
|
||||
|
||||
def check_token_counter() -> str:
|
||||
import litellm
|
||||
|
||||
count = litellm.token_counter(model="gpt-4o", text="hello world")
|
||||
_require(count > 0, f"token_counter returned {count!r}")
|
||||
return f"token_counter returned {count}"
|
||||
|
||||
|
||||
CHECKS: tuple[tuple[str, Callable[[], str]], ...] = (
|
||||
("environment is base-only", check_environment_is_base_only),
|
||||
("import litellm", check_import),
|
||||
("chat completion", check_completion),
|
||||
("embedding", check_embedding),
|
||||
("bundled model metadata", check_bundled_model_metadata),
|
||||
("token counter", check_token_counter),
|
||||
)
|
||||
|
||||
|
||||
def _run(check: Callable[[], str]) -> tuple[bool, str]:
|
||||
try:
|
||||
return True, check()
|
||||
except Exception:
|
||||
return False, traceback.format_exc()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(f"base SDK smoke check on {sys.executable}")
|
||||
for label, check in CHECKS:
|
||||
passed, detail = _run(check)
|
||||
if not passed:
|
||||
print(f"FAIL {label}:\n{detail}")
|
||||
print(f"A base `pip install litellm` is broken at: {label}")
|
||||
print("Something needed at import or call time is missing from [project].dependencies")
|
||||
print("in pyproject.toml. Declaring it only in an extra is what causes this.")
|
||||
return 1
|
||||
print(f"PASS {label}: {detail}")
|
||||
|
||||
print(f"\nall {len(CHECKS)} checks passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
6
uv.lock
generated
6
uv.lock
generated
|
|
@ -10,7 +10,7 @@ resolution-markers = [
|
|||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-07-29T19:04:35.546526Z"
|
||||
exclude-newer = "2026-07-29T22:09:54.255381Z"
|
||||
exclude-newer-span = "P3D"
|
||||
|
||||
[manifest]
|
||||
|
|
@ -4128,6 +4128,7 @@ dependencies = [
|
|||
{ name = "jsonschema" },
|
||||
{ name = "openai" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "tiktoken" },
|
||||
{ name = "tokenizers" },
|
||||
|
|
@ -4183,7 +4184,6 @@ proxy = [
|
|||
{ name = "mcp" },
|
||||
{ name = "orjson" },
|
||||
{ name = "polars" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pynacl" },
|
||||
{ name = "pyroscope-io", marker = "sys_platform != 'win32'" },
|
||||
|
|
@ -4385,7 +4385,7 @@ requires-dist = [
|
|||
{ name = "prisma", marker = "extra == 'extra-proxy'", specifier = ">=0.11.0,<1.0" },
|
||||
{ name = "prometheus-client", marker = "extra == 'proxy-runtime'", specifier = ">=0.20.0,<1.0" },
|
||||
{ name = "pydantic", specifier = ">=2.10.0,<3.0.0" },
|
||||
{ name = "pydantic-settings", marker = "extra == 'proxy'", specifier = ">=2.14.1,<3.0" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.14.1,<3.0" },
|
||||
{ name = "pyjwt", marker = "extra == 'proxy'", specifier = ">=2.13.0,<3.0" },
|
||||
{ name = "pynacl", marker = "extra == 'proxy'", specifier = ">=1.6.2,<2.0" },
|
||||
{ name = "pypdf", marker = "extra == 'proxy-runtime'", specifier = ">=6.12.0,<7.0" },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue