From 3d55afe38b51315669207d7f4d15a7fd27c49159 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 5 May 2026 15:45:13 -0700 Subject: [PATCH 1/3] [Infra] Packaging: Relax Core Runtime Pins To Ranges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 12 core `[project.dependencies]` entries in pyproject.toml were exact `==` pins, a side effect of the Poetry → uv migration. This forces every downstream package that lists litellm as a dependency to downgrade common runtime libraries (openai, pydantic, aiohttp, click, jsonschema, ...) to the exact versions we ship. Customers have flagged this as a coexistence blocker. Switch to lower-bounded ranges with upper bounds where the upstream package is pre-1.0 or has a known breaking-major-version policy. Reproducibility for our Docker proxy and CI continues to come from `uv.lock`, which is regenerated here as a metadata-only diff (no resolved versions or hashes change). Inspired by #26157 (which got stranded on `litellm_oss_staging_04_21_2026` when the forward-merge to internal staging in #26216 was closed). Floors in this PR are tighter than #26157's: they were validated by installing litellm at `--resolution=lowest-direct` and importing the openai-namespace symbols the codebase actually uses. Floor highlights vs #26157: - openai >= 2.20 (was 2.0) — Responses API symbols + `Omit` need a 2.x mid-range floor - httpx >= 0.28, < 1.0 (was no upper) — pre-1.0 - importlib-metadata >= 8.0 (was 6.0) — stay in tested major - tokenizers >= 0.20, < 1.0 (was 0.19, no upper) — pre-1.0 - aiohttp >= 3.10, < 4.0 (was no upper) — bound major - pydantic >= 2.5, < 3.0 — kept - All other floors: keep tested major, add upper bound Adds a `check-dependency-floors.yml` GitHub Actions workflow that installs litellm at `--resolution=lowest-direct` on Python 3.10 and 3.13 and import-checks every openai symbol the codebase uses, so a future floor regression fails fast in CI rather than silently in the field. --- .github/workflows/check-dependency-floors.yml | 117 ++++++++++++++++++ pyproject.toml | 27 ++-- uv.lock | 24 ++-- 3 files changed, 144 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/check-dependency-floors.yml diff --git a/.github/workflows/check-dependency-floors.yml b/.github/workflows/check-dependency-floors.yml new file mode 100644 index 00000000000..8fc5b8564ee --- /dev/null +++ b/.github/workflows/check-dependency-floors.yml @@ -0,0 +1,117 @@ +name: Check dependency floors + +on: + pull_request: + paths: + - 'pyproject.toml' + - '.github/workflows/check-dependency-floors.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + resolve-and-import: + name: Install at lowest-direct + smoke-import litellm + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.13"] + steps: + - name: Checkout PR + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Set up uv + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + with: + version: "0.10.9" + enable-cache: false + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Resolve litellm core deps at floor versions + run: | + uv venv --python ${{ matrix.python-version }} /tmp/floor-venv + uv pip install --python /tmp/floor-venv/bin/python --resolution=lowest-direct . + + - name: Smoke-import litellm at floor versions + run: | + /tmp/floor-venv/bin/python <<'PY' + import importlib + + # Import the SDK itself. + import litellm # noqa: F401 + + # Import every openai-namespace symbol the codebase actually uses + # at runtime. If a floor is set lower than the version that + # introduced any of these, the install will silently succeed and + # then fail at runtime in customer code. This list mirrors the + # openai imports under litellm/ — keep it in sync. + import openai # noqa: F401 + from openai import ( # noqa: F401 + AsyncAzureOpenAI, + AsyncOpenAI, + AzureOpenAI, + OpenAI, + APIError, + APITimeoutError, + Omit, + ) + from openai.types.responses import ( # noqa: F401 + ResponseFunctionToolCall, + ResponseInputImageParam, + ResponseOutputMessage, + ResponseReasoningItem, + ) + from openai.types.responses.response import ( # noqa: F401 + IncompleteDetails, + Response, + ResponseOutputItem, + Tool, + ToolChoice, + ) + from openai.types.responses.response_output_item import ( # noqa: F401 + ResponseApplyPatchToolCall, + ) + from openai.types.responses.response_text_config_param import ( # noqa: F401 + ResponseTextConfigParam, + ) + from openai.types.responses.tool_param import ( # noqa: F401 + FunctionToolParam, + ) + + # Sanity-check the other direct deps load. + for mod in ( + "pydantic", + "httpx", + "aiohttp", + "tokenizers", + "tiktoken", + "click", + "jinja2", + "jsonschema", + "importlib_metadata", + "fastuuid", + "dotenv", + ): + importlib.import_module(mod) + + print("All floor-version imports OK") + PY + + - name: Report resolved versions + if: always() + run: | + /tmp/floor-venv/bin/python -c " + from importlib.metadata import version + for pkg in ['litellm', 'openai', 'pydantic', 'httpx', 'aiohttp', 'tokenizers', 'tiktoken', 'click', 'jinja2', 'jsonschema', 'importlib-metadata', 'fastuuid', 'python-dotenv']: + try: + print(f'{pkg}: {version(pkg)}') + except Exception as e: + print(f'{pkg}: ({e})') + " diff --git a/pyproject.toml b/pyproject.toml index 48acf8d8712..61502993bd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,18 +10,21 @@ authors = [ { name = "BerriAI" }, ] dependencies = [ - "fastuuid==0.14.0", - "httpx==0.28.1", - "openai==2.33.0", - "python-dotenv==1.2.2", - "tiktoken==0.12.0", - "importlib-metadata==8.5.0", - "tokenizers==0.23.1", - "click==8.1.8", - "jinja2==3.1.6", - "aiohttp==3.13.4", - "pydantic==2.12.5", - "jsonschema==4.23.0", + # Ranges (not exact pins) so SDK consumers can coexist with their other + # deps. Reproducibility for our Docker/CI comes from `uv.lock`. The + # `dependency-floors` CI job validates the floors actually install. + "fastuuid>=0.14.0,<1.0", + "httpx>=0.28.0,<1.0", + "openai>=2.20.0,<3.0.0", + "python-dotenv>=1.0.0,<2.0", + "tiktoken>=0.7.0,<1.0", + "importlib-metadata>=8.0.0", + "tokenizers>=0.20.0,<1.0", + "click>=8.0.0,<9.0", + "jinja2>=3.1.0,<4.0", + "aiohttp>=3.10,<4.0", + "pydantic>=2.5.0,<3.0.0", + "jsonschema>=4.0.0,<5.0", ] [project.urls] diff --git a/uv.lock b/uv.lock index dde75bc1383..d8cbead4a2f 100644 --- a/uv.lock +++ b/uv.lock @@ -3261,7 +3261,7 @@ proxy-dev = [ [package.metadata] requires-dist = [ { name = "a2a-sdk", marker = "extra == 'extra-proxy'", specifier = "==0.3.24" }, - { name = "aiohttp", specifier = "==3.13.4" }, + { name = "aiohttp", specifier = ">=3.10,<4.0" }, { name = "anthropic", extras = ["vertex"], marker = "extra == 'proxy-runtime'", specifier = "==0.84.0" }, { name = "apscheduler", marker = "extra == 'proxy'", specifier = "==3.11.2" }, { name = "aurelio-sdk", marker = "python_full_version < '3.14' and extra == 'semantic-router'", specifier = "==0.0.19" }, @@ -3273,14 +3273,14 @@ requires-dist = [ { name = "azure-storage-file-datalake", marker = "extra == 'proxy-runtime'", specifier = "==12.20.0" }, { name = "backoff", marker = "extra == 'proxy'", specifier = "==2.2.1" }, { name = "boto3", marker = "extra == 'proxy'", specifier = "==1.43.1" }, - { name = "click", specifier = "==8.1.8" }, + { name = "click", specifier = ">=8.0.0,<9.0" }, { name = "cryptography", marker = "extra == 'proxy'", specifier = "==46.0.7" }, { name = "ddtrace", marker = "extra == 'proxy-runtime'", specifier = "==2.19.0" }, { name = "detect-secrets", marker = "extra == 'proxy-runtime'", specifier = "==1.5.0" }, { name = "diskcache", marker = "extra == 'caching'", specifier = "==5.6.3" }, { name = "fastapi", marker = "extra == 'proxy'", specifier = "==0.124.4" }, { name = "fastapi-sso", marker = "extra == 'proxy'", specifier = "==0.19.0" }, - { name = "fastuuid", specifier = "==0.14.0" }, + { name = "fastuuid", specifier = ">=0.14.0,<1.0" }, { name = "google-cloud-aiplatform", marker = "extra == 'google'", specifier = "==1.133.0" }, { name = "google-cloud-aiplatform", marker = "extra == 'proxy-runtime'", specifier = "==1.133.0" }, { name = "google-cloud-iam", marker = "extra == 'extra-proxy'", specifier = "==2.19.1" }, @@ -3289,10 +3289,10 @@ requires-dist = [ { name = "grpcio", marker = "extra == 'grpc'", specifier = "==1.78.0" }, { name = "grpcio", marker = "extra == 'proxy-runtime'", specifier = "==1.78.0" }, { name = "gunicorn", marker = "extra == 'proxy'", specifier = "==23.0.0" }, - { name = "httpx", specifier = "==0.28.1" }, - { name = "importlib-metadata", specifier = "==8.5.0" }, - { name = "jinja2", specifier = "==3.1.6" }, - { name = "jsonschema", specifier = "==4.23.0" }, + { name = "httpx", specifier = ">=0.28.0,<1.0" }, + { name = "importlib-metadata", specifier = ">=8.0.0" }, + { name = "jinja2", specifier = ">=3.1.0,<4.0" }, + { name = "jsonschema", specifier = ">=4.0.0,<5.0" }, { name = "langfuse", marker = "extra == 'proxy-runtime'", specifier = "==2.59.7" }, { name = "litellm-enterprise", marker = "extra == 'proxy'", editable = "enterprise" }, { name = "litellm-proxy-extras", marker = "extra == 'proxy'", editable = "litellm-proxy-extras" }, @@ -3301,7 +3301,7 @@ requires-dist = [ { name = "mcp", marker = "extra == 'proxy'", specifier = "==1.26.0" }, { name = "mlflow", marker = "extra == 'mlflow'", specifier = "==3.11.1" }, { name = "numpydoc", marker = "extra == 'utils'", specifier = "==1.8.0" }, - { name = "openai", specifier = "==2.33.0" }, + { name = "openai", specifier = ">=2.20.0,<3.0.0" }, { name = "opentelemetry-api", marker = "extra == 'proxy-runtime'", specifier = "==1.28.0" }, { name = "opentelemetry-exporter-otlp", marker = "extra == 'proxy-runtime'", specifier = "==1.28.0" }, { name = "opentelemetry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==1.28.0" }, @@ -3309,12 +3309,12 @@ requires-dist = [ { name = "polars", marker = "extra == 'proxy'", specifier = "==1.38.1" }, { name = "prisma", marker = "extra == 'extra-proxy'", specifier = "==0.11.0" }, { name = "prometheus-client", marker = "extra == 'proxy-runtime'", specifier = "==0.20.0" }, - { name = "pydantic", specifier = "==2.12.5" }, + { name = "pydantic", specifier = ">=2.5.0,<3.0.0" }, { name = "pyjwt", marker = "extra == 'proxy'", specifier = "==2.12.0" }, { name = "pynacl", marker = "extra == 'proxy'", specifier = "==1.6.2" }, { name = "pypdf", marker = "python_full_version < '3.14' and extra == 'proxy-runtime'", specifier = "==6.10.2" }, { name = "pyroscope-io", marker = "sys_platform != 'win32' and extra == 'proxy'", specifier = "==0.8.16" }, - { name = "python-dotenv", specifier = "==1.2.2" }, + { name = "python-dotenv", specifier = ">=1.0.0,<2.0" }, { name = "python-multipart", marker = "extra == 'proxy'", specifier = "==0.0.27" }, { name = "pyyaml", marker = "extra == 'proxy'", specifier = "==6.0.3" }, { name = "redisvl", marker = "python_full_version < '3.14' and extra == 'extra-proxy'", specifier = "==0.4.1" }, @@ -3325,8 +3325,8 @@ requires-dist = [ { name = "semantic-router", marker = "python_full_version < '3.14' and extra == 'semantic-router'", specifier = "==0.1.12" }, { name = "sentry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==2.21.0" }, { name = "soundfile", marker = "extra == 'proxy'", specifier = "==0.12.1" }, - { name = "tiktoken", specifier = "==0.12.0" }, - { name = "tokenizers", specifier = "==0.23.1" }, + { name = "tiktoken", specifier = ">=0.7.0,<1.0" }, + { name = "tokenizers", specifier = ">=0.20.0,<1.0" }, { name = "uvicorn", marker = "extra == 'proxy'", specifier = "==0.33.0" }, { name = "uvloop", marker = "sys_platform != 'win32' and extra == 'proxy'", specifier = "==0.21.0" }, { name = "websockets", marker = "extra == 'proxy'", specifier = "==15.0.1" }, From eff0f8c630b267f55ef1dbca15d05193422fbd2b Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 5 May 2026 15:48:59 -0700 Subject: [PATCH 2/3] [Infra] Packaging: Bump compiled-dep floors for cp313 wheel coverage CI matrix on Python 3.13 caught three floors that predate cp313 prebuilt wheels and would force users into a Rust/C build: - tiktoken: 0.7.0 -> 0.8.0 (cp313 wheels start at 0.8) - tokenizers: 0.20.0 -> 0.21.0 (cp313 wheels start at 0.21; sdist's pyproject.toml pre-0.21 is also malformed for modern build backends) - pydantic: 2.5.0 -> 2.10.0 (pydantic-core cp313 wheels start at 2.27, shipped with pydantic 2.10) Verified locally on Python 3.10 and 3.13: install at lowest-direct + import litellm + import every openai-namespace symbol the codebase uses all pass. --- pyproject.toml | 6 +++--- uv.lock | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 61502993bd0..67d23a6071b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ dependencies = [ "httpx>=0.28.0,<1.0", "openai>=2.20.0,<3.0.0", "python-dotenv>=1.0.0,<2.0", - "tiktoken>=0.7.0,<1.0", + "tiktoken>=0.8.0,<1.0", "importlib-metadata>=8.0.0", - "tokenizers>=0.20.0,<1.0", + "tokenizers>=0.21.0,<1.0", "click>=8.0.0,<9.0", "jinja2>=3.1.0,<4.0", "aiohttp>=3.10,<4.0", - "pydantic>=2.5.0,<3.0.0", + "pydantic>=2.10.0,<3.0.0", "jsonschema>=4.0.0,<5.0", ] diff --git a/uv.lock b/uv.lock index d8cbead4a2f..008d7872ac9 100644 --- a/uv.lock +++ b/uv.lock @@ -3309,7 +3309,7 @@ requires-dist = [ { name = "polars", marker = "extra == 'proxy'", specifier = "==1.38.1" }, { name = "prisma", marker = "extra == 'extra-proxy'", specifier = "==0.11.0" }, { name = "prometheus-client", marker = "extra == 'proxy-runtime'", specifier = "==0.20.0" }, - { name = "pydantic", specifier = ">=2.5.0,<3.0.0" }, + { name = "pydantic", specifier = ">=2.10.0,<3.0.0" }, { name = "pyjwt", marker = "extra == 'proxy'", specifier = "==2.12.0" }, { name = "pynacl", marker = "extra == 'proxy'", specifier = "==1.6.2" }, { name = "pypdf", marker = "python_full_version < '3.14' and extra == 'proxy-runtime'", specifier = "==6.10.2" }, @@ -3325,8 +3325,8 @@ requires-dist = [ { name = "semantic-router", marker = "python_full_version < '3.14' and extra == 'semantic-router'", specifier = "==0.1.12" }, { name = "sentry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==2.21.0" }, { name = "soundfile", marker = "extra == 'proxy'", specifier = "==0.12.1" }, - { name = "tiktoken", specifier = ">=0.7.0,<1.0" }, - { name = "tokenizers", specifier = ">=0.20.0,<1.0" }, + { name = "tiktoken", specifier = ">=0.8.0,<1.0" }, + { name = "tokenizers", specifier = ">=0.21.0,<1.0" }, { name = "uvicorn", marker = "extra == 'proxy'", specifier = "==0.33.0" }, { name = "uvloop", marker = "sys_platform != 'win32' and extra == 'proxy'", specifier = "==0.21.0" }, { name = "websockets", marker = "extra == 'proxy'", specifier = "==15.0.1" }, From 201fa5d42bf1487481dbdec161caee56d1f314e7 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 5 May 2026 16:51:56 -0700 Subject: [PATCH 3/3] [Infra] Packaging: Drop floor-check workflow + bound importlib-metadata Removing the new check-dependency-floors.yml workflow. It only fires when pyproject.toml changes, which is rare; for those PRs, a maintainer can run the same check by hand with one command. Documented that command in a pyproject.toml comment next to the deps. Also adds the missing upper bound on importlib-metadata (>=8.0.0,<9.0) for consistency with every other entry in the list. --- .github/workflows/check-dependency-floors.yml | 117 ------------------ pyproject.toml | 7 +- uv.lock | 2 +- 3 files changed, 5 insertions(+), 121 deletions(-) delete mode 100644 .github/workflows/check-dependency-floors.yml diff --git a/.github/workflows/check-dependency-floors.yml b/.github/workflows/check-dependency-floors.yml deleted file mode 100644 index 8fc5b8564ee..00000000000 --- a/.github/workflows/check-dependency-floors.yml +++ /dev/null @@ -1,117 +0,0 @@ -name: Check dependency floors - -on: - pull_request: - paths: - - 'pyproject.toml' - - '.github/workflows/check-dependency-floors.yml' - workflow_dispatch: - -permissions: - contents: read - -jobs: - resolve-and-import: - name: Install at lowest-direct + smoke-import litellm - runs-on: ubuntu-latest - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - python-version: ["3.10", "3.13"] - steps: - - name: Checkout PR - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - with: - version: "0.10.9" - enable-cache: false - - - name: Set up Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} - - - name: Resolve litellm core deps at floor versions - run: | - uv venv --python ${{ matrix.python-version }} /tmp/floor-venv - uv pip install --python /tmp/floor-venv/bin/python --resolution=lowest-direct . - - - name: Smoke-import litellm at floor versions - run: | - /tmp/floor-venv/bin/python <<'PY' - import importlib - - # Import the SDK itself. - import litellm # noqa: F401 - - # Import every openai-namespace symbol the codebase actually uses - # at runtime. If a floor is set lower than the version that - # introduced any of these, the install will silently succeed and - # then fail at runtime in customer code. This list mirrors the - # openai imports under litellm/ — keep it in sync. - import openai # noqa: F401 - from openai import ( # noqa: F401 - AsyncAzureOpenAI, - AsyncOpenAI, - AzureOpenAI, - OpenAI, - APIError, - APITimeoutError, - Omit, - ) - from openai.types.responses import ( # noqa: F401 - ResponseFunctionToolCall, - ResponseInputImageParam, - ResponseOutputMessage, - ResponseReasoningItem, - ) - from openai.types.responses.response import ( # noqa: F401 - IncompleteDetails, - Response, - ResponseOutputItem, - Tool, - ToolChoice, - ) - from openai.types.responses.response_output_item import ( # noqa: F401 - ResponseApplyPatchToolCall, - ) - from openai.types.responses.response_text_config_param import ( # noqa: F401 - ResponseTextConfigParam, - ) - from openai.types.responses.tool_param import ( # noqa: F401 - FunctionToolParam, - ) - - # Sanity-check the other direct deps load. - for mod in ( - "pydantic", - "httpx", - "aiohttp", - "tokenizers", - "tiktoken", - "click", - "jinja2", - "jsonschema", - "importlib_metadata", - "fastuuid", - "dotenv", - ): - importlib.import_module(mod) - - print("All floor-version imports OK") - PY - - - name: Report resolved versions - if: always() - run: | - /tmp/floor-venv/bin/python -c " - from importlib.metadata import version - for pkg in ['litellm', 'openai', 'pydantic', 'httpx', 'aiohttp', 'tokenizers', 'tiktoken', 'click', 'jinja2', 'jsonschema', 'importlib-metadata', 'fastuuid', 'python-dotenv']: - try: - print(f'{pkg}: {version(pkg)}') - except Exception as e: - print(f'{pkg}: ({e})') - " diff --git a/pyproject.toml b/pyproject.toml index 67d23a6071b..61229410ae5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,14 +11,15 @@ authors = [ ] dependencies = [ # Ranges (not exact pins) so SDK consumers can coexist with their other - # deps. Reproducibility for our Docker/CI comes from `uv.lock`. The - # `dependency-floors` CI job validates the floors actually install. + # deps. Reproducibility for our Docker/CI comes from `uv.lock`. + # When changing a floor, verify it installs + imports on every supported + # Python with: `uv pip install --resolution=lowest-direct .` "fastuuid>=0.14.0,<1.0", "httpx>=0.28.0,<1.0", "openai>=2.20.0,<3.0.0", "python-dotenv>=1.0.0,<2.0", "tiktoken>=0.8.0,<1.0", - "importlib-metadata>=8.0.0", + "importlib-metadata>=8.0.0,<9.0", "tokenizers>=0.21.0,<1.0", "click>=8.0.0,<9.0", "jinja2>=3.1.0,<4.0", diff --git a/uv.lock b/uv.lock index 008d7872ac9..5c353772b0e 100644 --- a/uv.lock +++ b/uv.lock @@ -3290,7 +3290,7 @@ requires-dist = [ { name = "grpcio", marker = "extra == 'proxy-runtime'", specifier = "==1.78.0" }, { name = "gunicorn", marker = "extra == 'proxy'", specifier = "==23.0.0" }, { name = "httpx", specifier = ">=0.28.0,<1.0" }, - { name = "importlib-metadata", specifier = ">=8.0.0" }, + { name = "importlib-metadata", specifier = ">=8.0.0,<9.0" }, { name = "jinja2", specifier = ">=3.1.0,<4.0" }, { name = "jsonschema", specifier = ">=4.0.0,<5.0" }, { name = "langfuse", marker = "extra == 'proxy-runtime'", specifier = "==2.59.7" },