mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
* fix: support OpenAI SDK 3.x while retaining 2.x compatibility * fix: preserve HTTPX compatibility across OpenAI SDK versions * refactor: trim OpenAI SDK compatibility patch * refactor: simplify SDK client defaults * test: cover SDK compatibility across provider routes * fix: use shared names for SDK transport helpers * fix: address SDK compatibility review feedback * test(openai): send requests through the SDK API factory clients * fix(exceptions): keep the provider response on PaymentRequiredError under httpx 2 * ci(base-sdk): count httpx2 as base-only when a base dependency brings it in OpenAI SDK 3 depends on httpx2, so the litellm-core install at highest resolution pulls it in through openai. The base-only check now resolves the declared base dependency closure of litellm and litellm-core and accepts extras-only modules owned by a distribution inside that closure * test: cover OpenAI SDK 3 client paths end to end and read SDK HTTP headers case-insensitively * test: read C02 spend rows once, wait out the worker healthcheck on respawn, and resolve module owners on Python 3.10 --------- Co-authored-by: Marcus Wood <marcuswood@openai.com> Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
251 lines
10 KiB
Python
251 lines
10 KiB
Python
"""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 argparse
|
|
import importlib.util
|
|
import re
|
|
import sys
|
|
import traceback
|
|
from collections.abc import Callable
|
|
from importlib.metadata import PackageNotFoundError, packages_distributions, requires
|
|
from itertools import chain
|
|
from typing import Final
|
|
|
|
EXTRAS_ONLY_MODULES = ("fastapi", "uvicorn", "keyring", "mcp", "mcp_types", "httpx2", "httpcore2")
|
|
SDK_DISTRIBUTIONS: Final = frozenset({"litellm", "litellm-core"})
|
|
REQUIREMENT_NAME: Final = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]*")
|
|
EXTRA_MARKER: Final = re.compile(r";.*\bextra\s*==")
|
|
|
|
|
|
def _require(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise AssertionError(message)
|
|
|
|
|
|
def _canonical(name: str) -> str:
|
|
return re.sub(r"[-_.]+", "-", name).lower()
|
|
|
|
|
|
def _base_requirements(distribution: str) -> frozenset[str]:
|
|
try:
|
|
declared: Final = requires(distribution) or ()
|
|
except PackageNotFoundError:
|
|
return frozenset()
|
|
return frozenset(
|
|
_canonical(match.group(0))
|
|
for requirement in declared
|
|
if not EXTRA_MARKER.search(requirement) and (match := REQUIREMENT_NAME.match(requirement))
|
|
)
|
|
|
|
|
|
def _base_closure(pending: frozenset[str], reached: frozenset[str] = frozenset()) -> frozenset[str]:
|
|
if not pending:
|
|
return reached
|
|
expanded: Final = reached | pending
|
|
return _base_closure(frozenset(chain.from_iterable(_base_requirements(name) for name in pending)) - expanded, expanded)
|
|
|
|
|
|
def check_environment_is_base_only() -> str:
|
|
from_base: Final = _base_closure(SDK_DISTRIBUTIONS)
|
|
owners: Final = packages_distributions()
|
|
installed: Final = tuple(name for name in EXTRAS_ONLY_MODULES if importlib.util.find_spec(name) is not None)
|
|
inherited: Final = tuple(
|
|
name for name in installed if from_base & {_canonical(owner) for owner in owners.get(name) or (name,)}
|
|
)
|
|
present: Final = tuple(name for name in installed if name not in inherited)
|
|
_require(
|
|
not present,
|
|
f"{', '.join(present)} installed, so this environment is not base-only and the run proves nothing",
|
|
)
|
|
from_dependencies: Final = f"; {', '.join(inherited)} come from base dependencies" if inherited else ""
|
|
return f"no extras-only packages present ({', '.join(EXTRAS_ONLY_MODULES)}){from_dependencies}"
|
|
|
|
|
|
def check_import() -> str:
|
|
from importlib.metadata import distributions as installed_distributions
|
|
from importlib.metadata import version
|
|
|
|
import litellm
|
|
from litellm.llms.brave.search.transformation import BraveSearchConfig
|
|
|
|
_require(callable(BraveSearchConfig), "Brave search configuration unavailable")
|
|
_require(bool(litellm.__file__), "litellm has no __file__")
|
|
from litellm._version import version as sdk_version
|
|
|
|
distributions = tuple(
|
|
distribution.metadata["Name"]
|
|
for distribution in installed_distributions()
|
|
if distribution.metadata["Name"] in ("litellm", "litellm-core")
|
|
)
|
|
_require(len(distributions) == 1, f"expected one SDK distribution, found {distributions}")
|
|
distribution = distributions[0]
|
|
_require(sdk_version == version(distribution), "SDK version does not match installed metadata")
|
|
_require("litellm.proxy.proxy_cli" not in sys.modules, "SDK import loaded the proxy CLI")
|
|
return f"imported {distribution} {sdk_version}"
|
|
|
|
|
|
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_mcp_install_guidance() -> str:
|
|
try:
|
|
import litellm.experimental_mcp_client
|
|
except ImportError as error:
|
|
_require("pip install 'litellm[mcp]'" in str(error), f"missing MCP installation guidance: {error}")
|
|
_require(isinstance(error.__cause__, ModuleNotFoundError), "original missing-dependency cause was lost")
|
|
_require(error.__cause__.name == "mcp", f"unexpected missing dependency: {error.__cause__}")
|
|
return "optional MCP client explains how to install litellm[mcp]"
|
|
raise AssertionError("MCP client imported without the MCP extra")
|
|
|
|
|
|
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}"
|
|
|
|
|
|
def check_tokenizer_dependencies() -> str:
|
|
import litellm
|
|
from litellm.rust_bridge import tokenizer
|
|
from litellm.litellm_core_utils.tokenizer import HuggingFace, HuggingFaceTokenizer, Tokenizer
|
|
from litellm.utils import claude_json_str
|
|
|
|
_require(isinstance(litellm.encoding, Tokenizer), "runtime alias rejects the default encoding")
|
|
native = tokenizer.native_anthropic()
|
|
if native is not None:
|
|
_require(bool(native.encode("hello")), "native tokenizer returned no tokens")
|
|
_require(isinstance(HuggingFaceTokenizer(native), HuggingFace), "runtime alias rejects native tokenizers")
|
|
if importlib.util.find_spec("tokenizers") is not None:
|
|
python_tokenizer = tokenizer.from_str(claude_json_str)
|
|
_require(bool(python_tokenizer.encode("hello").ids), "Python tokenizer returned no tokens")
|
|
_require(isinstance(python_tokenizer, HuggingFace), "runtime alias rejects Python tokenizers")
|
|
return "installed Python tokenizer and available native tokenizer work"
|
|
try:
|
|
tokenizer.from_str(claude_json_str)
|
|
except ImportError as error:
|
|
_require("pip install tokenizers" in str(error), f"missing tokenizer guidance: {error}")
|
|
else:
|
|
raise AssertionError("Python tokenizer loaded without tokenizers")
|
|
return "native tokenizer works; Python tokenizer reports its missing dependency"
|
|
|
|
|
|
def check_bedrock_credential_resolution() -> str:
|
|
import os
|
|
from unittest import mock
|
|
|
|
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
|
|
|
|
if importlib.util.find_spec("boto3") is None:
|
|
try:
|
|
BaseAWSLLM()._sign_request(
|
|
service_name="bedrock", headers={}, optional_params={"aws_region_name": "us-east-1"},
|
|
request_data={}, api_base="https://bedrock-runtime.us-east-1.amazonaws.com", api_key="",
|
|
)
|
|
except ImportError as error:
|
|
_require("pip install boto3" in str(error), f"missing installation guidance: {error}")
|
|
return "AWS signing explains how to install boto3"
|
|
raise AssertionError("AWS signing unexpectedly worked without boto3")
|
|
non_aws_environ = {k: v for k, v in os.environ.items() if not k.startswith("AWS_")}
|
|
with mock.patch.dict(os.environ, non_aws_environ, clear=True):
|
|
credentials = BaseAWSLLM().get_credentials(
|
|
aws_access_key_id="AKIA-fake-base-sdk-check",
|
|
aws_secret_access_key="fake-secret",
|
|
aws_region_name="us-east-1",
|
|
)
|
|
_require(
|
|
credentials.access_key == "AKIA-fake-base-sdk-check",
|
|
f"get_credentials returned access_key={credentials.access_key!r}",
|
|
)
|
|
return "bedrock credential resolution works (boto3 ships with the base SDK)"
|
|
|
|
|
|
CHECKS: tuple[tuple[str, Callable[[], str]], ...] = (
|
|
("environment is base-only", check_environment_is_base_only),
|
|
("import litellm", check_import),
|
|
("optional MCP installation guidance", check_mcp_install_guidance),
|
|
("chat completion", check_completion),
|
|
("embedding", check_embedding),
|
|
("bundled model metadata", check_bundled_model_metadata),
|
|
("token counter", check_token_counter),
|
|
("tokenizer dependencies", check_tokenizer_dependencies),
|
|
("bedrock credential resolution", check_bedrock_credential_resolution),
|
|
)
|
|
|
|
|
|
def _run(check: Callable[[], str]) -> tuple[bool, str]:
|
|
try:
|
|
return True, check()
|
|
except Exception:
|
|
return False, traceback.format_exc()
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--profile", choices=("legacy", "core", "dependencies"), default="legacy")
|
|
profile = parser.parse_args().profile
|
|
for module in ("boto3", "botocore", "tokenizers", "huggingface_hub"):
|
|
present = importlib.util.find_spec(module) is not None
|
|
_require(present == (profile != "core"), f"{profile}: unexpected presence of {module}: {present}")
|
|
_require(importlib.util.find_spec("jsonschema") is not None, "jsonschema must remain mandatory")
|
|
print(f"{profile} 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())
|